ZYNQでAXI DMAを試してみる
ZYNQで超単純なAXI DMAを試してみる.
AXI DMAとはAXIバスを使ったDirect Memory Accessのことで,PSを介さずにPL部分からメモリ(ZYBO-Z7の場合はDDR3メモリ)にデータを転送する方法のことである.
XilinxからはAXI Direct Memory AccessというIPが提供されていて,これを使うとDMAを実装できるようになる.今回はこのIPを使ってAXI DMAを試してみた.
こちらのページを参考にしてみた.
Lauri's blog | AXI Direct Memory Access
AXI DMA IPについて
IPのリファレンスはこちら.
LogiCORE IP AXI DMA v7.1 製品ガイド Vivado Design Suite PG021 2014 年 4月2日
IPは図のような構成をしているらしい.各ポート/ブロックについてまとめておく.
ブロック図を見てみると,読み込み/書き込みで非常に対象的な構造をしているIPであることに気づく.どうもAXI DataMoverというIPを読み込み/書き込みで2個くっつけて,Scatter/Gatherを追加したようなIPに見える.各ブロック図の機能についてまとめた.
名前 | 機能 |
---|---|
DataMover | データ転送ロジック.AXI4 Memory Mappedで読んだデータをAXI4-Streamに変換して出力する,あるいはAXI4 Streamで受けたものをAXI4 Memory Mappedでメモリに書き込む |
MM2S(S2MM) Cntl/Sts Logic | 読み出し(書き込み)の制御とステータス情報を保持している |
Registers | AXI DMAの設定に使用されるレジスタ.DMAでどのメモリアドレスから読み出すか,データの長さはどれくらいか,などを決めているはず |
Scatter/Gather | 使っていないのでよくわからない.断片化されたメモリからまとめて読み出す/書き込むといったときに使うみたい |
次に各ブロックから出ている矢印についてまとめた.
名前 | 機能 |
---|---|
AXI4 Memory Map Read | メモリからデータを読み込む信号 |
AXI4 Stream Master(MM2S) | Memory Mapで読み出したデータがStreamに変換され出力される.Memory-Mapped to Streamの略 |
AXI4 Control Steram(MM2S) | Memory Map ReadのステータスをStreamとして出力 |
AXI4-Lite | RegistersにAXI4-Liteで読み書きするためのもの |
AXI4 Memory Map Write/Read | Scatter/Gather(SG)でメモリに読み書きする場合の信号.SGの場合は別途ポートが準備されるみたい |
AXI4 Stream(S2MM) | Memory Map Writeの制御信号をAXI4-Stream形式で与える |
AXI4-Stream Slave (S2MM) | Stream形式のデータをPL中のロジックなどから受ける |
AXI4 Memory Map Write | メモリへデータを書き込む信号.AXI4-Stream Slave (S2MM)からのデータが移される |
SG以外の機能はAXI DataMover IPとほとんど変わらないみたい.State情報やControlなどはDataMoverのリファレンスに詳しく書いてあった*1.
VivadoでIPコアを使うには,IP Catalogを開いてAXI DMAなどと検索するとAXI Direct Memory AccessというIPが見つかるのでそれを使えばよい.今回は下のような設定でIPを使用する.
SGを使うかどうかなどのチェックボックスがある.必要があればチェックを入れる.
- Width of buffer length register
DMAで転送するデータの長さ.ここをn bitにすると2^n - (Data Width Byte)バイトまで転送できる.デフォルトでは14bitになっていて,32bit(=4Byte)で転送する場合は2^14 - 4 = 16380バイトまで転送できる.最大で8 MiB - 8 バイトまで行けるらしい.転送速度とかにはそんなに影響しないらしい.
- Address Width
Memory Map Read/Writeする場合のアドレス長だと思う.
- Memory Map Data Width
AXI4でデータを転送する際のデータバスの幅.64bitにしてZYNQ IPのHPかACPポートに接続するとスループットが良くなって早くなるらしい.
- Stream Data Width
AXI4-Streamで他のIPに接続する場合のデータバスの幅.
- Max Burst Size
バースト転送の最大サイズ.例えば16にすると,16個分のデータを1回のDMAで転送する.
IPコアから出ているポートについてまとめる.
ポート名 | 形式 | 用途 |
---|---|---|
S_AXI_LITE | AXI4_Lite | AXI経由でDMAコアの制御や転送済みバイト数を読み出したりするポート.ブロック図だとRegisterから出てくるAXI4-Liteに相当 |
S_AXIS_S2MM | AXI4-Stream | AXI4-StreamでDMAしたいデータを入力する.AXI4-Stream Slave (S2MM)に相当 |
s_axi_lite_aclk | S_AXI_LITE用のクロック | |
m_axi_mm2s_aclk | M_AXI_MM2S用のクロック | |
m_axi_s2mm_aclk | M_AXI_S2MM用のクロック | |
axi_resetn | IP全体のリセット.LOWでリセットがかかる | |
M_AXI_MM2S | AXI4 | Memory Mapでメモリからデータを読み出す.AXI4 Memory Map Readに相当 |
M_AXI_S2MM | AXI4 | AXI4-Streamで受けたデータをMemory Mapに変換して出力.AXI4 Memory Map Writeに相当 |
M_AXIS_MM2S | AXI4-Stream | M_AXI_MM2Sで読み出したデータをStream形式に変換して出力.AXI4 Stream Master (MM2S)に相当 |
作成したデザイン
AXI DMAを使ったデザインを作ってみた.
Zynqの設定について
AXI DMAを使うためにZynqの設定を行う.DMAのバスはZynqのHigh Performance(HP)ポートに接続して使うため,そのポートを有効にする.
PS-PL ConfigurationのタブからHP Slave AXI Interfaceを開いてS AXI HP0 interfaceにチェックを入れる.HPポートは合計で4個まで使えるようだが,具体的にどう使うのかはわからない*2.
ZynqのHPポートはAXI3バスになっていて,バースト転送の上限が16になっているなど,AXI4とは性能が異なるみたい.でもつなぐ際はバスの方でうまいことやってくれるみたいなので,ユーザー側からは気にしなくても良いんだと思う.
次にDMAに使うクロックを別途用意する.この操作は必ずしも必要でないと思うが,高速なデータ転送を行うにはそれなりに早いクロックを準備したほうがいいはず.
Clock ConfigurationのタブからPL Fabric Clocksを開きFCLK_CLK1に新しくチェックを入れる.また周波数を今回は150 MHzに設定した.以上の設定が終わったらOKで変更を反映させる.
また今回は割り込みを使わない.DMA転送が完了したときに割り込みを発生させるといったこともできるらしいが追々やっていこうと思う.
DMAのパスについて
ZYBO-Z7用に作成した今回のデザインでは,
- DDR3からDMA転送→PLへデータを送る→PL上でLoopback→転送データをDMAでDDR3に転送
のようにした.上記のblock designを見てもらうと,DMA IPコアのM_AXIS_MM2SとS_AXIS_S2MMが繋がっているのがわかると思う.この2つのポートをつなぐことでループバックを実現している.将来的にDMAを使った回路を作成する場合は,M_AXIS_MM2SとS_AXIS_S2MMに作成したIPを繋ぐといった拡張をすれば良い.
AXIバスのアドレス
特に設定してないが,ブロックデザインを作成した段階で数のように設定されていた.
S_AXI_LITEのアドレス(ここでは0x4040_0000+オフセット)に適当な値を書き込むことで,DMAのコントロールを行うことができる.今回使うレジスタを以下に示す.
レジスタ名 | オフセット | 説明 |
---|---|---|
MM2S_DMACR | 0x00 | MM2S DMA制御 |
MM2S_DMASR | 0x04 | MM2S DMAステータス |
MM2S_SA | 0x18 | MM2S DMAで読み出すメモリ初めのアドレスを格納 |
MM2S_LENGTH | 0x28 | MM2Sの転送長さ(バイト) |
S2MM_DMACR | 0x30 | S2MM DMA制御 |
S2MM_DMASR | 0x34 | S2MM DMAステータス |
S2MM_SA | 0x48 | S2MM DMAで書き込むメモリの先頭アドレスを格納 |
S2MM_LENGTH | 0x58 | S2MMの転送長さ(バイト) |
それぞれ32bitのレジスタである.制御レジスタやステータス・レジスタはそれぞれのビットに書き込んだり読み出したりすることでDMAの制御と状態を知ることができる.例としてMM2S_DMACRレジスタの割当を示す.
詳細についてはDMAのリファレンスを参考のこと.
Linux側の設定
Linux側で使用するメモリ領域を制限したり
ZED BoardでPLを自作した場合のDMAのやりかた: なひたふJTAG日記
DMA用のデバイスドライバを準備したり
Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ - Qiita
といった準備が本当は必要らしい.今回は超単純なDMAをやってみるだけなので,/dev/mem
を使ってDMAを行うことにする.
Device Treeの設定
Vivadoで作成したプロジェクトをもとに,デバイスツリーを作成する.この記事を参考のこと.pl.dtsiをもとにOverlay用のデバイスツリーを作成する.
/dts-v1/; /plugin/; / { fragment@0 { target-path = "/amba_pl"; __overlay__ { axi_dma_0: dma@40400000 { #dma-cells = <1>; clock-names = "s_axi_lite_aclk", "m_axi_mm2s_aclk", "m_axi_s2mm_aclk"; clocks = <&clkc 15>, <&clkc 16>, <&clkc 16>; //compatible = "xlnx,axi-dma-7.1", "xlnx,axi-dma-1.00.a"; compatible = "generic-uio"; reg = <0x40400000 0x10000>; xlnx,addrwidth = <0x20>; xlnx,sg-length-width = <0xe>; dma-channel@40400000 { compatible = "xlnx,axi-dma-mm2s-channel"; dma-channels = <0x1>; xlnx,datawidth = <0x20>; xlnx,device-id = <0x0>; }; dma-channel@40400030 { compatible = "xlnx,axi-dma-s2mm-channel"; dma-channels = <0x1>; xlnx,datawidth = <0x20>; xlnx,device-id = <0x0>; }; }; }; }; };
このデバイスツリーを使ってOverlayしておく./proc/device-tree/amba_pl/
にdma@40400000が見えるようになる.dmaノードの下には2つのdma-channelというノードがついている.それぞれDMA転送を行うチャンネルで,compatibleの値を見ればわかるように40400000がメモリ→PLの転送,40400030がPL→メモリの転送を行う.前者のアドレスの値はZynqで設定されたS_AXI_LITEのアドレスと一致している.後者はこのアドレスにS2MMのオフセット0x30を足したものに一致する.DMAの実行では,このアドレスを叩きに行って各種制御などを行う.ZynqのS_AXI_HP0のオフセットは,MM2S_SAで指定したメモリアドレスに対するオフセットという意味だと思う.つまり今の設定ではMM2S_SAで設定したアドレスがDMA転送されるが,オフセットを例えば0x50とかにすると設定したアドレス+0x50から読まれるとかになるんじゃないかな.ちゃんと調べないといけないが.痕多分アドレスは4の倍数とかになってないとダメな気がする.
DMAの実行
ソフトウェアを作って実際にDMAをやってみる.ここにあるCのコードをそのままZynq上に持っていきコンパイルする./dev/mem
に対して読み書きを行うので,実行はroot権限をつけて行う.
aho@zynq$ ls dma_test.c aho@zynq$ gcc dma_test.c aho@zynq$ sudo ./a.out Source memory block: 78563412 44332211 02000000 03000000 04000000 05000000 06000000 07000000 Destination memory block: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 Resetting DMA Stream to memory-mapped status (0x00000001@0x34): halted Memory-mapped to stream status (0x00000001@0x04): halted Halting DMA Stream to memory-mapped status (0x00000001@0x34): halted Memory-mapped to stream status (0x00000001@0x04): halted Writing destination address Stream to memory-mapped status (0x00000001@0x34): halted Writing source address... Memory-mapped to stream status (0x00000001@0x04): halted Starting S2MM channel with all interrupts masked... Stream to memory-mapped status (0x00000000@0x34): running Starting MM2S channel with all interrupts masked... Memory-mapped to stream status (0x00000000@0x04): running Writing S2MM transfer length... Stream to memory-mapped status (0x00000000@0x34): running Writing MM2S transfer length... Memory-mapped to stream status (0x00000000@0x04): running Waiting for MM2S synchronization... Waiting for S2MM sychronization... Stream to memory-mapped status (0x00001002@0x34): running idle IOC_Irq Memory-mapped to stream status (0x00001002@0x04): running idle IOC_Irq Destination memory block: 78563412 44332211 02000000 03000000 04000000 05000000 06000000 07000000
初めSourceのメモリの値が78563412 44332211...,転送先のメモリが00000000 00000000...となっている.DMAを実行すると,まずSourceのメモリがPLに転送される.転送後はLoopbackにより,Destination memoryへとデータが転送される.最後に表示されたDesitinationの中身を見てみると,78563412 44332211...となりもとの値に一致していることがわかる.
今後やってみたいこと
上記デザインでは,DMA転送をする際にPS側から制御レジスタを叩く必要があった.これを改良して,PL側で制御用のAXI4-Streamから制御用の信号を渡して常にDMAが行われているといった回路にしてみたい.PLで何らかの処理をリアルタイムに行い,その結果をリアルタイムにメモリに送っていくといった回路を考えている.あと割り込みも試してみたい.
*1:https://www.xilinx.com/support/documentation/ip_documentation/axi_datamover/v5_1/pg022_axi_datamover.pdf
*2:例えばHP0とHP1から同時にDMAができるのか,とかそのための設定とか