ZYBOを使ってLinuxからC言語のプログラムでDACを動かす

##前書き
ArtixやKintexを使用しているとモジュールのパラメータ調整が面倒だったり、三角関数が使いづらかったりと色々不便な点が多いです。そのためマイコンを横に置いたボードなどを見たことがありますが、ZYBOを使えば全て解決しそうです。Zynqは「XSDKを使えば動作するけど使いづらい」と感じて使用を控えていましたが、@ikwzm様のツールを使うと予想以上に簡単にLinux環境導入とPL部の設定ができたので、メモにまとめました。

今回はAXIを使って参照電圧をPSからPLに渡し、シリアル信号に変換してDACに伝送する回路を作ります。自作のドーターボードを使用するため、すぐに試すということはできないと思います。所有するボードを使用する際の参考になれば幸いです。
https://qiita.com/tvrcw/items/4d55311e83df6d35c1de
![IMG_6184.JPG](https://qiita-image-store.s3.amazonaws.com/0/43995/1da844c5-ac99-fad3-b63c-c32e0c04d42f.jpeg)
PL部のLEDを使えば簡単な検証はすぐに試せると思います。

##手順
### ZYBOでLinuxを走らせる
YoctoやPetaLinuxでも構わないのですが、整備された環境が@ikwzm様より配布されています。aptが使えて非常に便利です。
https://qiita.com/ikwzm/items/7e90f0ca2165dbb9a577
https://qiita.com/ikwzm/items/975ab6997905700dd2e0
デバイスドライバなどが整備されており、Device Tree Overlaysによりユーザ空間からのFPGAのロード、UIOの設定ができるので非常に便利です。また、U-Bootの設定によってPSから出力されるFCLKの設定が可能であるため、DACの出力周期を自由に変えられる点がとても助かります。
https://qiita.com/ikwzm/items/3253940484591da84777
ブート用SDカードができたら、Linuxが立ち上がるか確認します。シリアル通信のボーレートは115200に設定されています。

今回はDACの出力周波数を500kHzとするため、U-BOOTの設定をします。@ikwzm様のU-BOOTはuEnv.txtを読み込んで起動するため、FPGA部分の設定に関して以下のように設定を行います。

```uEnv.txt
fpga_load_cmd=fatload mmc 0 0x03000000 PL.bit && fpga loadb 0 0x03000000 $filesize
slcr_unlock_cmd=mw.l 0xF8000008 0xDF0D 
slcr_lock_cmd=mw.l 0xF8000004 0x767B
fclk0_cfg=mw.l 0xF8000170 0x00100A00
fclk1_cfg=mw.l 0xF8000180 0x01902800

fpga_set_cmd=run slcr_unlock_cmd && run fclk0_cfg && run fclk1_cfg && run slcr_lock_cmd
```
fclk_cfgがfclkの周波数を設定するためのコマンドです。周波数の決め方は以下に記載されています。
https://qiita.com/ikwzm/items/3253940484591da84777
FCLK1の周波数はIOPLL(1GHz)/Divisor1(40)/Divisor(50)=500kHzとなります。

(2018/10/26追記) @ikwzm様よりfclkcfgの使い方を教えて頂きました。
追記の修正部分の操作を行うと、ユーザ空間からFCLKの変更ができました。

fatload mmc 0 0x03000000 PL.bitに関して、VivadoでPSのブロックデザインのみからビットストリームファイルを生成して置いておけば問題ありません。PS部を除いたロジックのみを書いてロードすると、Linuxが[Starting Kernel...]で止まります。面倒であればこのコマンドを消してもいいと思います。

ブートメディアにあるDevice Treeを操作すると、PL回路との関係でLinuxが停止することがあります。このDevice TreeはPSの最小構成を動作させるための記述がされていると思いますが、ビットストリーム生成時に設定していないPS部のピンに関する記述がDevice Treeに存在すると停止することがあるそうです。Device Tree Overlayを使用してロードした回路に対応するデバイス情報を足すとエラーが起こりづらいと思います。

###Vivadoの操作

まずAXI4 PeripheralのIPを作ります。
https://qiita.com/tuttieee/items/cb0c32ebc75e4e89972c
AXIの仕様は以下のコードと生成されたAXIのコードを見れば理解できますが、特に変更する必要はないため飛ばして構いません。
https://dora.bk.tsukuba.ac.jp/~takeuchi/?%E9%9B%BB%E6%B0%97%E5%9B%9E%E8%B7%AF%2FHDL%2FVivado%E3%81%A7AXI%E3%83%90%E3%82%B9%E3%82%92%E5%88%A9%E7%94%A8
コード内に//Users to add ports here、//Add user logic hereという部分があるため、ここにコードを追記します。DAC制御の回路記述は以下をベースにします。
https://qiita.com/tvrcw/items/4d55311e83df6d35c1de

``` AXI_DAC_IP.v
// Users to add ports here
input wire fclk,
output wire nsc,sclk,dout,nclr, // DAC AD5542A

(省略)
// Add user logic here //
wire clk;
assign clk=S_AXI_ACLK;

//MSBにenable信号、[15:0]に指令信号
wire ena;
wire [15:0] dref;
assign ena  = slv_reg0[31];
assign dref = slv_reg0[15:0];

//FCLK1からパルス信号を取り出す
wire cnv_st;
reg[1:0] cnv_st_ps;
reg fclk_z1;
assign cnv_st=(cnv_st_ps==2'b01)? 1'b1:1'b0;
always@(posedge clk) begin
	cnv_st_ps<={fclk_z1,fclk};
	fclk_z1<=fclk;
end 
	
reg 		pclk;	initial pclk=0;
reg [2:0]	pcnt; 	initial pcnt=3'b0;
localparam	pmax=3'b100;
always@(posedge clk)
begin pcnt<=(pcnt==pmax)? 3'b0:pcnt+3'b1; pclk<=(pcnt==pmax)? ~pclk:pclk; end
	
reg [1:0]	state;	initial state=2'b0;
reg [15:0]	stdat;	initial stdat=16'b0;
reg [4:0] 	stcnt;	initial stcnt=5'b0;
reg 		stend;	initial stend=1'b0;
localparam [4:0] stmax=5'b10000;
reg [1:0]	ldcnt; 	initial ldcnt=2'b0;
reg 		ldend;	initial ldend=1'b0;
reg 		ldsig;	initial ldsig=1'b0;
localparam [1:0] ldmax=2'b01;

assign nsc=ldsig;
assign sclk=(state==2'b10)? pclk:1'b0;
assign dout=stdat[15];
assign nclr=ena;

//ステートマシン
always@(posedge clk)　begin
	case(state)
		2'b00: state<=(ena==1'b1&cnv_st==1'b1)? state+2'b1:state;
		2'b01: state<=(pcnt==pmax&pclk==1'b1)? state+2'b1:state;
		2'b10: state<=(stend==1'b1)? state+2'b1:state;
		2'b11: state<=(ldend==1'b1)? state+2'b1:state;
		default: state=2'b00;
	endcase	
end

always@(posedge clk) begin
	if(pcnt==pmax&pclk==1'b1) begin
		if(state==2'b10) begin
			stcnt<=(stcnt==stmax)? stmax:stcnt+5'b1;
			stend<=(stcnt==stmax)? 1'b1:1'b0;
			stdat<=(stcnt==5'b0)? dref:{stdat[14:0],1'b0};
		end
		else begin stcnt<=5'b0; stdat<=16'b0; stend<=1'b0; end

		if(state==2'b11) begin
			ldcnt<=(ldcnt==ldmax)? ldmax:ldcnt+2'b1;
			ldsig<=(ldcnt!=ldmax)? 1'b1:1'b0;
			ldend<=(ldcnt==ldmax)? 1'b1:1'b0;
		end
		else begin ldcnt<=2'b0; ldsig<=1'b0; ldend<=1'b0; end
	end
end
	
```
入出力を追加したので、このIPのラッパーにも入出力ポートの追記をしてください。IPを作成後、ブロックデザインでRun Block Automation, Run Connection AutomationでPSとIPを繋いでください。PSの設定でClock Configuration>PL Fabric ClocksのFCLK_CLK1にチェックを入れておきます。周波数の設定はU-BOOTの初期化時に行うので、適当でいいです。PSのブロックにFCLK_CLK1が追加されるので、作成したブロックのfclkと繋いでください。

繋いだときにIPにアクセスするためのアドレスが設定されます(0x43C0_000とか)。XSDKでもLinuxでも、とりあえずこのアドレスにアクセスして書き込みすればPLに情報が行くといった流れなのですが、XSDKやPetaLinuxで作ったLinuxではデバイスドライバが用意されているため、簡単にアクセスできます。自前で用意したLinuxでもデバイスドライバを作ったりする必要があるのですが、UIOを使ってもアクセスできます。また、@ikwzm様によりOverlaysを用いたUIOの設定が公開されており、任意の回路のロードとアクセスの環境が整備されています。
https://qiita.com/ikwzm/items/ec514e955c16076327ce
アクセス方法に関しては後述します。

作成したIPからDACへの制御信号が出力されるので、ピンアサインします。IPブロックにどこにも繋がっていないnsc,sclk,dout,nclrというポートがあるので、右クリックからcreate portします。これらをConstraintsに書けば動作します。今回はJB2を使用したので以下のような記述になりました。

``` Daughter.xdc
set_property -dict {PACKAGE_PIN T20 IOSTANDARD LVCMOS33} [get_ports nsc]
set_property -dict {PACKAGE_PIN U20 IOSTANDARD LVCMOS33} [get_ports dout]
set_property -dict {PACKAGE_PIN Y18 IOSTANDARD LVCMOS33} [get_ports sclk]
set_property -dict {PACKAGE_PIN Y19 IOSTANDARD LVCMOS33} [get_ports nclr]
```
Generate Bitstreamで回路を作成してください。
このビットストリームですが、そのままではLinuxからロードすることができません。以下を参考に.binを作ります。
https://qiita.com/ikwzm/items/1bb63be0b86a1e0e56fa

### Linuxでの操作

あとは@ikwzm様作成のOverlaysドライバを使用して、FPGA回路のロードと回路にアクセスするためのUIOの設定を行います。FPGAはOverlaysによりDevice Treeを追加するときに行われるようです。このロードには、ビットストリームファイルから生成された.binを"/lib/firmware/"の下に置き、Device Treeにロードする回路の名前を記述すれば良いようです。

``` devicetree.dts
/dts-v1/; /plugin/;
/ {
	fragment@0 {
		target-path = "/amba/fpga-region0";

		__overlay__ {
			#address-cells = <0x1>;
			#size-cells = <0x1>;

			firmware-name = "DAC.bin";

			led-uio@43c10000 {
				compatible = "generic-uio";
				reg = <0x43c00000 0x1000>;
			};

		};
	} ;
} ;
```
回路が正常にロードされると「fpga_manager fpga0: writing DAC.bin to Xilinx Zynq FPGA Manager」といったメッセージが出ます。このDevice Treeを読み込むためには.dtbに変換して@ikwzm様のOverlaysを使用する必要があります。@ikwzm様のOverlaysの使い方は以下に書いてあります。
https://qiita.com/ikwzm/items/ec514e955c16076327ce
上のDevice Treeですが、ロードする回路に合わせてUIOの設定をしています。今回は0x43c00000から0x1000のサイズ分確保してmmapなどでアクセスすることにしています。

ここまでで作成した回路がロードされ、UIOの設定ができている状態なので、あとはプログラムを書いてアクセスするだけです。mmapして出力参照電圧を回路に渡すプログラムを用意しました。

``` DAC.c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int uio_fd;
    if((uio_fd=open("/dev/uio0",O_RDWR))==-1){
        printf("Can not open /dev/uio0\n"); exit(1);
    }

    volatile unsigned int* map_addr;
    map_addr=(unsigned int*)mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, uio_fd, 0);	

	// enable DAC
	const unsigned int doff=pow(2,31);
	// reference voltage
	unsigned int d;
	
	for(int i=0;i<1e6;i++){
		// output range: [-10:10]
		// add mid scale pow(2,15)
		d=4*sin(0.01*i)/(20.0/pow(2,16))+pow(2,15);
		map_addr[0]=doff+d;
	}
    
	map_addr[0]=0;	//disable DAC
	munmap((void*)map_addr, 0x1000);
	close(uio_fd);

	return 0;
}

```
AXIのバスが確保する幅が32なので、unsigned intを使用すると都合が良いです。このプログラムを動作させるとDACがsin波を出力します。本当はリアルタイム性を確保したいのですが、今回は簡単な確認のため適当なコードになっています。以下に出力電圧を載せます。
![cut.png](https://qiita-image-store.s3.amazonaws.com/0/43995/d03ce675-d6c3-5c3e-fd72-a0e11545c315.png)
時間管理を特にしていないのですが、綺麗な正弦波が出力されました。ただし、ジッタがあるため時々歪みが生じます。

UIOを用いた書き込み速度をclock_gettimeを使用して測定したところ、32ビット書き込みに300 ns、sinの呼び出しに 200 ns程度かかっているようでした(clock_gettimeはRDTSC命令を使用したりするらしいのですがARMにはRDTSCがないのでどれくらいオーバーヘッドがあるかわかりません)。

一応Makefileを作りました。@ikwzm様からRakefileが提供されているのですが、自身で使用しているMakefileと統合する必要があったので作成しました。

``` Makefile.
CC	 := gcc
CCFLAGS   := -Wall -W -O3
CCLIBTAR  := -lm -lstdc++
CCOPTIONS := $(CCFLAGS) $(CCLIBTAR)

SRCS := $(shell ls *c)
OBJS := $(SRCS:%.c=%.o)

FPGA_BITSTREAM := $(shell ls *bin)

DTC := dtc -I dts -O dtb -o
DTSOURCE := $(shell ls *dts)
DTBLOB   := $(DTSOURCE:%.dts=%.dtbo)

DAC: DAC.c
	@gcc -o $@ $^ $(CCOPTIONS)

setup: OVERLAYS
	@cp -f $(FPGA_BITSTREAM) /lib/firmware/$(FPGA_BITSTREAM)

OVERLAYSPATH := /config/device-tree/overlays/uio-led

OVERLAYS:$(DTBLOB)
	@if [ -e $(OVERLAYSPATH) ];then rmdir $(OVERLAYSPATH); fi
	@mkdir $(OVERLAYSPATH)
	@cp $(DTBLOB) $(OVERLAYSPATH)/dtbo
	@echo 1 > $(OVERLAYSPATH)/status

$(DTBLOB): $(DTSOURCE)
	@$(DTC) $@ $<

.PHONY: clean
clean:
	@rm -f $(DTBLOB) $(OBJS)
	@if [ -e $(OVERLAYSPATH) ];then echo 0 > $(OVERLAYSPATH)/status; fi
	@if [ -e $(OVERLAYSPATH) ];then rmdir $(OVERLAYSPATH); fi
``` 
makeで実行ファイル生成、make setupして回路をロード、あとは実行するだけです。ディレクトリにDAC.c, devicetree.dts, DAC.binがあれば動作すると思います。このような感じでsin波を使えるようになりました。とても便利で非常に助かりました。

##追記
### fclkcfgを使用してFCLKの周波数を設定する
@ikwzm様よりコメントを頂き、FPGA Clock Configuration Device Driverを使用してみました。
https://qiita.com/ikwzm/items/aae3dab578e28c13bc65

起動時にロードする回路として、I/OピンからFCLKを直接出力する回路を作りました。FCLK1とFCLK2を使って確認します。
![Screenshot from 2018-10-26 13-33-58.png](https://qiita-image-store.s3.amazonaws.com/0/43995/5783655e-c7b5-7a23-54ba-51e3c9b2ac7f.png)
uEnv.txtを次のように変更して起動しました。

``` uEnv.txt
fpga_load_cmd=fatload mmc 0 0x03000000 PL.bit && fpga loadb 0 0x03000000 $filesize
slcr_unlock_cmd=mw.l 0xF8000008 0xDF0D 
slcr_lock_cmd=mw.l 0xF8000004 0x767B
fclk0_cfg=mw.l 0xF8000170 0x00100A00
fclk1_cfg=mw.l 0xF8000180 0x03202800
fclk2_cfg=mw.l 0xF8000190 0x01902800

fpga_set_cmd=run slcr_unlock_cmd && run fclk0_cfg && run fclk1_cfg && run fclk2_cfg && run slcr_lock_cmd
```
FCLK1の周波数を500 kHz、FCLK2の周波数を1 MHzとしています。このときの出力は下図のようになりました。
![Screenshot from 2018-10-26 14-19-20.png](https://qiita-image-store.s3.amazonaws.com/0/43995/8e5075ca-e48d-1825-f819-7742db24eefa.png)

所望の周波数通りに出力されていました。次にfclkcfgをロードしてみました。Device TreeとMakefileを用意してロードしてみました。FCLK1とFCLK2の周波数は1 MHzになるように設定しています。


``` devicetree.dts
/dts-v1/;/plugin/;
/ {
	fragment@0 {
		target-path="/amba";
		__overlay__{
			flck0 {
				compatible    = "ikwzm,fclkcfg-0.10.a";
				device-name   = "fpga-clk0";
				clocks        = <&clkc 15>,<&clkc 2>;
				insert-rate   = "100000000";
				insert-enable = <0>;
				remove-rate   = "100000000";
				remove-enable = <0>;
			};

			flck1 {
				compatible    = "ikwzm,fclkcfg-0.10.a";
				device-name   = "fpga-clk1";
				clocks        = <&clkc 15>,<&clkc 2>;
				insert-rate   = "1000000";
				insert-enable = <1>;
				remove-rate   = "1000000";
				remove-enable = <0>;
			};

			flck2 {
				compatible    = "ikwzm,fclkcfg-0.10.a";
				device-name   = "fpga-clk1";
				clocks        = <&clkc 15>,<&clkc 2>;
				insert-rate   = "1000000";
				insert-enable = <1>;
				remove-rate   = "1000000";
				remove-enable = <0>;
			};
		};
	};
};
```

```Makefile.
DTC := dtc -I dts -O dtb -o
DTSOURCE := $(shell ls *dts)
DTBLOB   := $(DTSOURCE:%.dts=%.dtbo)

OVERLAYSPATH := /config/device-tree/overlays/fclkcfg

OVERLAYS:$(DTBLOB)
	@if [ -e $(OVERLAYSPATH) ];then rmdir $(OVERLAYSPATH); fi
	@mkdir $(OVERLAYSPATH)
	@cp $(DTBLOB) $(OVERLAYSPATH)/dtbo
	@echo 1 > $(OVERLAYSPATH)/status

$(DTBLOB): $(DTSOURCE)
	@$(DTC) $@ $<

.PHONY: clean
clean:
	@rm -f $(DTBLOB) $(OBJS)
	@if [ -e $(OVERLAYSPATH) ];then echo 0 > $(OVERLAYSPATH)/status; fi
	@if [ -e $(OVERLAYSPATH) ];then rmdir $(OVERLAYSPATH); fi

```
makeしてロードすると、次のようなメッセージが返ってきました。
![Screenshot from 2018-10-26 14-05-53.png](https://qiita-image-store.s3.amazonaws.com/0/43995/c9ef5859-c565-d068-dbe3-e29c835bcefd.png)
メッセージから、FCLK1とFCLK2の周波数は1 MHzに変更されたように思います。
![Screenshot from 2018-10-26 14-08-55.png](https://qiita-image-store.s3.amazonaws.com/0/43995/2fecc5d2-e065-ae72-c82a-5b38fd6ae16f.png)
正常に読み込まれていそうなので、オシロスコープの出力を再度確認しました。
![Screenshot from 2018-10-26 14-19-47.png](https://qiita-image-store.s3.amazonaws.com/0/43995/85bd6189-d5ed-d014-1411-c5bc80a8eefc.png)
しかしながら、出力周波数は変更されていませんでした。解決したらまた追記したいと思います(@ikwzm様のご助言により解決しました)。

###修正
Device Treeの記述が誤っていたため、周波数の設定ができていませんでした。
clocksの第一引数に制御するFCLKの番号を渡すのですが、全て<&clkc 15>としていたためFCLK0の周波数のみを変更しようとしていました。修正後のDevice Treeは以下のようになりました。

``` devicetree.dts
/dts-v1/;/plugin/;
/ {
	fragment@0 {
		target-path="/amba";
		__overlay__{
			flck0 {
				compatible    = "ikwzm,fclkcfg-0.10.a";
				device-name   = "fpga-clk0";
				clocks        = <&clkc 15>,<&clkc 2>;
				insert-rate   = "100000000";
				insert-enable = <0>;
				remove-rate   = "100000000";
				remove-enable = <0>;
			};

			flck1 {
				compatible    = "ikwzm,fclkcfg-0.10.a";
				device-name   = "fpga-clk1";
				clocks        = <&clkc 16>,<&clkc 2>;
				insert-rate   = "1000000";
				insert-enable = <1>;
				remove-rate   = "1000000";
				remove-enable = <0>;
			};

			flck2 {
				compatible    = "ikwzm,fclkcfg-0.10.a";
				device-name   = "fpga-clk1";
				clocks        = <&clkc 17>,<&clkc 2>;
				insert-rate   = "1000000";
				insert-enable = <1>;
				remove-rate   = "1000000";
				remove-enable = <0>;
			};
		};
	};
};
```
変更後に再度ロードしたところ、しっかりと周波数が変更されていました。
![Screenshot from 2018-10-26 15-44-55.png](https://qiita-image-store.s3.amazonaws.com/0/43995/72ed4b59-ab9d-ef0f-b282-30d5b39a9d93.png)
コメントでご助言と修正のご指摘を頂いた@ikwzm様に感謝です。