HDL/VivadoでAXIバスを利用 のバックアップ(No.13)

更新


公開メモ

AXI バス

Xilinx の資料によれば、

AXI は、AMBA (Advanced Microcontroller Bus Architecture) 4 仕様

に基づいて標準化 された IP インターフェイスプロトコルです。

とのことで、例えば Zynq に内蔵された ARM プロセッサと、ユーザーロジックと、の間などが AXI バスで繋がれています。すなわち IP を自作したならば、AXI バスに繋がなければ、 CPU から利用できません。

でも逆に、一旦 IP を AXI バス互換にしてしまえば Vivado 上の GUI を用いて IP 通しを容易に接続できるなど、利点も多いようです。

axi-bus-variations.png

AXI バスはマスターとスレーブとを繋ぐ、1対1のバスだそうです。 複数を繋ぐときには Interconnect の IP を間に挟むことになります。

axi-bus-handshake.png

基礎となるプロトコル

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 が同時に立った時(上の図で黄色くハイライトされたクロック) に送受信が成立する、という規則が重要です。

このあたり、Silica の Designing a Custom AXI-lite Slave Peripheral の説明がとても分かりやすかったです。
http://silica.com/wps/wcm/connect/71b10b18-9c9c-44c6-b62d-9a031b8f3df8/SILICA_Xilinx_Designing_a_custom_axi_slave_rev1.pdf?MOD=AJPERES

AXI4-Lite と AXI4-Stream

CPU からレジスタを設定・読取するための AXI4-Lite と、
IP と CPU との間でデータを転送するための AXI4-Stream を使いたいです。

どちらも、上記の基本プロトコルを知っていれば動作の理解は難しくないようです。

AXI4-Lite における読み取り動作

  1. マスターからアドレスを送ります
    • araddr[?:0], arvalid, arready を使います
  2. スレーブが成否のステータスと共にデータを送ります
    • rdata[?:0], rresp[1:0], rvalid, rready を使います
    • rresp[1:0] は上位1ビットがゼロなら成功、1なら失敗になります
      • 00 : OKAY
      • 01 : Exclusive Access OK
      • 10 : Slave Error
      • 11 : Decode Error

例:

&tchart( aclk ~_~_~_~_~_~_~_ araddr =?===X=0x3000==X=?==== arvalid _~~~~___ arready _~~_ rdata =?=====X=0xbeef==X==?= rvalid ___~~~~_ rresp ====?==X00===X=?== rready _____~~_ );

AXI4-Lite における書き込み動作

  1. マスターからアドレスを送ります
    • awaddr[?:0], awvalid, awready を使います
  2. マスターからデータとストローブ信号を送ります
    • wdata[?:0], wstrb[?:0], wvalid, wready を使います
    • wstrb は wdata のうち実際に書き込む部位をバイト単位で指定します
    • アドレスと同時に送られることもあります
  3. スレーブから結果の成否を送ります
    • bresp[1:0], bvalid, bready を使います
    • bresp[1:0] は上位1ビットがゼロなら成功、1なら失敗になります
      • 00 : OKAY
      • 01 : Exclusive Access OK
      • 10 : Slave Error
      • 11 : Decode Error

例:

&tchart( aclk ~_~_~_~_~_~_~_ awaddr =?===X=0x3000==X=?==== awvalid _~~~~___ awready _~~_ wdata =?===X=0xbeef==X==?=== wvalid _~~~~___ wready _~~_ bresp ====?==X00=X=?==== bvalid _~~_ bready _~~~~___ );

AXI4-Stream によるデータ転送

  1. マスターからスレーブへストローブ信号、データ区切り信号と共にデータを送ります。
  • tdata[?:0], tstrb[?:0], tlast, tvalid, tready を使います
  • tlast は1まとまりのデータの区切りを表します

例:

