HDL/VivadoでAXIバスを利用 の履歴(No.40)
更新- 履歴一覧
- 差分 を表示
- 現在との差分 を表示
- ソース を表示
- 電気回路/HDL/VivadoでAXIバスを利用 へ行く。
AXI バス†
Xilinx の資料によれば、
AXI は、AMBA (Advanced Microcontroller Bus Architecture) 4 仕様 に基づいて標準化 された IP インターフェイスプロトコルです。
とのことで、例えば Zynq に内蔵された ARM プロセッサと、ユーザーロジックと、の間などが AXI バスで繋がれています。すなわち、何か IP を自作したならば、AXI バスに繋げられるようにしなければ その IP を CPU から利用することができません。
でも逆に、一旦 IP を AXI バス互換にしてしまえば Vivado 上の GUI を用いて IP 同士を容易に接続できるなど、利点も多いようです。
ということで、AXI バスを一通り使えるようになるよういろいろ調べました。
自作 IP を AXI-4 Lite バスに繋ぐための汎用コードは こちらです。
AXI バスの仕様†
- "AXI4 Overview" by Xilinx
http://www.em.avnet.com/en-us/design/trainingandevents/Documents/X-Tech%202012%20Presentations/XTECH_B_AXI4_Technical_Seminar.pdf
AXI バスには3つの規格があり、それぞれ AXI(-Full), AXI-Lite, AXI-Stream と呼ばれます。
どの規格も、マスターとスレーブとを繋ぐ1対1のバスになっていますので、 複数の機器を繋ぐときには Interconnect の IP を間に挟むことになります。
- AXI(-Full): マスターからスレーブのメモリやレジスタを読み書きするためのフルスペックプロトコル
- AXI-Lite: マスターからスレーブのレジスタを読み書きするための低速だが軽量なプロトコル
- AXI-Stream: 単純にマスターからスレーブへデータを受け渡す高速かつ軽量なプロトコル
細かい仕様は AMBA で定められています。
- AXI4-Full, -Lite 仕様 : http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ihi0022e/index.html
- AXI4-Stream 仕様 : http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ihi0051a/index.html
- AXI4-Lite 解説 : http://silica.com/wps/wcm/connect/71b10b18-9c9c-44c6-b62d-9a031b8f3df8/SILICA_Xilinx_Designing_a_custom_axi_slave_rev1.pdf?MOD=AJPERES
基礎となるプロトコル†
AXI ではデータ転送要求をするのは常にマスターですが、 実際のデータはマスターからスレーブへ送られることも、 スレーブからマスターへも送られることもあります。
AXI プロトコルにおけるデータ転送の基本となるのは、 DATA ライン、VALID ライン、READY ラインを用いた以下のようなプロトコルになります。
- DATA はデータを提示するための信号線です
- VALID は送信側が DATA に有効なデータを提示していることを示す信号線です
- READY は受信側が DATA を受け取れることを示す信号線です
- VALID と READY が同時に立った時点で送受信が成立したことになります
受け取りに時間の掛かる IP であれば、次のように valid が立ったのを見て ready を立てるのも良いですし、
&tchart( clock _~_~_~_~_~_~_~_~_~_~_ data =?====*=DATA========*=?==== valid ~~~~~~~~~~_ ready ________[~~]_ );
ready をフロー制御のように使いたければ、 次のように受け取り可能な間 ready を上げっぱなしにしても構いません。
&tchart( clock _~_~_~_~_~_~_~_~_~_~_ data =?============*D=*=?==== valid ________[~~]_ ready ~~~~~~~~~~_ );
とにかく、valid と ready が同時に立った時(上の図で黄色くハイライトされたクロック) に送受信が成立する、という規則が重要です。
注意点として、Valid が上がるのを見てから Ready を上げるのは問題ないですが、 Ready が上がるのを待って Valid を上げようとしてはいけません。 両者で待ち合いになってデッドロックが発生しかねません。
AXI4-Lite Slave と AXI4-Stream Master/Slave†
CPU から IP のレジスタを設定・読取するための AXI4-Lite と、
IP と CPU との間でデータを転送するための AXI4-Stream とを使いこなしたいです。
あと、AXI4-Stream は最後に DMA を使って SDRAM を読み書きすることになるので、 DMA も使いこなしたい。
ちょっとかじったところ、AXI4-Lite と AXI4-Stream は、上記の基本プロトコルさえ知っていれば動作の理解は難しくないようです。
AXI4-Lite における読み取り動作†
AXI4-Lite ではバースト転送機能がないため、アドレスを送るとデータが帰ってくる、という簡単なプロトコルになっています。
- マスターからアドレスを送ります
- araddr[?:0], arprot[2:0], arvalid, arready を使います
- スレーブが成否のステータスと共にデータを送ります
- rdata[?:0], rresp[1:0], rvalid, rready を使います
- rresp[1:0] は上位1ビットがゼロなら成功、1なら失敗になります
- 00 : OKAY
- 01 : Exclusive Access OK
- 10 : Slave Error
- 11 : Decode Error
arprot はアクセス許可情報を表すための信号です。 特にアクセス制限の必要がなければ 3'b000 で良いようです。
例:
&tchart(
aclk ~_~_~_~_~_~_~_
araddr =?===X=0x3000==X=?====
arprot =?===X=000==X=?====
arvalid _~~~~___
arready _[~~]_
);
&tchart(
rdata =?=====X=0xbeef==X==?=
rresp ====?==X00===X=?==
rvalid ___~~~~_
rready _____[~~]_
);
AXI4-Lite における書き込み動作†
こちらもバースト転送はないので、 マスターからアドレスとデータをそれぞれ別々のチャンネルから送ると、 スレーブから成否を返すという単純なプロトコルです。
- マスターからアドレスを送ります
- awaddr[?:0], awprot[2:0], awvalid, awready を使います
- マスターからデータとストローブ信号を送ります
- wdata[?:0], wstrb[?:0], wvalid, wready を使います
- wstrb は wdata のうち実際に書き込む部位をバイト単位で指定します
- アドレスと同時に送られることもあります
→ むしろデータが先に来る場合もあるそうです*12017-11-01 ZYNQ勉強会で教えていただきました
- スレーブから結果の成否を送ります
- bresp[1:0], bvalid, bready を使います
- bresp[1:0] は上位1ビットがゼロなら成功、1なら失敗になります
- 00 : OKAY
- 01 : Exclusive Access OK
- 10 : Slave Error
- 11 : Decode Error
アドレスとデータの転送は平行して行えます。データを送るのにアドレスの転送終了を待つ必要はありません。
awprot はアクセス許可情報を表すための信号です。 特にアクセス制限の必要がなければ 3'b000 で良いようです。
例:
&tchart(
aclk ~_~_~_~_~_~_~_
awaddr =?===X=0x3000==X=?====
awprot =?===X=000==X=?====
awvalid _~~~~___
awready _[~~]_
);
&tchart(
wdata =?===X=0xbeef==X==?===
tstrb =?===X=1111==X==?===
wvalid _~~~~___
wready _[~~]_
);
&tchart(
bresp ====?==X00=X=?====
bvalid _[~~]_
bready _~~~~___
);
AXI4-Stream によるデータ転送†
アドレス指定も何もなく、単にマスターからスレーブへデータだけが送られます。 データをパケットに分けるための区切り情報も表すことができます。
- マスターからスレーブへストローブ信号、データ区切り信号と共にデータを送ります。
- tdata[?:0], tstrb[?:0], tlast, tvalid, tready を使います
- tlast はひとまとまりのデータの区切りを表します
例:
&tchart( aclk ~_~_~_~_~_~_~_ tdata =?=X===D1X=?==X=D2X=? tlast ________~~ tvalid ~~~~[~~] tready __[~~]~~~~__ );
配線遅延などの制約が厳しくない限り、
FIFO の出口を AXI4-Stream マスターとして動作させるのであれば、例えば
LANG:verilog assign axis_tvalid = !fifo_empty; assign fifo_re = axis_tvalid & axis_tready; assign axis_tdata = fifo_o;
とすればよく、FIFO の入り口を AXI4-Stream スレーブとして動作させるのであれば、例えば
LANG: verilog assign axis_tready = !fifo_full; assign fifo_we = axis_tready & axis_tvalid; assign fifo_i = axis_tdata;
とすれば良いことになります。
配線やロジックの遅延が無視できないときは、 1クロックの遅延を入れるなどの工夫が必要になるかもしれません。
tuser などのオプショナルな線もあるので、 tlast で区切られるパケットを束ねて1まとまりにする際に 1まとまりの始めに立てるなどの使い方ができます。
ビデオ画像の送信では、
- 画像の頭に tuser を立てる
- 1ラインの終わりに tlast を立てる
というのが普通だそうです。*22017-11-01 ZYNQ勉強会で教えていただきました
Vivado の IP ひな形生成機構を利用する†
AXI バスに繋げられる IP を作るには IP 動作をバス仕様に合わせるだけでなく、 その IP を Vivado で扱えるようにするための作法に合わせる必要もあります。
すべてを一から作るのはいろんな仕様をすべて頭に入れなければならず大変なので、 手っ取り早く使うためには Vivado を使って IP のひな形を作成するのが良いようです。
以下、その手順を追ってみます。
プロジェクトを作成†
IP を利用する側となるメインプロジェクトを作成します。
[Create New Project] を選んで、
testing_axi4_lite という名前にしました。
RTL Project として、
[Create File] から "main.sv" を追加します。
[Add Existing IP]、[Add Constraint] は飛ばして、
Zynq 7020 を選択しました。
[Finish] でプロジェクトが生成されます。
ここではモジュール main にはポートを定義しません。
AXI4 Lite スレーブとなる IP のひな形を作成†
[Tools]-[Create and Package IP...] を選択
AXI4 ペリフェラル専用の Wizard があると書かれています。
そちらを選択して Next
"test_lite_slave" という名前にしました。
設定・読み出し可能なレジスタを4つ持つ AXI4 Lite Slave IP を作成します。
IP のひな形の他、IP を ARM から使うためのドライバのひな形まで生成してくれます。
AXI4 BFM Simulation はライセンスを別途購入しないと使えません。
[Edit IP] とすることで IP 編集用のプロジェクトが別途作成されます。 後々使うことになるのでぜひ作成しておくと良い・・・のですが・・・
これを選ぶと Vivado は ip_repo の直下にやたらとたくさんフォルダを 作ってしまうので、あとで非常にごちゃごちゃして嫌な感じです。 保存場所を選択できるようになっていれば良かったのに、 と思いました。
むしろここでは Edit IP をせず、後から IP Catalog 上で右クリックから Edit in IP Packager として、自分の好きな場所にプロジェクトを作成するのが 良いかもしれません。ただその場合には、Project Settings の [IP]-[Packager] で、[Delete project after package] を外しておかないと、 作成したプロジェクトは自動的に削除されてしまいますので注意して下さい。 (バージョン管理などを考えるとこういう一時プロジェクトで作業する人は多くないのではと 思うのですが、どうしてデフォルトの動作がこういうことになっているのか・・・)
上記どちらかの要領で作成したような IP パッケージング用のプロジェクトでは、 [Flow Navigator] の [Project Manager] から [Package IP] を選択することで 右側にいろいろな設定項目が現れます。
IP ソースを変更したり、これらの設定を変更したら、 最後に [Review and Package] を押すと、IP が更新されます。
AXI4 Lite マスターとなる IP のひな形を作成する†
上と同様にして、"test_lite_master" という IP を作成しました。
[Flow Navigator]-[Project Manager] で [IP Catalog] を選ぶと、
2つの IP が追加されていることを確認できます。
自動生成される IP の中身†
AXI4 Lite Slave†
4つのレジスタ slv_reg1, slv_reg2, slv_reg3, slv_reg4 を含んでいて、 AXI4 Lite インタフェースを用いて内容を読み書きできます。
これらのレジスタの値を既存の IP で読み取ったり、 レジスタの値の代わりに既存 IP からの出力を書き出すようにしたりすれば、 既存 IP を AXI4 Lite バスに接続することができます。
AXI4 Lite Master†
AXI4 バス経由で、Slave のレジスタに対して順に値を書き込むコードが生成されています。
GUI を使って配置する†
[Flow Navigator]-[IP Integrator]-[Create Block Design] から、
design_1 という Design を作成します。
[Add IP] から2つの IP を選んで Enter を押すと、
並びが逆な感じですが、2つの IP が配置されました。
S_AXI から M_AXI までマウスカーソルをドラッグすると、 2つのポートの間を配線できます。
Ctrl+K で aclk, arstn, init を入力ポートとして、 done を出力ポートとして作成し、それぞれ適切に配線します。
ごちゃごちゃしているので、 [Regenerate Layout] したところ、
見やすく配置されました。
"Address block is not mapped" というエラー†
ところが、これで [Validate Design (F6)] したところ、
CRITICAL WARNING: [BD 41-1356] Address block </test_lite_slave_0/S_AXI/S_AXI_reg> is not mapped into </test_lite_master_0/M_AXI>. Please use Address Editor to either map or exclude it.
というエラーが出てしまいました。
言われたとおり、Address Editor でアドレスを割り当てます。
右クリックから [Assign Address] を呼び、Range を最小の 4k にしました。
Diagram に戻り、 F6 したところ、
うまくいきました。
Verilog ソースとの統合†
[Flow navigator]-[IP Integrator]-[Generate Block Design] から、
[Out of context per Block Design] を選ぶと、
design_1.v が生成されます。
中身は、
design_1.v
LANGUAGE:verilog module design_1 (aclk, arstn, done, init); input aclk; input arstn; output done; input init; ...
のように、Ctrl+K で作成した aclk, arstn, init, done などのポートが見えています。
これを main.v でインスタンス化することで利用できます。
シミュレーションしてみる†
[Flow Navigator]-[Project Manager]-[Add Sources] から
[Add or create simulation sources] を選んで、
"design_1_test.sv" を作成します。
中身を次のようにして、クロック、リセット、開始トリガを供給しました。
design_1_test.sv
LANGUAGE:verilog `timescale 1ns / 1ps module design_1_test(); reg aclk = 0; reg arstn = 0; wire done; reg init = 0; design_1 uut (.*); always #10 aclk <= !aclk; initial begin repeat(10) @(posedge aclk); arstn <= 1; repeat(10) @(posedge aclk); init <= 1; @(posedge aclk); init <= 0; @(posedge done); @(posedge aclk); $stop; end endmodule
このファイルをトップレベルに指定して、
[Run Simulation] すると、
正しくシミュレーションできていることが分かります。
連続する書き込みに6クロックかかっているようです。
シミュレーション用の Master を作成する†
シミュレーション時に AXI4-Lite Slave となる IP と容易にデータをやりとりするために、 合成不可能な、シミュレーション専用の Master を作成しました。
始め、ひな形として作成した Master をごちょごちょっといじって 作ろうかとも思ったのですが、やはりこういったソフトウェア的なものは ソフトウェア的な書き方をした方がずっとすっきりして間違いも少ないので、 努めてソフトウェア的な書き方で行こうと思います。
まず、上記と同様の手順で AXI4_Lite_Master_BFM を作成しました。
AXI4_Lite_Master_BFM_v1_0.v の中身をごっそり書き換えて、 task のみを含む形にしてしまいます。
LANG:verilog `timescale 1 ns / 1 ps module AXI4_Lite_Master_BFM_v1_0 #( parameter integer C_M_AXI_ADDR_WIDTH = 32, parameter integer C_M_AXI_DATA_WIDTH = 32 ) ( output reg error = 0, input wire m_axi_aclk, input wire m_axi_aresetn, output reg [C_M_AXI_ADDR_WIDTH-1 : 0] m_axi_awaddr, output wire [2 : 0] m_axi_awprot, output reg m_axi_awvalid = 0, input wire m_axi_awready, output reg [C_M_AXI_DATA_WIDTH-1 : 0] m_axi_wdata, output wire [C_M_AXI_DATA_WIDTH/8-1 : 0] m_axi_wstrb, output reg m_axi_wvalid = 0, input wire m_axi_wready, input wire [1 : 0] m_axi_bresp, input wire m_axi_bvalid, output reg m_axi_bready = 0, output reg [C_M_AXI_ADDR_WIDTH-1 : 0] m_axi_araddr, output wire [2 : 0] m_axi_arprot, output reg m_axi_arvalid = 0, input wire m_axi_arready, input wire [C_M_AXI_DATA_WIDTH-1 : 0] m_axi_rdata, input wire [1 : 0] m_axi_rresp, input wire m_axi_rvalid, output reg m_axi_rready = 0 ); assign m_axi_awprot = 3'b000; assign m_axi_wstrb = 4'b1111; assign m_axi_arprot = 3'b000; task write( input [C_M_AXI_ADDR_WIDTH-1:0] addr, input [C_M_AXI_DATA_WIDTH-1:0] data ); begin m_axi_awvalid = 0; m_axi_wvalid = 0; m_axi_bready = 0; @(posedge m_axi_aclk) #1 fork begin // アドレスを出力し awvalid を立てて awready を待つ m_axi_awaddr = addr; m_axi_awvalid = 1; while(!m_axi_awready) @(posedge m_axi_aclk) #1; @(posedge m_axi_aclk) #1; m_axi_awvalid = 0; end begin // データを出力し wvalid を立てて wready を待つ m_axi_wdata = data; m_axi_wvalid = 1; while(!m_axi_wready) @(posedge m_axi_aclk) #1; @(posedge m_axi_aclk) #1; m_axi_wvalid = 0; end begin // bvalid が立ったら bready を返しエラーを読む while(!m_axi_bvalid) @(posedge m_axi_aclk) #1; @(posedge m_axi_aclk) #1; m_axi_bready = 1; error = m_axi_bresp[1]; @(posedge m_axi_aclk) #1; m_axi_bready = 0; @(posedge m_axi_aclk) #1; end join end endtask task read( input [C_M_AXI_ADDR_WIDTH-1:0] addr, output [C_M_AXI_DATA_WIDTH-1:0] data ); begin m_axi_arvalid = 0; m_axi_rready = 0; @(posedge m_axi_aclk) #1 fork begin // アドレスを出力し arvalid を立てて arready を待つ m_axi_araddr = addr; m_axi_arvalid = 1; while(!m_axi_arready) @(posedge m_axi_aclk) #1; @(posedge m_axi_aclk) #1; m_axi_arvalid = 0; end begin // rvalid が立ったら rready を返しエラーとデータを読む while(!m_axi_rvalid) @(posedge m_axi_aclk) #1; @(posedge m_axi_aclk) #1; m_axi_rready = 1; error = m_axi_rresp[1]; if(!error) data = m_axi_rdata; @(posedge m_axi_aclk) #1; m_axi_rready = 0; @(posedge m_axi_aclk) #1; end join end endtask task verify( input [C_M_AXI_ADDR_WIDTH-1:0] addr, input [C_M_AXI_DATA_WIDTH-1:0] expected ); reg [C_M_AXI_DATA_WIDTH-1:0] data; reg read_error; begin read(addr, data); error = data !== expected; if(error) $display("ERROR"); end endtask endmodule
これを test_lite_slave に繋ぐと、
write や verify のタスクを使って次のようなテストベンチを書くことができます。
LANG:verilog `timescale 1ns / 1ps module design_2_test(); reg aclk = 0; reg arstn = 0; design_2 uut (.*); // detect error always @(posedge uut.AXI4_Lite_Master_BFM_0.inst.error) $display("AXI4 error @ %t", $time); // generate clock always #5 aclk <= !aclk; initial begin repeat(10) @(posedge aclk); arstn <= 1; repeat(10) @(posedge aclk); uut.AXI4_Lite_Master_BFM_0.inst.write(0, 'h1234); uut.AXI4_Lite_Master_BFM_0.inst.verify(0, 'h1234); uut.AXI4_Lite_Master_BFM_0.inst.verify(0, 'h1235); // generates error uut.AXI4_Lite_Master_BFM_0.inst.write(0, 'h5678); uut.AXI4_Lite_Master_BFM_0.inst.verify(0, 'h5678); uut.AXI4_Lite_Master_BFM_0.inst.write(0, 'h0001); uut.AXI4_Lite_Master_BFM_0.inst.write(4, 'h0002); uut.AXI4_Lite_Master_BFM_0.inst.write(8, 'h0003); uut.AXI4_Lite_Master_BFM_0.inst.write(12, 'h0004); repeat(10) @(posedge aclk); $stop; end endmodule
実行結果は次のようになりました。
1回の書き込みに6クロック掛かっており、これは Vivado で自動生成した AXI4 Lite Master IP と同じ動作になっています。
vivado が自動で作る AXI-Lite スレーブの動作を理解する†
読み出し動作†
AXI4 の仕様で AXI4-Lite 読み出し時のスレーブ動作は次のように規定されています。
A3.3.1 Dependencies between channel handshake signals
- arvalid と arready
- マスターは arvalid を立てるのに arready を待ってはならない
- スレーブは arready を立てるのに arvalid を待ってよい
- スレーブは arready を立てるのに arvalid を待たなくてもよい
- rvalid と rready
- スレーブは rvalid を立てる前に arvalid と arready を待たなければならない
- スレーブは rvalid を立てるのに rready を待ってはならない
- マスターは rready を立てるのに rvalid を待ってよい
- マスターは rready を立てるのに rvalid を待たなくてもよい
前の読み出しデータに rready を発行するタイミングと 次の読み出し用に arvalid を発行するタイミングが規定されていないので、 このあたりがコードでどのように書かれているか、興味があります。
自動作成されるのと同じ動作をするコードを掲載すると、
LANG:verilog always @( posedge S_AXI_ACLK ) if ( !S_AXI_ARESETN ) begin S_AXI_ARREADY <= 0; end else if (~S_AXI_ARREADY && S_AXI_ARVALID) begin S_AXI_ARREADY <= 1; axi_araddr <= S_AXI_ARADDR; end else begin S_AXI_ARREADY <= 0; end assign S_AXI_RRESP = 0; // 'OKAY' response always @( posedge S_AXI_ACLK ) if ( !S_AXI_ARESETN ) begin S_AXI_RVALID <= 0; end else if (~S_AXI_RVALID && S_AXI_ARREADY && S_AXI_ARVALID) begin S_AXI_RVALID <= 1; end else if (S_AXI_RVALID && S_AXI_RREADY) begin S_AXI_RVALID <= 0; end assign slv_reg_rden = S_AXI_ARREADY & S_AXI_ARVALID & ~S_AXI_RVALID; always @( posedge S_AXI_ACLK ) if ( !S_AXI_ARESETN ) begin S_AXI_RDATA <= 0; end else if (slv_reg_rden) begin S_AXI_RDATA <= axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] == 0 ? slv_reg0 : axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] == 1 ? slv_reg1 : axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] == 2 ? slv_reg2 : axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] == 3 ? slv_reg3 : 0; end
状態遷移図を書いてみると次のようになります。
¨( @startuml skinparam state {
BackgroundColor<<Error>> Pink
}
[*] -right-> Idle Idle : arready = 0 Idle : rvalid = 0 Idle : raddr = ? Idle : rdata = ? Idle --> Idle : [ ~arvalid ] Idle -down-> Address_Ready : [ arvalid ] Address_Ready: arready = 1 Address_Ready: rvalid = 0 Address_Ready: raddr = <valid> Address_Ready: rdata = ? Address_Ready: (arvalid = 1) Address_Ready -right-> Data_Ready : [ 1 ] Data_Ready: arready = 0 Data_Ready: rvalid = 1 Data_Ready: raddr = ? Data_Ready: rdata = <valid> Data_Ready --> Data_Ready : [ ~rready & ~arvalid ] Data_Ready --> Idle : [ rready & ~arvalid ] Data_Ready --> Address_Ready : [ rready & arvalid ] Data_Ready --> Erroneous : [ ~rready & arvalid ] state Erroneous<<Error>> Erroneous: arready = 1 Erroneous: rvalid = 1 Erroneous: raddr = <new> Erroneous: rdata = <old> Erroneous: (arvalid = 1) Erroneous --> Data_Ready : [ ~rready ] Erroneous --> Idle : [ rready ] );
通常は Idle → Address_Ready → Data_Ready の3クロックで1回の読み出し動作になりますが、 注目すべきは Data_Ready から [ rready & arvalid ] のコンディションで直接 Address_Ready へ飛べることです。
実際に利用されることがあるのか不明ですが、原理的には下記タイミング図後半のように、 2クロックに1回の頻度で連続してデータを読み出せるのだと思います。 ただしこの場合にもレイテンシーは3クロックになります。
数では赤線がアドレス発行タイミング、黄色がデータ読み取りタイミングです。
&tchart( clock ~_~_~_~_~_~_~_~_~_~_~_~_ arvalid |~~~~_|~~~~~~~~_ raddr =?=X==1=X=?====X2===|X3===X?=== arready _~~___~~~~____ raddr_buf ===?=X=1========X2===X3===== rvalid _~~~~_~~~~ rden _~~___~~~~____ rdata =====?=X=d1========Xd2===Xd3=== rready _____[~~]_~~[~~]~~[~~]__ );
Data_Ready で [ ~rready & arvalid ] としてしまうとエラーになります。 その後 Data_Ready, Idle どちらに遷移したとしても、正しいデータの読み出しは行えません。
つまり、前のデータ読み取りと次のアドレス送信を同時に行うことはできますが、 前のデータ読み取りに先行して次のアドレスを送ってはいけない、ということになります。
書き込み動作†
AXI4 の仕様によれば、AXI4-Lite 書き込み時のスレーブ動作は以下の要求を満たす必要があります。
A3.3.1 Dependencies between channel handshake signals
- マスターは awvalid や wvalid を立てるのに awready や wready を待ってはならない
- スレーブは awready や wready を立てるのに awvalid や wvalid を待ってもよい
- スレーブは awready や wready を立てるのに awvalid や wvalid を待たなくてもよい
- スレーブは bvalid を立てる前に awvalid, awready, wvalid, wready を待たなければならない
(AXI4-Full のバースト転送では wlast も待たなければならない) - スレーブは bvalid を立てるのに bready を待ってはならない
- マスターは bready を立てるのに bvalid を待ってもよい
- マスターは bready を立てるのに bvalid を待たなくてもよい
気をつけなければならないのは 4. ですね。 wvalid と wready の両方を確認してからでないと bvalid を立ててはいけない、つまり、立てっぱなしにはできないそうです。
もう一つ、データが送られるのとアドレスが送られるのとは独立で、 どちらが先に届くかわからない点です。とはいえ、どちらかに ready を立てないと もう一方が送られないことはないのでしょうから、両方 valid が立ってから ready を立てる動作で問題ないことになります。
LANG:verilog always @( posedge S_AXI_ACLK ) if ( !S_AXI_ARESETN ) begin S_AXI_AWREADY <= 0; end else begin if (!S_AXI_AWREADY && S_AXI_AWVALID && S_AXI_WVALID) begin S_AXI_AWREADY <= 1; end else begin S_AXI_AWREADY <= 0; end end always @( posedge S_AXI_ACLK ) if ( !S_AXI_ARESETN ) begin axi_awaddr <= 0; end else begin if (!axi_awready && S_AXI_AWVALID && S_AXI_WVALID) begin axi_awaddr <= S_AXI_AWADDR; end end always @( posedge S_AXI_ACLK ) if ( !S_AXI_ARESETN ) begin axi_wready <= 0; end else begin if ( !axi_wready && S_AXI_WVALID && S_AXI_AWVALID) begin axi_wready <= 1; end else begin axi_wready <= 0; end end assign wen = S_AXI_READY && S_AXI_WVALID && axi_awready && S_AXI_AWVALID; integer byte_index; always @( posedge S_AXI_ACLK ) if ( !S_AXI_ARESETN ) begin slv_reg0 <= 0; slv_reg1 <= 0; slv_reg2 <= 0; slv_reg3 <= 0; end else if (wen) begin for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) begin case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0: if ( S_AXI_WSTRB[byte_index] == 1 ) slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; 2'h1: if ( S_AXI_WSTRB[byte_index] == 1 ) slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; 2'h2: if ( S_AXI_WSTRB[byte_index] == 1 ) slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; 2'h3: if ( S_AXI_WSTRB[byte_index] == 1 ) slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; default : ; endcase end end end assign S_AXI_BRESP = 0; // 'OKAY' response always @( posedge S_AXI_ACLK ) if ( !S_AXI_ARESETN ) begin S_AXI_BVALID <= 0; end else if (~S_AXI_BVALID && S_AXI_AWREADY && S_AXI_AWVALID && S_AXI_WREADY && S_AXI_WVALID) begin S_AXI_BVALID <= 1; end else if (S_AXI_BVALID && S_AXI_BREADY) begin S_AXI_BVALID <= 0; end
¨( state Write { [*] -right-> Idle Idle: awready = 0 Idle: wready = 0 Idle: awaddr = ? Idle: bvalid = 0 Idle: (wdata = ?) Idle --> Idle : [ ~awvalid | ~wvalid ] Idle --> Writing : [ awvalid & wvalid ] Writing: awready = 1 Writing: wready = 1 Writing: awaddr = <valid> Writing: bvalid = 0 Writing: (awvalid = 1) Writing: (wvalid = 1) Writing: (wdata = <valid>) Writing --> Idle
- [*] -right-> Idle2 state "Idle" as Idle2 Idle2: bvalid = 0 Idle2 --> Idle2 : [ state != Writing ] Idle2 --> Done : [ state == Writing ] Done: bvalid = 1 Done --> Done : [ ~bready ] Done --> Idle2 : [ bready ] } );
動作は単純で、
- awvalid & wvalid が揃えば awaddr をバッファして Writing へ
Writing にてバッファされた awaddr と生の wdata, wstrb を使って書き込み
- Writing したら bvalid を立て、bready で下げる。
- strb により書き込みはバイト単位、ハーフワード単位などでも可能
- 読み出しは常にワード単位でよい
となっている。
最短で2クロックに1データを書き込めることがわかります。
少し単純化†
マスター側のお行儀が良いことを前提とすれば、
実際に読み出しを行う条件は arready だけ見れば十分。
また書き込みについても
- awready と wready は同じ動作
- awaddr だけでなく wdata, wstrb もキャッシュすれば ready だけ見て書き込んで構わない
- !ready && bready で bvalid をクリアする
ということで、以下のように簡略化できます。
LANG:verilog localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1; localparam integer OPT_MEM_ADDR_BITS = 1; // 読み出し動作 reg [OPT_MEM_ADDR_BITS : 0] S_AXI_ARADDR_reg; reg [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_RDATA_reg; reg S_AXI_ARREADY_reg; reg S_AXI_RVALID_reg; assign S_AXI_ARREADY = S_AXI_ARREADY_reg; assign S_AXI_RDATA = read_data; assign S_AXI_RVALID = S_AXI_RVALID_reg; assign S_AXI_RRESP = 0; always @(posedge S_AXI_ACLK) if (!S_AXI_ARESETN) begin S_AXI_ARREADY_reg <= 0; S_AXI_RVALID_reg <= 0; end else begin if (!S_AXI_ARREADY_reg && S_AXI_ARVALID) begin S_AXI_ARREADY_reg <= 1; S_AXI_ARADDR_reg <= S_AXI_ARADDR[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]; end else begin S_AXI_ARREADY_reg <= 0; end if (S_AXI_ARREADY_reg) begin set_read_addr(S_AXI_ARADDR_reg); S_AXI_RVALID_reg <= 1; end else if (S_AXI_RREADY) begin S_AXI_RVALID_reg <= 0; end end
LANG:verilog // 書き込み動作 reg [OPT_MEM_ADDR_BITS : 0] S_AXI_AWADDR_reg; reg S_AXI_READY_reg; reg S_AXI_BVALID_reg; reg [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_WDATA_reg; reg [(C_S_AXI_DATA_WIDTH/8)-1 : 0] S_AXI_WSTRB_reg; assign S_AXI_AWREADY = S_AXI_READY_reg; assign S_AXI_WREADY = S_AXI_READY_reg; assign S_AXI_BVALID = S_AXI_BVALID_reg; assign S_AXI_BRESP = 0; always @(posedge S_AXI_ACLK) if (!S_AXI_ARESETN) begin S_AXI_READY_reg <= 0; S_AXI_BVALID_reg <= 0; initialize_data(); end else begin if (!S_AXI_READY_reg && S_AXI_AWVALID && S_AXI_WVALID) begin S_AXI_READY_reg <= 1; S_AXI_AWADDR_reg <= S_AXI_AWADDR[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]; S_AXI_WDATA_reg <= S_AXI_WDATA; S_AXI_WSTRB_reg <= S_AXI_WSTRB; end else begin S_AXI_READY_reg <= 0; end if (S_AXI_READY_reg) begin write_data(S_AXI_AWADDR_reg, S_AXI_WDATA_reg, S_AXI_WSTRB_reg); S_AXI_BVALID_reg <= 1; end else if (S_AXI_BREADY) begin S_AXI_BVALID_reg <= 0; end end // ここより上は汎用コード
LANG:verilog // 下の部分だけ変えればほとんどの用途に対応可能? reg [C_S_AXI_DATA_WIDTH-1:0] reg0 = 0; reg [C_S_AXI_DATA_WIDTH-1:0] reg1 = 0; reg [C_S_AXI_DATA_WIDTH-1:0] reg2 = 0; reg [C_S_AXI_DATA_WIDTH-1:0] reg3 = 0; reg [C_S_AXI_DATA_WIDTH-1:0] read_buff; task set_read_addr; input [OPT_MEM_ADDR_BITS : 0] addr; begin read_buff <= addr == 0 ? reg0 : addr == 1 ? reg1 : addr == 2 ? reg2 : addr == 3 ? reg3 : 0; end endtask assign read_data = read_buff; task initialize_data; begin reg0 <= 0; reg1 <= 0; reg2 <= 0; reg3 <= 0; end endtask task write_data; input [OPT_MEM_ADDR_BITS : 0] addr; input [C_S_AXI_DATA_WIDTH-1:0] data; input [(C_S_AXI_DATA_WIDTH/8)-1 : 0] strb; integer i; begin for ( i = 0; i < C_S_AXI_DATA_WIDTH/8; i = i+1 ) if (strb[i]) case (addr) 0: reg0[(8*i) +: 8] <= data[(8*i) +: 8]; 1: reg1[(8*i) +: 8] <= data[(8*i) +: 8]; 2: reg2[(8*i) +: 8] <= data[(8*i) +: 8]; 3: reg3[(8*i) +: 8] <= data[(8*i) +: 8]; endcase end endtask
レジスタの実際のビット幅が 8 bit の倍数でない場合†
write_data の
LANG:verilog for ( i = 0; i < C_S_AXI_DATA_WIDTH/8; i = i+1 ) if (strb[i]) case (addr) 0: reg0[(8*i) +: 8] <= data[(8*i) +: 8]; 1: reg1[(8*i) +: 8] <= data[(8*i) +: 8]; 2: reg2[(8*i) +: 8] <= data[(8*i) +: 8]; 3: reg3[(8*i) +: 8] <= data[(8*i) +: 8]; endcase
のところは、
LANG:verilog for ( i = 0; i < C_S_AXI_DATA_WIDTH; i = i+1 ) if (strb[i >> 3]) case (addr) 0: if(i < DATA_WIDTH1) reg0[i] <= data[i]; 1: if(i < DATA_WIDTH2) reg1[i] <= data[i]; 2: if(i < DATA_WIDTH3) reg2[i] <= data[i]; 3: if(i < DATA_WIDTH4) reg3[i] <= data[i]; endcase
のようにするといいみたい。
BRAM とつなぐ場合†
LANG:verilog // 下の部分だけ変えればほとんどの用途に対応可能? assign mem_addr = S_AXI_READY ? S_AXI_AWADDR_reg : S_AXI_ARADDR_reg; assign mem_in = S_AXI_WDATA_reg; assign mem_we = S_AXI_WSTRB_reg; assign mem_en = S_AXI_ARREADY_reg || S_AXI_READY_reg; task set_read_addr; input [OPT_MEM_ADDR_BITS : 0] addr; begin ; // do nothing end endfunction assign read_data = mem_out; task initialize_data; begin ; // do nothing end endtask task write_data; input [OPT_MEM_ADDR_BITS : 0] addr; input [C_S_AXI_DATA_WIDTH-1:0] data; input [(C_S_AXI_DATA_WIDTH/8)-1 : 0] strb; begin ; // do nothing end endtask
こうかな?
後で試す。
ドライバファイルについて†
AXI4_Lite_Slave_test.h
LANG:C #include "xil_types.h" #include "xstatus.h" #define AXI4_LITE_SLAVE_TEST_S_AXI_SLV_REG0_OFFSET 0 #define AXI4_LITE_SLAVE_TEST_S_AXI_SLV_REG1_OFFSET 4 #define AXI4_LITE_SLAVE_TEST_S_AXI_SLV_REG2_OFFSET 8 #define AXI4_LITE_SLAVE_TEST_S_AXI_SLV_REG3_OFFSET 12 #define AXI4_LITE_SLAVE_TEST_mWriteReg(BaseAddress, RegOffset, Data) \ Xil_Out32((BaseAddress) + (RegOffset), (u32)(Data)) #define AXI4_LITE_SLAVE_TEST_mReadReg(BaseAddress, RegOffset) \ Xil_In32((BaseAddress) + (RegOffset))
こんなマクロが定義されており、CPU から簡単にデータの読み書きができるようになっています。
しかも Xil_Out32 や Xil_In32 の中身は単なるメモリアクセスなんですね。
プロジェクト中で IP を利用した場合、これらのファイルは正しく SDK に引き継がれるようでした。 このあたりの仕組みもあとで勉強したいところです。
汎用ドライバを使い Linux 上からスレーブの動作を確認する†
電気回路/zynq/AXI4-LiteスレーブIPの動作テスト の手順で Linux 上のソフトから読み書きできることをテストしました。
AXI4-Stream を試す†
http://www.googoolia.com/wp/category/zynq-training/page/2/
によれば、AXI4-Stream を CPU モジュールに繋ぐには、 間に DMA コントローラを置いて、受け取ったデータを SDRAM へ流し込むのが定石のようです。
DMA コントローラとしては AXI DataMover という IP が汎用性の高いモジュールで、 AXI Central Direct Memory Access (AXI CDMA) というのはお手頃なモジュールのようです?
AXI-Stream マスター IP†
IP 側で生成したデータを CPU の管理するメモリに流し込む方向に連続データを転送する IP です。
カスタム IP 作成†
[Tools]-[Create and Package New IP...] から smastertest という IP を作り、 AXI-Stream マスターポートを持たせました。
- 0 から順にインクリメントする
- 下位8ビットが 0xff の時に tlast を立てる
という動作をさせます。
LANG:verilog reg [C_M_AXIS_TDATA_WIDTH-1 : 0] M_AXIS_TDATA_reg; assign M_AXIS_TSTRB = 4'b1111; reg M_AXIS_TLAST_reg; reg M_AXIS_TVALID_reg; assign M_AXIS_TDATA = M_AXIS_TDATA_reg; assign M_AXIS_TLAST = M_AXIS_TLAST_reg; assign M_AXIS_TVALID = M_AXIS_TVALID_reg; always @(posedge M_AXIS_ACLK) if (!M_AXIS_ARESETN) begin M_AXIS_TDATA_reg <= 0; M_AXIS_TLAST_reg <= 0; M_AXIS_TVALID_reg <= 0; end else begin // データ読み取り可能 M_AXIS_TVALID_reg <= 1; // 読まれたら更新する if (M_AXIS_TREADY) begin M_AXIS_TDATA_reg <= M_AXIS_TDATA_reg + 1'b1; // 256ワードごとに tlast を立てる M_AXIS_TLAST_reg <= (M_AXIS_TDATA_reg & 'hff) == 'hfe; end end
コネクション†
デザインに追加し、M00_AXIS ポートから S_AXI_HP0 へドラッグし線で結ぶと、 直接はつなげないので DMA コントローラを追加して良いか、と聞かれます。
OK すると自動的に必要な IP (axi_dma, axi_mem_intercon) を追加してくれますが、 さらに左上にアシスタンスが出るので、Run Connection Automation します。
すると axi_periph から axi_dma の AXI-Lite ポートとへの接続が追加されました。 さらにリセット線も追加されています。
Regenerate Layout した後の図がこちらです。
割り込み設定†
転送終了を表すために割り込みを設定します。
Processing System の IRQ_F2P につながっている xlconcat をダブルクリックして [Numbers of Ports] を 4 から 5 に増やします。 IRQ_F2P ポートの幅はすぐには反映されませんが、いずれかのタイミングで勝手に広がってくれるので特に編集の必要はありません。
増えたポートに axi_dma の s2mm_intout をつなぎます。 s2mm は stream to memory のことでしょうけれど、余分な m は何だろう???
Bitstream 生成†
Validate Design すると、これで問題ないと言われました。
[Generate Block Design] の後、[Generate Bitstream] します。
アドレス確認†
dma コントローラは 0x40400000 にある。
電気回路/z-turn/基本事項#r4d5943a によれば、 0x00000000-0x3fffffff は SDRAM の全範囲をカバーしている。
ソフトウェアからアクセスするには†
サードパーティー製のドライバを使い、Linux 上のアプリケーションで DMA によりデータを受け取ることができました。
コメント・質問†
AXI_Full†
伊藤康彦 ()
とてもいい記事ですね。AXIに自作IPを接続するのに悩んでいる方々には、
一気に問題が解決する素晴らしい内容です。
当方も数年前に同様の内容で悩んでおりまして、AXI-Fullのひな型をかなり時間をかけて修正して AXI_mem_intercon に自作IPを直接繋いでいましたが、AXI-stream で user IP を作成して、DMA を自動生成させるというのは、大幅な設計時間短縮になりますね。
たいへん参考になりました。
ありがとうございました。
ドライバファイルについて†
とんとん ()
きっとご存知かもしれませんが、SDKに関してポインタでアドレスを指定すれば、ドライバファイルのAPI関数を使わなくても独自で作ったAXI IPのレジスタにアクセスできます。