iverilog 第一次仿真完整流程:testbench、编译、vvp、VCD 四步闭环
更新时间:2026-11-03。理论篇 仿真是什么 讲过 testbench、DUT、波形各是什么,本篇解决更具体的问题:在自己电脑上,第一次用开源的 Icarus Verilog 把一个设计真正跑起来,要敲哪些命令、每步吃什么吐什么。配套的进阶实验可以看 同步 FIFO 的 iverilog 回归,Verilog 本身的语法在 Verilog 语法速览。
本文要回答的问题
- 一份 RTL 写好后,从源码到看到波形,中间到底经过哪几步?
iverilog编译和vvp运行为什么是两条命令,各自的参数怎么选?sim.vvp、wave.vcd这些产物分别是什么,VCD 文件里面长什么样?- 新手第一次跑仿真最容易卡在哪几个检查点?
整条闭环只有四步,先给一张总览卡,后面逐段展开:
读这张卡注意两件事:testbench 和 RTL 是两个文件、一起编译,新手常漏掉 TB 文件导致激励根本不存在;真正敲的命令只有第 2、3 步两条,第 4 步是可选的人工复核——有自校验打印时不看波形也能知道对错。
一、这一步在数字设计流程里的位置
仿真是「写完 RTL、做综合之前」的第一道验证。上游给你的是一份或多份 .v 设计文件,你要产出的是两类证据:终端里的自校验结论和可供人工复核的波形文件。这条工具链内部的数据流如下:
读这张图抓住一个分叉:vvp 一次运行同时吐两样东西——$display/$monitor 的文本进终端,$dumpvars 记录的信号变化进 VCD 文件。前者给脚本做 PASS/FAIL 判定,后者给人眼看时序。图里没有任何一步涉及综合或硬件,仿真全程是软件在演电路。
本次的 DUT 是一个 4 位带使能计数器:复位时清零,en=1 时每个上升沿加一,计到 15 且仍使能时 carry 拉高一拍。testbench 则负责搭测试台,骨架固定是四件事:
`timescale 1ns/1ps
module tb_counter4;
reg clk = 0, rst_n = 0, en = 0; // TB 驱动的信号声明为 reg
wire [3:0] q; wire carry; // 接 DUT 输出的声明为 wire
counter4 dut (.clk(clk), .rst_n(rst_n), .en(en), .q(q), .carry(carry));
always #5 clk = ~clk; // ① 时钟:周期 10ns
initial begin
$dumpfile("wave.vcd"); // ② 波形记录:文件名 + 记录层次
$dumpvars(0, tb_counter4);
#22 rst_n = 1; #40 en = 1; // ③ 激励:按时间施加复位与使能
/* ……逐拍自校验…… */
#502 $finish; // ④ 结束:不写仿真会一直挂着
end
endmoduleTB 里用的 initial、#延迟、$finish 都是仿真专用语法,不能综合;TB 与 DUT 通过端口名连接(.clk(clk)),TB 侧没有的输入会悬空,这也是为什么两个文件必须一起编译。
二、命令逐段解读:iverilog 与 vvp 的参数
最小命令是 iverilog -g2012 -Wall -o sim.vvp counter.v tb_counter.v,再 vvp sim.vvp。每个参数的含义和不加的后果:
| 参数 | 作用 | 不加会怎样 |
|---|---|---|
-g2012 | 按 IEEE 1800-2017 之前的 SystemVerilog 兼容语法解析 | 默认按 Verilog-2005,logic、always_ff 等语法直接报错 |
-o sim.vvp | 指定编译产物文件名 | 默认生成 a.out,多项目时极易混淆 |
-Wall | 打开全部告警(缺 timescale、位宽截断、隐式线网等) | 警告静默,很多 bug 失去提前暴露的机会 |
-s <top> | 显式指定仿真顶层模块 | 多个「没被例化」的模块时顶层可能选错 |
-D NAME=v / -I dir / -y dir | 定义宏、include 搜索目录、模块库目录 | 条件编译缺宏、include 找不到文件 |
iverilog 这一步做的是语法检查加设计精化(elaboration):模块不存在、端口对不上、重名定义都会在此阶段报错,根本走不到运行。vvp 阶段常用的开关只有两个:-n 让仿真在 $finish 后不进交互模式直接退出(写回归脚本时用),-l file 把终端输出同时写一份日志文件。
波形记录靠 TB 里两个系统任务:$dumpfile("wave.vcd") 定文件名,$dumpvars(0, tb_counter4) 中第一个参数 0 表示从给定层次往下所有层级的信号都记录,换成 1 则只记顶层一层——层次给浅了是「波形里找不到 DUT 内部信号」最常见的原因。
三、输入输出文件:吃什么、吐什么、长什么样
整条链涉及三种文件。输入是两个 Verilog 源文件:counter.v 是可综合 RTL,tb_counter.v 是不可综合的测试台。iverilog 吐出的 sim.vvp 是 Icarus 自定义的仿真字节码,只能交给同版本 vvp 执行,不是可综合网表,也不能跨平台拷贝。
vvp 运行时生成的 wave.vcd 是关键产物。VCD(Value Change Dump)是纯文本的工业标准波形格式,开头声明时间精度和每个信号的编号,后面只记「什么时刻哪个信号变成了什么」。本次真实生成的文件头部如下:
$timescale
1ps
$end
$scope module tb_counter4 $end
$var wire 4 ! q [3:0] $end
$var wire 1 " carry $end
$var reg 1 # clk $end
$var reg 1 $ en $end$var wire 4 ! q 表示 4 位宽、内部编号为 !、名字叫 q 的信号;文件正文里每一条形如 #205000 的时间戳后跟 b1111 !,就表示 205ns 时 q 变成了二进制 1111。正因为是纯文本,VCD 可以用 grep、可以被任何波形工具读取,也能用版本管理工具追踪——这是它比各家专有波形格式长寿的原因。看波形时执行 gtkwave wave.vcd,在 SST 窗口把 tb_counter4/dut 下的信号拖到波形区即可,读波形的方法见 看波形基本功。
四、常见错与检查点
第一次跑这条链,九成的卡顿集中在下面四处。它们不是工具报错大全,而是正常流程里最该先自查的检查点:
| 现象 | 先查什么 | 纠正 |
|---|---|---|
| 有 sim.vvp、仿真也跑完了,但 VCD 不存在或波形是空的 | initial 开头是否有 $dumpfile/$dumpvars,dump 层次是否给了 0 | 补齐两个系统任务,机制与排错见 VCD 没有波形 |
| 命令行长时间不返回,像「卡死」 | TB 有没有 $finish;时钟 always 本身永远不停,必须靠 initial 收尾 | 在激励序列末尾加 $finish;脚本里再加 vvp -n |
| 编译报 duplicate module,或顶层不是 TB | 命令行是否重复给了同一文件;工程里是否有两个未被例化的模块 | 去重,并用 -s tb_counter4 显式指定顶层,参见 重名模块报错 |
| 延迟单位对不上,波形时间轴全是奇怪数字 | 每个文件开头是否有 `timescale 1ns/1ps | 给所有源文件补 timescale,-Wall 会直接点名缺失文件,详见 timescale 继承问题 |
一个判断顺序的小经验:编译阶段的错看文件与语法,运行阶段的错看 TB。RTL 再复杂,iverilog 过了就说明结构上连得上;跑起来行为不对,问题几乎都在激励施加时机、采样沿和自校验模型上。
五、真实跑一遍:计数器从 0 数到回卷
用前面两份源文件在 Icarus Verilog 13.0 上执行 make run(内部即编译加运行两条命令),真实终端输出如下:
实录里第 9 行编译命令带 -Wall 但没有任何告警回显,说明 timescale、位宽这些静态检查全过;第 11 行 VCD info 证明波形文件已打开;monitor 表从第 13 行开始:0ns 复位有效时 q 先是不定态 x,5ns 第一个下降沿采样时已被异步复位清成 0,22ns 复位释放后计数仍保持 0——因为 en 还没开。
激励设计了三个观察点,关键几行原样摘录如下:
195ns 1 1 e 0
205ns 1 1 f 1
215ns 1 1 0 0
225ns 1 1 1 0205ns 这一拍 q=f 且 carry=1,进位只在「值为 15 且使能」时拉高一拍;紧接的 215ns q 回卷成 0,4 位加法回卷正确。继续数完 40 个使能拍后拉低 en,记分板逐拍比对的总账是:
455ns 1 1 8 0
462ns 1 0 8 0
RESULT: PASS final q=8
tb_counter.v:52: $finish called at 502000 (1ps)40 = 2×16 + 8,所以回卷两次后停在 8;462ns 关掉使能后再走两拍,q 稳稳保持 8,保持功能同时被验证。RESULT: PASS 是 TB 自己打的自校验结论,最后一行是仿真器正常收尾的标志——看到这行才说明 $finish 被执行、仿真没有挂死。复现时建议按「跑通 → 改坏 → 修好」玩一遍:故意删掉 $dumpvars 或把复位释放提前,亲眼看 VCD 变空或计数错位,比读十遍流程都记得牢。
实验环境
- 机器/系统:macOS(Darwin 25.5,Apple Silicon)
- 工具版本:Icarus Verilog 13.0(stable,原生安装于
/usr/local/bin,iverilog -V照抄) - 日期:2026-11-03
- 完整命令:
make clean→make run(内部执行iverilog -g2012 -Wall -o sim.vvp counter.v tb_counter.v与vvp sim.vvp)→wc -c sim.vvp wave.vcd(产物 5786 / 2910 字节)→gtkwave wave.vcd人工复核波形
代码获取
完整 RTL、testbench 与 Makefile 已同步到公开仓库:geek-doc.cn/experiments/flow-iverilog-first-simulation,克隆后进入该目录执行 make run 即可复现本文全部输出,make clean 删除编译与波形产物。
这一篇在知识体系里的位置
- 想补上 testbench、DUT、波形这些概念再动手:读 仿真是什么;
- VCD 拿到手后怎么从波形里定位第一个错:读 看波形基本功;
- 四步闭环跑熟后,看它怎么升级成带 scoreboard 的随机回归:读 同步 FIFO 的 iverilog 回归实测;
- 忘了端口连接、非阻塞赋值这些语法:回 Verilog 语法速览。
总结
iverilog 第一次仿真完整流程就是四步:写一个产生时钟、复位、激励并例化 DUT 的 testbench;用 iverilog -g2012 -Wall -o sim.vvp 把 RTL 和 TB 一起编译;用 vvp sim.vvp 运行,看终端 monitor 表和自校验 PASS;需要人工复核时用 gtkwave 打开 $dumpvars 生成的 wave.vcd 纯文本波形。记住三个检查点——编译过不了查文件与语法,波形空查 dump 任务,命令挂住查 $finish——你就已经掌握了数字设计里重复频率最高的一条工具链。