&tchart( aclk ~_~_~_~_~_~_~_ tdata =?=X===D1X=?==X=D2X=? tstrb =?=X==1111=X==?=X=1111X=? 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;

とすれば良いことになります。

Vivado の IP ひな形生成機構を利用する

AXI バス自身の細かい仕様は AMBA で定められています。

いきなり仕様通りの IP を書くのは大変ですが、 手っ取り早く使うために、Vivado を使って IP のひな形を作成できるようです。

以下、まずはその手順を追ってみます。

プロジェクトを作成

IP を利用するプロジェクトを作成します。

new-project.png

[Create New Project] を選んで、

project-name.png

testing_axi4_lite という名前にしました。

rtl-project.png

RTL Project として、

main-sv.png

[Create File] から "main.sv" を追加します。

[Add Existing IP]、[Add Constraint] は飛ばして、

zynq-7020.png

Zynq 7020 を選択しました。

project-summary.png

[Finish] でプロジェクトが生成されます。

no-ports.png

ここではモジュール main にはポートを定義しません。

AXI4 Lite スレーブとなる IP のひな形を作成

create-ip-menu.png

[Tools]-[Create and Package IP...] を選択

create-ip.png

AXI4 ペリフェラル専用の Wizard があると書かれています。

create-axi4-peripheral.png

そちらを選択して Next

test_lite_slave.png

"test_lite_slave" という名前にしました。

lite_slave_config.png

設定・読み出し可能なレジスタを4つ持つ AXI4 Lite Slave IP を作成します。

IP のひな形の他、IP を ARM から使うためのドライバのひな形まで生成してくれます。

lite_slave_creation.png

AXI4 BFM Simulation はライセンスを別途購入しないと使えません。

[Edit IP] とすることで IP 用のプロジェクトが別途作成されます。 後々使うことになるのでぜひ作成しておくと良いです。

lite_slave_project.png

ここで作成したのような 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] を選ぶと、

lite_sm_generated.png

2つの IP が追加されていることを確認できます。

GUI を使って配置する

[Flow Navigator]-[IP Integrator]-[Create Block Design] から、

create_block_design.png

design_1 という Design を作成します。

add-lite-ips.png

[Add IP] から2つの IP を選んで Enter を押すと、

lite-ips-placed.png

並びが逆な感じですが、2つの IP が配置されました。

S_AXI から M_AXI までマウスカーソルをドラッグすると、 2つのポートの間を配線できます。

Ctrl+K で aclk, arstn, init を入力ポートとして、 done を出力ポートとして作成し、それぞれ適切に配線します。

design-routed.png

ごちゃごちゃしているので、 [Regenerate Layout] したところ、

layout-regenerated.png

見やすく配置されました。

"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.png

右クリックから [Assign Address] を呼び、Range を最小の 4k にしました。

address-assigned.png

Diagram に戻り、 F6 したところ、

validation-successful.png

うまくいきました。

Verilog ソースとの統合

[Flow navigator]-[IP Integrator]-[Generate Block Design] から、

generate-block-design.png

[Out of context per Block Design] を選ぶと、

verilog-source-generated.png

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-simulation-source.png

[Add or create simulation sources] を選んで、

create-design_1_test.png

"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

このファイルをトップレベルに指定して、

set-as-top.png

[Run Simulation] すると、

axi4-lite-simulated.png

正しくシミュレーションできていることが分かります。

連続する書き込みに6クロックかかっているようです。

自動生成される 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 のレジスタに順に値を書き込むコードが生成されています。

書き込み手順を変えれば任意の IP と繋げられるはずですが、 ちょっと複雑なので理解するのに時間が掛かります。。。

AXI4 Lite Master を読む

シミュレーション時に AXI4 Lite バス経由で IP を操作したいので、 AXI4 Lite Master の動作を理解するためソースをしっかり読んでみようと思います。

メインのステートマシン

IDLE
init_txn_pulse で INIT_WRITE へ
INIT_WRITE
writes_done で INIT_READ へ
INIT_READ
reads_done で INIT_COMPARE へ
INIT_COMPARE
無条件に IDLE へ

という簡単なものでした。

INIT_WRITE フェーズで書き込んだデータと同じ値が INIT_READ フェーズで読み出せることを確認するようになっています。

INIT_WRITE の動作

READ の動作も WRITE の動作と似ているので、 WRITE が理解できればほぼ全てを理解できるようでした。

メインロジック

LANGUAGE:verilog
if (writes_done) begin
  mst_exec_state <= INIT_READ;
