關鍵詞:testbench,仿真,文件讀寫

Verilog 代碼設計完成後,還需要進行重要的步驟,即邏輯功能仿真。仿真激勵文件稱之為 testbench,放在各設計模塊的頂層,以便對模塊進行係統性的例化調用進行仿真。

毫不誇張的說,對於稍微複雜的 Verilog 設計,如果不進行仿真,即便是經驗豐富的老手,99.9999% 以上的設計都不會正常的工作。不能說仿真比設計更加的重要,但是一般來說,仿真花費的時間會比設計花費的時間要多。有時候,考慮到各種應用場景,testbench 的編寫也會比 Verilog 設計更加的複雜。所以,數字電路行業會具體劃分設計工程師和驗證工程師。

下麵,對 testbench 做一個簡單的學習。

testbench 結構劃分

testbench 一般結構如下:

其實 testbench 最基本的結構包括信號聲明、激勵和模塊例化。

根據設計的複雜度,需要引入時鍾和複位部分。當然更為複雜的設計,激勵部分也會更加複雜。根據自己的驗證需求,選擇是否需要自校驗和停止仿真部分。

當然,複位和時鍾產生部分,也可以看做激勵,所以它們都可以在一個語句塊中實現。也可以拿自校驗的結果,作為結束仿真的條件。

實際仿真時,可以根據自己的個人習慣來編寫 testbench,這裏隻是做一份個人的總結。

testbench 仿真舉例

前麵的章節中,已經寫過很多的 testbench。其實它們的結構也都大致相同。

下麵,我們舉一個數據拚接的簡單例子,對 testbench 再做一個具體的分析。

一個 2bit 數據拚接成 8bit 數據的功能模塊描述如下:

實例

