一句话结论
Verilator 5.052 的语法覆盖比“只能跑可综合子集”的印象完整得多:延时(含 real 变量延时)、real 运算、pullup/nmos 开漏、三态 inout、SVA、covergroup 都实现了;不支持的集中在对门级精度有意义的少数语法上——tran 双向开关、specify 门延时、驱动强度仲裁、Verdi 专有系统任务。危险的不是报错那类,而是“接受但静默失真”那类。
判定依据:同一份源码分别给 Verilator 5.052 和 VCS 2023.12,逐条 A/B 对照;语法支持矩阵用 39 个独立小用例(每条语法单独一个文件)分别做 lint、build、run 得到,不是查文档抄来的。
环境与方法
| 项 | 值 |
|---|---|
| 模拟器 | Verilator 5.052(2026-09-05),gcc 12.5.0 |
| 平台 | CentOS 7.9 / glibc 2.17,无 root |
| 对照 | VCS 2023.12-SP1 |
| 方法 | 每条语法一个独立文件 → verilator --lint-only --timing -Wno-fatal → 可疑者再 --binary --timing build + run |
| 统计 | 39 个用例:lint 38 PASS / 1 FAIL;FAIL 的那条(tran)build 也失败 |
跑通一份真实 design 要改什么(简写)
对象:7 系列 FPGA 上的 1080p 时序控制器,29 个模块,100 MHz 输入、490 MHz LVDS 串行输出、I2C 从机,testbench 跑 43 ms 仿真时间。
必须改的四处:
| # | 改动 | 原因 |
|---|---|---|
| 1 | $fsdbDumpfile/$fsdbDumpvars/$fsdbSuppress → $dumpfile/$dumpvars |
没有 Verdi PLI,$fsdb* 是未知系统任务 |
| 2 | unisims + secureip 换成行为级 fast 模型 | secureip 是加密源码,Verilator 读不了;unisims 的 SERDES 又依赖它 |
| 3 | testbench 内显式写 glbl glbl(); |
有原语用 XMR glbl.GSR,且实例名必须正好是 glbl |
| 4 | 波形格式换 VCD 或 FST | 见“波形相关的坑” |
结果:TB 抓帧写出的两张 BMP 与 VCS 逐字节相同(md5 一致),结束时刻到 1 ps 一致;编译 4.6 s(-j32)对 VCS 23 s,仿真 52 s 对 51 s。
支持矩阵(实测)
完全支持,且与 VCS 行为一致
| 类别 | 构造 |
|---|---|
| 时序/组合块 | always_ff / always_comb / always_latch / always @(*) |
| 复合类型 | packed struct、unpacked struct、packed union、typedef enum + '( ) 强转 |
| 包与接口 | package + import、interface + modport |
| 类 | class、new、virtual function(隐式 static 生命期会告 IMPLICITSTATIC) |
| 容器 | 动态数组 int a[]、队列 int q[$]、关联数组 int tbl[string] |
| 字符串 | string、$sformatf |
| 赋值模式 | '{default:1'b1} 打包模式、'{0,1,2,3} 数组模式 |
| 生成 | generate for + genvar |
| 断言 | 立即断言 assert(...) else $error、并发断言 assert property(真正执行,见下) |
| 覆盖率 | covergroup/coverpoint + --coverage(verilator_coverage 会报 covergroup bin 命中率) |
| 进程控制 | fork/join、join_any、join_none、wait fork、disable fork、disable {具名fork} |
| 过程连续赋值 | force / release |
| 文件与随机 | $readmemh/$readmemb、$fopen/$fwrite/$fclose、$urandom、randcase |
| 延时 | 过程块 #(0.250ns)、连续赋值 assign #(real变量) |
| 三态与门级 | pullup/pulldown、nmos/pmos、and/nand/or/nor/xor/buf/not、tri inout |
| 其它 | casez/casex、unique case、有符号算术、数组端口、let、inside、XMR(u_sub.sig 读写)、DPI-C import |
两个容易踩反的结论,都是实测推翻的旧印象:
disable {具名fork}是支持的。fork: W ... join_any; disable W;实测只留下胜方分支的打印;disable fork同样生效。- SVA 不只是“能解析”,违例时真的会报
%Error: Assertion failed ... %Error: Verilog $stop并中止仿真——CI 里这条要当失败处理。
不支持(硬报错)
| 构造 | 报错 |
|---|---|
tran / tranif0 / tranif1 双向开关 |
%Error-UNSUPPORTED: Verilog 1995 gate primitive: tranif1,build 直接失败 |
| 加密的厂商模型(secureip 一类) | 源码不可读,无法解析 |
$fsdb* 等 Verdi 专有系统任务 |
无对应 PLI,未知系统任务 |
门级/SDF 反标、$setup/$hold timing check |
无等价实现 |
接受但静默失真(最需要警惕)
| 构造 | Verilator | VCS | 说明 |
|---|---|---|---|
驱动强度:pullup 对 assign (weak0,weak1) s=0 |
0 |
1 |
不报警,任何两个驱动按值仲裁,强度不参与 |
specify ... endspecify 路径延时 |
y 在 2000 ps 翻转 | y 在 3000 ps 翻转 | 只有 %Warning-SPECIFYIGN,延时被丢弃 |
$dumpoff / $dumpon |
被解析但不生效 | 正常 | dump 会一路写到仿真结束 |
--no-timing 或 +notimingchecks |
每条 #延时 变成 %Warning-STMTDLY: Ignoring delay,所有边沿塌到 t=0 |
— | 配上 -Wno-fatal 就是静默改变行为 |
关于 --timing:5.x 默认就开着,不加也是同一个结果;要防的是把它关掉(或 +notimingchecks 这种“接受并忽略”的选项)。建议脚本里显式写 --timing,让意图留在命令行上。
波形相关的坑
$dumpvars必须配--trace(VCD)或--trace-fst(FST)。少了这个编译期开关,$dump*是静默空操作:跑完没有文件,也没有任何提示。$dumpoff/$dumpon无效(见上表),所以“只 dump 一段窗口”做不到;能调的只有$dumpvars(层级, 作用域)的层级。- 体积量级差三个数量级:同一个 43 ms、两条 490 MHz 总线的 design,只 dump TB 与 DUT 顶层两层——VCD 12.4 GB,FST 123 MB。没有 Verdi 的 suppress 等价物,压不下去就只能改层级或换 FST。
- 抓波形本身是主要成本:同一配置下无波形 52 s,FST 152 s(约 3 倍),VCD 约 98 s;而 VCS 那边开 FSDB 只多 0 s。要快就别默认开波形。
- Verdi 读不了 FST,GTKWave 原生支持;FST 也要 lz4 头文件才能编(
fst2vcd可转 VCD 给别的工具)。 $time返回“整数个时间单位”:1 ns/1 ps 的设计里%0t打印会显示42922750.000 ns,看着像比 VCS 差 355 ps,其实 VCD 里的最后时间戳是42922749645 ps,两侧一致。判断时间轴要看波形时间戳,不要看$time的打印。
写 TB / RTL 时的规矩(checklist)
- 注释不要以
// Verilator开头。Verilator 会把// verilator ...当 pragma,报%Error-BADVLTPRAGMA: Unknown verilator comment。用// NOTE:、// VLNOTE:之类前缀。 - 原语先确认有没有行为级模型。只有加密 secureip 版本的 IP,Verilator 直接没戏;能拿到 fast 行为模型的,功能仿真完全够用。
- XMR 到 global(如
glbl.GSR)必须有一个合适的实例名:testbench 里写glbl glbl();(实例名与模块同名),Verilator 靠向上搜索解析。 -Wno-fatal必须配警告过滤。把UNSUPPORTED/SPECIFYIGN/STMTDLY/WIDTHEXPAND/MULTIDRIVEN这些 code 单独 grep 出来逐条判断;一口气-Wno-fatal当“零警告”用,等于把静默失真放过去。- 断言失败会
$stop中止仿真,回归脚本的退出码/日志匹配要按失败处理,不能当“跑完了”。 - 不要依赖驱动强度和门延时。开漏 + 上拉这种“strong0 压过 pull1”的常见拓扑结果正确,但“pull1 压过 weak0”这类就错,而且没有警告。
- 先
--lint-only --timing再 build:lint 阶段把语法问题一次清干净,比每次 4 s 编译去试快得多。 - dump 层级按需给:
$dumpvars(1, tb)加$dumpvars(1, dut)通常就够定位,全层次 dump 的体积和耗时都不划算。
什么时候不该用 Verilator
| 场景 | 原因 |
|---|---|
| 门级 / 后仿、SDF 反标 | 无 specify/门延时、无 timing check |
| 依赖驱动强度的模型 | 强度不参与仲裁,静默错 |
| 只有加密模型可用的 IP | 源码读不了 |
流程绑定 Verdi($fsdb*、KDB) |
无 PLI;波形只能 FST/VCD |
| 期望仿真速度大幅提升 | 本例 52 s 对 51 s:编译快 5 倍,仿真没加速(时钟沿数量主导,不是事件调度开销) |
复现方法
每个语法一条用例、一个文件,先 lint 后 build,脚本骨架:
# 单条语法
verilator --lint-only --timing -Wno-fatal --top-module m case.sv
# 可疑的(断言/覆盖率/进程控制)再真跑
verilator --binary --timing -Wno-fatal --coverage --top-module m -o V case.sv && ./obj_dir/V
# 覆盖率汇总(会列出 covergroup 命中率)
verilator_coverage coverage.dat
要看“和商用模拟器是否一致”,就把同一个文件交给两个模拟器各跑一遍,比较打印出的时刻与数值(本文所有“一致/不一致”都来自这种做法)。
局限
本文数据来自 Verilator 5.052 单一版本、39 个语法用例和一份 29 模块的真实 design,不代表更早/更晚版本的行为差异;program、clocking 块、DPI-C 只验证了“能解析/能编译”,运行时语义没有逐条核对。升级 Verilator 后建议重跑一遍这套 lint 矩阵。