想象一下,你設計了一輛汽車(FPGA設計),在把它開上真正的馬路之前,你肯定要先在測試場地試一試對吧?TestBench就是這個"測試場地"!
在這個虛擬的測試場地裏,你可以:
- 自由控制輸入:比如模擬踩油門、剎車
- 觀察輸出結果:看看車速、轉向是否正常
- 發現潛在問題:在上路前就把Bug修好
❗ 為什麼不能直接下載到板子上測試?
很多初學者(包括我最開始)都犯過這個錯誤:
- 寫完代碼直接下載到FPGA板子
- 發現不對勁,再改代碼
- 再下載,再測試…
這樣做的問題:
- ⏰ 時間成本高:每次下載編譯要等很久
- 調試困難:板子上看不到內部信號
- 可能損壞硬件:嚴重的邏輯錯誤可能燒壞芯片
正確的流程應該是:
寫代碼 → 寫TestBench → 仿真測試 → 修改完善 → 下載到板子
一個完整的TestBench需要完成這四件事:
1️⃣ 實例化待測模塊(DUT)
DUT = Design Under Test(被測設計),就是把你要測試的模塊"召喚"出來
2️⃣ 編寫測試激勵
給模塊的輸入端口送信號,就像給汽車踩油門一樣
3️⃣ 觀察輸出結果
通過波形窗口或終端打印,看看輸出是否符合預期
4️⃣ 自動化驗證(進階)
讓程序自動比對實際輸出和預期輸出,發現不一致就報警
先來看一個最簡單的TestBench骨架:
`timescale 1ns/1ns // 時間單位和精度
module testbench; // TestBench模塊名(通常無輸入輸出)
// 1. 信號聲明
reg 輸入信號; // 輸入用reg類型
wire 輸出信號; // 輸出用wire類型
// 2. 實例化待測模塊
待測模塊名 實例名(
.端口1(信號1),
.端口2(信號2)
);
// 3. 生成時鐘信號(如果需要)
initial begin
clk = 0;
forever #10 clk = ~clk; // 每10個時間單位翻轉一次
end
// 4. 生成測試激勵
initial begin
// 初始化
輸入信號 = 0;
// 施加激勵
#20 輸入信號 = 1;
#30 輸入信號 = 0;
// 結束仿真
#100 $stop;
end
// 5. 監控輸出(可選)
initial begin
$monitor("時間=%t, 輸入=%b, 輸出=%b", $time, 輸入信號, 輸出信號);
end
endmodule
⏱️ 第一關:時間刻度(timescale)
timescale告訴仿真器:"1個時間單位"等於多長的真實時間。
`timescale 時間單位/時間精度
`timescale 10ns/1ns // 單位10ns,精度1ns
module testbench;
reg set;
initial begin
#1 set = 0; // 延時1個時間單位 = 10ns
#1.8 set = 1; // 延時1.8個時間單位 = 18ns
end
endmodule
解釋:
#1:延時1個時間單位 → 實際延時10ns#1.8:延時1.8個時間單位 → 實際延時18ns(會按精度1ns舍入)
⚠️ 重要提示
時間單位和精度的可選值:
- 只能是:
1、10、100 - 單位可以是:
s(秒)、ms(毫秒)、us(微秒)、ns(納秒)、ps(皮秒)、fs(飛秒) - 精度必須 ≤ 時間單位
實用建議:
`timescale 1ns/1ns // ✅ 推薦:ns級精度足夠大多數場景
`timescale 1ns/1ps // ❌ 不推薦:精度太高,仿真慢,通常用不到
時序邏輯電路必須有時鐘,TestBench中生成時鐘非常簡單!
方法一:使用forever循環(最常用)
parameter Period = 20; // 時鐘週期20ns
reg clk;
initial begin
clk = 0;
forever #(Period/2) clk = ~clk; // 每10ns翻轉一次
end
解釋:
Period = 20:時鐘週期20ns → 頻率50MHz#(Period/2):延時半個週期(10ns)後翻轉forever:一直循環,直到仿真結束
方法二:使用always(不推薦初學者)
parameter Period = 20;
reg clk;
initial clk = 0;
always #(Period/2) clk = ~clk;
**問題:**如果我想生成一個100MHz的時鐘,週期應該設置為多少?
點擊查看答案
// 100MHz → 週期 = 1/100MHz = 10ns
parameter Period = 10;
initial是TestBench的核心工具,用來編寫測試激勵。
- 只執行一次:從仿真開始時(時間0)執行
- 不可綜合:只用於仿真,不能下載到FPGA
- 可併發運行:可以寫多個
initial塊,它們同時開始執行 - 塊內順序執行:一個
initial塊內部的語句按順序執行
reg reset, enable;
wire [7:0] data_out;
initial begin
// 初始化信號
reset = 1;
enable = 0;
// 復位10ns
#10 reset = 0;
// 100ns後使能
#100 enable = 1;
// 200ns後停止仿真
#200 $stop;
end
// 塊1:生成時鐘
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 塊2:生成復位
initial begin
reset = 1;
#15 reset = 0;
end
// 塊3:測試激勵
initial begin
data_in = 8'h00;
#30 data_in = 8'hAA;
#50 data_in = 8'h55;
#100 $finish;
end
時序圖示意:
時間: 0 5 10 15 20 25 30 ...
clk: _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_...
reset: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|________________...
data_in: 00 | AA ...
️ 第四關:常用系統函數
系統函數都以$開頭,是仿真器提供的特殊功能。
1. $finish vs $stop
|
函數
|
功能
|
使用場景
|
|
|
停止仿真並退出
|
測試完成
|
|
|
暫停仿真
|
需要手動檢查某個狀態
|
initial begin
// ... 測試代碼 ...
wait(data_ready); // 等待數據準備好
$stop; // 暫停,可以手動查看波形
end
2. $display - 打印信息
initial begin
$display("仿真開始!");
// 打印變量值
$display("時間=%t, a=%b, b=%d", $time, signal_a, signal_b);
// 自動換行
$display("Hello");
$display("World"); // 會在下一行顯示
end
格式説明符:
%t:時間(配合realtime)
%b:二進制%d:十進制%h:十六進制%o:八進制%c:ASCII字符%s:字符串
3. $monitor - 監控信號變化
**特點:**只在信號變化時自動打印
initial begin
$monitor("時間=%t, clk=%b, data=%h", $time, clk, data);
end
對比:
// $display:每次都打印
always @(posedge clk) begin
$display("clk上升沿"); // 每個時鐘都打印
end
// $monitor:只在變化時打印
initial begin
$monitor("data=%d", data); // data變化時才打印
end
4. $timeformat - 格式化時間顯示
initial begin
// 以ns為單位,小數點後1位,後綴"ns",最小寬度12
$timeformat(-9, 1, "ns", 12);
$monitor("%t: clk=%b", $realtime, clk);
// 輸出: " 10.5ns: clk=1"
end
參數説明:
- 第1個參數:單位(0=秒, -3=毫秒, -6=微秒, -9=納秒, -12=皮秒)
- 第2個參數:小數位數
- 第3個參數:單位後綴字符串
- 第4個參數:最小顯示寬度
現在我們通過一個完整的例子,把所有知識點串起來!
功能説明:
- 5位移位寄存器
reset=1時清零load=1時加載數據- 根據
sel選擇移位方向
module shift_reg(
input clock,
input reset,
input load,
input [1:0] sel,
input [4:0] data,
output reg [4:0] shiftreg
);
always @(posedge clock) begin
if (reset)
shiftreg <= 5'b00000;
else if (load)
shiftreg <= data;
else begin
case (sel)
2'b00: shiftreg <= shiftreg; // 保持不變
2'b01: shiftreg <= shiftreg << 1; // 左移
2'b10: shiftreg <= shiftreg >> 1; // 右移
default: shiftreg <= shiftreg;
endcase
end
end
endmodule
`timescale 1ns/1ns
module testbench;
// 1. 信號聲明
reg clock;
reg reset;
reg load;
reg [1:0] sel;
reg [4:0] data;
wire [4:0] shiftreg;
// 2. 實例化待測模塊
shift_reg dut (
.clock(clock),
.reset(reset),
.load(load),
.sel(sel),
.data(data),
.shiftreg(shiftreg)
);
// 3. 生成時鐘信號(週期100ns, 頻率10MHz)
initial begin
clock = 0;
forever #50 clock = ~clock; // 每50ns翻轉
end
// 4. 生成測試激勵
initial begin
// 初始化
reset = 1;
load = 0;
sel = 2'b00;
data = 5'b00000;
// 復位200ns
#200 reset = 0;
// 加載數據:00001
load = 1;
#200 data = 5'b00001;
// 左移測試
#100 sel = 2'b01;
load = 0;
// 右移測試
#200 sel = 2'b10;
// 結束仿真
#1000 $stop;
end
// 5. 監控輸出
initial begin
// 設置時間格式
$timeformat(-9, 1, "ns", 12);
// 打印表頭
$display("===========================================");
$display("Time | Clk | Rst | Ld | Sel | Data | ShiftReg");
$display("===========================================");
// 監控信號變化
$monitor("%t | %b | %b | %b | %b | %b | %b",
$realtime, clock, reset, load, sel, data, shiftreg);
end
endmodule
時間段 | 操作 | shiftreg變化
0-200ns | 復位 | 00000
200-300ns | 加載00001 | 00001
300-500ns | 左移2次 | 00010 → 00100
500-1500ns | 右移多次 | 00010 → 00001 → 00000
手動看波形太累?讓程序幫你自動檢查!
- 預先定義"正確答案"
- 在關鍵時刻檢查實際輸出
- 不匹配就報錯
initial begin
// 測試用例1:加法器測試
a = 4'd5;
b = 4'd3;
#10; // 等待計算完成
// 檢查結果
if (sum == 4'd8) begin
$display("✅ 測試通過:5+3=8");
end else begin
$display("❌ 測試失敗:期望8,實際%d", sum);
$stop; // 暫停仿真,方便調試
end
end
integer i;
reg [7:0] test_vectors [0:9]; // 10組測試數據
reg [7:0] expected_results [0:9];
initial begin
// 初始化測試數據
test_vectors[0] = 8'h12; expected_results[0] = 8'h24;
test_vectors[1] = 8'h34; expected_results[1] = 8'h68;
// ...
// 批量測試
for (i = 0; i < 10; i = i + 1) begin
data_in = test_vectors[i];
#10;
if (data_out == expected_results[i]) begin
$display("測試%0d:通過", i);
end else begin
$display("測試%0d:失敗(期望%h,實際%h)",
i, expected_results[i], data_out);
$stop;
end
end
$display("所有測試通過! ");
$finish;
end
⚠️ 新手常見錯誤
❌ 錯誤1:輸入信號用wire
// ❌ 錯誤
wire data_in; // 輸入應該用reg!
// ✅ 正確
reg data_in;
**原因:**reg類型可以賦值,wire不能在initial或always塊中直接賦值。
❌ 錯誤2:忘記初始化
// ❌ 錯誤:clk初始值不確定
reg clk;
always #5 clk = ~clk; // 可能一直是x
// ✅ 正確
reg clk;
initial clk = 0;
always #5 clk = ~clk;
❌ 錯誤3:時間單位太小
// ❌ 不推薦:仿真太慢
`timescale 1ps/1fs
// ✅ 推薦:夠用就行
`timescale 1ns/1ns
❌ 錯誤4:沒有停止條件
// ❌ 錯誤:仿真會一直運行
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 沒有$stop或$finish!
// ✅ 正確
initial begin
#1000 $finish; // 1000ns後結束
end
模板1:基礎測試
`timescale 1ns/1ns
module tb_模塊名;
// 信號聲明
reg clk, rst;
// ... 其他信號
// 實例化DUT
模塊名 dut (/* 端口連接 */);
// 生成時鐘
initial begin
clk = 0;
forever #10 clk = ~clk;
end
// 測試激勵
initial begin
rst = 1;
#100 rst = 0;
// ... 測試代碼
#1000 $finish;
end
// 監控輸出
initial $monitor("%t: ...", $time);
endmodule
模板2:自檢測試
`timescale 1ns/1ns
module tb_模塊名;
// 信號聲明
reg clk, rst;
integer pass_count, fail_count;
// 實例化DUT
模塊名 dut (/* 端口連接 */);
// 時鐘生成
initial begin
clk = 0;
forever #10 clk = ~clk;
end
// 測試主程序
initial begin
pass_count = 0;
fail_count = 0;
// 初始化
rst = 1;
#100 rst = 0;
// 測試用例1
test_case_1();
// 測試用例2
test_case_2();
// 報告結果
$display("======== 測試結果 ========");
$display("通過:%d, 失敗:%d", pass_count, fail_count);
$finish;
end
// 測試用例任務
task test_case_1;
begin
// ... 施加激勵
#10;
// 檢查結果
if (output_signal == expected_value) begin
$display("✅ 測試1通過");
pass_count = pass_count + 1;
end else begin
$display("❌ 測試1失敗");
fail_count = fail_count + 1;
end
end
endtask
endmodule
第一階段:掌握基礎(1-2周)
- ✅ 理解TestBench的作用
- ✅ 會寫簡單的initial塊
- ✅ 能生成時鐘和基本激勵
- ✅ 會用$display打印信息
第二階段:熟練運用(2-4周)
- ✅ 能為中等複雜度模塊寫TestBench
- ✅ 會用$monitor監控信號
- ✅ 理解時間建模
- ✅ 能看懂仿真波形
第三階段:進階提升(1-2個月)
- ✅ 掌握自動化驗證技巧
- ✅ 會寫可重用的TestBench
- ✅ 學習SystemVerilog驗證方法
- ✅ 瞭解UVM驗證方法論
練習1:全加器測試
編寫TestBench測試一個1位全加器,要求:
- 窮舉所有8種輸入組合
- 自動檢查輸出是否正確
- 打印測試報告
參考答案
`timescale 1ns/1ns
module tb_full_adder;
reg a, b, cin;
wire sum, cout;
integer i;
full_adder dut(a, b, cin, sum, cout);
initial begin
$display("===== 全加器測試 =====");
for (i = 0; i < 8; i = i + 1) begin
{a, b, cin} = i;
#10;
$display("a=%b b=%b cin=%b → sum=%b cout=%b",
a, b, cin, sum, cout);
end
$finish;
end
endmodule
練習2:計數器測試
為一個4位加法計數器編寫TestBench,要求:
- 生成復位信號
- 計數20個時鐘週期
- 檢查是否按0-15循環計數