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

更新


公開メモ

AXI バス

Xilinx の資料によれば、

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

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

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

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

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

Xilinx の AXI バスに関するちゃんとしたリファレンスはこのあたりになります。

(英語版は v14.3、日本語版は v13.4)

でも、これらを端から端まで読んで仕様をコードに落とし込むのは片手間ではできない感じです。

より手っ取り早く理解するには 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

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

自動生成される 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_Master_BFM を作成しました。

書き込みを行うタスク

LANG:verilog
task write(addr, data);
input [C_M_AXI_ADDR_WIDTH-1:0] addr;
input [C_M_AXI_DATA_WIDTH-1:0] data;
begin
  reg error;
  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;
    end
  join

  // エラーがなければ 1 を返す
  write = !error;
end

動作未確認。

読み出しを行うタスク

LANG:verilog
task [C_M_AXI_DATA_WIDTH-1:0] read(addr);
input [C_M_AXI_ADDR_WIDTH-1:0] addr;
begin
  reg error;
  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)
        read = M_AXI_RDATA;
      @(posedge M_AXI_ACLK) #1; 
      M_AXI_RREADY = 0;
    end
  join

end

動作未確認。

コメント・質問





Counter: 104983 (from 2010/06/03), today: 5, yesterday: 0