UIOの使い方
ZYNQを使った組み込みLinuxで、PLに実装したAXI経由のIPにアクセスしたい場合、デバイスドライバが必要です。
Cosmo-Zの場合、0xb8800000からの64kバイトと、0x20000000からの512MByteを使用します。以下の2つの領域を使用します。0xb8800000にはAXI経由でコントロールレジスタが実装されていて、0x20000000からは波形バッファのメモリとなっています。
そのような場合、UIOを使うと簡単です。
UIOは普通はカーネルに組み込まれているので、これを使うには、デバイスツリーに以下のような記述をします。
/dts-v1/; / { ・・・ amba { ・・・ cosmoz-reg@b8800000 { compatible = "generic-uio"; reg = <0xb8800000 0x10000>; }; cosmoz-mem@20000000 { compatible = "generic-uio"; reg = <0x20000000 0x20000000>; }; ・・・・ }; ・・・ chosen { bootargs = "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw earlyprintk rootfstype=ext4 rootwait uio_pdrv_genirq.of_id=generic-uio devtmpfs.mount=1 earlycon"; stdout-path = "serial0:115200n8"; }; ・・・
割込みを使わないのであれば、この記述で十分です。
AXIバスにつながったIOを操作するためのuioはambaセクションに書きます。
cosmoz-reg@b8800000 というのはラベルです。自分のドライバの名前と先頭アドレスを書いておきます。compatible = "generic-uio";というのは、UIOドライバを使うことを意味します。reg = <0xb8800000 0x10000>;は、確保したい先頭アドレスとサイズを書いておきます。
こういう記述をコントロールレジスタ用と、メモリ用に2個書きます。
そして、chosenのbootargsに、uio_pdrv_genirq.of_id=generic-uioを書きます。これで、UIOドライバが使えるようになります。
デバイスツリーは、SDカードの先頭パーティションに書かれているので、マウントして、dtcコマンドを使ってテキストに戻します。
$ mount /dev/mmcblk0p1 /mnt $ dtc -I dtb -O dts /mnt/devicetree.dtb -o/mnt/devicetree.dts
そして上記の変更を施したのち、
$ dtc -I dts -O dtb /mnt/devicetree.dts -o/mnt/devicetree.dtb
で、バイナリに戻します。
dtcがお使いにZYNQ Linuxに入っていない場合は、
apt-get install device-tree-compiler
でインストールします。
さて、UIOでメモリマップドIOにアクセスするには、以下のようなプログラムを書きます。
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/mman.h> #include <fcntl.h> #include <unistd.h> #include <stdint.h> #include <sys/ioctl.h> int main() { int fd_regs = open("/dev/uio0",O_RDWR); int fd_mems = open("/dev/uio1",O_RDWR); unsigned long *regs; unsigned long *mems; printf("UIO driver test\n"); if(!fd_regs) { printf("Can not open /dev/uio0\n"); return 1; } regs = (unsigned long *)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd_regs, 0); for(int i=0;i<6;i++) { printf("FPGA reg(%x) data=%08lx\n",i,regs[i]); } regs[0] = 0x123; mems = (unsigned long *)mmap(NULL, 0x20000000, PROT_READ | PROT_WRITE, MAP_SHARED, fd_mems, 0); for(int i=0;i<16;i++) { unsigned long offset = 0x10000 << i; if(offset >= 0x20000000 / 4) break; printf("write offset=%x\n",offset * 4); mems[offset] = i; } int baseaddr = 0x20000000; for(int i=0;i<16;i++) { unsigned long offset = 0x10000 << i; if(offset >= 0x20000000 / 4) break; printf("addr=%08lx, val=%08lx\n",baseaddr + offset, mems[offset]); } munmap(regs,0x1000); munmap(mems,0x20000000); close(fd_regs); close(fd_mems); return 0; }
このプログラムを簡単に説明します。
まず、open()で、/dev/uio0をオープンしたら、mmapでその仮想アドレスを取得します。
mmapの第一引数は通常はNULLを指定します。第二引数は確保したいメモリサイズで、単位はバイトです。第三引数はPROT_READ | PROT_WRITEにして、第四引数はMAP_SHAREDにしておきます。こうしておけば他のプログラムからアクセスした際にも問題が起きず、読み書き可能な空間としてマッピングされます。
第5引数はopenで取得したファイルハンドラを指定します。
第6引数はオフセットですが。ここは0にします。UIOドライバで0以外の値を指定するとこの関数は失敗してしまうようです。
mmapで得られた仮想アドレスはvoid型のポインタなので、適当な型にキャストしてから使います。*regs = 0x123やreg[0]=0x123とすれば、物理アドレスの0xb8800000番地に0x00000123が書き込まれ、AXIバスが動き始めます。
上のプログラムではregsはunsigned long *なので、regs[1]にアクセスすると、0xb8800004番地が読み書きされます。
このようにして、FPGAのコントロールレジスタやメモリ空間にアクセスしたら、(プロセス終了時に自動的に開放されるそうですが)最後にmunmapで開放します。munmapの第二引数は、mmapで確保したサイズです。
デバイスツリーで申告したアドレスが、実際にはPLに何もつながっていなかったりした場合は、読み書きしようとしたときに固まってしまうか、Bus Errorというのが出ます。
このようにUIOを使えば、PLに実装したペリフェラルや物理メモリを自由に使うことができるようになります。
最近のコメント