2015年5月6日水曜日

Altera SoC Helio で SDRAM の HPS/FPGA 共有(作業編)

前回の記事で詳細には踏み込んでいないものの、ほぼ必要な情報が揃っているはずです。
実際の作業を始めましょう。
でもまず、作業を始めるのにあたって必要なものが欠けています。
Helio として最低限動作可能な FPGA 側のリファレンス実装です。

作業編

リファレンスデザインの入手

Helio は GSRD (Golden System Reference Design) というものに則って設計/実装/提供されています。
生憎こちら、GSRD という枠組みについては詳しくなく、どこからどこまでが GSRD なのかはよくわからないのですが説明によると, Altera か Xilinx かにはよらず

FPGA(GHRD) + ARM SoC + Linux 環境  = GSRD

ということのようです。
Altera だろうとそうでなかろうと GSRD に従った設計がなされているものについてはある程度似たような環境が用意されているということですね。
ここで重要なのは、主に FPGA 側の設計のリファレンスデザインである GHRD (Golden Hardware Reference Design) のほうですね。

Getting Start Guide に従ってサンプルを動かした人ならば既に手元にあるはずです。
しかしながらドキュメントに従って cv_soc_devkit_ghrd.tar.gz をゲットしてしまった人は残念。理由はわかりませんがこれは Helio では動かず、 Helio には以下から専用のものを入手しましょう。
helio_ghrd_5csxc4_v14.*.zip が正解です(Quartus II のバージョンに合わせて選んでください)。

http://www.rocketboards.org/foswiki/Documentation/HelioResourcesForRev14

に一通りあります (ボードの Revision が 14 用。 Quartus のバージョンと紛らわしいですが、今はたまたまどっちも 14 です)。
間違えると回路が正しくても Linux がブートしなくなります。
なんというハメでしょうね。僕はこれにハマって二三日悩みました。(.sof イメージは焼いても電源を切ると揮発するので、復旧自体は簡単です)

GHRD の中身は全て Quartus II のプロジェクト及び Qsys で扱う IP とサンプルの helio_ghrd_top.v です。
自分でビルドしない限り、一切のビルド物は含まれていません。

helio_ghrd_top.v を開くと、殆どの信号は soc_system という下位回路に結線されているのがわかります。
その soc_system が GHRD の本体であり、 HPS のインターフェイスを実装した(あるいはこれから実装する)回路であります。

ところが soc_system の実装自体は .v としては付属しません。
soc_system は全て IP として提供されており、 soc_system.qsys から Qsys で生成して入手します。
このへんの手順は Getting Start Guide でも一通り説明されていますので、僕のような noob でも脳細胞が Hi-Z になったり X になったりすることなく進めました。

これからの作業は殆ど Qsys と普通のエディタで行います。 Quartus II はビルド(Analysis & Synthesis, Place & Route, Fitting) と死んで再起動くらいしかしません。

なぜここで GHRD を必要とするかはここまででお解りと思います。
このリファレンスデザインを Qsys でカスタマイズしたり、外部に設計した回路に繋げることでシステムを実装していくわけです。
どれを IP にしてくっつけるか、どれを外部に引き出すかは任意です。

SDRAM Controller のインターフェイスを取り出す

soc_system の hps_0 というインスタンスに結線されている f2h_sdram_data というのが SDRAM Controller のインターフェイスです。
これは信号の種類を見れば解る通り Avalon MM Slave のインターフェイスの形をしております。
hps_0 というインスタンス(下位回路への結線)は、ソフト的にいうと複数のインターフェイスを多重継承したようなイメージですね。
飽くまでこれはイメージで、実際には各信号ごとに wire が結線されております。

Qsys を使ってリファレンスデザインをカスタマイズしていくと書きましたが、 IP を変更すると毎回 "Generate HDL" を実行せねばならず、そこそこ時間がかかってしまいます。
ここはこのインターフェイスを外部に引き出して、 IP の外にある helio_ghrd_top.v で回路を実装したほうがよいでしょう。