module  data_consolidation
    (
        input           clk ,
        input           rstn ,
        input [1:0]     din ,          //data in
        input           din_en ,
        output [7:0]    dout ,
        output          dout_en        //data out
     );

   // data shift and counter
    reg [7:0]            data_r ;
    reg [1:0]            state_cnt ;
    always @(posedge clk or negedge rstn) begin
        if (!rstn) begin
            state_cnt     <= 'b0 ;
            data_r        <= 'b0 ;
        end
        else if (din_en) begin
            state_cnt     <= state_cnt + 1'b1 ;    //數據計數
            data_r        <= {data_r[5:0], din} ;  //數據拚接
        end
        else begin
            state_cnt <= 'b0 ;
        end
    end
    assign dout          = data_r ;

    // data output en
    reg                  dout_en_r ;
    always @(posedge clk or negedge rstn) begin
        if (!rstn) begin
            dout_en_r       <= 'b0 ;
        end
        //計數為 3 且第 4 個數據輸入時,同步輸出數據輸出使能信號
        else if (state_cnt == 2'd3 & din_en) begin  
            dout_en_r       <= 1'b1 ;
        end
        else begin
            dout_en_r       <= 1'b0 ;
        end
    end
    //這裏不直接聲明dout_en為reg變量,而是用相關寄存器對其進行assign賦值
    assign dout_en       = dout_en_r;

endmodule

對應的 testbench 描述如下,增加了文件讀寫的語句:

實例

`timescale 1ns/1ps

   //============== (1) ==================
   //signals declaration
module test ;
    reg          clk;
    reg          rstn ;
    reg [1:0]    din ;
    reg          din_en ;
    wire [7:0]   dout ;
    wire         dout_en ;

    //============== (2) ==================
    //clock generating
    real         CYCLE_200MHz = 5 ; //
    always begin
        clk = 0 ; #(CYCLE_200MHz/2) ;
        clk = 1 ; #(CYCLE_200MHz/2) ;
    end

    //============== (3) ==================
    //reset generating
    initial begin
        rstn      = 1'b0 ;
        #8 rstn      = 1'b1 ;
    end

    //============== (4) ==================
    //motivation
    int          fd_rd ;
    reg [7:0]    data_in_temp ;  //for self check
    reg [15:0]   read_temp ;     //8bit ascii data, 8bit \n
    initial begin
        din_en    = 1'b0 ;        //(4.1)
        din       = 'b0 ;
        open_file("../tb/data_in.dat", "r", fd_rd); //(4.2)
        wait (rstn) ;    //(4.3)
        # CYCLE_200MHz ;

        //read data from file
        while (! $feof(fd_rd) ) begin  //(4.4)
            @(negedge clk) ;
            $fread(read_temp, fd_rd);
            din    = read_temp[9:8] ;
            data_in_temp = {data_in_temp[5:0], din} ;
            din_en = 1'b1 ;
        end

        //stop data
        @(posedge clk) ;  //(4.5)
        #2 din_en = 1'b0 ;
    end

    //open task
    task open_file;
        input string      file_dir_name ;
        input string      rw ;
        output int        fd ;

        fd = $fopen(file_dir_name, rw);
        if (! fd) begin
            $display("--- iii --- Failed to open file: %s", file_dir_name);
        end
        else begin
            $display("--- iii --- %s has been opened successfully.", file_dir_name);
        end
    endtask

    //============== (5) ==================
    //module instantiation
    data_consolidation    u_data_process
    (
      .clk              (clk),
      .rstn             (rstn),
      .din              (din),
      .din_en           (din_en),
      .dout             (dout),
      .dout_en          (dout_en)
     );

    //============== (6) ==================
    //auto check
    reg  [7:0]           err_cnt ;
    int                  fd_wr ;

    initial begin
        err_cnt   = 'b0 ;
        open_file("../tb/data_out.dat", "w", fd_wr);
        forever begin
            @(negedge clk) ;
            if (dout_en) begin
                $fdisplay(fd_wr, "%h", dout);
            end
        end
    end

    always @(posedge clk) begin
        #1 ;
        if (dout_en) begin
            if (data_in_temp != dout) begin
                err_cnt = err_cnt + 1'b1 ;
            end
        end
    end

    //============== (7) ==================
    //simulation finish
    always begin
        #100;
        if ($time >= 10000)  begin
            if (!err_cnt) begin
                $display("-------------------------------------");
                $display("Data process is OK!!!");
                $display("-------------------------------------");
            end
            else begin
                $display("-------------------------------------");
                $display("Error occurs in data process!!!");
                $display("-------------------------------------");
            end
            #1 ;
            $finish ;
        end
    end

endmodule // test

仿真結果如下。由圖可知,數據整合功能的設計符合要求:

testbench 具體分析

1)信號聲明

testbench 模塊聲明時,一般不需要聲明端口。因為激勵信號一般都在 testbench 模塊內部,沒有外部信號。

聲明的變量應該能全部對應被測試模塊的端口。當然,變量不一定要與被測試模塊端口名字一樣。但是被測試模塊輸入端對應的變量應該聲明為 reg 型,如 clk,rstn 等,輸出端對應的變量應該聲明為 wire 型,如 dout,dout_en。

2)時鍾生成

生成時鍾的方式有很多種,例如以下兩種生成方式也可以借鑒。

實例

initial clk = 0 ;
always #(CYCLE_200MHz/2) clk = ~clk;

initial begin
    clk = 0 ;
    forever begin
        #(CYCLE_200MHz/2) clk = ~clk;
    end
end      

需要注意的是,利用取反方法產生時鍾時,一定要給 clk 寄存器賦初值。

利用參數的方法去指定時間延遲時,如果延時參數為浮點數,該參數不要聲明為 parameter 類型。例如實例中變量 CYCLE_200MHz 的值為 2.5。如果其變量類型為 parameter,最後生成的時鍾周期很可能就是 4ns。當然,timescale 的精度也需要提高,單位和精度不能一樣,否則小數部分的時間延遲賦值也將不起作用。

3)複位生成

複位邏輯比較簡單,一般賦初值為 0,再經過一段小延遲後,複位為 1 即可。

這裏大多數的仿真都是用的低有效複位。

4)激勵部分

激勵部分該產生怎樣的輸入信號,是根據被測模塊的需要來設計的。

本次實例中:

  • (4.1) 對被測模塊的輸入信號進行一個初始化,防止不確定值 X 的出現。激勵數據的產生,我們需要從數據文件內讀入。
  • (4.2) 處利用一個 task 去打開一個文件,隻要指定文件存在,就可以得到一個不為 0 的句柄信號 fp_rd。fp_rd 指定了文件數據的起始地址。
  • (4.3) 的操作是為了等待複位後,係統有一個安全穩定的可測試狀態。
  • (4.4) 開始循環讀數據、給激勵。在時鍾下降沿送出數據,是為了被測試模塊能更好的在上升沿采樣數據。

利用係統任務 $fread ,通過句柄信號 fd_rd 將讀取的 16bit 數據變量送入到 read_temp 緩存。

輸入數據文件前幾個數據截圖如下。因為 $fread 隻能讀取 2 進製文件,所以輸入文件的第一行對應的 ASCII 碼應該是 330a,所以我們想要得到文件裏的數據 3,應該取變量 read_temp 的第 9 到第 8bit 位的數據。

信號 data_in_temp 是對輸入數據信號的一個緊隨的整合,後麵校驗模塊會以此為參考,來判斷仿真是否正常,模塊設計是否正確。

  • (4.5) 選擇在時鍾上升沿延遲 2 個周期後停止輸入數據,是為了被測試模塊能夠正常的采樣到最後一個數據使能信號,並對數據進行正常的整合。

當數據量相對較少時,可以利用 Verilog 中的係統任務 $readmemh 來按行直接讀取 16 進製數據。保持文件 data_in.dat 內數據和格式不變,則該激勵部分可以描述為:

實例

    reg [1:0]    data_mem [39:0] ;
    reg [7:0]    data_in_temp ;  //for self check
    integer      k1 ;
    initial begin
        din_en    = 1'b0 ;
        din       = 'b0 ;
        $readmemh("../tb/data_in.dat", data_mem);
        wait (rstn) ;
        # CYCLE_200MHz ;

        //read data from file
        for(k1=0; k1<40; k1=k1+1)  begin
            @(negedge clk) ;
            din    = data_mem[k1] ;
            data_in_temp = {data_in_temp[5:0], din} ;
            din_en = 1'b1 ;
        end

        //stop data
        @(posedge clk) ;
        #2 din_en = 1'b0 ;
     end

5)模塊例化

這裏利用 testbench 開始聲明的信號變量,對被測試模塊進行例化連接。

6)自校驗

如果設計比較簡單,完全可以通過輸入、輸出信號的波形來確定設計是否正確,此部分完全可以刪除。如果數據很多,有時候拿肉眼觀察並不能對設計的正確性進行一個有效判定。此時加入一個自校驗模塊,會大大增加仿真的效率。

實例中,我們會在數據輸出使能 dout_en 有效時,對輸出數據 dout 與參考數據 read_temp(激勵部分產生)做一個對比,並將對比結果置於信號 err_cnt 中。最後就可以通過觀察 err_cnt 信號是否為 0 來直觀的對設計的正確性進行判斷。

當然如實例中所示,我們也可以將數據寫入到對應文件中,利用其他方式做對比。

7)結束仿真

如果我們不加入結束仿真部分,仿真就會無限製的運行下去,波形太長有時候並不方便分析。Verilog 中提供了係統任務 $finish 來停止仿真。

停止仿真之前,可以將自校驗的結果,通過係統任務 $display 在終端進行顯示。

文件讀寫選項

用於打開文件的係統任務 $fopen 格式如下:

fd = $fopen("<name_of_file>", "mode")

和 C 語言類似,打開方式的選項 "mode" 意義如下:

r隻讀打開一個文本文件,隻允許讀數據。
w隻寫打開一個文本文件,隻允許寫數據。如果文件存在,則原文件內容會被刪除。如果文件不存在,則創建新文件。
a追加打開一個文本文件,並在文件末尾寫數據。如果文件如果文件不存在,則創建新文件。
rb隻讀打開一個二進製文件,隻允許讀數據。
wb隻寫打開或建立一個二進製文件,隻允許寫數據。
ab追加打開一個二進製文件,並在文件末尾寫數據。
r+讀寫打開一個文本文件,允許讀和寫
w+讀寫打開或建立一個文本文件,允許讀寫。如果文件存在,則原文件內容會被刪除。如果文件不存在,則創建新文件。
a+讀寫打開一個文本文件,允許讀和寫。如果文件不存在,則創建新文件。讀取文件會從文件起始地址的開始,寫入隻能是追加模式。
rb+讀寫打開一個二進製文本文件,功能與 "r+" 類似。
wb+讀寫打開或建立一個二進製文本文件,功能與 "w+" 類似。
ab+讀寫打開一個二進製文本文件,功能與 "a+" 類似。

源碼下載

Download