ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

簡単に作るVerilogテストベンチ

 人が作ったVerilogソースや、書き立てほやほやのVerilogソースを簡単にテストしたい時ってありますよね。OpenCoresで公開されているEthmacを使って、簡単にテストベンチを作る方法をまとめてみました。目標はできるだけ楽に動作を確認することですが、初心者向けに一歩ずつテストベンチを動かしていく方法にもなっています。ここで作るテストベンチは、正式な(本格的な)テストまでのつなぎとして使うような物を想定しています。テストベンチを書くための文法などは、他のサイトや書籍を参考にしてください。

Ethmacの入手先はここです。
http://opencores.org/project,ethmac
シミュレータはVeritakを使用しています。

DUTとは

 最初に言葉の説明を。テストベンチを書くときに、テストの対象となるモジュールをDUT(Device Design Under Test)と呼びます。俺たちがやっているのはテストじゃ無いんだ検証なんだよ!と言い張る人達は、DUTではなくDUV(Device Design under Verification)と呼んだりしますが、同じ意味です。DUTという言葉はできあがった半導体にも使う言葉なので、かなり広い意味で使います。ここでは、動かして動作を確認するVerilogのmoduleという意味で使います。

テストベンチに必要なregとwireの定義

 テストベンチを作る時は、まずportの無いモジュールを宣言しますとか、timescaleがうんたらかんたらは、本に書いてあるとおもうので省略します。次にテストベンチで使用するregとwireの宣言を行います。これはDUTのポート宣言から機械的に行う事ができます。DUTは、eth_top.vのeth_topなので、ethmacのポート宣言から、regとwireの宣言を作ります。

eth_top.vのポート宣言(一部)

// WISHBONE common
input           wb_clk_i;     // WISHBONE clock
input           wb_rst_i;     // WISHBONE reset
input   [31:0]  wb_dat_i;     // WISHBONE data input
output  [31:0]  wb_dat_o;     // WISHBONE data output
output          wb_err_o;     // WISHBONE error output

 inputをregに、outputをwireに置換します。DUTの入力は値を保持する必要があるのでreg宣言、DUTの出力は繋がっていればよいのでwire宣言です。エディタの置換で一気にやってしまいましょう。

   reg           wb_clk_i;     // WISHBONE clock
   reg           wb_rst_i;     // WISHBONE reset
   reg [31:0]    wb_dat_i;     // WISHBONE data input
   wire [31:0]   wb_dat_o;     // WISHBONE data output
   wire          wb_err_o;     // WISHBONE error output

