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

更新


公開メモ

AXI バス

Xilinx の資料によれば、

AXI は、AMBA (Advanced Microcontroller Bus Architecture) 4 仕様 に基づいて標準化 された IP インターフェイスプロトコルです。

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

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

ということで、AXI バスを一通り使えるようになるよういろいろ調べました。

AXI バスの仕様

AXI バスには3つの規格があり、それぞれ AXI(-Full), AXI-Lite, AXI-Stream と呼ばれます。

どの規格も、マスターとスレーブとを繋ぐ1対1のバスになっていますので、 複数の機器を繋ぐときには Interconnect の IP を間に挟むことになります。

axi-bus-variations.png

  • AXI(-Full): マスターからスレーブのメモリやレジスタを読み書きするためのフルスペックプロトコル
  • AXI-Lite: マスターからスレーブのレジスタを読み書きするための低速だが軽量なプロトコル
  • AXI-Stream: 単純にマスターからスレーブへデータを受け渡す高速かつ軽量なプロトコル

細かい仕様は AMBA で定められています。

基礎となるプロトコル

AXI ではデータ転送要求をするのは常にマスターですが、 実際のデータはマスターからスレーブへ送られることも、 スレーブからマスターへも送られることもあります。

AXI プロトコルにおけるデータ転送の基本となるのは、 DATA ライン、VALID ライン、READY ラインを用いた以下のようなプロトコルになります。

axi-bus-handshake.png

  • 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 ではバースト転送機能がないため、アドレスを送るとデータが帰ってくる、という簡単なプロトコルになっています。

  1. マスターからアドレスを送ります
    • araddr[?:0], arprot[2: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

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 における書き込み動作

こちらもバースト転送はないので、 マスターからアドレスとデータをそれぞれ別々のチャンネルから送ると、 スレーブから成否を返すという単純なプロトコルです。

  1. マスターからアドレスを送ります
    • awaddr[?:0], awprot[2: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

アドレスとデータの転送は平行して行えます。データを送るのにアドレスの転送終了を待つ必要はありません。

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 によるデータ転送

アドレス指定も何もなく、単にマスターからスレーブへデータだけが送られます。 データをパケットに分けるための区切り情報も表すことができます。

  1. マスターからスレーブへストローブ信号、データ区切り信号と共にデータを送ります。
  • 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;

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

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

AXI バスに繋げられる IP を作るには IP 動作をバス仕様に合わせるだけでなく、 その IP を Vivado で扱えるようにするための作法に合わせる必要もあります。

すべてを一から作るのはいろんな仕様をすべて頭に入れなければならず大変なので、 手っ取り早く使うためには 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 編集用のプロジェクトが別途作成されます。 後々使うことになるのでぜひ作成しておくと良い・・・のですが・・・

これを選ぶと Vivado は ip_repo の直下にやたらとたくさんフォルダを 作ってしまうので、あとで非常にごちゃごちゃして嫌な感じです。 保存場所を選択できるようになっていれば良かったのに、 と思いました。

むしろここでは Edit IP をせず、後から IP Catalog 上で右クリックから Edit in IP Packager として、自分の好きな場所にプロジェクトを作成するのが 良いかもしれません。ただその場合には、Project Settings の [IP]-[Packager] で、[Delete project after package] を外しておかないと、 作成したプロジェクトは自動的に削除されてしまいますので注意して下さい。

not_delete_project.png

上記どちらかの要領で作成したような IP パッケージング用のプロジェクトでは、 [Flow Navigator] の [Project Manager] から [Package IP] を選択することで 右側にいろいろな設定項目が現れます。

lite_slave_project.png

IP ソースを変更したり、これらの設定を変更したら、 最後に [Review and Package] を押すと、IP が更新されます。

AXI4 Lite マスターとなる IP のひな形を作成する

上と同様にして、"test_lite_master" という IP を作成しました。

[Flow Navigator]-[Project Manager] で [IP Catalog] を選ぶと、

lite_sm_generated.png

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] から、

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クロックかかっているようです。

シミュレーション用の 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 に繋ぐと、

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 と同じ動作になっています。

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

状態遷移図を書いてみると次のようになります。

&uml( @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

  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 を立ててはいけない、つまり、立てっぱなしにはできないそうです。

もう一つ、データが送られるのとアドレスが送られるのとは独立で、 どちらが先に届くかわからない点です。とはいえ、どちらかに 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  

&uml( 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

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 マスターポートを持たせました。

create-smastertest.png

  • 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 コントローラを追加して良いか、と聞かれます。

smaster-added.png

OK すると自動的に必要な IP (axi_dma, axi_mem_intercon) を追加してくれますが、 さらに左上にアシスタンスが出るので、Run Connection Automation します。

connection-automation.png

すると axi_periph から axi_dma の AXI-Lite ポートとへの接続が追加されました。 さらにリセット線も追加されています。

Regenerate Layout した後の図がこちらです。

dma-registers-are-accessible.png

割り込み設定

転送終了を表すために割り込みを設定します。

Processing System の IRQ_F2P につながっている xlconcat をダブルクリックして [Numbers of Ports] を 4 から 5 に増やします。 IRQ_F2P ポートの幅はすぐには反映されませんが、いずれかのタイミングで勝手に広がってくれるので特に編集の必要はありません。

numbers-of-ports.png


増えたポートに axi_dma の s2mm_intout をつなぎます。 s2mm は stream to memory のことでしょうけれど、余分な m は何だろう???

connect-dma-intout.png

Bitstream 生成

Validate Design すると、これで問題ないと言われました。
[Generate Block Design] の後、[Generate Bitstream] します。

アドレス確認

smaster-address-editor.png

dma コントローラは 0x40400000 にある。

電気回路/z-turn/基本事項#r4d5943a によれば、 0x00000000-0x3fffffff は SDRAM の全範囲をカバーしている。

ソフトウェアからアクセスするには

サードパーティー製のドライバを使い、Linux 上のアプリケーションで DMA によりデータを受け取ることができました。

電気回路/zynq/DMA処理

コメント・質問




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

とんとん? ()

きっとご存知かもしれませんが、SDKに関してポインタでアドレスを指定すれば、ドライバファイルのAPI関数を使わなくても独自で作ったAXI IPのレジスタにアクセスできます。

  • はい、そのようですね。「こういう風にドライバを作ったらいいのでは?」というサジェスチョンと捕えるのが良いのでしょうか。 -- 武内 (管理人)?

Counter: 104342 (from 2010/06/03), today: 2, yesterday: 0