ADCデータキャプチャのためのLinuxとFPGAの設計
現在Elecrowにて製作中のADC基板ができてきた折に、Z-turnボードと接続してイーサネットにてADCデータを取得するためのファームウェア周りを作成しています。
FPGAについて
Zynqに接続されているDDRメモリにADCデータを保存します。 DMAマスタをRTLで記述するのは割と面倒(とにかくAXIはポートが多いので、どうしても行数は増える)なので、できるだけ楽をしたいと思います。 そういうわけで、Vivado HLS一択です。DMAコントローラのコードは次のように至極単純です。
ちなみに、AXI DMAを使えばいいじゃないか、という意見もありそうです。 しかしながら、AXI DMAは一回の転送で最大8MBしか転送できないという理解不能な制限があるので、今回はわざわざ作成しました。 SGDMAを駆使すればできると思いますが、とりあえずデータを取り込むことが目的なので、そのあたりは別途余裕ができたところで検討したいと思います。
#include "hls_stream.h"
#include "ap_int.h"
#include "axi_dma.h"
void axi_dma(ap_uint<64>* addr, ap_uint<32> offset, ap_uint<32> len, hls::stream<ap_uint<64> > &in){
#pragma HLS INTERFACE s_axilite port=return
#pragma HLS INTERFACE s_axilite port=offset
#pragma HLS INTERFACE s_axilite port=len
#pragma HLS INTERFACE m_axi depth=4096 port=addr
#pragma HLS INTERFACE axis port=in
ap_uint<32> i;
for (i = 0; i < len; i++){
#pragma HLS PIPELINE
addr[offset+i] = in.read();
}
}
ん?Hugoでc++のシンタックスハイライトが効かない…
AXI Liteインタフェースでoffsetアドレス、転送長をレジスタで設定します。また、モジュールの開始終了もAXI Lite経由で操作できるようになっています。
テストベンチは次のようになりました。
#include "hls_stream.h"
#include "ap_int.h"
#include "axi_dma.h"
int main(){
ap_uint<64> buf[4096];
hls::stream<ap_uint<64> > fifo;
for (int i = 0; i < 1024; i++){
fifo.write(i);
}
axi_dma(buf, 16, 1024, fifo); // 16*8=128bytes offset
for (int i = 0; i < 1024; i++){
if (buf[16+i] != i)
return 1;
}
return 0;
}
一応オフセットも動作しているか確認するようになっています。 シミュレーション後、上記のモジュールをaxi_dmaという名前でIPにして、Vivado IP Integratorでインスタンスを置きました。
DMA IPはInterconnectと100MHzでインタフェースし、IPの前にAXI Stream Data FIFOを置いてクロック変換を行います。 ADCのデータは40MHzで入力されるので、FIFOの入力側は40MHzで駆動します。
ちなみに、FPGA実機にて32bitのカウンタを作成し、DMAを行ったときの開始部分をキャプチャした画像が以下になります。 100MHzで駆動しています。
AWVALIDのタイミングで転送先アドレスを設定し、WVALID, WREADYのハンドシェークでデータを転送しています。 WDATAが0,1,2,…とカウントアップしています(32bitカウンタを64bitに入れているので、1転送で0,1が同時に送られている)。 WREADYがLowになる期間がバーストの間にあるため、最高速が出ているわけではありません。 ただ、入力側はもっと遅い(40MHz)ので、とりあえずこれで良しとします。
Linuxについて
次に、ZynqのARMで動作させるLinuxですが、 U-Boot と Linux Kernel のメインラインで Zynq を動かす を参考に、次のようなboot.tclを作成し、XMDコンソールからsource boot.tclすることでu-bootを起動します。 u-boot, KernelはZ-turnボードに付属しているものを使用します。
connect arm hw
rst -slcr
fpga -f design_1_wrapper.bit
source ps7_init.tcl
ps7_init
ps7_post_config
targets 64
dow -data u-boot.bin 0x04000000
con 0x04000000
u-bootが起動したところでキーボードで介入し、TeraTermの次のマクロを使用してLinuxをNFS rootで起動します。 DTBはTFTPにて取得しています。 LinuxのルートファイルシステムはarmhfのUbuntu Trusty 14.04 LTS (no kernel)を取得して展開して、 Z-turn Boardから参照できるようにしています。
timeout=15
sendln 'setenv ipaddr 192.168.0.90'
sendln 'setenv serverip 192.168.0.100'
sendln 'tftpboot 0x2a00000 zynq-zturn.dtb'
wait 'Bytes transferred'
sendln 'tftpboot 0x3000000 uImage'
wait 'Bytes transferred'
sendln 'setenv bootargs mem=512M console=ttyPS0,115200 root=/dev/nfs rw nfsroot=192.168.0.100:/srv,v3,tcp rootwait ip=192.168.0.90:192.168.0.100:192.168.0.100'
sendln 'bootm 0x3000000 - 0x2a00000'
また、Z-turn Boardに添付されているDTBそのものでは、ZylonのIPにレジスタアクセスするところでカーネルの起動が止まってしまっているようでした。 今回のFPGAデザインには当該IPは存在せず、ADCに必要な最小限のものになっています。 そのため、DTSの記述を修正し、当該IP部を無効にして、DTCにてDTBを再度生成しました。
加えて、単に上記のルートファイルシステムを展開しただけだと、ttyPS0が見つからなくてコンソール出力が得られないので、/dev/ttyPS0を
# mknod -m 600 ttyPS0 c 250 0
にて作成し、/etc/init/ttyPS0.confを下記のように作成しました。
start on stopped rc RUNLEVEL=[2345] and (
not-container or
container CONTAINER=lxc or
container CONTAINER=lxc-libvirt)
stop on runlevel [!2345]
respawn
exec /sbin/getty -8 -a root 115200 ttyPS0
Ubuntuの起動後にMakeやgccをインストールして、クロスコンパイルではなく、ターゲット上でアプリケーションを開発します。
Linux上のアプリケーションについて
Helioボード: LinuxとFPGAでSDRAMをシェアするによれば、 bootargsで指定したmem=512Mにて、DDRの上位側512MBはLinuxからは使用しないようになります。 そういうわけで、[0x2000_0000, 0x4000_0000)の範囲がFPGAからDMAを行う転送先アドレスになります。
次に、UIO(User space IO)の割り込みの使い方の例を参考に、 DTSにUIOを追加し、今回作成したDMAモジュールのレジスタアクセスと割り込みをハンドルできるようにします。
また、/dev/memをmmapすることで、DMAで転送したデータをアプリケーションから参照できるようにし、 データをTCPにて開発機で動作するC#アプリケーションに転送できるようにしました。
次は、FPGA上で適当な周期の正弦波を作って、PC上にてFFTして結果が正しいことを確認したいと思います。