AXI4-LiteスレーブIPの動作テスト の履歴(No.5)
更新概要†
z-turn の PL 領域に自作の AXI4-Lite スレーブ IP を置き、 Linux 上のプログラムから UIO モジュール経由でメモリマップレジスタにアクセスできることを確認する。
自作 IP の作成†
リファレンスプロジェクト mys-xc7z020-trd.xpr を立ち上げて、design_1_i を開く。
[Tools]-[Create and Package New IP...]
[Create a new AXI4 peripheral]
適当な名前 "accesstest" をつけた。
AXI Lite の Slave を作成。メモリサイズは最小の 64 で、実際の記憶領域は4ワード。
[Add IP to the repository]
IP の編集†
[Window]-[IP Catalog] として IP Catalog を開き、
上で作った accesstest_v1.0 上で右クリック後 [Edit in IP Packager]
編集用のプロジェクトをどこに作るか聞かれます。
このとき vivado は編集が終わったら完全に消去するつもりの一時的なプロジェクトを 作成する気まんまんで "C:/z-turn/mys-xc7z020-trd/mys-xc7z020-trd.tmp" などという場所に "accesstest_v1_0_v1_0_project" などという名前のプロジェクトを 作ろうとします。
すでに IP 本体の開発が済んでいて、IP を登録するためだけのプロジェクトであれば そのような一時プロジェクトでかまわないのでしょうけれど、まっさらから IP 開発する場合であればテストコードなどを含めた恒久的なプロジェクトを作りたくなる はずなので、これらを変更して、まともな場所にまともなプロジェクトとして保存します。
このあたり作法はもしかすると本来とは異なるのかもしれないので、 IP 開発に慣れたら再度レビューしたいと思います。
さしあたり以下のようにしました。
すると新しい vivado ウィンドウに C:/z-turn/ip/accesstest/accesstest.xpr というプロジェクトが開きます。
accesstest_v1_0_S00_AXI_inst をダブルクリックして accesstest_v1_0_S00_AXI.v を開きます。
accesstest_v1_0_S00_AXI モジュールは AXI Lite スレーブインターフェースのみ 備えたモジュールで、この AXI Lite スレーブインターフェースは IP のメインモジュール accesstest_v1_0 を通じて IP 外に直接出て行きます。
S00_AXI モジュール内で AXI バスからの指示に応答し、IP の動作を変更したり、 IP の状態を返したりすれば AXI バス経由でアクセス可能な IP ができあがります。
上記の操作で自動的に作成された S00_AXI モジュール内にはダミーのレジスタが 4つ作成されており(slv_reg[0-4])、すでに AXI4-Lite バスから読み書きが可能になっています。
ですのでこのままビルドすれば読み書きのテストができるはずなのですが、 そのままだと書いて、読むだけ、なので本当にうまくいっているか確信が持ちにくい。
そこで、
LANG:verilog always @(*) begin // Address decoding for reading registers case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0 : reg_data_out <= slv_reg0; 2'h1 : reg_data_out <= slv_reg1; 2'h2 : reg_data_out <= slv_reg2; 2'h3 : reg_data_out <= slv_reg3; default : reg_data_out <= 0; endcase end
となっている部分を意地悪く、
LANG:verilog always @(*) begin // Address decoding for reading registers case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0 : reg_data_out <= slv_reg1; 2'h1 : reg_data_out <= slv_reg2; 2'h2 : reg_data_out <= slv_reg3; 2'h3 : reg_data_out <= slv_reg0; default : reg_data_out <= 0; endcase end
として、読み出し時に1番地だけずれたデータを読み出すようにしておきます。
読み出しアドレス | 実際に読まれるアドレス |
0 | 1 |
1 | 2 |
2 | 3 |
3 | 0 |
IP のリパッケージング†
verilog ファイルを編集し、保存してから Package IP のページに戻ると、 [File Groups] の左側のアイコンが緑のチェックマークから編集済みのマークに変化しています。
この状態で右側の [Merge Change] を押すと、編集マークが消えます。
そして [Re-Package IP] を押すと、更新された IP がインストールされるのですが、、、
その前に [Tools]-[Project Settings...] から、
[Delete project after packaging] をクリアしておかないと、 パッケージングが済み次第、プロジェクトが跡形もなく消え去ります。
上記チェックを外してから、おもむろに [Re-Package IP] を押せば
と出て完了です。
再度プロジェクトを開いたとき†
Project Manager の下に [Package IP] ボタンがあるので、これを押すと Package IP パネルが現れます。
作った IP を PS につなぐ†
mys-xc7z020-trd.xpr プロジェクトに戻り、 design_1 の Diagram 上で右クリックから [Add IP...]
Search に "access" まで入力して、accesstest_v1.0 を選択後、ENTER
そのまま [Run Connection Automation] します。
clock についてのオプションも auto で OK
つながりました。
[Regenerate Layout] して整えます。
アドレス設定を確認†
[Address Editor] を開いて、accesstest_0 の Offset Address を確認します。
bitstream の生成†
[IP Integrator] の [Generate Block Design] をしてから、
[Program and Debug] の [Generate Bitstream]
# Generate Bitstream だけで Generate Block Design もしてくれるべきだと
# 思うのだけれど、どうして自動化されないんだろう???
生成されたスレーブ IP の動作†
電気回路/HDL/VivadoでAXIバスを利用#h589575e にて勉強しました。
Linux の Device Tree に IP を登録する†
https://formalism.github.io/blog/posts/2014/05/zynqpllinux-dts/
を参考に UIO デバイスを使います
実は割り込みを使用しないデバイスを登録する方法がよくわかりません???
割り込みを使わないモジュールでも、割り込み番号を指定してやらないと
uio_pdrv_genirq 43c00000.accesstest: failed to get IRQ uio_pdrv_genirq: probe of 43c00000.accesstest failed with error -22
などとして怒られてしまいます。
http://fpga.org/2013/05/28/how-to-design-and-access-a-memory-mapped-device-part-two/
によれば、ダミーの番号を指定しておけばいいそうです。
z-turn ボード付属の devicetree.dtb に、
LANG:dts amba { compatible = "simple-bus"; #address-cells = <0x1>; #size-cells = <0x1>; interrupt-parent = <0x3>; ranges; + accesstest@43c00000 { + compatible = "generic-uio", "uio", "uio_pdrv"; + reg = <0x43c00000 0x1000>; + interrupts = < 0 57 0 >; + interrupt-parent = <0x3>; + };
を追加したところ、/dev/uio0 というデバイスができました。
自分で xilinx linux をコンパイルした新しい Linux Kernel と dtb だと ちょっとうまくいかなくて悩んでいます。
以下は VirtualBox にインストールした Debian8 のコンソール
~/linux-zynq-stable 以下にカーネルソースがあるものとします。
LANG:console $ mkdir ~/devicetree $ cd ~/devicetree $ cp ../linux-zynq-stable/arch/arm/boot/dts/zynq-zturn-myir.dts . $ cp ../linux-zynq-stable/arch/arm/boot/dts/zynq-7000.dtsi . $ cp ../linux-zynq-stable/arch/arm/boot/dts/skeleton.dtsi . $ dtc -O dtb -o zynq-zturn-myir.dtb zynq-zturn-myir.dts $ ls -l zynq-zturn-myir.dtb -rw-r--r-- 1 osamu osamu 8878 2月 7 23:47 zynq-zturn-myir.dtb $ cp zynq-zturn-myir.dts accesstest.dts
ソフトウェアから使う†
https://formalism.github.io/blog/posts/2014/05/zynqpllinux/
を参考に、
LANG:c #include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <errno.h> #define LOG(CMD) CMD; printf("%s\n", #CMD); #define LOG2(CMD) printf("%s => 0x%08x\n", #CMD, CMD); int main() { int uiofd; uiofd = open("/dev/uio0", O_RDWR); if (uiofd < 0) { perror("uio open:"); return errno; } unsigned int* map_addr = (unsigned int*)mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, uiofd, 0); if (!map_addr){ fprintf(stderr, "mmap failed\n"); return errno; } LOG(map_addr[0] = 0); LOG(map_addr[1] = 1); LOG(map_addr[2] = 2); LOG(map_addr[3] = 3); LOG2(map_addr[0]); LOG2(map_addr[1]); LOG2(map_addr[2]); LOG2(map_addr[3]); LOG(((unsigned char*)map_addr)[0] = 1); LOG2(map_addr[3]); LOG(((unsigned char*)map_addr)[1] = 2); LOG2(map_addr[3]); LOG(((unsigned char*)map_addr)[2] = 3); LOG2(map_addr[3]); LOG(((unsigned char*)map_addr)[3] = 4); LOG2(map_addr[3]); LOG(map_addr[0] = 0); LOG2(map_addr[3]); LOG(((unsigned char*)map_addr)[3] = 4); LOG2(map_addr[3]); LOG(((unsigned char*)map_addr)[2] = 3); LOG2(map_addr[3]); LOG(((unsigned char*)map_addr)[1] = 2); LOG2(map_addr[3]); LOG(((unsigned char*)map_addr)[0] = 1); LOG2(map_addr[3]); LOG(map_addr[0] = 0); LOG2(map_addr[3]); LOG(((unsigned short int*)map_addr)[0] = 0x1234); LOG2(map_addr[3]); LOG(((unsigned short int*)map_addr)[1] = 0x5678); LOG2(map_addr[3]); LOG(map_addr[0] = 0); LOG2(map_addr[3]); LOG(((unsigned short int*)map_addr)[1] = 0x1234); LOG2(map_addr[3]); LOG(((unsigned short int*)map_addr)[0] = 0x5678); LOG2(map_addr[3]); munmap(map_addr, 4096); close(uiofd); return errno; }
として、
LANG:console $ gcc accesstest.c -o accesstest $ ./accesstest uio open:: Permission denied $ sudo ./accesstest map_addr[0] = 0 map_addr[1] = 1 map_addr[2] = 2 map_addr[3] = 3 map_addr[0] => 0x00000001 map_addr[1] => 0x00000002 map_addr[2] => 0x00000003 map_addr[3] => 0x00000000 ((unsigned char*)map_addr)[0] = 1 map_addr[3] => 0x00000001 ((unsigned char*)map_addr)[1] = 2 map_addr[3] => 0x00000201 ((unsigned char*)map_addr)[2] = 3 map_addr[3] => 0x00030201 ((unsigned char*)map_addr)[3] = 4 map_addr[3] => 0x04030201 map_addr[0] = 0 map_addr[3] => 0x00000000 ((unsigned char*)map_addr)[3] = 4 map_addr[3] => 0x04000000 ((unsigned char*)map_addr)[2] = 3 map_addr[3] => 0x04030000 ((unsigned char*)map_addr)[1] = 2 map_addr[3] => 0x04030200 ((unsigned char*)map_addr)[0] = 1 map_addr[3] => 0x04030201 map_addr[0] = 0 map_addr[3] => 0x00000000 ((unsigned short int*)map_addr)[0] = 0x1234 map_addr[3] => 0x00001234 ((unsigned short int*)map_addr)[1] = 0x5678 map_addr[3] => 0x56781234 map_addr[0] = 0 map_addr[3] => 0x00000000 ((unsigned short int*)map_addr)[1] = 0x1234 map_addr[3] => 0x12340000 ((unsigned short int*)map_addr)[0] = 0x5678 map_addr[3] => 0x12345678
ちゃんとワード単位、バイト単位、ハーフワード単位で読み書きできることを確認できた。
uio ドライバを使えばベアメタルと同じ感覚でレジスタにアクセスできて 何だか怖いけど便利。
LANG:console $ sudo chown root:root accesstest $ sudo chmod 4755 accesstest $ ls -l accesstest -rwsr-xr-x 1 root root 8108 Feb 8 06:41 accesstest $ ./accesstest map_addr[0] = 0 map_addr[1] = 1 map_addr[2] = 2 map_addr[3] = 3 ...
のように setuid しておけば sudo しなくても実行できるようにできる。 (もっと怖い?)