end else
if (~axi_awvalid && ~axi_wvalid && ~M_AXI_BVALID  && 
        ~start_single_write && ~write_issued && 
        ~last_write) begin
  start_single_write <= 1;
  write_issued  <= 1;
end else 
if (axi_bready) begin
  write_issued  <= 0;
end else begin
  start_single_write <= 0;
end
  • axi_bready が立てば次のクロックで write_issued が降りる
  • axi_bready が立っていなければ次のクロックで start_single_write が降りる
  • 通常は start_single_write が立った次のクロックで axi_bready が立つことは無いので、 start_single_write のパルスは必ず1クロック幅になる

write_issued はここでしか参照されておらず、 start_single_write を立てた後、 まだ axi_bready が検出されていないことを表すフラグになっている。

last_write は続きの書き込み要求があるかどうかを表すフラグ。

  • アドレス線 = AWVALID を立てて AWREADY を待つ
  • データ線 = WVALID を立てて WREADY を待つ
  • 書き込み完了 = M_AXI_BVALID が立ったら BREADY を返す

と言う動作なので、上記の大きな if は

  • アドレス線出力中でなく
  • データ選出力中でなく
  • 書き込み完了待ちでなく
  • 書き込み開始フラグが立っておらず
  • 引き続きの書き込み要求がない

という条件を表している。

アドレス線

awvalid を立てて awready を待つ。

awready が立ったら4増やす。

LANGUAGE:verilog
 // axi_awvalid の設定
 // 有効な書き込みアドレスが出力されていることを示す
 always @(posedge M_AXI_ACLK) begin
   if (M_AXI_ARESETN == 0 || init_txn_pulse == 1) begin
     axi_awvalid <= 0;
   end else 
   if (start_single_write) begin
     axi_awvalid <= 1;
   end else 
   if (axi_awvalid && M_AXI_AWREADY) begin
     axi_awvalid <= 0;
   end
 end
 
 // 書き込みアドレス
 assign M_AXI_AWADDR   = C_M_TARGET_SLAVE_BASE_ADDR + axi_awaddr;
 
 // axi_awaddr の設定
 always @(posedge M_AXI_ACLK) begin
   if (M_AXI_ARESETN == 0  || init_txn_pulse == 1) begin
     axi_awaddr <= 0;
   end else 
   if (axi_awvalid && M_AXI_AWREADY) begin
     axi_awaddr <= axi_awaddr + 32'h00000004;
   end
 end

データ線

WVALID を立てて WREADY を待つ

LANGUAGE:verilog
 // axi_wvalid の設定
 always @(posedge M_AXI_ACLK) begin
   if (M_AXI_ARESETN == 0  || init_txn_pulse == 1) begin
     axi_wvalid <= 0;
   end else 
   if (start_single_write) begin
     axi_wvalid <= 1;
   end else 
   if (axi_wvalid && M_AXI_WREADY) begin
     axi_wvalid <= 0;
   end
 end
 
 // axi_wdata の設定
 always @(posedge M_AXI_ACLK) begin
   if (M_AXI_ARESETN == 0 || init_txn_pulse == 1 ) begin
     axi_wdata <= C_M_START_DATA_VALUE;
   end else
   if (M_AXI_WREADY && axi_wvalid) begin
     axi_wdata <= C_M_START_DATA_VALUE + write_index;
   end
 end

書き込み完了

LANGUAGE:verilog
 // axi_bready の設定
 always @(posedge M_AXI_ACLK) begin
   if (M_AXI_ARESETN == 0 || init_txn_pulse == 1) begin
     axi_bready <= 0;
   end else 
   if (~axi_bready && M_AXI_BVALID) begin
     axi_bready <= 1;
   end else begin
     axi_bready <= 0;
   end
 end
 
 // Flag write errors
 assign write_resp_error = (axi_bready & M_AXI_BVALID & M_AXI_BRESP[1]);

書き込みデータ数のカウント