信号の接続

 次にDUTのインスタンシエーションと信号の接続行います。これも機械的に行います。まず、DUTのインスタシエーション eth_top u0 を書いた後に、eth_topのポート宣言をコピーします。

   eth_top u0
     (
      // WISHBONE common
      wb_clk_i, wb_rst_i, wb_dat_i, wb_dat_o, 
      
      // WISHBONE slave
      wb_adr_i, wb_sel_i, wb_we_i, wb_cyc_i, wb_stb_i, wb_ack_o, wb_err_o, 

 これに上で宣言したreg、wireを接続すれば良いので、wb_clk_i,を、.wb_clk_i(wb_clk_i),のように変換していきます。信号名の前に.を付け、,を(),に変え、()の中に同じ信号名を入れます。
 全てを接続するとこんな感じですね。これで全ての信号が正しい方向、正しいビット幅で接続されます。

   eth_top u0
     (
      .wb_clk_i(wb_clk_i), .wb_rst_i(wb_rst_i), .wb_dat_i(wb_dat_i), .wb_dat_o(wb_dat_o), 
      .wb_adr_i(wb_adr_i), .wb_sel_i(wb_sel_i), .wb_we_i(wb_we_i), .wb_cyc_i(wb_cyc_i), 
      .wb_stb_i(wb_stb_i), .wb_ack_o(wb_ack_o), .wb_err_o(wb_err_o), 

 Emacsのマクロを使って処理した記憶がありますが、しょせんモジュール一つなので手で変換していってもそんな手間ではありません。

重要:信号の接続まではDUTのポート宣言から機械的に行う。

いったんコンパイル

 全ての信号が接続された時点で、一度シミュレーションを実行しましょう。ファイルが足りないとか、XXXの定義がされていないとか、そういうのはこの辺りで一度確認しておくと後が楽です。実績があると聞いたRTLが、そもそも文法エラーでどうしようもないってことも、よく極まれにあります。
 eth_topに対してシミュレーションをしてみると、こんな表示が出てきました。

------------- シミュレーションを開始します。--------------------

          *********************************************
          =============================================
          eth_top.v will be removed shortly.
          Please use ethmac.v as top level file instead
          =============================================
          *********************************************

---------- シミュレーションを終了します。time=0----------

 ドキュメントには、eth_top.vがトップと書いてありますが、今はethmac.vがトップのようですね。こういう事は、よく極まれにあります。

重要:信号が接続できたら1回動かす。

クロックを作る

DUTとテストベンチに文法エラーが無いことが確認出来たらクロックを作ります。ethmacでは、Wishboneのクロックと、送信用受信用のクロックの3種類のクロックが必要です。クロックを入れないとリセットシーケンスが動かない事もあるので、今は必要が無いと感じるクロックも一応入れておきます。Wishboneのクロックは100MHz、送受信用のクロックは125MHzにします。クロックの宣言と周波数の設定ができたら、initial 文を作りクロックを初期化します。今回はWishboneしか動かす気が無いのでinitial文は一つですが、普通はクロック単位でinitial文を書きます。

   // clkの生成
   parameter WB_PERIOD = 10.0; 
   parameter TX_PERIOD =  8.0;
   parameter RX_PERIOD =  8.0;
   
   always # (WB_PERIOD/2) wb_clk_i = ~wb_clk_i;
   always # (TX_PERIOD/2) mtx_clk_pad_i = ~mtx_clk_pad_i;
   always # (RX_PERIOD/2) mrx_clk_pad_i = ~mrx_clk_pad_i;

   initial begin
      #1 wb_clk_i = 1; mtx_clk_pad_i = 1; mrx_clk_pad_i = 0;
      
      #1000 $finish;
   end

 initial 文の最初に #1 の遅延を入れているのは、時刻0の動作がシミュレータによって違うため、互換性を取るために入れています。(厳密には、クロックではなく後で出てくる信号に対して遅延を入れたいので、この時点では#1は特に意味はありません)
mtx_clk_pad_i = 1; mrx_clk_pad_i = 0; と、初期値を変えてあるのは、同じ周波数で位相の違うクロックであることを明確にするためです。ここで手を抜くと、受信回路が送信クロックで動いていたとか、クロックの載せ替え回路にバグがあるのに完璧に動くとか、シミュレータでは動いているのに実機に行ったら全く動かない回路を作ってしまいます。お手軽テストベンチでも、ここだけはケアしておきましょう。

重要:源振が違う同じ周波数のクロックは、シミュレータ上でも位相を明確にずらす。

リセットを入れる

 シミュレータでクロックが動いていることが確認出来たら、DUTの入力の値を設定し、リセット信号を入れましょう。DUTの入力は、上の方でreg宣言しているので、そこをコピーして持ってきて、ビット幅は気にせずにとりあえず全部0に。
 リセットに関しては正論理なのでL→H→Lと遷移させます。回路によっては、L→Hの立ち上がりでリセットがかかる可能性も有るので、正論理だからといっていきなりHを入れるのは時々はまります。これで大半の出力信号はX(不定)では無くなるはずです。

   initial begin
	  #1 	
 	    wb_clk_i = 1; mtx_clk_pad_i = 1; mrx_clk_pad_i = 0;
	  wb_rst_i = 0; wb_dat_i = 0; wb_adr_i = 0; wb_sel_i = 0;
	  wb_we_i = 0; wb_cyc_i = 0; wb_stb_i = 0;
	  m_wb_dat_i = 0; m_wb_ack_i = 0; m_wb_err_i = 0;

	  mrxd_pad_i = 0; mrxdv_pad_i = 0; mrxerr_pad_i = 0; 
	  mcoll_pad_i = 0; mcrs_pad_i = 0; md_pad_i = 0;

	  //リセット信号のL→H→Lの遷移	  
	  # (WB_PERIOD * 3)  wb_rst_i = 1; 
	  # (WB_PERIOD * 5)  wb_rst_i = 0; 

      
      # (WB_PERIOD * 100) $finish;
      
   end

重要:リセット信号は、必ず非リセットの状態から変化させる。

レジスタに書き込もう

 できればライト動作の前にリードを行いたいのですが、ethmacにはテストに使えそうな初期値が読み出せるレジスタが無いので、ライトから動作確認してみます。アドレス0x40にMAC_ADDR0という32bit自由に書き込めるレジスタがあるので、そこに適当な値を書き込んでみます。先ほどのリセット信号の後に、このような記述を追加します。DUTに入力される信号がWishbone のバスサイクルの図と同じになるように変化させます。読み書きはfunctionやtaskにした方が・・・とか、BFM(bus function model)使え!というのは、次のステップです。まずはDUTが正しくR/Wに応答しているかを確認してから、少しずつ機能を足していきましょう。

      //write 0x40(MAC_ADDR0) 
      # (WB_PERIOD) 
      wb_dat_i = 32'h12345678; wb_adr_i = 10'h010; 
      wb_sel_i = 4'b1111;
      wb_we_i = 1; wb_cyc_i = 1; wb_stb_i = 1;
      # (WB_PERIOD * 2) 
      wb_dat_i = 0; wb_adr_i = 0; 
      wb_sel_i = 0;
      wb_we_i = 0; wb_cyc_i = 0; wb_stb_i = 0;

レジスタにライトできているかどうかは、シミュレータで中身を表示させてみれば分かります。合わせて、Wishboneの信号の動きも確認しておきましょう。

レジスタを読もう

次に同じ所を読み出してみましょう。

    //read 0x40(MAC_ADDR0)
      # (WB_PERIOD * 2) 
      wb_adr_i = 10'h010; 
      wb_sel_i = 4'b1111;
      wb_we_i = 0; wb_cyc_i = 1; wb_stb_i = 1;
      # (WB_PERIOD * 2) 
	  $display("READ:%02h %08h", wb_adr_i, wb_dat_o);
      wb_dat_i = 0; wb_adr_i = 0; 
      wb_sel_i = 0;
      wb_we_i = 0; wb_cyc_i = 0; wb_stb_i = 0;

実行するとこのように表示され、レジスタが読めていることが分かります。

READ:010 12345678
Info: $finishコマンドを実行します。time=1151

この後は?

 これでethmacレジスタのR/Wが出来ていることは分かりました。この後は、ethmacに接続されるPHYを追加し、Wishbone側はBFMに変更し、Xilixのメモリを使うオプションを有効にするなど、少しずつ規模を大きくシミュレーションを正確にしていきます。ここまで動いていれば、実機に持って行ってパソコンやマイコンボードからレジスタR/Wできる仕組みを作っても良いと思います。

まとめ

大事なところをまとめました。

  • 信号の接続まではDUTのポート宣言から機械的に行う。
  • 信号が接続できたら1回動かす。
  • 源振が違う同じ周波数のクロックは、シミュレータ上でも位相を明確にずらす。
  • リセット信号は、必ず非リセットの状態から変化させる。

最終的なテストベンチ

// OpenCoresで公開されているEthermac用のテストベンチ
// 
// Eathermacプロジェクト  
// http://opencores.org/project,ethmac


// timescaleの設定
// `timescale 1ns/1ps

`include "ethmac_defines.v"
`include "timescale.v"


module ethermac_tb ();

   // WISHBONE slave
   reg           wb_clk_i;     // WISHBONE clock
   reg           wb_rst_i;     // WISHBONE reset
   reg [31:0]    wb_dat_i;     // WISHBONE data input
   wire [31:0]   wb_dat_o;     // WISHBONE data output
   wire          wb_err_o;     // WISHBONE error output
   
   reg [11:2]    wb_adr_i;     // WISHBONE address reg
   reg [3:0] 	   wb_sel_i;     // WISHBONE byte select input
   reg           wb_we_i;      // WISHBONE write enable input
   reg           wb_cyc_i;     // WISHBONE cycle input
   reg           wb_stb_i;     // WISHBONE strobe input
   wire          wb_ack_o;     // WISHBONE acknowledge output
   
   // WISHBONE master
   wire [31:0]   m_wb_adr_o;
   wire [3:0]    m_wb_sel_o;
   wire          m_wb_we_o;
   reg [31:0]    m_wb_dat_i;
   wire [31:0]   m_wb_dat_o;
   wire          m_wb_cyc_o;
   wire          m_wb_stb_o;
   reg           m_wb_ack_i;
   reg           m_wb_err_i;

   wire [2:0]  m_wb_cti_o;   // Cycle Type Identifier
   wire [1:0]  m_wb_bte_o;   // Burst Type Extension
   
   // Tx
   reg 	 mtx_clk_pad_i; // Transmit clock (from PHY)
   wire [3:0]  mtxd_pad_o;    // Transmit nibble (to PHY)
   wire 	 mtxen_pad_o;   // Transmit enable (to PHY)
   wire 	 mtxerr_pad_o;  // Transmit error (to PHY)
   
   // Rx
   reg 	 mrx_clk_pad_i; // Receive clock (from PHY)
   reg [3:0] 	 mrxd_pad_i;    // Receive nibble (from PHY)
   reg 	 mrxdv_pad_i;   // Receive data valid (from PHY)
   reg 	 mrxerr_pad_i;  // Receive data error (from PHY)
   
   // Common Tx and Rx
   reg 	 mcoll_pad_i;   // Collision (from PHY)
   reg 	 mcrs_pad_i;    // Carrier sense (from PHY)
   
   // MII Management interface
   reg 	 md_pad_i;      // MII data input (from I/O cell)
   wire 	 mdc_pad_o;     // MII Management data clock (to PHY)
   wire 	 md_pad_o;      // MII data output (to I/O cell)
   wire 	 md_padoe_o;    // MII data output enable (to I/O cell)
   
   wire 	 int_o;         // Interrupt output
   
   //DUT
   ethmac u0
     (
      .wb_clk_i(wb_clk_i), .wb_rst_i(wb_rst_i), .wb_dat_i(wb_dat_i), .wb_dat_o(wb_dat_o), 
      .wb_adr_i(wb_adr_i), .wb_sel_i(wb_sel_i), .wb_we_i(wb_we_i), .wb_cyc_i(wb_cyc_i), 
      .wb_stb_i(wb_stb_i), .wb_ack_o(wb_ack_o), .wb_err_o(wb_err_o), 
      .m_wb_adr_o(m_wb_adr_o), .m_wb_sel_o(m_wb_sel_o), .m_wb_we_o(m_wb_we_o), 
      .m_wb_dat_o(m_wb_dat_o), .m_wb_dat_i(m_wb_dat_i), .m_wb_cyc_o(m_wb_cyc_o), 
      .m_wb_stb_o(m_wb_stb_o), .m_wb_ack_i(m_wb_ack_i), .m_wb_err_i(m_wb_err_i), 
      .m_wb_cti_o(m_wb_cti_o), .m_wb_bte_o(m_wb_bte_o),

      .mtx_clk_pad_i(mtx_clk_pad_i), .mtxd_pad_o(mtxd_pad_o), .mtxen_pad_o(mtxen_pad_o), .mtxerr_pad_o(mtxerr_pad_o),
      .mrx_clk_pad_i(mrx_clk_pad_i), .mrxd_pad_i(mrxd_pad_i), .mrxdv_pad_i(mrxdv_pad_i), .mrxerr_pad_i(mrxerr_pad_i), .mcoll_pad_i(mcoll_pad_i), .mcrs_pad_i(mcrs_pad_i),      
      .mdc_pad_o(mdc_pad_o), .md_pad_i(md_pad_i), .md_pad_o(md_pad_o), .md_padoe_o(md_padoe_o),
      
      .int_o(int_o)
      );

   // clkの生成
   parameter WB_PERIOD = 10.0; 
   parameter TX_PERIOD =  8.0;
   parameter RX_PERIOD =  8.0;
   
   always # (WB_PERIOD/2) wb_clk_i = ~wb_clk_i;
   always # (TX_PERIOD/2) mtx_clk_pad_i = ~mtx_clk_pad_i;
   always # (RX_PERIOD/2) mrx_clk_pad_i = ~mrx_clk_pad_i;

   initial begin
      #1 	
    wb_clk_i = 1; mtx_clk_pad_i = 1; mrx_clk_pad_i = 0;
      wb_rst_i = 0; wb_dat_i = 0; wb_adr_i = 0; wb_sel_i = 0;
      wb_we_i = 0; wb_cyc_i = 0; wb_stb_i = 0;
      m_wb_dat_i = 0; m_wb_ack_i = 0; m_wb_err_i = 0;
      
      mrxd_pad_i = 0; mrxdv_pad_i = 0; mrxerr_pad_i = 0; 
      mcoll_pad_i = 0; mcrs_pad_i = 0; md_pad_i = 0;
      
      //Reset
      # (WB_PERIOD * 3)  wb_rst_i = 1; 
      # (WB_PERIOD * 5)  wb_rst_i = 0; 
      
      //write 0x40(MAC_ADDR0) 
      # (WB_PERIOD) 
      wb_dat_i = 32'h12345678; wb_adr_i = 10'h010; 
      wb_sel_i = 4'b1111;
      wb_we_i = 1; wb_cyc_i = 1; wb_stb_i = 1;
      # (WB_PERIOD * 2) 
      wb_dat_i = 0; wb_adr_i = 0; 
      wb_sel_i = 0;
      wb_we_i = 0; wb_cyc_i = 0; wb_stb_i = 0;
      
   //read 0x40(MAC_ADDR0)
      # (WB_PERIOD * 2) 
      wb_adr_i = 10'h010; 
      wb_sel_i = 4'b1111;
      wb_we_i = 0; wb_cyc_i = 1; wb_stb_i = 1;
      # (WB_PERIOD * 2) 
   $display("READ:%02h %08h", wb_adr_i, wb_dat_o);
      wb_dat_i = 0; wb_adr_i = 0; 
      wb_sel_i = 0;
      wb_we_i = 0; wb_cyc_i = 0; wb_stb_i = 0;
	  
	  
      # (WB_PERIOD * 100) $finish;
      
   end
   
endmodule // ethermac_tb