3、Verilog仿真与测试平台:Testbench结构、initial/always块、$display/$monitor、波形查看(VCD)、基本仿真流程
说实话,很多初学者写Verilog,代码看着挺漂亮,一仿真就翻车。为什么?因为测试平台没写好。我见过太多人把精力全放在RTL设计上,结果Testbench写得一塌糊涂,最后查bug查到怀疑人生。
今天我们就聊聊仿真测试这件事。说白了,Testbench就是给你的设计搭个舞台,让它自己演一遍,你在旁边看它演得对不对。
3.1 Testbench的基本结构
一个标准的Testbench长什么样?我习惯把它分成三块:
- 激励生成:给DUT(Design Under Test)喂数据
- DUT实例化:把待测模块接进来
- 结果检查:看输出对不对
给你看个最简单的例子:
module tb_counter();
// 1. 信号声明
reg clk;
reg rst_n;
wire [3:0] count;
// 2. DUT实例化
counter u_counter (
.clk (clk),
.rst_n (rst_n),
.count (count)
);
// 3. 时钟生成
always #5 clk = ~clk;
// 4. 激励
initial begin
clk = 0;
rst_n = 0;
#20 rst_n = 1;
#200 $finish;
end
// 5. 结果监控
initial begin
$monitor("time=%0t, count=%d", $time, count);
end
endmodule
嗯,这里要注意:Testbench里没有端口列表。它是个封闭的环境,所有信号都在内部定义。
3.2 initial块与always块
这两个块是Verilog仿真的核心。我刚开始学的时候总搞混,后来想明白了一件事:
- initial块:只执行一次。适合做初始化、复位、顺序激励
- always块:循环执行。适合做时钟、周期性信号
重要区别:initial块在仿真0时刻开始执行,执行完就结束。always块会一直重复执行,直到仿真结束。
举个例子,我项目中常用的几种写法:
// 时钟生成 - always块
always #5 clk = ~clk;
// 复位序列 - initial块
initial begin
rst_n = 0;
#100 rst_n = 1;
#1000 rst_n = 0;
#50 rst_n = 1;
end
// 数据激励 - initial块
initial begin
data_in = 0;
#10 data_in = 8'hA5;
#10 data_in = 8'h5A;
#10 data_in = 8'hFF;
end
我的习惯:把时钟和复位放在一个initial块里,数据激励放在另一个initial块里。这样结构清晰,调试时一眼就能找到问题。
3.3 $display与$monitor
这两个系统函数,说白了就是打印信息用的。但用法有讲究:
| 函数 | 触发时机 | 典型用法 |
|---|---|---|
| $display | 执行到该语句时打印一次 | 打印关键节点信息 |
| $monitor | 监控变量变化时自动打印 | 持续跟踪信号变化 |
| $strobe | 当前时间步结束时打印 | 打印稳定后的值 |
我曾经踩过一个坑:用$display打印组合逻辑的输出,结果发现打印的值和波形对不上。为什么?因为$display在赋值语句之前就执行了。后来改用$strobe才搞定。
initial begin
// 错误示范
$display("count = %d", count); // 可能打印的是旧值
// 正确做法
#1 $display("count = %d", count); // 等一个delta周期
// 或者用
$strobe("count = %d", count); // 稳定后打印
end
注意:$monitor在整个仿真中只能激活一个。如果你写了多个$monitor,只有最后一个生效。我建议只在顶层Testbench里用一个$monitor,其他调试信息用$display。
3.4 VCD波形查看
光看打印信息是不够的。遇到复杂问题,必须看波形。VCD(Value Change Dump)是最通用的波形格式,几乎所有仿真器都支持。
生成VCD文件很简单:
initial begin
$dumpfile("wave.vcd"); // 指定文件名
$dumpvars(0, tb_counter); // 记录所有信号
end
这里有个参数要注意:
- 0:记录所有层级的所有信号
- 1:只记录当前层级
- 2:记录当前层级及下一级
我一般用0,省事。但如果设计很大,VCD文件会爆炸。这时候可以指定只记录某些信号:
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb_counter.u_counter.clk); // 只记录时钟
$dumpvars(0, tb_counter.u_counter.count); // 只记录计数
end
3.5 基本仿真流程
完整的仿真流程,我总结成四步:
- 编译:把Verilog文件编译成可执行文件
- 运行:执行仿真,生成VCD文件
- 调试:看波形、看打印信息
- 验证:检查结果是否符合预期
用命令行跑仿真大概是这样的:
# 1. 编译
iverilog -o tb_counter tb_counter.v counter.v
# 2. 运行
vvp tb_counter
# 3. 查看波形
gtkwave wave.vcd
核心思想:仿真不是为了证明代码是对的,而是为了找到代码哪里错了。所以Testbench要写得比RTL代码更仔细。
3.6 知识体系总览
我把本章的核心逻辑画了张图,方便你理解:
这张图把整个仿真流程串起来了。你写Testbench的时候,脑子里要有这张图:激励怎么来?DUT怎么接?结果怎么看?
我的建议:刚开始写Testbench,别追求花哨。先把最基本的initial/always块用好,把$display和$monitor用熟。等这些基础打牢了,再学UVM、随机约束这些高级玩法。
好了,关于仿真测试平台,核心内容就这些。记住一句话:好的Testbench,能帮你省下80%的调试时间。我当年就是吃了这个亏,后来才老老实实把Testbench当正事来写。
公众号:蓝海资料掘金营,微信deep3321