Verilator --timing 延时模型流程:让井号delay 真的等上纳秒
更新时间:2026-11-03。Verilator 的传统画像是「周期精确、不认时间」的高速仿真器,
#10这类延时语句过去只会换来一句 STMTDLY 告警然后被静默忽略。5.x 补上了事件驱动的延时模型,开关就是--timing。本篇讲这一步本来怎么走:延时模型在编译运行流程里处于什么位置、相关参数各管什么、不开会报什么、5.020 实际支持到哪。老版本「延时直接忽略」的行为见 Verilator 报 Ignoring delay on this delayed statement,选项怎么选的排错版见 Verilator NEEDTIMINGOPT:--timing 与 --no-timing 怎么选。
本文要回答的问题
--timing打开的「延时模型」在 Verilator 编译运行流程里处于哪一步,和周期精确模式差在哪?--timing、--no-timing、--timescale、--timescale-override各自什么场景加,不加会怎样?- 代码里有
#delay却不显式表态时,5.020 会报什么,退出码是多少? - 除了井号延时,
@(posedge clk)、wait()、命名事件、fork/join这些事件控制在 5.020 支持到什么程度?
先给一张概念卡,把延时模型这一步的边界框出来:
读这张卡抓住一条主线:--timing 不是让代码「能过编译」的宽容开关,而是在 Verilator 把 Verilog 翻译成 C++ 之前,给模型里装入事件队列和调度协程——#2 因此成为时间轴上一个真实的唤醒点。代价是链接额外的 timing 运行库、仿真比纯周期模式慢;可综合性则不受任何影响,门延时写法仍然不能拿去综合。
一、这一步在仿真流程里的位置
Verilator 的正常仿真链路是「Verilog 精化 → 翻译成 C++ → g++ 编译 → 运行可执行文件」。延时模型插在第一环:精化阶段看到 #delay、@、wait 这类时间相关构造时,决定是建事件队列还是拒绝/忽略。
读这张图看两件事。第一,--timing 的作用点在翻译之前,所以它影响的是生成出来的 C++ 形态——实际编译日志里能看到 verilated_timing.o、verilated_threads.o 被链接进来,编译参数带 -fcoroutines,这就是事件调度器的实体。第二,注意图上那条 NEEDTIMINGOPT 标注的分叉:源码含延时却不表态时,流程在精化阶段就断掉,根本到不了 C++ 那一步。本篇的最小设计是一个输出带 1ns 惯性延时的反相器:
`timescale 1ns/1ps
module inv(input wire a, output wire y);
assign #1 y = ~a; // a 变化 1ns 后 y 才翻转
endmoduletestbench 用 #2 间隔给激励,并在电平稳定后采样——采样点故意比门延时晚 1ns,避免同一时刻「激励恢复」和「线网更新」的调度顺序之争。
二、命令逐段解读
完整命令用 --binary 一步到位(Verilator 5.x 直接完成翻译、编译、链接):
docker run --rm -v "$PWD":/work -w /work geek-eda:0.3-sta \
verilator --binary --timing tb.v inv.vdocker 包装负责把当前目录挂进容器,工具本体的参数逐个拆:
| 参数 | 作用 | 不加 / 选错会怎样 |
|---|---|---|
--timing | 启用延时模型:建事件队列,#delay、事件控制按时间轴调度 | 用 --cc/--lint-only 时显式必选,否则报 NEEDTIMINGOPT 直接退出 |
--no-timing | 显式选择「忽略全部延时」,语句按零延时顺序执行 | 每个延时点报 STMTDLY,告警默认致命、退出码 1 |
--timescale <单位/精度> | 给源码里没写 `timescale `` 的文件补一个缺省时间尺度 | 源码自带 timescale 时不影响;全部缺失时 #1 的时间单位无定义 |
--timescale-override <单位/精度> | 强制覆盖所有文件的 timescale,无视源码声明 | 不加则尊重源码;加错会把别人的 1ns 偷偷换成你给的单位 |
--binary | 翻译后直接调用 g++ 产出可执行文件(等价 --cc --exe 加构建) | 换成 --cc 只产 C++ 和 Makefile,要自己再 make |
| 文件列表 | 参与编译的全部源码,testbench 与 DUT 都列出 | 漏文件报 cannot find module containing |
两个实测出来的行为细节值得记住。其一,表态义务随模式而变:5.020 下 --cc 和 --lint-only 遇到延时构造而未给选项时,报 %Error-NEEDTIMINGOPT 终止;--binary 不加选项时实测会按启用延时直接编译运行,但这属于可以少打字、不应当依赖的行为——脚本里统一显式写 --timing,换小版本、换同事读脚本才不会产生歧义。其二,--timescale-override 是全局强压,多文件工程里有人写 1ns/1ps、有人写 1ns/10ps 时,它会把精度统一到你给的值,$time 的记账粒度随之变化,只建议在仿真平台统一收口时使用。
三、输入输出文件长什么样
输入侧两份文件:DUT inv.v 与自检测试台 tb.v,都带 `timescale 1ns/1ps——第一参数是延时单位(#1 即 1ns),第二参数是时间精度,决定仿真器记账的最小刻度。
输出侧,--binary 在 obj_dir/ 下先吐翻译产物 Vtb.cpp、Vtb.h、Vtb__Syms.cpp,再由 g++ 链接成可执行文件 Vtb。延时模式下链接命令里固定多出 timing 运行库,这是判断「模型真的启用了」的物证:
g++ verilated.o verilated_timing.o verilated_threads.o Vtb__ALL.a \
-pthread -lpthread -latomic -o Vtb运行期输出里有个容易误读的点:$time 按精度单位报告。1ns/1ps 下 #2 之后打印出的是 t=2000——2000ps 就是 2ns,不是延时被放大了一千倍。
四、常见错与检查点
不表态:NEEDTIMINGOPT。 用 --cc 或 --lint-only 编译含延时的代码又没给选项,5.020 在每个延时点报 %Error-NEEDTIMINGOPT: Use --timing or --no-timing to specify how delays should be handled,结尾 Exiting due to 7 error(s),退出码 1。检查点:先确认你的延时是否承担语义——testbench 激励时序、门延时模型要保留就加 --timing;确认是无用残留才选 --no-timing 并清理代码。
显式忽略:STMTDLY。 加 --no-timing 后编译并不通过,而是每个延时点改报 %Warning-STMTDLY: Ignoring delay on this statement due to --no-timing,告警在默认策略下致命。它的意思是「我照你说的把延时删了,但后果你确认」——检查点:延时若影响激励与采样的先后,忽略后测试结论直接作废。
时间尺度缺失或打架。 源码没有 `timescale 又不给 --timescale,#1 失去单位依据;多文件精度不一致时,$time 的数字对不上手算值。检查点:每个文件头写齐 timescale,需要统一时用 --timescale-override 显式收口。
同刻竞争采样。 testbench 在 #1 时刻既恢复执行又撞上 DUT 线网更新,读到旧值不一定是 bug。检查点:采样点相对门延时留出余量(本篇错开 1ns),别把调度顺序问题当成功能错误。
五、真实跑一遍
最小设计在容器里走完整条链路。第一步,不带任何 timing 选项用 --cc 编译,精化阶段即被 NEEDTIMINGOPT 拦下:
读这份输出看两处:报错精确指到 inv.v:4:10 的 #1 和 tb.v 的每个 #2,连连续赋值上的门延时也在统计内;7 条 error 里没有一条是语法错误——工具拒绝的是「你不告诉我时间语义该怎么处理」,而不是代码本身。
加上 --timing 重新编译运行,三行采样完整呈现 1ns 门延时:a 为 0 时 y 在 1ns 后变 1,a 拉高后 y 在下一个纳秒变 0,$time 以 ps 记账。
t=2000 a=0 y=1 是第一拍的关键证据:a 在零时刻置 0,惯性延时 1ns 后 y 才翻成 1,采样发生在 2ns,值已经稳定;后两行依次是 a=1→y=0、a=0→y=1。若延时被忽略,这三行只会在同一时刻打印且 $time 恒为 0。
最后探一下事件控制的支持边界:同一个 5.020 下,边沿等待 @(posedge clk)、电平 wait(a)、命名事件 ->e/@e、fork/join 并行块在 --timing 模式全部编译通过并按时间先后触发:
三个触发时刻 5ns、7ns、12ns 与 always #5 clk 翻转、#7 a=1、#12 ->e 逐条对得上。这说明 5.020 的延时模型不只是认井号延时,testbench 常用的事件控制原语已经成体系可用;但它仍是较新的模型,复杂源码迁过来要以本版本实测为准,不要拿旧版文档的「不支持」清单下结论。
实验环境
- 机器/系统:Mac mini(Docker Desktop)/ 容器镜像 geek-eda:0.3-sta(Debian 基础)
- 工具版本:
Verilator 5.020 2024-01-01 rev (Debian 5.020-1)(容器内verilator --version真实输出) - 日期:2026-11-03
- 完整命令:
bash run.sh(容器内依次执行--version、--help查 timing 选项、无选项--cc、--binary --timing编译运行、--no-timing对照、事件控制探针编译运行)
代码获取
最小 DUT、testbench、事件控制探针与复现脚本已放到公开仓库:geek-doc.cn/experiments/flow-verilator-timing-model,装好 docker 后执行 make run(或 bash run.sh)即可依次复现 NEEDTIMINGOPT 报错、延时仿真输出、STMTDLY 对照与事件探针结果。
这一篇在知识体系里的位置
- 前置基础:Verilator 的一键编译流程见 Verilator 二进制仿真:--binary 直出可执行,本篇是在它之上加一个时间维度;
- 报错深读:不加选项时的 NEEDTIMINGOPT 处置见 Verilator NEEDTIMINGOPT:--timing 与 --no-timing 怎么选,老版本 STMTDLY 行为见 Verilator 报 Ignoring delay on this delayed statement;
- 对照工具:同一份 testbench 在事件驱动仿真器下天然支持延时,可对照 同步 FIFO Verilog 实现与回归实测 里 iverilog 的用法。
总结
Verilator --timing 延时模型流程的核心是「先表态、再编译」:源码含 #delay 时用 verilator --binary --timing 让精化阶段建事件队列,门延时与语句延时按 timescale 真正推进,产物是链接了 verilated_timing 运行库的可执行文件;不表态在 --cc/--lint-only 下报 NEEDTIMINGOPT,选 --no-timing 则延时被忽略并以 STMTDLY 致命告警;5.020 实测边沿等待、wait、命名事件与 fork/join 均可用,$time 按 timeprecision 记账——延时解决的是仿真时间语义,不改变 RTL 的可综合性。