📘

AXI-Fullで多倍長転送

2023/05/29に公開

暗号回路では256ビットとかの数値を扱うのでAXIを使うとバースト転送が欲しくなる。(AXI-FULLの規格上は256ビットバスも作れるが、Zynqにつながってるバスが32bitなのでハード的に32bit転送に制限される。)
ネットに転がっている情報はAXI-LiteかAXI-Streamが多くてAXI-Fullを簡単に使うにはどうすればよいかわからなかったのでここに記す。

テンプレート作成

スクラッチからAXIを書いてる人とかもいるけど、VivadoにAXIのテンプレートがついてるのでそれを使う。
Create and Package New IP->Create a new AXI4 peripheralでAXI4マスタを作る。

最後Edit IPにすると編集できる。

ソースコードとしてはラッパーファイルと本体の2種類ができる。
さてこのコードが何をしているかというと、

// Example State machine to initialize counter, initialize write transactions, 
// initialize read transactions and comparison of read data with the 
// written data words.
parameter [1:0] IDLE = 2'b00, // This state initiates AXI4Lite transaction 
		// after the state machine changes state to INIT_WRITE 
		// when there is 0 to 1 transition on INIT_AXI_TXN
	INIT_WRITE   = 2'b01, // This state initializes write transaction,
		// once writes are done, the state machine 
		// changes state to INIT_READ 
	INIT_READ = 2'b10, // This state initializes read transaction
		// once reads are done, the state machine 
		// changes state to INIT_COMPARE 
	INIT_COMPARE = 2'b11; // This state issues the status of comparison 
		// of the written data with the read data	

この辺に書いてあるように、サンプルとして4状態のステートマシンを持っている。

参考

初期状態IDLEからINIT_AXI_TXN信号でINIT_WRITEに遷移しテストデータを書き込む。書き込み終わったらINIT_READに遷移して書き込んだ値を読み取りINIT_COMPAREに遷移、値を比較するという流れ。
利用するときはINIT_COMPAREはいらないとして、INIT_READINIT_WRITEを好きなタイミングでトリガすればバーストリード/ライトできるんじゃないか?ちなみにINIT_WRITEは外部からのINIT_AXI_TXN信号でトリガされるので、ここに書き込み信号を入れてやればいい。

AXI Verification IPによる検証

一応やってみたが、マスタ検証時はBRAMで十分な気がしたので見る価値ないかも

パッケージしたAXIテンプレートをAXI VIPにつないで検証してみる。VIPの使い方は

参考

を参考。
適当に繋いで、AXI_BURST_LENを8にしている。m00_axi_init_axi_txnINIT_WRITEになるので、ここにRUN信号を入れることで書き込みを開始する。

上記サイトやサンプルプログラム(ブロックデザインのIPを右クリックしてOpen IP Example Design)を参考に次のようなテストベンチを用意

tb_axi.sv
`timescale 1ns / 1ps

import axi_vip_pkg::*;
import design_1_axi_vip_0_0_pkg::*;

module tb_axi();
reg rst, clk, run;
parameter int RST_WAIT = 77;
parameter int CLK_WAIT = 10;

task clk_gen();
  clk = 0;
  forever #(CLK_WAIT/2) clk = ~clk;
endtask

task rst_gen();
  rst = 1;
  #(RST_WAIT);
  rst = 0;
endtask

design_1_wrapper DUT(
  .RUN(run),
  .CLK(clk),
  .RSTN(~rst)
);

design_1_axi_vip_0_0_slv_t agent;
task init_agent();
  agent = new("design_1_axi_vip_0_0_slv", DUT.design_1_i.axi_vip_0.inst.IF);
  agent.start_slave();
endtask


initial begin
  fork
    clk_gen();
    init_agent();
  join_none
  run <= 0;
  rst_gen();
  #100
  run <= 1;
  #CLK_WAIT
  run <= 0;

  #(CLK_WAIT * 10);
  $finish();
end
endmodule

まずインポートパッケージについて

import axi_vip_pkg::*;
import design_1_axi_vip_0_0_pkg::*;

axi_vip_pkg::*は固定?design_1_axi_vip_0_0_pkg::*はデザインごとに異なるので適宜設定。
IP Sourcesの

にソースコードがあるのでそれを指定すればいい。
ちなみにこのパッケージに

design_1_axi_vip_0_0_pkg.sv
///////////////////////////////////////////////////////////////////////////
// How to start the verification component
///////////////////////////////////////////////////////////////////////////
//      design_1_axi_vip_0_0_slv_t  design_1_axi_vip_0_0_slv;
//      initial begin : START_design_1_axi_vip_0_0_SLAVE
//        design_1_axi_vip_0_0_slv = new("design_1_axi_vip_0_0_slv", `design_1_axi_vip_0_0_PATH_TO_INTERFACE);
//        design_1_axi_vip_0_0_slv.start_slave();
//      end

