3、仿真与测试平台(Testbench)编写
仿真,说白了就是让代码先跑一遍。你想想看,写了几百行Verilog,直接上板子?我年轻时干过这事,结果板子冒烟了。从那以后,我养成了先仿真的习惯。仿真不花钱,烧芯片才心疼。
3.1 仿真概念:你到底在验证什么?
仿真就是给设计喂数据,看它吐出来的对不对。我们验证的是逻辑行为,不是物理特性。说白了,就是检查你的代码有没有按你想的干活。
我个人习惯把仿真分成两层:
- 功能仿真:只看逻辑对不对,不考虑延迟。跑得快,适合早期调试。
- 时序仿真:带上门延迟和布线延迟。慢,但更接近真实芯片。
我在项目中遇到过一件事:功能仿真全过,时序仿真全挂。原因是组合逻辑路径太长,信号没在时钟沿前稳定下来。嗯,这就是为什么我建议你两种仿真都要做。
核心原则:仿真通过 ≠ 上板没问题,但仿真不通过 = 上板一定有问题。
3.2 $display 和 $monitor:仿真界的printf
这两个系统函数,是调试的利器。$display 就像C语言的printf,执行一次打印一次。$monitor 则不同,只要监控的信号变了,它就自动打印。
看个例子:
initial begin
// 只打印一次
$display("仿真开始,时间 = %0t", $time);
// 持续监控
$monitor("时间 = %0t, clk = %b, data = %h", $time, clk, data);
end
我曾经犯过一个低级错误:在always块里放$monitor。结果每次触发都重新注册监控器,打印信息爆炸了。记住,$monitor一般只放在initial块里,调用一次就够了。
小技巧:用 %0t 打印时间,比 %t 更紧凑。用 %h 看十六进制,比 %b 省眼睛。
3.3 initial/always块在仿真中的使用
仿真里,initial块是老大。它只执行一次,适合做初始化、生成激励。always块则反复执行,适合生成时钟这类周期信号。
我常用的时钟生成模板:
// 时钟生成
initial begin
clk = 0;
end
always #5 clk = ~clk; // 周期10ns,100MHz
// 复位信号
initial begin
rst_n = 0;
#20;
rst_n = 1;
#100;
rst_n = 0;
#20;
rst_n = 1;
end
注意,仿真里的initial块是顺序执行的。你写#10延时,它就真等10个时间单位。这和硬件里的行为完全不同,别搞混了。
避坑指南:我曾经在initial块里写了一个死循环,忘了加延时。仿真器直接卡死,我还以为是软件坏了。后来发现,是循环里没有#延时,仿真时间不推进,永远跑不完。
3.4 波形查看:Vivado Simulator vs ModelSim
波形是仿真的可视化结果。我个人偏爱ModelSim,但Vivado Simulator也够用。两者核心操作差不多:
| 操作 | Vivado Simulator | ModelSim |
|---|---|---|
| 添加信号 | 右键 → Add to Wave | 右键 → Add Wave |
| 运行仿真 | Run All / Run For | run / run 100ns |
| 查看内部信号 | 需要设置mark_debug | 直接添加即可 |
| 波形导出 | File → Export | File → Save Format |
我个人习惯:先用$display快速定位问题,再用波形细看时序。别一上来就盯着波形看,那太费眼了。
3.5 常见仿真错误:我踩过的坑
仿真报错,别慌。大部分错误就那几种:
- 时间精度不匹配:`timescale 1ns/1ps 和 `timescale 1ns/100ps 混用,仿真结果对不上。统一用 1ns/1ps 最省事。
- 信号未初始化:仿真默认值是'X'。你看到波形全是红色,八成是没给初值。在initial块里赋个0就好。
- 组合逻辑环路:always @(*) 里写了 a = a + 1,仿真直接崩。组合逻辑不能有反馈,这是常识。
- 仿真时间不够:跑10ns就停了,复位还没结束。我建议至少跑够100个时钟周期。
我的经验:仿真报错时,先看第一条错误。后面的错误往往是连锁反应,第一条修好了,后面的自动消失。
3.6 本章知识体系
下面这张图,帮你理清仿真测试的脉络:
这张图把仿真测试的核心内容串起来了。你写Testbench时,就照着这个框架来:先搞清楚要验证什么,再用$display和波形看结果,最后避开那些常见坑。
最后说一句:仿真是个熟能生巧的活。你多写几个Testbench,自然就找到感觉了。别怕报错,报错是好事,总比上板烧芯片强。