Helioボード: オンチップメモリーにDMA転送で書き込む実験
はじめに
これまでの実験で、Linuxのユーザーランドからオンチップメモリーに読み書きできるようになりました。 今回は、ユーザーランドからではなく、FPGA内部の信号元から書き込む実験をします。 書き込む内容は、単純なカウンタ回路で生成したストリーム信号です。 IPコアとして提供されている、DMAコントローラとAvalon FIFOメモリーを用います。 これらのIPコアと自作のストリーム信号をQsysで合成し、ユーザーランドからDMAコントローラを制御することでDMA転送を実現します。
この記事を読むことで、Direct Memory Access(パソコン用語としても聞いたことがある方も多いと思います)についての理解が深まり、DMAの本質に近づけます。
flickr: perlin flow particle ribbon 1901
全体像
シグナルジェネレータで生成された信号データをオンチップメモリーに書き込みます。ざっくりした流れは以下の通りです。
- CPUがDMAコントローラの転送を命令する。
- DMAコントローラが転送開始する。
- シグナルジェネレータで信号を生成しFIFOに送信する。DMAコントローラはFIFOから読んでオンチップメモリーに書き込む。
- 指定した信号長をオンチップメモリーに書き込んだら、DMAコントローラは処理を終了する。
- FIFOが満タンになり、シグナルジェネレータが停止する。
ポイントは中間に挿入されているFIFOで、これによりAvalon Streaming SinkからAvalon Memory-Mapped Slaveにインタフェースを変換しています。なぜこの変換が必要かと言うと、DMAコントローラはAvalon Memory-Mapped Slaveとしか接続できないためです。
+-----------------------+ | Signal Generator | +-----------------------+ ↓↓↓↓ Avalon Streaming Source ↓↓↓↓ ↓↓↓↓ Avalon Streaming Sink +-----------------------+ | FIFO | +-----------------------+ ↓↓↓↓ Avalon Memory-Mapped Slave ↓↓↓↓ ↓↓↓↓ Avalon Memory-Mapped Master (Read) +-----------------------+ | | Avalon Memory-Mapped Slave | DMA Controller | <================== 制御信号 | | +-----------------------+ ↓↓↓↓ Avalon Memory-Mapped Master (Write) ↓↓↓↓ ↓↓↓↓ Avalon Memory-Mapped Slave +-----------------------+ | On-Chip Memory | +-----------------------+
シグナルジェネレータ
Avalon Streaming Sourceを実装しています。 生成している信号は単純に0、1、2と増えていくカウンタです。 より正確には、カウントイネーブル付きの同期式カウンタです。 avalonst_source_readyをenable信号としてとらえると、理解できると思います。(本記事の最後の参考情報に書籍をリンクしておきました)
Avalon Interface Specificationsの「5.6 Signal Details」によると、Avalon Streaming Sourceは一番シンプルな構成だと4本のインタフェースで構成できます。
- valid: Sourceがデータを送信する時にassertする
- data: Sinkに送信するデータ
- error: データにエラーが含まれる場合のビットマスク
- ready: Sinkが受信準備ができたらassertされる
channelはoptionalと書かれているので、今回はとりあえず無視しました。
実装は以下の通りです。
module signal_generator( clk, reset_n, avalonst_source_valid, avalonst_source_data, avalonst_source_error, avalonst_source_ready ); input clk; input reset_n; output avalonst_source_valid; output [ 31: 0] avalonst_source_data; output [ 7: 0] avalonst_source_error; input avalonst_source_ready; reg out_valid; reg [ 31: 0] out_data; reg [ 7: 0] out_error; always @(posedge clk or negedge reset_n) begin if (~reset_n) begin out_valid <= 1'b0; out_data <= 32'b0; out_error <= 8'b0; end else if (avalonst_source_ready) begin out_valid <= 1'b1; out_data <= out_data + 1; out_error <= 8'b0; end end assign avalonst_source_valid = out_valid; assign avalonst_source_data = out_data; assign avalonst_source_error = out_error; endmodule
これを以前やったようにコンポーネント化して、Qsysで配置します。
Component EditorでAvalon Streaming SouceのData bits per symbolを8から16に変更しておきます。これをやっておかないと、QsysでFIFOとFIFOと接続すると以下のエラーが発生します。
Error: soc_system.signal_generator_0.avalon_streaming_source/fifo_0.in: The source has 8 bits per symbol, while the sink has 16.
Avalon FIFO Memory
Avalon FIFO Memoryの仕様書によると、FIFOは以下の4つの構成を取れます。
- Avalon-MM write slave to Avalon-MM read slave
- Avalon-ST sink to Avalon-ST source
- Avalon-MM write slave to Avalon-ST source
- Avalon-ST sink to Avalon-MM read slave
今回の場合は、前述した通り、Avalon Streaming SinkからAvalon Memory-Mapped Slaveにインタフェースを変更したいので、4つ目の構成を使用することになります。
QsysのIP CatalogからAvalon FIFO Memoryを探してウイザードを開始します。
以下のように設定にします。その他はデフォルトのままです。
- Status port -> Create status interface for inputを外す
- Input type: AVALONST_SINK (変更)
- Output type: AVALONMM_READ (デフォルトのまま)
- Enable packet data: チェックを外す
一番シンプルな構成にしたかったので、status interfaceとpacket dataのサポートは外しました。 設定項目の変更によってFIFOの信号線がどう変化するか観察すると理解が深まります。 Block DiagramのShow signalsのチェックを入れると信号線も表示できます。
DMAコントローラ
DMAコントローラは、Avalon Memory-Mapped Slaveのcontrol portから制御します。 レジスタマップの仕様の通りにレジスタに必要なデータを書き込むことで、DMA転送できます。今回の場合は、FIFOからオンチップメモリーへの転送になります。
DMAコントローラへの大まかな指示手順は以下の通りです。
- コピー元アドレスを書き込む
- コピー先アドレスを書き込む
- 転送データ長を書き込む
- その他制御情報を書き込み、転送を開始する
- 転送完了したか確認する
最後の5は、普通は割り込みを使うと思いますが、まだそのやり方が理解できていないので、DMAコントローラに問い合わせます。
QsysのIP CatalogからDMA Controllerを探してウイザードを開始します。設定はデフォルトのままにします。
結線
Qsysで以下のように結線します。dma_0、fifo_0、signal_generator_0が今回追加したインスタンスです。
- dma_0のcontrol_port_slaveはh2f_axi_masterと結線
- dma_0のirqはf2h_irq0と結線 (結線するだけで、割り込みは今回使用しません)
後から参照しやすいように、コンポーネントとベースアドレスをまとめると以下のようになります。
コンポーネント | ベースアドレス |
---|---|
HPS-to-FPGAブリッジ (h2f_axi_master) | 0xC0000000 |
DMAコントローラ | 0x00010000 |
FIFO | 0x0000 |
オンチップメモリー | 0x00000000 |
以上で回路設計の作業は完了です。コンパイルしてFPGAに書き込みます。
DMA転送の前に
まずは、レジスタマップの仕様を見ながら、各レジスタのアドレスを求めます。
メモリーマップI/Oから操作する際のDMAコントローラのアドレスは、h2f_axi_masterのベースアドレスが0xC0000000、DMAコントローラのベースアドレスが0x00010000なので、0xC0000000 + 0x00010000 = 0xC0010000になります。
各レジスタのアドレスは以下のようになります。
レジスタ名 | アドレス |
---|---|
status | 0xC0010000 |
readaddress | 0xC0010004 |
writeaddress | 0xC0010008 |
length | 0xC001000C |
control | 0xC0010018 |
※アドレス = 0xC0010000 + オフセット * 4
また、メモリーマップI/Oから操作する際のオンチップメモリーのアドレスは、h2f_axi_masterのベースアドレスが0xC0000000、オンチップメモリーのベースアドレスが0x00000000なので、0xC0000000 + 0x00000000 = 0xC0000000になります。
DMA転送してみる
再掲になりますが、DMA転送の手順は以下の4ステップです。
- コピー元アドレスを書き込む
- コピー先アドレスを書き込む
- 転送データ長を書き込む
- その他制御情報を書き込み、転送を開始する
- 転送完了したか確認する
この通りにDMAコントローラのレジスタを操作することで、DMA転送できます。
では、実際にやってみます。前々回紹介したユーティリティを使ってレジスタを操作していきます。
1. コピー元アドレスを書き込む
0xC0010004がreadaddressレジスタです。 書き込むデータはFIFOの先頭アドレスは0なので0x00000000とします。
root@socfpga:~# ./devmem2 0xC0010004 w 0x00000000
2. コピー先アドレスを書き込む
0xC0010008がwriteaddressレジスタです。 オンチップメモリーのアドレス0xC0000000を指定します。
root@socfpga:~# ./devmem2 0xC0010008 w 0xC0000000
3. 転送データ長を書き込む
0xC001000Cがlengthレジスタです。 データ長は12バイト = 0x0000000Cとしました。
root@socfpga:~# ./devmem2 0xC001000C w 0x0000000C
4. その他制御情報を書き込み、転送を開始する
ここは少しややこしいです。今回の場合は、以下のビットを立てます。
- 32ビット幅で転送→WORDビットを1に
- 転送開始→GOビットを1に
- lengthレジスタが0になったらトランザクションを終了→LEENビットを1に
- FIFOの先頭アドレスから常に読みたいのでコピー元のアドレスを固定→RCONビットを1に
以上のフラグから、controlレジスタの値を求めると、0x18Cになります。
- WORD(2) = 22 = 0x4
- GO(3) = 23 = 0x8
- LEEN(7) = 27 = 128 = 0x80
- RCON(8) = 2~8 = 256 = 0x100
= 0x18C
ちなみに、LEEN(7)を立てないと、DMA転送開始後、statusレジスタBUSY(1)が立ったままになり、DMA転送が完了しません。
0xC0010018はcontrolレジスタです。
root@socfpga:~# ./devmem2 0xC0010018 w 0x0000018C
5. 転送完了したか確認する
0xC0010000がstatusレジスタです。 0x11が返りました。 これは、DONE(0)とLEN(4)のフラグが立っているということで、DMA転送が完了し、lengthレジスタが0になったことを示しています。つまり、正常に転送できました。
root@socfpga:~# ./devmem2 0xC0010000 w /dev/mem opened. Memory mapped at address 0x76f5e000. Value at address 0xC0010000 (0x76f5e000): 0x11
オンチップメモリーの内容を確認してみます。0x10000、0x20000、0x30000とカウンタ的に増えているのがわかります。0x1、0x2、0x3にならなかった原因はまだよくわかっていません。
root@socfpga:~# ./devmem2 0xC0000000 w /dev/mem opened. Memory mapped at address 0x76fcc000. Value at address 0xC0000000 (0x76fcc000): 0x10000 root@socfpga:~# ./devmem2 0xC0000004 w /dev/mem opened. Memory mapped at address 0x76fb1000. Value at address 0xC0000004 (0x76fb1004): 0x20000 root@socfpga:~# ./devmem2 0xC0000008 w /dev/mem opened. Memory mapped at address 0x76fa5000. Value at address 0xC0000008 (0x76fa5008): 0x30000 root@socfpga:~# ./devmem2 0xC000000C w /dev/mem opened. Memory mapped at address 0x76fc9000. Value at address 0xC000000C (0x76fc900c): 0x0
ちなみに、同様の手順で再度DMA転送してみると、0x40000、0x50000、0x60000となり、前回の続きから値が取り出せているのがわかります。
root@socfpga:~# ./devmem2 0xC0000000 w /dev/mem opened. Memory mapped at address 0x76f21000. Value at address 0xC0000000 (0x76f21000): 0x40000 root@socfpga:~# ./devmem2 0xC0000004 w /dev/mem opened. Memory mapped at address 0x76f94000. Value at address 0xC0000004 (0x76f94004): 0x50000 root@socfpga:~# ./devmem2 0xC0000008 w /dev/mem opened. Memory mapped at address 0x76f2b000. Value at address 0xC0000008 (0x76f2b008): 0x60000 root@socfpga:~# ./devmem2 0xC000000C w /dev/mem opened. Memory mapped at address 0x76f53000. Value at address 0xC000000C (0x76f5300c): 0x0
参考情報
Interface 2009年1月号 「FPGA評価キットを使ったグラフィック・イコライザの設計製作」からAvalon Streaming InterfaceとAvalon FIFO Memoryの組み合わせのヒントを得ました。以下のCD-ROM書籍で全文検索して見つけました。
シグナルジェネレータで用いたカウントイネーブル付きの同期式カウンタは、以下の書籍を参考にしました。
おわりに
DMAコントローラとAvalon FIFO Memoryは、未経験のコンポーネントで、2つ組み合わせて動作するか心配でしたが、無事動作させることに成功しました。
仕様書を読んで忠実に従えばなんとかなることもわかりました。今回の実験では、以下の仕様書を何度も読み返しました。
ただ、まだ課題があり、割り込みを使ったDMA転送通知まではできていません。また、欲を言うと、オンチップメモリーではなく、Helioボードに実装されているSDRAMに転送したかったりします。これらについても今後実験できればと思っています。