#author("2020-03-09T04:14:54+00:00","default:osana","osana") #author("2022-05-28T01:48:55+00:00","","") [[zFIFO - an AXI DMA driver for Zynq and ZynqMP]] * zFIFO むけの PL design を作る [#y2374b09] ここでは、自分で作った FPGA デザインを PL にのせて、zFIFO 経由で Linux からアクセスできるようにするための基本的な手順を解説します。基本的には、 - PLにAXI DMA (scatter & gather mode) を実装 - ユーザの設計したロジックはこの AXI DMA に AXI Streamで接続 することになります。 ** Vivadoのブロックデザインを用意する [#m2ffa4b5] zFIFO のソースコードと一緒に、Ultra96 (無印およびv2)、ZYBO (無印および Z7-20) 用のブロックデザインを生成するための Tcl スクリプトを配布しています。現在配布されているものは Vivado 2019.2 用です。 Ultra96v2 を使う場合は Avnet の BDF repository (https://github.com/Avnet/bdf) からボード定義をダウンロードし、セットアップしておく必要があります。それ以外の場合は Vivado で Tools->Download Latest Boards を実行して、Xilinx Board Storeからダウンロードしたボード定義ファイルがあればOKです。 スクリプトは examples/fifo/ 以下の、各ボードに対応したフォルダ以下にあります。各フォルダには design_ps.tcl というブロックデザインのファイルがありますが、それではなく u96.tcl や zybo.tcl のような、ボードの名前のついたファイルを使います。 手順は簡単で、 + Vivado で空のプロジェクトを作成 (デバイスの設定とかは適当でもかまいません) + Vivado の Tools -> Run Tcl Script で、上記の Tcl script (たとえば examples/fifo/u96v2/u96v2.tcl) を実行 とするだけです。こうすると、以下のようにAXI DMA を AXI Stream Data FIFO でループバックするだけのブロックデザインを含むプロジェクトが生成されますので、そのままビットストリームを生成すれば DMA loopback の例題として動かすことが可能です。また、AXI Stream Data FIFO を高位合成やRTLで設計した自前のロジックに置き換えれば、自分のアプリケーションを載せることもできます。 #ref(PL Design Primer/ps_design.png,50%); ** Vivado HLSでのコード例 [#d691802d] Vivado HLSでコードを書くときのポイントは、 + 入出力ともAXI Streamなので、hls::streamクラスを使うのが簡単です -- TLAST信号をつけるには正式にはap_intクラスを使う方法がサポートされていますが、これだと型が整数に限られますし、なにかと面倒です。以下の例のようにboolがひとつついた構造体を使うとうまく合成できます + 出力 (FPGA -> Linux) については、Linux 側で長さが事前にわかっていることが必要 の2点です。 1次元の整数配列の総和をもとめるVivado HLS用コードの例を以下に挙げます。 #geshi(c++,number){{ #include "hls_stream.h" struct int_s{ int data; bool last; }; void vec_accum (hls::stream<int_s>& a, hls::stream<int_s>& b){ #pragma HLS INTERFACE axis port=a #pragma HLS INTERFACE axis port=b int_s aa, bb; int sum=0, i=0; do { aa = a.read(); int x = aa.data; sum += x; i ++; } while(aa.last == 0); bb.data=i; bb.last=0; b.write(bb); bb.data=sum; bb.last=1; b.write(bb); } }} これを普通にVivado HLSで合成すると、ブロックレベルのハンドシェイク信号と、a, b ふたつの AXI Stream インタフェイスが生成されます。ブロックレベルハンドシェイク信号は ap_start を常時1になるようにconstantブロックへ接続しておくだけでOKです。AXI Streamインタフェイスは、DMA loopback の例題に含まれている AXI Stream Data FIFO を削除して、その代わりに AXI DMA controller へ接続します。最終的に、以下のようなブロックデザインができます。 #ref(PL Design Primer/bd3.png,40%); Vivadoで生成したビットストリームは、/boot/pl.bin に置くなどすれば、再起動時に読み込まれます。開発中は JTAG 経由で直接 PL を書き換えてしまっても (その時に PS からアクセスしていなければ、ドライバを rmmod しなくても) 大丈夫です。 ** Linux (PS) 側のコード [#gbd857c3] 上記のHLSコードを動かすための、PS側のzfifoなコード (vec-accum-ps.c) は以下のようになります。 #geshi(c++,number){{ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include "zfifo.h" int main(){ int main(){ int fd = open("/dev/zfifo0", O_RDWR | O_SYNC); if (fd<0) { printf("Can't open /dev/zfifo0!\n"); return -1; } unsigned size = 10; int send[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int recv[2] = { 0 }; zf_send(fd, (char*)send, sizeof(int)*size); zf_recv(fd, (char*)recv, sizeof(int)*2); printf("Length: %d, Sum: %d\n", recv[0], recv[1]); close(fd); return 0; } }} 配列に書いておいたデータを zf_send() で送信、zf_recv() で PL からのデータを受信します。このとき、PL から実際に DMA で送られてくるデータ長と zf_recv() に指定するデータ長が一致しないと、バッファがあふれたり、ドライバがデータを待ち続けて zf_recv() が終了しないなどの問題が発生しますので、注意が必要です。 ダウンロード時点で用意されている DMA loopback の例題と同じく、 $ sudo sh load_driver でドライバをロードしたあと、 $ gcc libzfifo.c vec-accum-ps.c -o vec-accum $ ./vec-accum Length: 10, Sum: 55 のようにして実行し、動作確認ができます。 ** 制限事項など [#z5e83ab1] zfifo によるデータ転送には以下のような制約があります。 - AXI DMA で PL から受信するデータ長と zf_recv() の第2引数で指定するバイト数は一致する必要があります -- zf_send()、zf_recv() ともに、タイムアウトはありません -- 現在のところDMAの終了検出は割り込みでなくドライバでのポーリングによって行っており、CPU資源を食います、すみません... - 送受信するデータのメモリアドレスのアライメントは AXI DMA の設定しだいと思われます -- Allow unaligned transfer の設定をしたコアなら、4バイト(8バイト)境界にアラインしていなくても、転送バイト数が4や8の倍数でなくても送受信できるかもしれませんが、試していません -- ふつうに配列として宣言したりmalloc() した領域は4バイトあるいは8バイト境界にアラインしているはずなので、int や long の配列を扱ったりする場合はあまり気にならないはずです