helio_ghrd_top.v からは hps_0 の上位回路である soc_system しか見えません。
まずは hps_0 の f2h_sdram_data を soc_system に引き出さねばなりません。
普通の FPGA であればピンに出ているのですが、 SoC では外部といっても内部です。 ピンが出ているとは限りません。
このへんは Qsys を使い慣れていなければわからないので、身近なハード屋さんに助けてもらいました。
引き出したいインターフェイスを右クリックして "Export as" を選べばオッケーのようです。
Export できた!

hps_0 の上のほう、 f2h_sdram0_data の左側がタグのようになっています。これが外部に引き出せた状況です。
各カラムは左から Name, Description, Export name, Associate Clock となっております。
Export の column にある名前で上位回路に自動的に結線されます。
典型的には、
(接続先インスタンス名)_(信号名) 

という名前になるようですね。

バス幅の決定と HDL の再生成


それから hps_0 を右クリックして "Edit" を選ぶと、プロパティシートから細かい設定を変更できます。
デフォルトは f2h_sdram_data のバス幅として 256bit が設定されています。
メモリのデータバスは 16+16bit で 32bit なので、まずはデータバスに合わせておこうと32 に変更します。
そして Generate HDL を再度行います。
この場合はこうなりました。
        input  wire [29:0]  hps_0_f2h_sdram0_data_address,
        input  wire [7:0]   hps_0_f2h_sdram0_data_burstcount,
        output wire         hps_0_f2h_sdram0_data_waitrequest,
        output wire [31:0] hps_0_f2h_sdram0_data_readdata,
        output wire         hps_0_f2h_sdram0_data_readdatavalid,
        input  wire         hps_0_f2h_sdram0_data_read,
        input  wire [31:0] hps_0_f2h_sdram0_data_writedata,
        input  wire [3:0]  hps_0_f2h_sdram0_data_byteenable,
        input  wire         hps_0_f2h_sdram0_data_write,
あとはこれに対して Master となる回路を書けばメモリのアクセスが可能になります。

ところで、元々 f2h_sdram0_data はどこに繋がっているのでしょうか。
Export すればこのインターフェイスを外から使えるようになりますが、それだけでは元々ここに繋がっていた回路は使えなくなってしまいます。
f2sdram_only_master に繋がっているのは Qsys 上でも確認できますが、この副作用を看過できるかどうかは f2sdram_only_master の先を注意深く調べる必要があります。
調べてみたところ、これは幾つかの FIFO を経由して JTAG に繋がっていました。
JTAG からのリクエストを非同期で処理するための回路のようです。
SDRAM Controller はこれを調停可能なはずですが、今回は華麗にカットしました。

Master の回路を書く

Avalon MM の仕様書より、タイミングチャートを調べて実装します。
メモリアクセスの方法は、基本的な同期 RW 以外にもパイプラインリード、バースト転送などがありますが、今回はまず基本的なライトだけを実装します。

Figure 3-3: Read and Write Transfers with Waitrequest の右半分がそれです。

言葉で説明すると、

a. (Master が)パラメータを設定して Write を立てると
b. ビジー状態を示す WaitRequest が(Slave によって) 1 にドライブされ、
c. 書き込みが完了すると (Slave によって) WaitRequest が 0 にドライブされるので、
d. (Master は)パラメータは次のクロックの立ち上がりまでは保持し続けること

ということです。
今度は W と R がゲシュタルト崩壊しそうなので WaitRequest は Busy と読み替えたほうがいいかも知れません。
Write を立てるときには WaitRequest は(あと当然 Write も) 0 でなければいけません。
書き込みのパラメータはアドレス、書き込みマスク(byteenable)、書き込むデータの三つです。
これらパラメータが設定されるのは Write と同時かそれ以前で、クロック同期である必要はありません。(書き込みデータだけは WaitRequest が立つ瞬間まで遅延できるようです)
今回はバーストライトではないので burstcount は 1 のままにします。

byteenable はかなり使い手のあるパラメータで、実際に書き込むバイトを自由に設定できます。バイト同士が連続である必要も、popcount が 2^n でなければならないといった制約もありません。

1 ワードのデータを書き込むだけならただの組み合わせ回路でもできるのですが、やはり沢山書いてみたいので少し凝りましょう。
WaitRequest と書き込み信号をイベントとしたクロック同期する FSM を中心に構成しましょう。
そうしておくことで、連続したリクエストに対しても d の制約を満たすことができます。

