##前書き
ArtixやKintexを使用しているとモジュールのパラメータ調整が面倒だったり、三角関数が使いづらかったりと色々不便な点が多いです。そのためマイコンを横に置いたボードなどを見たことがありますが、ZYBOを使えば全て解決しそうです。Zynqは「XSDKを使えば動作するけど使いづらい」と感じて使用を控えていましたが、@ikwzm様のツールを使うと予想以上に簡単にLinux環境導入とPL部の設定ができたので、メモにまとめました。
今回はAXIを使って参照電圧をPSからPLに渡し、シリアル信号に変換してDACに伝送する回路を作ります。自作のドーターボードを使用するため、すぐに試すということはできないと思います。所有するボードを使用する際の参考になれば幸いです。
https://qiita.com/tvrcw/items/4d55311e83df6d35c1de
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部分の設定に関して以下のように設定を行います。
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
// 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を使用したので以下のような記述になりました。
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にロードする回路の名前を記述すれば良いようです。
/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して出力参照電圧を回路に渡すプログラムを用意しました。
#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波を出力します。本当はリアルタイム性を確保したいのですが、今回は簡単な確認のため適当なコードになっています。以下に出力電圧を載せます。
時間管理を特にしていないのですが、綺麗な正弦波が出力されました。ただし、ジッタがあるため時々歪みが生じます。
UIOを用いた書き込み速度をclock_gettimeを使用して測定したところ、32ビット書き込みに300 ns、sinの呼び出しに 200 ns程度かかっているようでした(clock_gettimeはRDTSC命令を使用したりするらしいのですがARMにはRDTSCがないのでどれくらいオーバーヘッドがあるかわかりません)。
一応Makefileを作りました。@ikwzm様からRakefileが提供されているのですが、自身で使用している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を使って確認します。
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としています。このときの出力は下図のようになりました。
所望の周波数通りに出力されていました。次にfclkcfgをロードしてみました。Device TreeとMakefileを用意してロードしてみました。FCLK1とFCLK2の周波数は1 MHzになるように設定しています。
/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>;
};
};
};
};
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してロードすると、次のようなメッセージが返ってきました。
メッセージから、FCLK1とFCLK2の周波数は1 MHzに変更されたように思います。
正常に読み込まれていそうなので、オシロスコープの出力を再度確認しました。
しかしながら、出力周波数は変更されていませんでした。解決したらまた追記したいと思います(@ikwzm様のご助言により解決しました)。
###修正
Device Treeの記述が誤っていたため、周波数の設定ができていませんでした。
clocksの第一引数に制御するFCLKの番号を渡すのですが、全て<&clkc 15>としていたためFCLK0の周波数のみを変更しようとしていました。修正後のDevice Treeは以下のようになりました。
/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>;
};
};
};
};
変更後に再度ロードしたところ、しっかりと周波数が変更されていました。
コメントでご助言と修正のご指摘を頂いた@ikwzm様に感謝です。