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でインスタンスを置きました。

IPI画面

DMA IPはInterconnectと100MHzでインタフェースし、IPの前にAXI Stream Data FIFOを置いてクロック変換を行います。 ADCのデータは40MHzで入力されるので、FIFOの入力側は40MHzで駆動します。

ちなみに、FPGA実機にて32bitのカウンタを作成し、DMAを行ったときの開始部分をキャプチャした画像が以下になります。 100MHzで駆動しています。

AXI DMA

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して結果が正しいことを確認したいと思います。

comments powered by Disqus