testbench 到底是什么
testbench 就是一个 Verilog 模块。它和可综合模块的区别在于里面装的东西:
- 它没有端口:它是仿真层次结构的顶层。
- 它实例化被测设计。
- 它通过
initial和always块驱动 DUT 的输入。 - 它用
$display、$monitor或导出 VCD 来观察 DUT 的输出。 - 它用
$finish结束仿真。
这就是它的全部工作。Verilog 没有专门的“testbench”关键字,判断一个模块是不是 testbench,要看它做了什么。
标准骨架
整个模式就是这样。五个部分,容易复制粘贴,也容易改写。
对于组合逻辑的 DUT(比如这个加法器),不需要时钟。对于时序逻辑的 DUT,就需要了,这是下一层内容。
为时序 DUT 生成时钟
DUT 有 clk 输入时,testbench 就必须产生一个时钟。标准的一行写法:
reg clk = 0;
always #5 clk = ~clk;
它每 5 个时间单位翻转一次 clk。每次翻转是半个时钟周期,所以完整周期是 10 个单位(一个周期 = 高 5 + 低 5)。如果时间单位是 1ns / 1ps(大多数仿真器的默认值),这就是一个 100 MHz 的时钟。
根据你想模拟的频率选择半周期。在同样的时间单位下,10 MHz 的时钟用 #50;200 MHz 的时钟用 #2.5(或者改用别的时间单位)。
复位时序
同步设计需要一个复位信号:从 0 时刻开始保持高电平若干个周期,然后释放。惯用写法:
reg reset = 1; // start asserted
initial begin
// Hold reset for a few clocks.
#20 reset = 0;
end
这会让 reset 一直保持高电平到时刻 20,然后拉低。等到 reset 下降时,时钟已经走了几个周期,每个触发器都已锁存复位值,设计处于已知状态。
如果是低电平有效的复位(用 reset_n 代替 reset),就把初始值反过来:
reg reset_n = 0; // active-low: 0 means asserted
initial begin
#20 reset_n = 1;
end
完整的时序逻辑 testbench
把时钟、复位和激励组合在一起:
这就是一个 4 位计数器的完整 testbench。点击“运行”,你会看到计数器退出复位、在使能期间向上计数、使能拉低时暂停、使能拉高时继续,最后停止。
关于这个结构,有三点值得注意:
- 三个独立的活动来源:时钟生成器(一个
always)、激励(一个initial)和监视器(另一个always)。每个只负责一件事。 - 激励块里的
#延迟以时间单位计算,而不是时钟周期。如果时钟周期是 10 个单位,那么#10恰好是一个周期。有些 testbench 改用@(posedge clk),不管周期是多少,它都正好前进一个周期。 - 监视器在
always @(posedge clk)块里使用$display。 这样每个周期都会打印一次。想要更复杂的输出,可以改用$monitor(下一篇会讲)。
按周期施加激励
有时候“等 N 个周期”比“等 N 个时间单位”更好读:
initial begin
@(posedge clk); // wait for next clock edge
reset = 0;
repeat (5) @(posedge clk); // wait 5 more cycles
enable = 1;
repeat (8) @(posedge clk);
enable = 0;
repeat (10) @(posedge clk);
$finish;
end
@(posedge clk) 会阻塞,直到下一个时钟上升沿。repeat (N) @(posedge clk); 等待 N 个周期。这种写法与时钟周期无关:即使改变时钟频率,激励按周期计算的行为也保持不变。
刚开始写 testbench 时,# 延迟更简单。对于可能在多种时钟频率下运行的正式 testbench,按周期写更稳妥。
自检测试
到目前为止,testbench 只是打印发生了什么,由你自己去读输出。自检的 testbench 则会检查输出,并报告通过或失败:
initial begin
#1;
a = 10; b = 20;
#1;
if (sum !== 30) begin
$display("FAIL: 10 + 20 = %0d (expected 30)", sum);
$finish;
end
a = 250; b = 5;
#1;
if (sum !== 255) begin
$display("FAIL: 250 + 5 = %0d (expected 255)", sum);
$finish;
end
$display("PASS");
$finish;
end
为了安全,使用 !==(case 不等运算符):即使某个操作数含有未知位,它也不会返回 x。模式是:驱动输入,等信号稳定,与预期值比较,报告通过或失败。
自检在回归测试套件中非常有价值:成千上万个测试可以无人值守地运行,只有失败的才需要关注。
接下来
现在你已经掌握了足以测试任何模块的 testbench 结构。接下来的文档介绍 testbench 里用到的工具:Display 和 Monitor 用于更丰富的文本输出,Dumpfile 和 VCD 用于图形化的波形调试,Timescale 和延迟 用于精确控制仿真时间与现实时间的对应关系。
常见问题
Verilog 中的 testbench 是什么?
testbench 是一个 Verilog 模块,通常没有端口,唯一的用途是检验被测设计(DUT)。它实例化 DUT、生成时钟、通过 initial 和 always 块施加激励、用 $display/$monitor 观察输出,还可以导出 VCD 波形,最后用 $finish 结束仿真。
如何在 Verilog testbench 中生成时钟?
标准写法只有一行:always #5 clk = ~clk;(前面要先声明 reg clk = 0;)。它每隔 5 个仿真时间单位翻转一次 clk,周期就是 10 个单位(如果时间单位是纳秒,就是 100 MHz 的时钟)。#5 是半个周期:clk 一半时间为高,一半时间为低。
Verilog 中的 DUT 是什么?
DUT 是 Design Under Test 的缩写,即被测设计,也就是 testbench 要检验的那个模块。惯例是在 testbench 中把 DUT 实例命名为 dut 或 u_dut:my_module dut(.clk(clk), .reset(reset), .in(in), .out(out));。名字只是个标签,重要的是它被实例化了、端口连到了 testbench 的信号上,并且 testbench 在驱动这些信号。
Verilog 仿真应该运行多长时间?
足够覆盖你想验证的全部内容,然后 $finish。大多数 testbench 会设置一个明确的时间上限(#1000 $finish),以免仿真一直卡在等待一个永远不会发生的事件上。在这段时间里施加激励,等 DUT 稳定下来,最好再加上几条自检的 if 语句,在输出与预期不符时打印 FAIL。