メモリレイアウトを考える

回路の構成には関係ありませんが FPGA と HPS で物理アドレスを配分しなければいけません。
しなくてもいいのですが、現時点でバッティングしてよいことは何もありませんのでとりあえず 512MB ずつ半分にして HPS には 0x000000000-0x1fffffff, FPGA には 0x20000000-0x3fffffff としておきましょう。
これは完全に自由にできるわけではありません。FPGA 側がハイアドレスを使うことは決まっていますし、アドレス空間の最上位 1GB の空間は予約されています。
詳細は Device Handbook Volume 3: 9-11 Boot ROM Mapping をご覧ください。
そんなに情熱を注ぐところでもないです。

HPS 側で物理メモリを制限するためには、 Linux の boot に UART で割り込んで抑制し、 代わりに上がってくる SOPC shell 上で editenv コマンドを使って mmcboot 環境変数に書き込めばよいです。
言葉で書くとなんか大変そうに見えますが EFI shell とか EPROM shell みたいなもんです。
こちらの blog の JTAG を使って DDR3 をシェアする記事が大変大変参考になります。

出来上がった回路はこうなりました。
なんか色々怪しいですが。

https://github.com/roentgen/sdram_helio/blob/master/helio_ghrd_top.v

CPU 向けのインターフェイスを用意する

起動したらずっとメモリに書き込み続ける回路ならば不要なのですが、それはちょっと熱や消費電力(それから精神衛生面)に優しくないので、 ARM 上で動くプログラムからメモリの書き込みと停止をコントロールできるようにしましょう。

CPU とのインターフェイスは、今や SDRAM さえ使用可能ですが、最もお気楽な方法としては MemoryMapped-IO があります(以下 MMIO)。
今回はハードウェアらしく MMIO を使います。

MMIO は、CPU からコントロールレジスタが見えないようなデバイスを扱うのにとても歴史がある方法で、個人的にも古い友人のような感慨があります。
この方法では物理アドレスの一部を予約し、そこに対する CPU からのアクセスを(メモリではなく)外部バスにリダイレクトするのです。
リダイレクト先は(典型的には) FPGA のレジスタになります。

……と、理論上は簡単なのですが、 Altera SoC におけるここの実装は相当に複雑です。
Device Handbook Volume3: 8-3 HPS-FPGA Bridges Block Diagram and System Integration の Figure 8-1 をご覧くださいませ。
HPS->FPGA と FPGA->HPS 二つのブリッジがあるのはわかるのですが、その間に Lightweight HPS-to-FPGA Bridge なるものが介在しています。
これらは AHB/AXI なのですが、にも関わらず soc_system の IP 上見えているのは AvalonMM Slave です。
32bit アクセスだけは lightweight を通って、64bit はネイティブにそれぞれのブリッジ上を通る。 128bit はブリッジ上の FIFO で分割される……という風に読めますがそれにしては AHB の Master/Slave が逆のようにも思いますし、よくわからないです

よくわからないものの、使い方は非常に簡単です。
一つだけ注意することがありましたが、それだけです。
GHRD にはとてもよいサンプルとして sysid_qsys と led_pio という二つの IP が組み込まれています。
これな
MMIO の物理アドレス 0x10000 からとなっていますが、実際は FGPA ペリフェラルの物理アドレス空間は 0xff200000 からなのでここからのオフセットです。
物理アドレス 0xff210000 がこのレジスタです。
コードも簡単で、ここへのアクセスに対し定数を readdata に突っ込むだけのコードになっています。
インターフェイスは Avalon-MM Slave になります。

ただし、前述した HPS-to-FPGA Peripheral のよくわからないことの一つでよくわからないままなのですが、どうやら注意しなければいけないことがあります。
IP を設計するときは Avalon-MM Slave と思っておいていいのですが Qsys で繋ぐときに h2f_lw_axi_master とも接続しないとこの単純な回路さえ値が見えません。

(実際 sysid_qsys は接続されています)
sysid_qsys を見てみると fpga_only_master の Avalon MM Master に繋ぐだけでなく、 hps0 の h2f_lw_axi_master とも接続されています。
インターフェイスには出て来ない AXI を接続しなければいけないとは、記述的にはまったく想像がつかないです。
ですがたしかに HPS-to-FPGA Peripheral のブロック図からすると、ここのブリッジは AXI でのみ繋がっているはずで Avalon ではないはずです。
つまり、バスが AXI であっても Avalon に見せる魔術があるのだろうとは予想されますが、今回枝葉なのであまり詳しくみてません。「へー、なるほどねー」くらいのことです。

lw (Lightweight)なのは MMIO の幅が 32bit だからなのでしょうか。64bit か 128bit のときは別のマスタと接続する必要があるのかも知れませんが、オービターがうまいことやってくれるような気もします。

一方、書き込むほうは led_pio を参考にしたほうがよいでしょう。
有意な writedata があるときだけライトストローブが有効になるようにします。

   assign wr_strobe = chipselect && write;

この wr_strobe を見て内部のレジスタを変更するようにすればよいです。
state_splat は 1bit reg, wd は 8bit reg です。 その他、サイクルカウンタに使ったレジスタなどが見えますが、重要なのは state_splat だけです。

   always @(posedge clk or posedge rst) begin
       if (rst == 1) begin
           state_splat = 0;
           wd = 0;
      end
      else if (clk) begin
          if (wr_strobe) begin
              state_splat <= writedata ? 1 : 0;
              wd <= writedata[7:0];
          end
      end
   end
  
   assign readdata[31] = reset_n;
   assign readdata[30] = mem_rst;
   assign readdata[29] = state_splat;
   assign readdata[28:8] = regcc[31:11];
   assign readdata[7:0] = wd;

これを module に仕立て上げ IP として Qsys に追加しましょう。
コードはこちら。
https://github.com/roentgen/sdram_helio/blob/master/mem_sdram_interface.v

具体的な手順は……スクリーンショットを保存しておらずですね……再びこちらの記事がとてもとても参考になります。ありがとうございます。

Qsys は任意の .v を検証し、インターフェイスを明示的に選ばせることで IP として使用可能にします。
(.v はそのまま soc_system/synthesis/submodules/ 以下にコピーされる)
IP として登録するのに clock, reset が超重要です。
これは module の信号リストの順番が結構センシティブですので、信号リスト先頭から clock, reset の順番にしておいたほうが無難のようです。
reset は正論理か負論理かも設定できます(IP をインスタンス化するときにケアされます)。
 
正しく設定しておくと色々ご利益がありそうですが、よくわからんので今回は雑に設定しました。
名前はどうしてこんな長い名前にしてしまったのかと後悔しつつも mem_sdram_interface とし MMIO のベースアドレスは 0x10010 としました。

ちなみに qsys の検証は、簡単なシンタックスチェックが入るだけで合成はしません。
ですのでエラーがあると HDL を生成してからあとで Quartus II で発覚することになります。事前にチェックしときましょうね。面倒だからといって submodules 以下のファイルを直接いじったりしてはいけませんよ(実証済み)。

出来上がりの確認

MMIO で非ゼロを受けて memwrite_master を駆動するシステムができました。
MMIO でゼロが書き込まれるまで、 memwrite_master はアドレスからアドレスまで定数を書き込み続けます。

Qsys ではこうなります。

全体像
一番下の mem_sdram_interface が MMIO の Slave です。
いくつかよぶんな信号線が生えていますが、これは後述するサイクルカウンタの値を設定するためのものです。
memwrite_master は IP にしてないのでここには出ません。

CPU 側のプログラム

とりあえず動かしてみるために、ちょっとしたプログラムを書いて回路をキックします。
MMIO の物理アドレスを指定して mmap し、仮想アドレスに対して 32bit データを書き込むだけです。
1 を書き込むと memwrite_master が動きだし 0 を書くと止まります。
memwrite_master は動き続ける限りあるアドレスの範囲に書き込み続けます。

https://github.com/roentgen/sdram_helio/blob/master/addrmap.cpp

