3、Testbench基础:Testbench的结构、initial与always块、时间尺度与延迟控制
说实话,很多刚入行的朋友觉得写Testbench就是给设计加点激励,跑个波形看看。嗯,这话没错,但太糙了。我见过太多人因为Testbench写得随意,结果仿真半天发现是TB自己出了问题,白白浪费一整天。
今天咱们就把Testbench的底裤扒干净。从结构到语法,从时间尺度到延迟控制,我把自己踩过的坑都摊开来讲。
3.1 Testbench的基本结构
一个标准的Testbench,说白了就是一个没有输入输出端口的模块。它负责给DUT(Design Under Test)喂数据,然后观察DUT的反应。
我个人习惯把Testbench分成这么几块:
- 模块声明:没有端口列表,直接
module tb_top; - 信号定义:声明所有需要连接到DUT的wire和reg
- DUT实例化:把DUT拿过来,连上线
- 激励生成:用initial或always块产生时钟、复位、数据
- 结果检查:可以手动看波形,也可以用assertion自动检查
给你看个最简单的模板:
module tb_counter;
// 信号定义
reg clk;
reg rst_n;
reg en;
wire [7:0] count;
// DUT实例化
counter u_dut (
.clk (clk),
.rst_n(rst_n),
.en (en),
.count(count)
);
// 时钟生成
initial begin
clk = 0;
forever #5 clk = ~clk; // 周期10ns
end
// 激励
initial begin
rst_n = 0;
en = 0;
#20 rst_n = 1;
#10 en = 1;
#200 en = 0;
#100 $finish;
end
// 监控
initial begin
$monitor("time=%0t, count=%d", $time, count);
end
endmodule
你看,结构就这么清晰。每个initial块各司其职,互不干扰。我在项目中遇到过有人把所有东西塞进一个initial块里,结果代码又臭又长,调试起来想死的心都有。
3.2 initial块与always块
这两个是Verilog里最常用的过程块。很多人分不清什么时候用哪个,我一句话给你讲明白:
- initial块:只执行一次。适合做初始化、复位、单次激励。
- always块:反复执行。适合做时钟、周期性信号、组合逻辑。
先说说initial块。它从仿真0时刻开始执行,遇到延迟就等,执行完最后一句就结束。注意,多个initial块是并行执行的,别以为它们有先后顺序。
initial begin
clk = 0;
rst_n = 0;
#100 rst_n = 1; // 100ns后释放复位
end
initial begin
#200 $finish; // 200ns后结束仿真
end
这两个initial块同时开始。一个在搞复位,一个在倒计时。互不干扰。
再来看always块。它有个敏感列表,列表里的信号一变,它就执行一次。如果没有敏感列表,那就是个死循环——嗯,时钟就是这么来的。
// 时钟生成 - 没有敏感列表,用forever
always begin
#5 clk = ~clk;
end
// 组合逻辑 - 敏感列表里是所有输入
always @(a or b or sel) begin
if (sel) y = a;
else y = b;
end
// 时序逻辑 - 敏感列表是时钟沿
always @(posedge clk or negedge rst_n) begin
if (!rst_n) q <= 0;
else q <= d;
end
3.3 时间尺度与延迟控制
这个坑我踩过,而且踩得挺疼。时间尺度 `timescale 决定了你的仿真时间单位,搞错了整个仿真节奏全乱。
语法很简单:
`timescale 1ns / 1ps
前面是时间单位,后面是时间精度。单位决定了 #1 代表多久,精度决定了仿真器能分辨的最小时间。
举个例子:
`timescale 10ns / 1ns
initial begin
#1 clk = 1; // 这其实是10ns后
#0.1 clk = 0; // 精度1ns,0.1会被四舍五入成0ns
end
我曾经在项目中把精度设得比单位还大,结果仿真器直接报错。记住,精度必须小于等于单位。
延迟控制有三种方式:
| 方式 | 写法 | 说明 |
|---|---|---|
| 常规延迟 | #10 |
等待10个时间单位 |
| 赋值延迟 | #5 a = b; |
先等5个单位,再赋值 |
| 事件控制 | @(posedge clk) |
等待时钟上升沿 |
这里有个容易犯的错:
// 错误写法 - 非阻塞赋值加延迟,容易出问题
always @(posedge clk) begin
#2 a <= b; // 延迟2个单位后采样b,再赋值给a
end
3.4 知识体系总览
说了这么多,咱们用一张图把Testbench的基础知识串起来:
3.5 避坑指南
最后,把我这些年踩过的坑总结一下,你遇到了能少走弯路:
我曾经犯过的错:
- 忘记加 `timescale:仿真器默认用1ns/1ns,但不同工具行为不一样。我建议每个TB文件第一行都写上,养成习惯。
- initial块里用非阻塞赋值:比如
#5 a <= 1;,结果a的变化时间点和你预期的不一样。我后来统一用阻塞赋值,除非是时钟沿对齐的场景。 - 多个always块驱动同一个信号:仿真器会报多驱动错误。每个信号只在一个always或initial块里赋值。
- 延迟精度不够:比如
`timescale 1ns/1ns下写#0.1,仿真器直接忽略。想精细控制就用1ns/1ps。
嗯,Testbench的基础就这些。说白了就是搭好架子,选对块,控好时间。你写个两三个TB,这些就全熟了。记住,TB是给DUT服务的,别把TB写得比DUT还复杂,那就本末倒置了。
可可精品资料库