LANGUAGE:verilog
 // write_index の設定
 always @(posedge M_AXI_ACLK) begin
   if (M_AXI_ARESETN == 0 || init_txn_pulse == 1) begin
     write_index <= 0;
   end else 
   if (start_single_write) begin
     write_index <= write_index + 1;
   end
 end
 
 // last_write の設定
 always @(posedge M_AXI_ACLK) begin
   if (M_AXI_ARESETN == 0 || init_txn_pulse == 1) begin
     last_write <= 0;
   end else 
   if ((write_index == C_M_TRANSACTIONS_NUM) && M_AXI_AWREADY) begin
     last_write <= 1;
   end
 end
 
 // writes_done の設定
 // 書き込み完了は M_AXI_BVALID && axi_bready で判定する
 always @(posedge M_AXI_ACLK) begin
   if (M_AXI_ARESETN == 0 || init_txn_pulse == 1) begin
     writes_done <= 0;
   end else 
   if (last_write && M_AXI_BVALID && axi_bready) begin
     writes_done <= 1;
   end
 end

ふむ、これを変更して何かするのは大変そうですね・・・

シミュレーション用の Master を作成する

シミュレーション時に AXI4-Lite Slave となる IP と容易にデータをやりとりするために、 合成不可能な、シミュレーション専用の 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'b001;

  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 に繋ぐと、

axi4_lite_master_bfm-design.png

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

実行結果は次のようになりました。

axi4_lite_master_bfm-test.png

1回の書き込みに6クロック掛かっており、これは Vivado で自動生成した AXI4 Lite Master IP と同じ動作になっています。

汎用 DIO スレーブを作成する

AXI4-Lite スレーブ作成の例として、汎用 DIO モジュールを作成してみます。

[Tool]-[Create and Package IP] から [new AXI4 peripheral] で "AXI4_Lite_Slave_DIO" を作り、 AXI4_Lite_Slave_DIO_v1_0_S_AXI.v を次のように変更しました。

まず、module AXI4_Lite_Slave_DIO_v1_0_S_AXI のポート設定に以下を加えます。

LANG:verilog
    // Users to add ports here
    input wire [C_S_AXI_DATA_WIDTH-1:0] idata0,
    input wire [C_S_AXI_DATA_WIDTH-1:0] idata1,
    input wire [C_S_AXI_DATA_WIDTH-1:0] idata2,
    input wire [C_S_AXI_DATA_WIDTH-1:0] idata3,
    output wire [C_S_AXI_DATA_WIDTH-1:0] odata0,
    output wire [C_S_AXI_DATA_WIDTH-1:0] odata1,
    output wire [C_S_AXI_DATA_WIDTH-1:0] odata2,
    output wire [C_S_AXI_DATA_WIDTH-1:0] odata3,
    // User ports ends

そして、

LANG:verilog
    assign odata0 = slv_reg0;
    assign odata1 = slv_reg1;
    assign odata2 = slv_reg2;
    assign odata3 = slv_reg3;

として出力を繋ぎ、

LANG:verilog
    always @(*)
      case (axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB])
        0: reg_data_out <= idata0;
        1: reg_data_out <= idata1;
        2: reg_data_out <= idata2;
        3: reg_data_out <= idata3;
      endcase

として入力を繋げば完了です。

さらにこれを IP の外に引き出さなければならないので、 module AXI4_Lite_Slave_DIO_v1_0 の方も、ポートに

LANG:verilog
   // Users to add ports here
   input wire [C_S_AXI_DATA_WIDTH-1:0] idata0,
   input wire [C_S_AXI_DATA_WIDTH-1:0] idata1,
   input wire [C_S_AXI_DATA_WIDTH-1:0] idata2,
   input wire [C_S_AXI_DATA_WIDTH-1:0] idata3,
   output wire [C_S_AXI_DATA_WIDTH-1:0] odata0,
   output wire [C_S_AXI_DATA_WIDTH-1:0] odata1,
   output wire [C_S_AXI_DATA_WIDTH-1:0] odata2,
   output wire [C_S_AXI_DATA_WIDTH-1:0] odata3,
   // User ports ends

を加え、

