用好$display和$monitor,让 iVerilog 调试不再“盲跑”
你有没有过这样的经历?写完一段 Verilog 代码,激动地运行iverilog仿真,结果波形打不开、GTKWave 加载失败,或者干脆懒得看几百行的.vcd文件……最后只能靠猜:“到底进没进这个状态机?”“数据传过去了吗?”——这种“盲跑式”调试,效率低不说,还特别容易崩溃。
别急,其实iVerilog自带了两个超级实用的“调试助手”:$display和$monitor。它们就像代码里的“打印语句”,不需要图形界面,也不依赖外部工具,直接在终端输出信号状态,帮你快速看清设计行为。
今天我们就来聊聊这两个看似简单、实则威力巨大的系统任务,看看它们是怎么把复杂的硬件仿真变得“人话可读”的。
先从一个最熟悉的动作说起:$display
如果你会 C 语言,那你一定用过printf。而$display就是 Verilog 中的“printf”。它做的事情非常直白:在仿真执行到这一行时,立刻打印一条信息,然后换行。
比如:
initial begin reg [7:0] data; data = 8'hA5; $display("Hello, I'm debugging!"); $display("Data is: %h (hex), %b (binary)", data, data); $display("Current time: %t ns", $time); end运行后你会看到类似这样的输出:
Hello, I'm debugging! Data is: a5 (hex), 10100101 (binary) Current time: 0 ns是不是瞬间就清楚当前发生了什么?
它适合干什么?
- 关键节点打标:比如复位结束、启动信号拉高、状态跳转等。
- 条件触发打印:只在满足某个条件时才输出,避免信息爆炸。
- 初始化检查:确认变量是否按预期赋值。
举个真实场景:你在调试一个状态机,怀疑它卡在某个状态没跳出去。这时候你可以在每个状态转移的地方加一句$display:
case (state) IDLE: if (start) begin $display("%t: [STATE] IDLE → RUN", $time); state <= RUN; end RUN: if (done) begin $display("%t: [STATE] RUN → DONE", $time); state <= DONE; end endcase这样一跑仿真,日志里清清楚楚告诉你“什么时候跳到了哪个状态”。如果某条没出现,那问题肯定出在条件判断或时序上——定位起来快得飞起。
注意事项
$display是“一次性”的。你想看多少次变化,就得手动写多少次调用。- 支持格式化输出:
%d:十进制%b:二进制%h:十六进制%t:时间(配合$time使用)%s:字符串- 输出自带换行,不用自己加
\n。
想偷懒?试试$monitor:自动监听信号变化
如果说$display是“你让我打我才打”,那$monitor就是“只要动了我就吼一嗓子”。
它的语法和$display几乎一样:
$monitor("Time=%t | clk=%b | reset=%b | data=%h", $time, clk, reset, data);但行为完全不同:只要clk、reset或data中任意一个变了,这行就会自动输出一次!
这意味着你只需要设置一次,就能持续跟踪多个信号的状态流转。
实战例子
假设你要验证一个简单的寄存器模块:
// DUT: 简单锁存器 module reg8 ( input clk, input reset, input en, input [7:0] d_in, output reg [7:0] q_out ); always @(posedge clk or posedge reset) begin if (reset) q_out <= 8'h00; else if (en) q_out <= d_in; end endmodule再写个 testbench,加上$monitor:
module tb_reg8; reg clk, reset, en; reg [7:0] d_in; wire [7:0] q_out; // 实例化 DUT reg8 uut (.clk(clk), .reset(reset), .en(en), .d_in(d_in), .q_out(q_out)); // 设置监控 initial begin $monitor("T=%0t | clk=%b | rst=%b | en=%b | din=%0h | qout=%0h", $time, clk, reset, en, d_in, q_out); end // 时钟生成 always #5 clk = ~clk; // 激励 initial begin clk = 0; reset = 1; en = 0; d_in = 8'h00; #10 reset = 0; // 释放复位 #10 en = 1; #10 d_in = 8'hAA; // 写入 AA #10 d_in = 8'hFF; // 写入 FF #20 $finish; end endmodule运行iverilog -o sim tb_reg8.v reg8.v && vvp sim后,你会看到:
T=0 | clk=x | rst=x | en=x | din=xx | qout=xx T=5 | clk=1 | rst=1 | en=0 | din=00 | qout=00 T=10 | clk=0 | rst=0 | en=0 | din=00 | qout=00 T=15 | clk=1 | rst=0 | en=1 | din=AA | qout=00 T=25 | clk=1 | rst=0 | en=1 | din=FF | qout=AA看到了吗?虽然我们只写了一次$monitor,但它在每一个信号变化的时刻都给出了反馈。你可以清晰地看到:
- 复位释放后,输出保持为 0;
- 第一次写入AA,下一个时钟上升沿后qout更新;
- 接着写FF,qout变成了之前的AA—— 符合时序逻辑特性!
这比翻波形图还直观,尤其适合初学者理解“边沿触发”和“延迟响应”的概念。
两者怎么选?什么时候用谁?
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| 查看特定事件发生时刻的状态 | $display | 精准控制,不干扰其他流程 |
| 跟踪多个信号的整体交互过程 | $monitor | 自动输出,省事高效 |
| 高频信号参与监控(如时钟) | ❌慎用$monitor | 每次翻转都输出,日志爆炸 |
| 条件性调试(如错误告警) | $display+if判断 | 只在异常时提醒 |
| 快速搭建测试平台骨架 | $monitor | 一行代码搞定全局观察 |
✅黄金建议:
初期用$monitor快速建立整体视图;
定位问题时用$display打“断点”深入细节。
避坑指南:新手常踩的几个雷
1. 日志太多,根本看不清
原因:把clk这种高频信号放进$monitor。
解决办法:去掉时钟,只保留控制信号和数据:
// 错误示范 $monitor("clk=%b data=%h", clk, data); // 每5ns就输出一次! // 正确做法 $monitor("T=%t | data=%h | valid=%b", $time, data, valid);2. 输出全是x和z
原因:信号未初始化,早期值不确定。
对策:在initial块中给所有激励信号明确初值:
initial begin clk = 0; reset = 1; en = 0; d_in = 8'h00; end这样$monitor的第一条输出就不会是一堆xxx,更有参考价值。
3.$monitor被覆盖了
Verilog 规定:整个仿真中只能有一个生效的$monitor。如果你写了多个,只有最后一个起作用。
比如:
initial $monitor("A=%b", sig_a); initial $monitor("B=%b", sig_b); // 上面那行被干掉了!所以记得统一管理你的监控点,别到处乱设。
高阶技巧:让输出更专业
给输出加颜色(Linux/macOS)
虽然不是标准功能,但在支持 ANSI 颜色的终端里,可以这样增强可读性:
$display("\033[32mPASS: Data matched at %t\033[0m", $time); // 绿色通过 $display("\033[31mERROR: Timeout!\033[0m"); // 红色报错输出重定向到文件
方便后期分析或自动化测试:
vvp sim > sim.log或者在代码中结合系统命令:
initial begin $system("exec > sim.log"); // 重定向stdout(部分平台支持) end⚠️ 注意:
$dumpfile是用于生成 VCD 波形的,不能用来记录$display输出。要记录文本日志,请用 shell 重定向。
结语:别小看“打印”这件事
在很多人眼里,$display和$monitor是“入门级”工具,高级工程师早就用 UVM、覆盖率驱动验证了。这话没错,但在真实的开发过程中,尤其是 FPGA 原型验证、课程项目、竞赛开发中,这两招依然是最快、最直接的调试手段。
它们不花哨,但足够锋利。就像螺丝刀之于电工,键盘之于程序员——越是基础,越离不开。
下一次当你面对一堆看不懂的波形或无从下手的 bug 时,不妨停下来,在关键位置加一句$display,或者设一个$monitor。也许那一行小小的输出,就是解开谜题的第一把钥匙。
如果你也喜欢这种“看得见”的调试方式,欢迎留言分享你的实战经验!你是
$display党还是$monitor党?