Verilator --cc 编译仿真全流程:生成 C++、手写 harness、make、运行四步
更新时间:2026-11-03。仿真是什么 讲过事件驱动仿真与 testbench 的基本原理;iverilog 走的是「Verilog testbench + 解释器」路线,本篇换一条更快也更工程化的路线:Verilator 把 RTL 编译成 C++,测试台也用 C++ 写。为什么有人放着 vvp 不用、偏要写 C++,可以看 Verilator 与 VCS 的主要差异。
本文要回答的问题
verilator --cc之后obj_dir/里那一堆文件分别是什么,我真正要碰的是哪几个?- C++ testbench harness 怎么写时钟、复位和自校验,
Vtop.h暴露了哪些 API? --cc、--exe、--trace、-f Vtop.mk各管什么,不加会怎样?- 为什么波形开关要在「编译时」和「运行时」各给一次?
整条链路四步,先给总览,再逐段拆:
读这张卡抓住一个本质区别:Verilator 不是「带波形窗口的仿真器」,它是一个编译器——Verilog 进、C++ 出,剩下的事全部交给 g++ 和你自己写的 main 函数。所以这里没有 $monitor、没有 #10 clk=~clk,激励要靠 C++ 代码一拍一拍驱动。
一、这一步在数字设计流程里的位置
Verilator 仿真处在「RTL 写完、综合之前」,和 iverilog 仿真同一个验证位置,只是工具链内部多了一层 C++。上游是可综合的 .v 设计和一份 C++ harness,下游是一个普通的可执行文件与可选的 VCD 波形:
读这张图注意两个输入的角色不同:top.v 被翻译成 C++,而 tb_top.cpp 不翻译,它以 C++ 原样进编译,通过 Vtop.h 里声明的类去操作设计。产物也分叉:obj_dir/ 是中间产物目录,真正发给同事的是里面的 Vtop 可执行文件;VCD 不是默认产物,只有 harness 打开 trace 时才生成。
本篇的 DUT 是一个 4 位计数器,复位清零,之后每个上升沿加一,并在 q==15 那拍把 tick 拉高一个周期——计数器与分频脉冲是同一个东西:16 拍一个脉冲就是十六分频。
module top (
input wire clk,
input wire rst_n,
output reg [3:0] q,
output reg tick
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
q <= 4'd0;
tick <= 1'b0;
end else begin
q <= q + 4'd1;
tick <= (q == 4'd14);
end
end
endmodule顶层模块名取 top 不是随便起的:Verilator 的生成类、Makefile 和可执行文件全部按顶层名命名,模块叫 top,产物就是 Vtop.h、Vtop.mk、Vtop,这也是全网示例里 Vtop 这个名字的由来。
二、命令逐段解读:verilate、make、运行
第一步的完整命令是:
verilator --cc --exe --trace --top-module top top.v tb_top.cpp| 参数 | 作用 | 不加会怎样 |
|---|---|---|
--cc | 把 Verilog 翻译成 C++,并生成配套 Makefile | 不给转换目标(--cc/--sc/--binary)Verilator 直接报错退出 |
--exe | 声明后面跟着的是可执行仿真的 C++ harness,把它写进 Makefile 的编译列表 | 只生成模型库,Makefile 里没有 main,链接不出可执行文件 |
--trace | 在生成代码里插入波形记录逻辑,Makefile 自动带 -DVM_TRACE=1 并链 verilated_vcd_c | harness 里的 trace 调用没有实现,运行时无法产生 VCD |
--top-module top | 显式指定顶层模块 | 工程里有多个未被例化的模块时顶层可能选错 |
-Wall / -Wno-fatal | 打开全部风格/位宽告警;后者让告警不致命 | lint 问题静默;Verilator 默认告警即失败,抄旧代码时常需要 -Wno-fatal 先跑通 |
这一步只生成不编译,想偷懒可以加 --build 让它顺手把 g++ 也跑了,但第一次走流程建议拆开,才看得清 obj_dir/ 里多了什么。第二步用生成的 Makefile 编译:
make -C obj_dir -f Vtop.mk Vtop-C obj_dir 让 make 进产物目录工作,-f Vtop.mk 指定 Verilator 生成的脚本,结尾的 Vtop 是目标名(同样随顶层名变)。第三步直接运行:
./obj_dir/Vtop # 无波形,纯自校验
./obj_dir/Vtop --trace # 再跑一次,产出 wave.vcd注意这里的 --trace 是我们自己的 harness 在 argv 里约定的开关,和 verilate 阶段的编译开关同名但不同层:编译时 --trace 决定「有没有记录波形的能力」,运行时 --trace 决定「这次跑记不记录」。能力没编进来,运行时再要波形也没有。
三、输入输出文件与 harness 关键 API
verilate 吃 top.v 和 tb_top.cpp,吐一个 obj_dir/。本次真实生成的文件(节选)如下:
Vtop.cpp
Vtop.h
Vtop.mk
Vtop__Syms.cpp
Vtop__Syms.h
Vtop__Trace__0.cpp
Vtop___024root.h
Vtop___024root__Slow.cpp
Vtop__pch.h
Vtop_classes.mk大部分文件不用打开:Vtop___024root*、Vtop__Syms* 是内部实现,Vtop__Trace* 是波形插桩。harness 里真正要 include 的公共头只有 Vtop.h,编译规则只认 Vtop.mk。这个头文件给你的 API 永远是同一套:
Vtop类:设计本身,new Vtop建一个实例,端口就是公开成员,名字与 Verilog 端口原样对应(dut->clk、dut->q);dut->eval():求值一次组合逻辑与时钟沿更新——不调用它,你改多少输入信号内部都不会动;dut->trace(tfp, 99)与tfp->dump(time):挂接波形器并在每个时间点转储,99 是记录层次深度;double sc_time_stamp():全局函数,波形器靠它拿当前仿真时间,开 trace 就必须定义,否则链接报未定义引用。
harness 的核心骨架如下,时钟沿、复位和采样全是普通 C++:
#include "Vtop.h"
#include "verilated.h"
#include "verilated_vcd_c.h"
static vluint64_t main_time = 0;
double sc_time_stamp() { return main_time; }
static void step_half(Vtop *dut, VerilatedVcdC *tfp, int value) {
dut->clk = value;
dut->eval();
main_time += 5; // 半周期 5ns
if (tfp) tfp->dump(main_time);
}驱动时序就是先给两个复位周期(结束时让 clk 停在低电平,保证第 0 拍是干净的上升沿),再释放复位、循环 40 个整周期,在上升沿之后采样 tick 计数。运行时加 --trace 生成的 VCD 是标准纯文本格式,头部与 iverilog 产出的几乎一样:
$version Generated by VerilatedVcd $end
$timescale 1ps $end
$scope module TOP $end
$var wire 1 # clk $end
$var wire 1 $ rst_n $end
$var wire 4 % q [3:0] $end
$var wire 1 & tick $end时间精度由 harness 里的 main_time 增量决定(本例 5ns 半步),Verilog 源里不需要写 `timescale;波形读法与 VCD 工具通用,见 看波形基本功。
四、常见错与检查点
第一次走这条链,卡点几乎都在「C++ 世界的规矩」上:
| 现象 | 先查什么 | 纠正 |
|---|---|---|
链接报错 undefined reference to sc_time_stamp | harness 是否定义了这个全局函数 | 开 trace 必须提供 double sc_time_stamp(),直接照抄骨架 |
程序能跑,但 q 永远是 0 | 时钟翻转后有没有调用 eval() | 每次改 clk 都要 dut->eval(),Verilator 不会自己推进 |
编译报 dut->Q/rst 不是成员 | 端口名拼写与方向是否和 Verilog 一致 | 成员名原样保留(q、rst_n),输出端口直接读,别自己造名字 |
运行 Vtop --trace 却没有 wave.vcd | verilate 时是否给了 --trace,harness 是否解析了 argv | 编译开关与运行开关两处都要开;traceEverOn(true) 与 tfp->open 缺一不可 |
另外两个口径问题:Verilator 默认对告警零容忍,老代码先加 -Wall -Wno-fatal 让仿真跑起来,再按 Verilator lint 高频告警矩阵 逐条清;Verilog 里的 #延迟 在这条链不被支持,源文件报延时相关错误时看 时序延时不支持的处理,找不到模块文件则看 模块文件定位报错。
五、真实跑一遍:40 个时钟周期两个分频脉冲
在 Verilator 5.020(geek-eda:0.3-sta 容器)里从 verilate 到运行完整走一遍,第一次运行(无波形参数)的真实终端输出如下:
表头 cycle q tick 是 harness 在每个上升沿之后采样打印的。复位释放后第 0 拍 q 变 1,逐拍累加;关键的三个观察点原样摘录:
14 15 1
15 0 0
...
30 15 1
...
ticks=2 final q=8
RESULT: PASS第 14 行(第 15 个计数拍)q=15 且 tick=1,分频脉冲正好在计满 16 拍时出现;下一拍 q 回卷到 0、tick 拉低。40 拍里这样的脉冲出现在第 14、30 行,共 2 次,与 40 = 2×16 + 8 吻合;结束时 q=8,记分板同时核对脉冲数与终值后打印 RESULT: PASS,进程以退出码 0 结束——这意味着它可以直接放进 CI 回归脚本,靠退出码判定。紧接着带 --trace 再跑一遍,第二次同样 PASS,并在工作目录落下 1382 字节的 wave.vcd,用 gtkwave 打开即可逐拍复核。
实验环境
- 机器/系统:macOS(Darwin 25.5,Apple Silicon)上 Docker Desktop,容器镜像
geek-eda:0.3-sta(Debian bookworm 系) - 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1),容器内 g++ (Debian 12.2.0) + GNU make(
verilator -V照抄) - 日期:2026-11-03
- 完整命令:
verilator --cc --exe --trace --top-module top top.v tb_top.cpp→make -C obj_dir -f Vtop.mk Vtop→./obj_dir/Vtop→./obj_dir/Vtop --trace→head wave.vcd
代码获取
完整 RTL、C++ harness、Makefile 与容器复现脚本已同步到公开仓库:geek-doc.cn/experiments/flow-verilator-cc-simulation,克隆后进入该目录执行 make run(需要 Docker 与 geek-eda:0.3-sta 镜像)即可复现全部输出,本机已装 Verilator 5.x 时用 make NATIVE=1 run,make clean 删除 obj_dir/ 与波形产物。
这一篇在知识体系里的位置
- 想先搞懂 testbench、事件驱动仿真这些概念:读 仿真是什么;
- 同一条验证位、另一套开源工具的对照流程:读 同步 FIFO 的 iverilog 回归实测,看 Verilog 写 TB 的事件驱动路线;产出的 VCD 怎么读看 看波形基本功;
- 编译期告警怎么逐条治:读 Verilator lint 高频告警矩阵;
- 想知道 Verilator 相对商业仿真器快在哪、又牺牲了什么:读 Verilator 与 VCS 的主要差异。
总结
Verilator --cc 编译仿真全流程就是四步:用 verilator --cc --exe --trace --top-module top 把 RTL 翻译成 obj_dir/ 下的 C++ 与 Vtop.mk;手写 C++ harness,include Vtop.h,把端口当成员变量,每次翻转时钟后调用 eval(),需要波形就实现 sc_time_stamp 并挂 VerilatedVcdC;用 make -C obj_dir -f Vtop.mk Vtop 编译链接出原生可执行文件;运行 ./obj_dir/Vtop 靠自校验与退出码判定,加 --trace 再跑一次产出 wave.vcd。记住两个开关分两层、改信号必须 eval、端口名原样对应这三个检查点,这条产出可进 CI 的高速仿真链就跑通了。