LANG:verilog
   AXI4_Lite_Slave_DIO_v1_0_S_AXI_inst (
        .idata0(idata0),
        .idata1(idata1),
        .idata2(idata2),
        .idata3(idata3),
        .odata0(odata0),
        .odata1(odata1),
        .odata2(odata2),
        .odata3(odata3),

のように繋ぎます。

これで AXI4-Lite 経由で idataX, odataX へアクセスできるようになりました。

ドライバファイルについて

AXI4_Lite_Slave_DIO.h

LANG:C
#include "xil_types.h"
#include "xstatus.h"

#define AXI4_LITE_SLAVE_DIO_S_AXI_SLV_REG0_OFFSET 0
#define AXI4_LITE_SLAVE_DIO_S_AXI_SLV_REG1_OFFSET 4
#define AXI4_LITE_SLAVE_DIO_S_AXI_SLV_REG2_OFFSET 8
#define AXI4_LITE_SLAVE_DIO_S_AXI_SLV_REG3_OFFSET 12

#define AXI4_LITE_SLAVE_DIO_mWriteReg(BaseAddress, RegOffset, Data) \
  	Xil_Out32((BaseAddress) + (RegOffset), (u32)(Data))

#define AXI4_LITE_SLAVE_DIO_mReadReg(BaseAddress, RegOffset) \
    Xil_In32((BaseAddress) + (RegOffset))

こんなマクロが定義されており、CPU から簡単にデータの読み書きができる雰囲気です。

しかも Xil_Out32 や Xil_In32 の中身は単なるメモリアクセスなんですね。

AXI4-Lite のスレーブを作る

vivado がはき出す AXI4-Lite スレーブのひな形コードがあまり分かりやすくなく、 無駄に遅延が入っているようにも思えるので、AXI4-Lite スレーブを一から作るために タイミングと基本動作をおさらいしたいと思います。

ごめんなさい、現状では動作未確認です

以下の記述はもっともらしく書いてありますが、実記での動作は未確認です。

追って動作確認を行います。

AXI4-Lite 読み出し時のスレーブ動作

AXI4 の仕様では次のように規定されています。

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 を待たなくてもよい

スレーブ側が必ず次のクロックでデータを返せる場合の例。

&tchart( aclk ~_~_~_~_~_~_~_~_~_ araddr ==?X=A1X=?X=A2X=?==X=A3X=?== arvalid ~~~~_~~_ arready ~~~~~~~~~
rdata ====?X=D1X=?X=D2==X=?X=D3X=? rvalid __[~~]~~[~~][~~] rready ~~~~~~~~~~ );

arready = !rvalid の関係があるので、

LANG: verilog
always @(posedge aclk)
  if (!resetn) begin
    arready <= 1;
  end else
  if (arready) begin
    if (arvalid) begin
      rdata <= get_registers(araddr);
      arready <= 0;
    end
  end else begin
    if (rready) begin
      arready <= 1;
    end
  end
assign rvalid = !arready;

のようにすれば最短2クロックで読み出し動作を繰り返せるスレーブになります。

ただし、

LANG:verilog
      rdata <= register[araddr];

この部分はマスターから送られてくる araddr をデータ選択回路にそのまま繋いでいるため、配線遅延が大きくなる可能性があります。

この遅延のためにバスクロックが制限されてしまうようであれば、 一旦 araddr をバッファするために、arvalid から rready の間を1クロック空ける必要があります。

&tchart( aclk ~_~_~_~_~_~_~_~_~_ araddr ==?X=A1X===?X=A2X=?====== arvalid __~~_~~_____ arready ~~~~_~~___~
rdata ======?X=D1X=?==X===D2X=? rvalid ___[~~]_~~[~~]__ rready ~~~~~~~~~~ );

この場合、rready と rvalid の時間変化は

rready100100
rvalid001001

のようになるため、

LANG: verilog
always @(posedge aclk)
  if (!resetn) begin
    arready <= 1;
    rvalid <= 0;
  end else
  if (arready && !rvalid) begin
    if (arvalid) begin
      araddr_buf <= araddr;
      arready <= 0;
    end
  end else 
  if (!arready && !rvalid) begin
    rvalid <= 1;
    rdata <= get_register(araddr_buf);
  end else begin
//  if (rready) begin
    if (rready || arready) begin // recovery from erroneous state
      arready <= 1;
      rvalid <= 0;
    end
  end

とするのが良さそうです。

万一 arready && rvalid になってしまったときのことを考えて、 最後の判定を if (rready) ではなく if (rready || arready) としてみましたが、マスター側の状態が分からないので、 必ずしもこれが最良かどうか自信がありません。

この回路では最高で3クロックに一回読み込み動作が可能になります。

AXI4-Lite 書き込み時のスレーブ動作

AXI4 の仕様によれば、以下の要求を満たす必要があります。

A3.3.1 Dependencies between channel handshake signals

  1. マスターは awvalid や wvalid を立てるのに awready や wready を待ってはならない
  2. スレーブは awready や wready を立てるのに awvalid や wvalid を待ってもよい
  3. スレーブは awready や wready を立てるのに awvalid や wvalid を待たなくてもよい
  4. スレーブは bvalid を立てる前に awvalid, awready, wvalid, wready を待たなければならない
    (AXI4-Full のバースト転送では wlast も待たなければならない)
  5. スレーブは bvalid を立てるのに bready を待ってはならない
  6. マスターは bready を立てるのに bvalid を待ってもよい
  7. マスターは bready を立てるのに bvalid を待たなくてもよい

気をつけなければならないのは 4. ですね。 wvalid と wready の両方を確認してからでないと bvalid を立ててはいけないそうです。

データが送られる前に必ずアドレスが送られるのかどうか、 つまり wvalid が立つ前に必ず awvalid が立つのかどうか、 上では明記されていないのですが、 A1.3 AXI Architecture のところには

The AXI protocol permits address information to be issued ahead of the actual data transfer

と書かれていますし、Figure A1-2 でも Address and control が Write data に先行していますので、 恐らく awvalid の前に wvalid が立つことは無いと思われます。

すると、スレーブ側が書き込み要求をいつでも受け取れる場合、 下図のように wvalid が立つまで awready を上げないようにして、 awvalid && wvalid を待ってから処理すると実装が楽そうです。

&tchart( @w_caption 50 aclk ~_~_~_~_~_~_~_~_~_~_~_ awaddr ==?X=A1==X=A2X=A3==X=?X=A4==X=A5X=? awvalid ~~~~~~~~~~~~~~~~__ awready __[~~~~][~~]__[~~~~] wdata ====?X=D1X=D2X=?X=D3X=?==X=D4X=D5X=? wvalid __~~~~~~__~~~~ wready __~~~~~~__~~~~ bresp ====00================== bvalid __~~~~~~~~~~~~ bready ~~~~~~~~____~~~~~~~~~

bvalid_hold _______~~___ );

この場合、awready, wready は wvalid に連動して動かせばよく、 書き込み動作も wvalid に同期します。

bvalid は bready を待たなければなりませんので、 bvalid && !bready の条件で値を保持します。

LANG:verilog
always @(posedge aclk) begin
  if (!resetn) begin
    bvalid_hold <= 0;
  end else begin
    bvalid_hold <= bvalid && !bready;
  end
  if (wvalid)
    set_register(awaddr, wdata);
end
assign awready = wvalid;
assign wready = wvalid;
assign bresp = 0;
assign bvalid = wvalid || bvalid_hold;

ただ、この回路ではマスターからバスラインを通って届いた wvalid をバスラインを通じてマスターに返すことになるため、配線遅延が大きくなる心配があります。

また、registers への書き込みにマスターから届いた awaddr や wdata をそのまま使っているのも配線遅延的に不利になります。

この遅延が問題になるようであれば、 バッファを入れて、1クロックの遅延を許容する必要があります。

wvalid, awaddr, wdata をバッファすることにより、これらの遅延が影響しなくなります。

&tchart( @w_caption 60 aclk ~_~_~_~_~_~_~_~_~_~_~_ awaddr ==?X=A1====X=A2==X=?X=A3====X=? awvalid ~~~~~~~~~~~~~~~~__ awready ____[~~][~~]____[~~] wdata ====?X=D1==X=D2==X=?==X=D3==X=? wvalid _~~~~~~~~_~~~~__ wready ____~~~~____~~ bresp ====00================== bvalid ____~~~~~~__~~ bready ~~~~~~~~____~~~~~~~~~

ready ____~~~~____~~ awaddr_buf ====?X=A1====X=A2==X=?X=A3==== wdata_buf ======?X=D1==X=D2==X=?==X=D3== );

LANG:verilog(linenumber)
always @(posedge aclk)
  if (!resetn) begin
    ready <= 0;
    bvalid <= 0;
  end else begin
    ready <= !ready && awvalid && wvalid;
    bvalid <= (!ready && awvalid && wvalid) ||
              (bvalid && !bready);
    awaddr_buf <= awaddr;
    wdata_buf <= wdata;
    if (ready)
      set_register(awaddr_buf, wdata_buf);
  end
assign awready = ready;
assign wready = ready;
assign bresp = 0;

実は2行目は、本来なら次のように wvalid だけを待てば良いのですが、

LANG:verilog
  ready <= !ready && wvalid;

前者のようにすることで、万一 !awvalid のまま wvalid が立ったとしても、 不用意な書き込みをしてしまわないようになっています。

この回路は最短で2クロックに1回データを書き込めます。

wstrb について

ストローブ信号は1バイトにつき1ビットで表されているので、 書き込みのマスクに使うには1ビットを8ビットに広げる必要があります。

LANG:verilog
wire [C_S_AXI_DATA_WIDTH-1 : 0] strobe_expended;

generate
  genvar i;
  for(i=0; i<C_S_AXI_DATA_WIDTH/8; i=i+1) begin:strobe_bits
    assign strobe_expended[i*8+7:i*8] = {8{wstrb_buf[i]}};
  end
endgenerate

とすることで、wstrb_buf を広げて strobe_expended を作成できます。

これを使って、

LANG:verilog
register <= (wdata & strobe_expended) | (register & ~strobe_expended);

とすれば、必要なバイトだけを更新できるはずです。

合わせると

例えば AXI4-Lite に繋げられる汎用 DIO を以下のように作れます。

get_register と set_register を書き換えるだけで 様々な AXI4-Lite スレーブを作成できるようになっています。

LANG:verilog(linenumber)
`timescale 1 ns / 1 ps

module AXI4_Lite_Slave_DIO_v1_0_S_AXI #(
  parameter integer C_S_AXI_DATA_WIDTH	= 32,
  parameter integer C_S_AXI_ADDR_WIDTH	= 4
) (
  // Users to add ports here
  input wire [C_S_AXI_DATA_WIDTH-1:0] idata0,
  input wire [C_S_AXI_DATA_WIDTH-1:0] idata1,
  input wire [C_S_AXI_DATA_WIDTH-1:0] idata2,
  input wire [C_S_AXI_DATA_WIDTH-1:0] idata3,
  output reg [C_S_AXI_DATA_WIDTH-1:0] odata0,
  output reg [C_S_AXI_DATA_WIDTH-1:0] odata1,
  output reg [C_S_AXI_DATA_WIDTH-1:0] odata2,
  output reg [C_S_AXI_DATA_WIDTH-1:0] odata3,
  // User ports ends

  input wire  S_AXI_ACLK,
  input wire  S_AXI_ARESETN,
  input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_AWADDR,
  input wire [2 : 0] S_AXI_AWPROT,
  input wire  S_AXI_AWVALID,
  output wire  S_AXI_AWREADY,
  input wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_WDATA,
  input wire [(C_S_AXI_DATA_WIDTH/8)-1 : 0] S_AXI_WSTRB,
  input wire  S_AXI_WVALID,
  output wire  S_AXI_WREADY,
  output wire [1 : 0] S_AXI_BRESP,
  output reg  S_AXI_BVALID,
  input wire  S_AXI_BREADY,
  input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_ARADDR,
  input wire [2 : 0] S_AXI_ARPROT,
  input wire  S_AXI_ARVALID,
  output reg  S_AXI_ARREADY,
  output reg [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_RDATA,
  output wire [1 : 0] S_AXI_RRESP,
  output reg  S_AXI_RVALID,
  input wire  S_AXI_RREADY
);

function [C_S_AXI_DATA_WIDTH-1 : 0] get_register;
  input [C_S_AXI_ADDR_WIDTH-1 : 0] addr;
  begin
    get_register =
      addr & 'h0c == 0 ? idata0 :
      addr & 'h0c == 4 ? idata1 :
      addr & 'h0c == 8 ? idata2 :
      addr & 'h0c == 12 ? idata3 : 0;
  end
endfunction

task set_register;
  input [C_S_AXI_ADDR_WIDTH-1 : 0] addr;
  input [C_S_AXI_DATA_WIDTH-1 : 0] data;
  input [C_S_AXI_ADDR_WIDTH-1 : 0] strobe;
  begin
    case(addr & 'h0c)
    0:  odata0 <= (data & strobe) | (odata0 & ~strobe);
    4:  odata1 <= (data & strobe) | (odata1 & ~strobe);
    8:  odata2 <= (data & strobe) | (odata2 & ~strobe);
    12: odata3 <= (data & strobe) | (odata3 & ~strobe);
    endcase
  end
endtask

//////////////////////////////////////////////////////

reg [C_S_AXI_ADDR_WIDTH-1 : 0] araddr_buf;
always @(posedge S_AXI_ACLK)
  if (!S_AXI_ARESETN) begin
    S_AXI_ARREADY <= 1;
    S_AXI_RVALID <= 0;
  end else
  if (S_AXI_ARREADY && !S_AXI_RVALID) begin
    if (S_AXI_ARVALID) begin
      araddr_buf <= S_AXI_ARADDR;
      S_AXI_ARREADY <= 0;
    end
  end else 
  if (!S_AXI_ARREADY && !S_AXI_RVALID) begin
    S_AXI_RVALID <= 1;
    S_AXI_RDATA <= get_register(araddr_buf);
  end else begin
//  if (rready) begin
    if (S_AXI_RREADY || S_AXI_ARREADY) begin // recovery from erroneous state
      S_AXI_ARREADY <= 1;
      S_AXI_RVALID <= 0;
    end
  end
  
reg valid_buf = 0;
reg [C_S_AXI_ADDR_WIDTH-1 : 0] awaddr_buf;
reg [C_S_AXI_DATA_WIDTH-1 : 0] wdata_buf;
reg [(C_S_AXI_DATA_WIDTH/8)-1 : 0] wstrb_buf;
wire [C_S_AXI_DATA_WIDTH-1 : 0] strobe_expended;

generate
  genvar i;
  for(i=0; i<C_S_AXI_DATA_WIDTH/8; i=i+1) begin:strobe_bits
    assign strobe_expended[i*8+7:i*8] = {8{wstrb_buf[i]}};
  end
endgenerate

always @(posedge S_AXI_ACLK) 
  if (!S_AXI_ARESETN) begin
    valid_buf <= 0;
    S_AXI_BVALID <= 0;
  end else begin
    valid_buf <= !valid_buf && S_AXI_AWVALID && S_AXI_WVALID;
    S_AXI_BVALID <= (!valid_buf && S_AXI_AWVALID && S_AXI_WVALID) ||
                    (S_AXI_BVALID && !S_AXI_BREADY);
    awaddr_buf <= S_AXI_AWADDR;
    wdata_buf <= S_AXI_WDATA;
    wstrb_buf <= S_AXI_WSTRB;
    if (valid_buf)
      set_register(awaddr_buf, wdata_buf, strobe_expended);
  end

assign S_AXI_AWREADY = valid_buf;
assign S_AXI_WREADY = valid_buf;
assign S_AXI_BRESP = 0;

endmodule

イレギュラーな条件からの復帰について

ちゃんと考えると、結局 vivado が生成するような回路になる可能性があります???

上記 AXI4-Lite スレーブを試してみる

z-turn ボードの Hello World サンプルコードを元にして、

AR# 56609: 2013.2 Vivado IPI、Zynq-7000 - IPI 外のカスタム AXI HDL を Zynq AXI インターフェイスに接続する方法
http://japan.xilinx.com/support/answers/56609.html

を参考に、design_1 から M_AXI ポートを外部へ引き出し、そこに上記スレーブを繋ぐ。

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) というのはお手頃なモジュールのようです?

コメント・質問





Counter: 104371 (from 2010/06/03), today: 31, yesterday: 0