LoginSignup
9
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-25

前書き

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

今回はAXIを使って参照電圧をPSからPLに渡し、シリアル信号に変換してDACに伝送する回路を作ります。自作のドーターボードを使用するため、すぐに試すということはできないと思います。所有するボードを使用する際の参考になれば幸いです。
https://qiita.com/tvrcw/items/4d55311e83df6d35c1de
IMG_6184.JPG
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;

//MSBenable信号、[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
時間管理を特にしていないのですが、綺麗な正弦波が出力されました。ただし、ジッタがあるため時々歪みが生じます。

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
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

所望の周波数通りに出力されていました。次に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
メッセージから、FCLK1とFCLK2の周波数は1 MHzに変更されたように思います。
Screenshot from 2018-10-26 14-08-55.png
正常に読み込まれていそうなので、オシロスコープの出力を再度確認しました。
Screenshot from 2018-10-26 14-19-47.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
コメントでご助言と修正のご指摘を頂いた@ikwzm様に感謝です。

9
9
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9