Verilator --binary 快速仿真:一条命令出可执行文件,什么时候仍要回到 --cc
更新时间:2026-11-03。仿真是什么 讲过仿真器在软件里逐拍演 RTL,而 Verilator 走的是「把 Verilog 翻译成 C++ 再编译成原生程序」的编译型路线。本篇回答流程上的一个具体问题:Verilator 5.x 的
--binary模式把过去的三步压成一条命令后,构建产物和传统--cc模式差在哪、什么时候图省事、什么时候必须回到老路。速度对比不是本篇重点,关心「比 iverilog 快多少」可读 verilator --binary 千万拍实测。
本文要回答的问题
verilator --binary一条命令背后替你做了哪三件事?自动生成的 main 长什么样?- 传统
verilator --cc模式只跑一条命令为什么出不来可执行文件,还差哪一步? --build-jobs/-j并行度怎么给?-j 0是合法参数吗?- 同一份计数器 RTL,两种模式各自吃什么文件、吐什么产物?
- 什么场景下仍然应该老老实实用
--cc加手写 C++ harness?
先给结论卡,两栏的差异后文逐条用真跑产物验证:
读这张卡抓住一条主线:两种模式翻译 Verilog 到 C++ 的内核完全相同,差别只在「入口 main 谁来写、编译这一步谁来按」。--binary 替你把两件杂事包了,--cc 把这两个钩子留给你。
一、这一步在流程里的位置
仿真是「写完 RTL、做综合之前」的功能验证环节。上游是可综合的 .v 设计文件,Verilator 这一步的下游不是网表,而是一个可执行程序:运行它、看终端里的自校验 PASS/FAIL,就是一次仿真。两条模式在工具内部的分叉与合流如下:
读这张图注意中间的合流点:不管走哪条岔路,最后都要变成一个原生可执行文件。区别是模式 A 的箭头由 Verilator 一次走完,模式 B 在 C++ 产物 处故意停住,等你(或你的构建系统)发第二道命令。图里没有任何一步涉及综合,Verilator 全程只做仿真,不产出可流片的网表。
二、命令逐段解读:两条命令的每个参数
模式 A 的完整命令(本文实验真实使用,日志第 11 行原样可见):
verilator --binary --timing -j 0 -Wall --top-module tb_counter \
--Mdir obj_dir_bin counter.v tb_counter.v模式 B 是两条命令,先翻译再编译:
verilator --cc counter.v --exe tb_main.cpp --Mdir obj_dir_cc
make -C obj_dir_cc -f Vcounter.mk -j"$(nproc)" Vcounter每个参数的用途与不加的后果:
| 参数 | 作用 | 不加会怎样 |
|---|---|---|
--binary | 生成 C++、自动生成 main、自动 make,三合一 | 默认等价于 --cc,只翻译不编译 |
--cc | 只把 RTL 翻译成 C++ 与 Makefile | —(传统默认行为) |
--exe <file> | 把你的 C++ harness 编进最终程序的链接清单 | 产物是没有 main 的库,无法链接成可执行文件 |
--build | --cc 后顺手自动 make(两步合一的折中) | 必须自己进 obj_dir 调 make |
--timing | 支持 #5 延迟、事件等待等行为级语法 | TB 里的 always #5 clk=~clk 报 NEEDTIMINGOPT;--binary 默认已带 |
-j 0 / --build-jobs | 编译并行度,0 表示按 CPU 核数拉满 | 单核编译,大设计构建明显变慢 |
--top-module tb_counter | 显式指定仿真顶层 | 顶层随文件顺序推断,可能错选成 DUT |
--Mdir dir | 指定产物目录 | 默认都堆在 obj_dir,两种模式互相覆盖 |
-Wall | 打开全部风格与位宽告警 | 告警静默,笔误失去提前暴露机会 |
--build-jobs 与 -j 的语义以本机 verilator --help 原文为准:--build-jobs <jobs> 是「Parallelism for --build」,-j <jobs> 同时管 --build-jobs/--verilate-jobs。有一个容易混淆的坑:Verilator 自己的 -j 0 合法(0 = 全部核),但下一层的 GNU make 不接受 -j 0——本次实验踩实了这个区别,手工 make 时报用法错误,改成 -j"$(nproc)"(机器上解析为 -j12)才通过。给 Verilator 的并行度和给 make 的并行度是两道独立的参数。
自动生成的入口文件在日志里可以直接核验:模式 A 的产物清单里出现了 Vtb_counter(可执行文件)和 Vtb_counter__main.cpp(自动生成的入口),后者第 9 行就是 int main(int argc, char** argv, char**)。也就是说,写 Verilog testbench 时你完全不用碰 C++,Verilator 替你补了一个「构造顶层、反复 eval、直到 $finish」的标准 main。
三、输入输出文件:两种模式各吃什么、吐什么
模式 A 吃两个 Verilog 文件:counter.v 是可综合的 8 位使能计数器(异步复位、计到 255 时 carry 拉高一拍),tb_counter.v 是行为级测试台,用 always #5 造时钟、释放复位后跑 300 个使能拍。吐在 obj_dir_bin/ 里的关键产物是可执行文件 Vtb_counter 与自动入口 Vtb_counter__main.cpp;从编译回显还能看到 -fcoroutines 标志,这是 timing 模式用 C++20 协程实现延迟等待的痕迹。
模式 B 只给 Verilator 一个 RTL 文件,入口改由手写的 tb_main.cpp 承担——自己 new 顶层、自己翻时钟、自己在第 100 个上升沿检查 q == 100:
dut->rst_n = 1;
dut->en = 1;
for (int i = 0; i < 100; ++i) {
dut->clk = 0; dut->eval();
dut->clk = 1; dut->eval();
}
const bool pass = (dut->q == 100);只跑 --cc 时,obj_dir_cc/ 里只有翻译产物,没有任何可执行文件,真实清单开头几行如下:
Vcounter.cpp
Vcounter.h
Vcounter.mk
Vcounter__Syms.cpp
Vcounter__Syms.h
Vcounter___024root.h
...
NOTE: Vcounter not built yet; --cc only generates C++ and a MakefileVcounter.cpp/.h 是翻译后的设计模型,Vcounter.mk 是给 GNU make 用的构建脚本,tb_main.cpp 此时尚未编译。执行第二道 make -f Vcounter.mk Vcounter 后,日志里能看到 g++ -c -o tb_main.o ../tb_main.cpp 与最后的链接行,Vcounter 这才出现。这就是「--cc 要两步」的字面含义。
四、常见错与检查点
下面四个检查点全部来自本次真跑,前两个是实际撞到的:
| 现象 | 先查什么 | 纠正 |
|---|---|---|
%Warning-BLKSEQ 后直接 %Error: Exiting | TB 的 always #5 clk=~clk 在 -Wall 下被当成时序块阻塞赋值 | 时钟行是惯例写法,用 /* verilator lint_off BLKSEQ */ 局部豁免 |
产物叫 Vcounter 而不是 Vtb_counter,TB 像没生效 | 是否漏了 --top-module,顶层按文件顺序被推断成 DUT | 显式加 --top-module tb_counter,多顶层语义见 找不到模块文件的排查 |
--cc 跑完目录里没有可执行文件 | 这不是报错,是设计如此:--cc 只生成 | 追加 --build,或自己 make -C obj_dir -f Vtop.mk |
手工 make -j 0 打印一堆 Usage | -j 0 是 Verilator 的语法,GNU make 只认正整数 | make 用 -j"$(nproc)",或省略并行度 |
新手最常见的认知偏差是第三个:看到 --cc 静默成功就去找可执行文件。记住判据——--cc 的成功标准是产物目录里出现 .mk 文件,不是出现可执行文件;要一条命令出程序,要么 --binary,要么 --cc --build。此外全工程的 `timescale 要统一,混用时 Verilator 会报 TIMESCALEMOD 且把 warning 当 error 退出,这个坑在 千万拍实测篇 里有完整复现。
五、真实跑一遍:同一计数器,两种模式各跑一次
两种模式在 geek-eda:0.3-sta 容器(Verilator 5.020)里顺序执行。模式 A 的自动入口证据与运行结果:
读这段输出抓三个点:产物清单里 Vtb_counter 与 Vtb_counter__main.cpp 并列,证明入口是生成的而非手写的;grep 在该文件第 9 行找到 main 函数;运行后计数器在 t=2585000(皮秒,即 2585ns)报告进位脉冲,300 个使能拍后 300 = 256 + 44,回卷一次停在 44,TEST PASS 收尾,最后的 Verilog $finish 说明仿真正常结束而非挂死。
模式 B 手写 harness 的运行结果:
五秒不到的输出背后是完整的两步构建:先 --cc 翻译出 15 个 C++/Makefile 产物,再手工 make -j12 编译 harness 与运行库并链接。两种模式功能结论一致,差别只在构建组织方式。建议复现时做一个小实验:删掉 --top-module 重跑模式 A,亲眼看到顶层被推断成 counter、产物改名——比读三遍文档都能记住显式指定顶层的重要性。
什么时候仍然应该用 --cc?三类需求:一是自定义 C++ harness,要在激励里调用 C++ 参考模型、DPI-C、GoogleTest,或对数十万组参数做扫描,Verilog TB 表达不了;二是接入既有构建系统,--cc 只生成不编译,编译参数、链接顺序、增量构建全部交给你的 Makefile/CMake;三是需要把 RTL 产物当库复用——Vcounter__ALL.a 静态库可以链接进多个不同的测试程序,这是 --binary 一步到位的可执行文件给不了的组织自由度。
实验环境
- 机器/系统:macOS(Darwin 25.5,Apple Silicon)上运行 Docker 容器
geek-eda:0.3-sta(Ubuntu 24.04,x86_64) - 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1),
verilator --version照抄;容器内 GNU make 并行度经nproc解析为 12 - 日期:2026-11-03
- 完整命令:
docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta bash run.sh(脚本内顺序执行verilator --binary --timing -j 0 -Wall --top-module tb_counter --Mdir obj_dir_bin counter.v tb_counter.v与verilator --cc counter.v --exe tb_main.cpp --Mdir obj_dir_cc+make -C obj_dir_cc -f Vcounter.mk -j$(nproc) Vcounter,并分别运行两个可执行文件)
代码获取
完整 RTL、Verilog testbench、C++ harness 与对照脚本已同步到公开仓库:geek-doc.cn/experiments/flow-verilator-binary-mode,克隆后进入该目录执行 make run 即可在容器中复现两种模式的全部输出,make clean 删除两个 obj_dir 构建目录。
这一篇在知识体系里的位置
- 想先理解 testbench、DUT、波形这些概念再动手:读 仿真是什么;
- 关心
--binary相比事件驱动仿真器快多少、构建时间划不划算:读 verilator --binary 千万拍速度实测; - 想把
-Wall与 lint 告警纳入提交前检查:读 Verilator lint 静态检查实操 与 九类高频告警实测; - 同一簇的事件驱动路线对照:iverilog 第一次仿真完整流程(testbench、编译、运行、波形四步闭环)。
总结
Verilator --binary 快速仿真的本质是替你包办三件事:把 RTL 翻译成 C++、自动生成一个「构造顶层并 eval 到 $finish」的 V<top>__main.cpp、自动调 make 完成 g++ 编译,一条命令直接产出可执行文件;传统 --cc 在翻译后故意停手,入口 harness 与编译时机都交给你,换来接入 C++ 参考模型、自定义构建与一库多测的自由度。记住三个细节:行为级 TB 要 --timing(--binary 默认已带)、Verilator 的 -j 0 不等于 GNU make 的 -j 0、多模块工程显式写 --top-module——日常冒烟用 --binary,搭长期验证平台时再回到 --cc。