のようにVIPの起動方法が書いてある。
テストベンチはrunを上げているだけ。

実行結果は

こんな感じで、runの立ち上げ理から3クロック後にvalid/readyが全部上がってバースト転送開始している。転送アドレスは0x40000000で、これは単なる初期値なので、指定したいときはほかの入力が必要。転送アドレスは1ワードずつ増加するのではなく、バースト長単位=8*32/8ずつ増加してる。
書き込むデータは1, 2,...という感じで、これも自動生成なので、実際使うときは与えないといけない。

ここまでやって気づいたが、マスタをテストするときはVIPじゃなくてAXI-BRAMでいいのでは?

AXI BRAMによるテスト

こんな感じ

で単にAXI BRAMと自作AXIをつなげる。コントローラはインタフェースを1にして幅32ビット。
自作AXIはRUN_RとRUN_Wでそれぞれバーストリード/ライトを開始するようにしている。

これは、

my_AXI_v1_0_M00_AXI.sv
  //implement master command interface state machine

  always @ ( posedge M_AXI_ACLK)
  begin
    if (M_AXI_ARESETN == 1'b0 )
      begin
	// reset condition
	// All the signals are assigned default values under reset condition
	mst_exec_state      <= IDLE;
	start_single_burst_write <= 1'b0;
	start_single_burst_read  <= 1'b0;
	ERROR <= 1'b0;
      end
    else
      begin

	// state transition
	case (mst_exec_state)

	  IDLE:
	    // This state is responsible to wait for user defined C_M_START_COUNT
	    // number of clock cycles.
	    if ( init_txn_pulse == 1'b1)
	      begin
		mst_exec_state  <= INIT_WRITE;
	      end
			else if ( init_rxn_pulse == 1'b1)
	      begin
		mst_exec_state  <= INIT_READ;
	      end
	    else
	      begin
		mst_exec_state  <= IDLE;
	      end

	  INIT_WRITE:
	    // This state is responsible to issue start_single_write pulse to
	    // initiate a write transaction. Write transactions will be
	    // issued until burst_write_active signal is asserted.
	    // write controller
	    if (writes_done)
	      begin
		mst_exec_state <= IDLE;//
	      end
	    else
	      begin
		mst_exec_state  <= INIT_WRITE;

		if (~axi_awvalid && ~start_single_burst_write && ~burst_write_active)
		  begin
		    start_single_burst_write <= 1'b1;
		  end
		else
		  begin
		    start_single_burst_write <= 1'b0; //Negate to generate a pulse
		  end
	      end

	  INIT_READ:
	    // This state is responsible to issue start_single_read pulse to
	    // initiate a read transaction. Read transactions will be
	    // issued until burst_read_active signal is asserted.
	    // read controller
	    if (reads_done)
	      begin
		mst_exec_state <= IDLE;
	      end
	    else
	      begin
		mst_exec_state  <= INIT_READ;

		if (~axi_arvalid && ~burst_read_active && ~start_single_burst_read)
		  begin
		    start_single_burst_read <= 1'b1;
		  end
	       else
		 begin
		   start_single_burst_read <= 1'b0; //Negate to generate a pulse
		 end
	      end
	  default :
	    begin
	      mst_exec_state  <= IDLE;
	    end
	endcase
      end
  end //MASTER_EXECUTION_PROC

のように、テンプレートのステートマシン記述を書き換えることで実現している。
あと比較機能とか、不要と思われる機能はテンプレートからなるべく排除している。

テストベンチは

tb_axi_bram.sv
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: 
// Engineer: 
// 
// Create Date: 05/29/2023 05:47:25 PM
// Design Name: 
// Module Name: tb_axi_bram
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////


module tb_axi_bram();
reg rst, clk, run_r, run_w;
parameter int RST_WAIT = 77;
parameter int CLK_WAIT = 10;

task clk_gen();
  clk = 0;
  forever #(CLK_WAIT/2) clk = ~clk;
endtask

task rst_gen();
  rst = 1;
  #(RST_WAIT);
  rst = 0;
endtask

design_2_wrapper DUT(
  .RUN_R(run_r),
  .RUN_W(run_w),
  .CLK(clk),
  .RSTN(~rst)
);

initial begin
  fork
    clk_gen();
  join_none
  run_w <= 0;
  run_r <= 0;
  rst_gen();
  #100
  run_w <= 1;
  #CLK_WAIT
  run_w <= 0;

  #(CLK_WAIT * 30);
  run_r <= 1;
  #CLK_WAIT
  run_r <= 0;

  $finish();
end
endmodule

run_wとrun_rをアサートするだけ。

実行結果

書き込み


run_wがアサートされると、awvalid/ready, wvalid/readyが立ち上がり、アドレス0x40000000に対してバーストライトが開始される。アドレス+0x20されているのではバースト単位で変化する模様。wdataはテンプレートで自動生成されており、1からインクリメントされている。
[0], [1], [2], [3]となっている部分がBRAMの生データで、1, 2, 3, 4となってるのでちゃんと動いてそうである。

読み出し

続いて読み出しをテスト
多倍長を扱いたいので、32ビット8ワードを256ビットレジスタに入れる。テンプレート中にM_AXI_RVALIDのif文があるので

my_AXI_v1_0_M00_AXI.sv
/*                                                                      
 The Read Data channel returns the results of the read request          

 In this example the data checker is always able to accept              
 more data, so no need to throttle the RREADY signal                    
 */               
  reg [255:0] my_reg;                                                      
  always @(posedge M_AXI_ACLK)                                          
  begin                                                                 
    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 )                  
      begin                                                             
	axi_rready <= 1'b0;      
	my_reg <= 0;     
      end                                                               
    // accept/acknowledge rdata/rresp with axi_rready by the master     
    // when M_AXI_RVALID is asserted by slave                           
    else if (M_AXI_RVALID)                       
      begin      
	 my_reg[32*read_index+:32] <= M_AXI_RDATA;                    
	 if (M_AXI_RLAST && axi_rready)          
	  begin                                  
	    axi_rready <= 1'b0;                  
	  end                                    
	 else                                    
	   begin                                 
	     axi_rready <= 1'b1;                 
	   end                                   
      end                                        
    // retain the previous value                 
  end        

こんな感じでRVALIDの時にmy_regの該当indexに32ビットを格納している。


実行結果はこのように、run_rアサート後にアドレス0x40000000からバーストリードが開始されている。これは先ほどBRAMに書き込んだアドレスなので、1, 2, 3,...がBRAMから読み出され、256ビットのmy_regが最終的に0x000000080000000700000006...になってるのでちゃんと動いてるっぽい。

感想

AXI-FULLも意外と簡単に動かせる。いや、本当はページ境界とか考えといけないらしいがまあ動けばいっかの精神。
テストではアドレスとデータはテンプレートが生成していたので、実際利用するときはそれらを外から持ってきて、データ準備できたらRUNをアサートするようにすればよい

Discussion