このプログラムは単純に、物理アドレス指定してコントロールレジスタのマップされてる領域と、実際に書き込みが行われる DDR3 上の領域を mmap し、ダンプします。
/dev/mem を open しますので、その権限が必要ですが、全てユーザーランドからコントロールできておりドライバは不要です。
(実際もっとページ属性を細かくコントロールしてやろうとか、カーネルモードでしか触れないレジスタを触ろうとか、 マルチプロセスに対応してやろうとかなるとドライバやカーネルモジュールを作る必要がでてきますが……)

open() システムコールのフラグに O_SYNC を指定しています。
最近のカーネルではこれを立てて /dev/mem を open すると uncached になるらしいです。
(kernel が STRICT_DEVMEM 付きでビルドされてないといけないようですが、たぶんそうなってるでしょう)

サイクルカウンタを実装してパフォーマンスを測定する

これは後付けですが、パフォーマンスを測定するためのサイクルカウンタを実装して、メモリの fill が終わったら fin を上げて自律的にリセットしてサイクルカウンタを MMIO のレジスタに書き込み、 CPU からサイクル数を計測できるようにもしましょう。


この場合のリセットの方法はいろいろあると思いますが、ハードリセットほどは単純でなく自律的にやるのは結構苦労しました。
簡単そうに見えたのですがね、リセット解除しないと次の実行を始められないので。
リセット解除は FSM を別に作るかそもそもワンショットパルスにするべきかはたまた CPU からやったほうがいいのかも知れませんが、この辺はノウハウを溜めないといけないな〜。

ともあれ、満身創痍ですがサイクルの測定はできました。
実行してみると物理アドレス 0x20000000 から 0x2000 * 32bytes の領域に書き込めています。
fin が立ったあと mem_sdram_interface の WE  に間に合わないのかサイクルカウンタが終了時にリセットされてないように見えますが、開始時にリセットされているのでよしとしましょう。

結果

master 側の fpga_clock50 は 50MHz です。
なんと 0x10000 回の 32bit ライトアクセスで 500ms もかかった計算になります。
遅い!!
500KB/s しか出てないじゃないか!
一回のライトリクエストごとに 390 サイクル (7us@50MHz) かかったことになります。
大体 40-50 サイクルじゃないかな〜と思っていたのに……これはちょっと遅過ぎます。

メモリは 200MHz 駆動だけどもコントローラは 50MHz だし、 SDRAM Controller がボトルネックになっているようです。
確かめてみるためにデータレートを上げてみます。

Qsys で HPS のプロパティシートを開き f2sdram0_data のビット幅を 256 に変更します。(32bit にしていた)
デフォルト 256 ですがメモリのデータバスが 32bit だし 256 は厳しかろうと思っていたのですが、メモリバンドネックではなさそうなので 256 で試します。
memwrite_master の wire 幅にも影響があるのでそっちも修正して試してみると……。

結果はトータル 0x061a << 3 カウント。 1 トランザクションあたりのサイクル数は 390 で同じでした。
ただしトランザクション回数が 1/8 になったぶんバスバンドはリニアに向上し 4MB/s 程度出ます。かかった時間も 60msec 程度と大人しめ。
Avalon MM の Master/Slave 間にブリッジが存在しないのであれば、 Multiport SDRAM  Controller のクロックが低過ぎるか、スマートすぎて DDR3 の帯域を生かせないということです。

ただしまだ結論は出せません。
だってメモリコントローラがバスネックになるのじゃ ARM から見ても DDR3 の帯域がでないはずで、じゃあ DDR3 を積む必要なんかないじゃないかということになります。
SDRAM Controller Subsystem の説明ではコントローラへのアクセスは AXI だったはず。

ならば現時点の仮説は

1. Avalon が悪い。 AXI を使え
2. メモリコントローラの実力だ。 バーストモードか DMA を使え
3. GHRD が悪い。 クロックを上げろ

の組み合わせということになります。

なぜ遅いかはまだ解らないにしても、ひとまず実験の結果は出ました。
まとめましょう。
  • FPGA と HPS で SDRAM を共有することは(解ってしまえば)簡単だ
  • Avalon MM 同期 Write を使ったメモリの書き込み@50MHz ではメモリの帯域が出せない(256bit でも 2MB/s が上限)
  • ライトトランザクションにはデータ幅によらず 390 サイクル, 7us@50MHz かかる
以上となりました。

0 件のコメント:

コメントを投稿