Verilog 代碼設(shè)計(jì)完成后,還需要進(jìn)行重要的步驟,即邏輯功能仿真。仿真激勵(lì)文件稱之為 testbench,放在各設(shè)計(jì)模塊的頂層,以便對(duì)模塊進(jìn)行系統(tǒng)性的例化調(diào)用進(jìn)行仿真。
毫不夸張的說,對(duì)于稍微復(fù)雜的 Verilog 設(shè)計(jì),如果不進(jìn)行仿真,即便是經(jīng)驗(yàn)豐富的老手,99.9999% 以上的設(shè)計(jì)都不會(huì)正常的工作。不能說仿真比設(shè)計(jì)更加的重要,但是一般來說,仿真花費(fèi)的時(shí)間會(huì)比設(shè)計(jì)花費(fèi)的時(shí)間要多。有時(shí)候,考慮到各種應(yīng)用場景,testbench 的編寫也會(huì)比 Verilog 設(shè)計(jì)更加的復(fù)雜。所以,數(shù)字電路行業(yè)會(huì)具體劃分設(shè)計(jì)工程師和驗(yàn)證工程師。
下面,對(duì) testbench 做一個(gè)簡單的學(xué)習(xí)。
testbench 一般結(jié)構(gòu)如下:
其實(shí) testbench 最基本的結(jié)構(gòu)包括信號(hào)聲明、激勵(lì)和模塊例化。
根據(jù)設(shè)計(jì)的復(fù)雜度,需要引入時(shí)鐘和復(fù)位部分。當(dāng)然更為復(fù)雜的設(shè)計(jì),激勵(lì)部分也會(huì)更加復(fù)雜。根據(jù)自己的驗(yàn)證需求,選擇是否需要自校驗(yàn)和停止仿真部分。
當(dāng)然,復(fù)位和時(shí)鐘產(chǎn)生部分,也可以看做激勵(lì),所以它們都可以在一個(gè)語句塊中實(shí)現(xiàn)。也可以拿自校驗(yàn)的結(jié)果,作為結(jié)束仿真的條件。
實(shí)際仿真時(shí),可以根據(jù)自己的個(gè)人習(xí)慣來編寫 testbench,這里只是做一份個(gè)人的總結(jié)。
前面的章節(jié)中,已經(jīng)寫過很多的 testbench。其實(shí)它們的結(jié)構(gòu)也都大致相同。
下面,我們舉一個(gè)數(shù)據(jù)拼接的簡單例子,對(duì) testbench 再做一個(gè)具體的分析。
一個(gè) 2bit 數(shù)據(jù)拼接成 8bit 數(shù)據(jù)的功能模塊描述如下:
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 ; //數(shù)據(jù)計(jì)數(shù)
data_r <= {data_r[5:0], din} ; //數(shù)據(jù)拼接
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
//計(jì)數(shù)為 3 且第 4 個(gè)數(shù)據(jù)輸入時(shí),同步輸出數(shù)據(jù)輸出使能信號(hào)
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變量,而是用相關(guān)寄存器對(duì)其進(jìn)行assign賦值
assign dout_en = dout_en_r;
endmodule
對(duì)應(yīng)的 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
仿真結(jié)果如下。由圖可知,數(shù)據(jù)整合功能的設(shè)計(jì)符合要求:
testbench 模塊聲明時(shí),一般不需要聲明端口。因?yàn)榧?lì)信號(hào)一般都在 testbench 模塊內(nèi)部,沒有外部信號(hào)。
聲明的變量應(yīng)該能全部對(duì)應(yīng)被測試模塊的端口。當(dāng)然,變量不一定要與被測試模塊端口名字一樣。但是被測試模塊輸入端對(duì)應(yīng)的變量應(yīng)該聲明為 reg 型,如 clk,rstn 等,輸出端對(duì)應(yīng)的變量應(yīng)該聲明為 wire 型,如 dout,dout_en。
生成時(shí)鐘的方式有很多種,例如以下兩種生成方式也可以借鑒。
initial clk = 0 ;
always #(CYCLE_200MHz/2) clk = ~clk;
initial begin
clk = 0 ;
forever begin
#(CYCLE_200MHz/2) clk = ~clk;
end
end
需要注意的是,利用取反方法產(chǎn)生時(shí)鐘時(shí),一定要給 clk 寄存器賦初值。
利用參數(shù)的方法去指定時(shí)間延遲時(shí),如果延時(shí)參數(shù)為浮點(diǎn)數(shù),該參數(shù)不要聲明為 parameter 類型。例如實(shí)例中變量 CYCLE_200MHz 的值為 2.5。如果其變量類型為 parameter,最后生成的時(shí)鐘周期很可能就是 4ns。當(dāng)然,timescale 的精度也需要提高,單位和精度不能一樣,否則小數(shù)部分的時(shí)間延遲賦值也將不起作用。
復(fù)位邏輯比較簡單,一般賦初值為 0,再經(jīng)過一段小延遲后,復(fù)位為 1 即可。
這里大多數(shù)的仿真都是用的低有效復(fù)位。
激勵(lì)部分該產(chǎn)生怎樣的輸入信號(hào),是根據(jù)被測模塊的需要來設(shè)計(jì)的。
本次實(shí)例中:
利用系統(tǒng)任務(wù) ?$fread
? ,通過句柄信號(hào) ?fd_rd
? 將讀取的 16bit 數(shù)據(jù)變量送入到 ?read_temp
? 緩存。
輸入數(shù)據(jù)文件前幾個(gè)數(shù)據(jù)截圖如下。因?yàn)?nbsp;?$fread
? 只能讀取 2 進(jìn)制文件,所以輸入文件的第一行對(duì)應(yīng)的 ASCII 碼應(yīng)該是 330a,所以我們想要得到文件里的數(shù)據(jù) 3,應(yīng)該取變量 ?read_temp
? 的第 9 到第 8bit 位的數(shù)據(jù)。
信號(hào) ?data_in_temp
? 是對(duì)輸入數(shù)據(jù)信號(hào)的一個(gè)緊隨的整合,后面校驗(yàn)?zāi)K會(huì)以此為參考,來判斷仿真是否正常,模塊設(shè)計(jì)是否正確。
當(dāng)數(shù)據(jù)量相對(duì)較少時(shí),可以利用 Verilog 中的系統(tǒng)任務(wù) ?$readmemh
? 來按行直接讀取 16 進(jìn)制數(shù)據(jù)。保持文件 ?data_in.dat
? 內(nèi)數(shù)據(jù)和格式不變,則該激勵(lì)部分可以描述為:
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
這里利用 testbench 開始聲明的信號(hào)變量,對(duì)被測試模塊進(jìn)行例化連接。
如果設(shè)計(jì)比較簡單,完全可以通過輸入、輸出信號(hào)的波形來確定設(shè)計(jì)是否正確,此部分完全可以刪除。如果數(shù)據(jù)很多,有時(shí)候拿肉眼觀察并不能對(duì)設(shè)計(jì)的正確性進(jìn)行一個(gè)有效判定。此時(shí)加入一個(gè)自校驗(yàn)?zāi)K,會(huì)大大增加仿真的效率。
實(shí)例中,我們會(huì)在數(shù)據(jù)輸出使能 ?dout_en
? 有效時(shí),對(duì)輸出數(shù)據(jù) dout 與參考數(shù)據(jù) ?read_temp
?(激勵(lì)部分產(chǎn)生)做一個(gè)對(duì)比,并將對(duì)比結(jié)果置于信號(hào) ?err_cnt
? 中。最后就可以通過觀察 ?err_cnt
? 信號(hào)是否為 0 來直觀的對(duì)設(shè)計(jì)的正確性進(jìn)行判斷。
當(dāng)然如實(shí)例中所示,我們也可以將數(shù)據(jù)寫入到對(duì)應(yīng)文件中,利用其他方式做對(duì)比。
如果我們不加入結(jié)束仿真部分,仿真就會(huì)無限制的運(yùn)行下去,波形太長有時(shí)候并不方便分析。Verilog 中提供了系統(tǒng)任務(wù) ?$finish
? 來停止仿真。
停止仿真之前,可以將自校驗(yàn)的結(jié)果,通過系統(tǒng)任務(wù) ?$display
? 在終端進(jìn)行顯示。
用于打開文件的系統(tǒng)任務(wù) ?$fopen
? 格式如下:
fd = $fopen("<name_of_file>", "mode")
和 C 語言類似,打開方式的選項(xiàng) ?"mode"
? 意義如下:
r | 只讀打開一個(gè)文本文件,只允許讀數(shù)據(jù)。 |
---|---|
w | 只寫打開一個(gè)文本文件,只允許寫數(shù)據(jù)。如果文件存在,則原文件內(nèi)容會(huì)被刪除。如果文件不存在,則創(chuàng)建新文件。 |
a | 追加打開一個(gè)文本文件,并在文件末尾寫數(shù)據(jù)。如果文件如果文件不存在,則創(chuàng)建新文件。 |
rb | 只讀打開一個(gè)二進(jìn)制文件,只允許讀數(shù)據(jù)。 |
wb | 只寫打開或建立一個(gè)二進(jìn)制文件,只允許寫數(shù)據(jù)。 |
ab | 追加打開一個(gè)二進(jìn)制文件,并在文件末尾寫數(shù)據(jù)。 |
r+ | 讀寫打開一個(gè)文本文件,允許讀和寫 |
w+ | 讀寫打開或建立一個(gè)文本文件,允許讀寫。如果文件存在,則原文件內(nèi)容會(huì)被刪除。如果文件不存在,則創(chuàng)建新文件。 |
a+ | 讀寫打開一個(gè)文本文件,允許讀和寫。如果文件不存在,則創(chuàng)建新文件。讀取文件會(huì)從文件起始地址的開始,寫入只能是追加模式。 |
rb+ | 讀寫打開一個(gè)二進(jìn)制文本文件,功能與 "r+" 類似。 |
wb+ | 讀寫打開或建立一個(gè)二進(jìn)制文本文件,功能與 "w+" 類似。 |
ab+ | 讀寫打開一個(gè)二進(jìn)制文本文件,功能與 "a+" 類似。 |
點(diǎn)擊這里下載源碼
更多建議: