%Warning-INITIALDLY 排查:Verilator 忽略 initial 块里的延时赋值
更新时间:2026-10-29。FAQ 速查页,属于 EDA 报错排查系列。把一段在事件驱动仿真器(VCS/XSim/Icarus)里跑得好好的
initial初始化序列丢给 Verilator,迎面就是一串「延时被忽略」的报错或告警——这不是代码语法错,而是#10这类时间控制本来就不在 Verilator 的仿真模型里。同簇的#delay泛用告警见 Verilator 报 STMTDLY,本篇聚焦initial块场景下三个容易混淆的告警族。
首屏答案:initial 块里写 #10 rst_n = 1'b1; 想「延时后释放复位」,Verilator 5.020 默认直接报 %Error-NEEDTIMINGOPT 拒绝处理;加 --no-timing 后降为 %Warning-STMTDLY(语句延时被忽略);若写成 rst_n <= #10 1'b1; 这种带延时的非阻塞赋值,还会多一条 %Warning-INITIALDLY——非阻塞赋值在 initial/final 块里会被按阻塞赋值执行,语义悄悄改变。修复方向只有一个:把「等 N 个时间单位」改写成「等 N 个时钟沿」,用计数器加 always @(posedge clk) 释放复位,复跑 -Wall 零告警才算闭环。
本文要回答的问题
- NEEDTIMINGOPT、STMTDLY、INITIALDLY 三条消息有什么区别,分别对应什么写法?
- 为什么 Verilator 宁可报错也不肯「假装执行」这个延时?
initial里的非阻塞赋值被当阻塞执行,会改变什么行为?- 复位释放、上电初始化这类需求,可综合语境下正确的写法是什么?
一、报错原文与现象
最小复现是一段典型的「上电复位序列」:initial 块里先拉低复位,再用 # 延时控制释放时机:
module initdly_bad (
input wire clk,
output reg rst_n,
output reg init_done
);
initial begin
rst_n = 1'b0;
init_done = 1'b0;
#10 rst_n = 1'b1; // 期望:延时 10 时间单位后释放复位
#5 init_done = 1'b1; // 期望:再过 5 时间单位置位完成标志
end
endmodule对这段代码跑 verilator --lint-only -Wall,真实输出如下:
读这段输出抓两点:第一,报的是 Error 不是 Warning——5.020 对「代码里出现了延时、但你没告诉它怎么处理延时」这件事的态度是直接拒绝,要求显式加 --timing 或 --no-timing 二选一;第二,行号钉得很准,9:5 与 10:5 就是两条 # 语句的位置,指认范围精确到列。这与老版本「一句 Unsupported: Ignoring delay 告警然后静默忽略」的行为完全不同,CI 里从旧版升到 5.x 的工程会在这里集中爆雷。
二、工具与版本
本篇实验在 Linux 容器环境用 Verilator 5.020(2024-01-01,Debian 打包版)实测,命令统一为 verilator --lint-only -Wall [--no-timing] <file>.v。版本行为差异是本篇的关键背景:
| 场景 | 老版本(约 4.x 及更早默认行为) | 5.020 实测行为 |
|---|---|---|
initial 里 #10 语句 | %Warning-STMTDLY: Unsupported: Ignoring delay 告警后忽略 | 默认 %Error-NEEDTIMINGOPT 拒绝;--no-timing 下退化为 STMTDLY 告警 |
<= #10 值(带延时的 NBA) | 延时静默忽略 | 额外多一条 %Warning-INITIALDLY |
--timing 选项 | 不存在 | 存在,但只面向 testbench 场景,可综合 RTL 不该靠它 |
也就是说,在 5.020 上搜「Verilator initial 延时被忽略」会同时撞见三个告警族的名字,它们是三种不同的写法问题,不是同一条告警的三种说法,下一节逐条区分。
三、根本原因:三个告警族分别指什么
Verilator 是零延时、两态(0/1,无 X/Z)的周期级仿真器,它的执行模型里没有「仿真时间推进到 10 个单位后再执行这一句」这件事。事件驱动仿真器靠时间队列调度 #10 之后的语句,Verilator 把代码编译成按时钟沿求值的 C++,时间队列根本不存在——延时不是「被不支持」,而是无处可放。三个告警族对应三种写法:
- NEEDTIMINGOPT(错误):代码里出现任何
#延时,而命令行既没给--timing也没给--no-timing。这是 5.x 的「必须先表态」闸门,不是代码本身的问题定性。 - STMTDLY(告警):
--no-timing模式下,整条#10 语句的延时被忽略,语句立即执行。rst_n在 0 时刻就被拉成 1,你想要的「先复位 10 拍」的行为直接消失。 - INITIALDLY(告警):
initial/final块里的非阻塞赋值<=会被按阻塞赋值=执行。NBA 的核心语义是「本时间步结束才生效、多块并行更新不互相干扰」,退化成阻塞后赋值立即生效——如果同一块里后面的语句读取这个信号,读到的就是新值而不是旧值,与事件驱动仿真器的行为产生分歧。
实测把两种写法各跑一遍(输出节选,完整记录见文末代码获取):
这段输出里三条告警要分开读:ASSIGNDLY 说赋值上的 #10 时间控制被忽略;UNUSEDSIGNAL 是顺带的「clk 没人用」(恰恰暴露了作者没打算用时钟);INITIALDLY 才是本篇主角——This will be executed as a blocking assignment '='! 把语义改变直接写在了告警里。排查收敛路径一张卡看全:
读这张漏斗抓一个分流点:代码是可综合 RTL 还是纯 testbench。RTL 里的 initial 延时序列(复位释放、上电等待)必须改写成时钟沿驱动;testbench 激励如果要用 Verilator 跑,正确出口是 --timing(5.x 已支持常见 testbench 延时语法)或把激励挪到 C++ 驱动层,而不是在 RTL 里留 # 再 lint_off 消音。
四、排查清单
拿到这一族报错后按清单机械执行:
- 先看是 Error 还是 Warning:
%Error-NEEDTIMINGOPT只是要求你显式选--timing/--no-timing,不代表代码判死刑;真正的定性看它之后跟着的 Warning 族名。 - 按族名对号写法:STMTDLY 找
#N 语句;ASSIGNDLY 找<= #N 值;INITIALDLY 找initial块里的所有<=。 - 评估语义损失:延时被忽略后,「先复位一段时间再释放」这类时序意图是否直接消失?多数情况是,那这条告警就不是噪声而是行为分歧预警。
- 查 INITIALDLY 的真实风险:
initial块里<=之后有没有语句再读这个信号;有读就有「读到新值还是旧值」的仿真器间分歧。 - 分流处置:RTL 一律改写;testbench 走
--timing;只有确认语义无关紧要的边角才考虑lint_off,并在注释里写清理由。 - 收口复验:改完重跑同一条
-Wall命令,退出码 0、无任何 DLY 字样才算闭环。
五、修复代码与复跑确认
本例的意图是「复位拉低一段时间再释放,然后置位完成标志」。可综合语境下「一段时间」的唯一载体是时钟周期,改写成计数器加时钟沿:
module initdly_fixed (
input wire clk,
output reg rst_n,
output reg init_done
);
reg [3:0] cnt;
initial begin
cnt = 4'd0;
rst_n = 1'b0;
init_done = 1'b0;
end
always @(posedge clk) begin
if (cnt < 4'd10)
cnt <= cnt + 4'd1;
if (cnt == 4'd9)
rst_n <= 1'b1; // 第 10 个时钟沿释放复位
if (cnt == 4'd10)
init_done <= 1'b1; // 再晚一拍置位完成标志
end
endmodule要点有三:initial 里只留零时刻的静态初值(这部分 Verilator 完全支持,FPGA 上电初值语义也吻合);所有「等一段再动作」的逻辑搬进 always @(posedge clk),用计数器数拍数代替 # 数时间单位;时序块里的 <= 是合法且正确的,INITIALDLY 只针对 initial/final 块。对修复版复跑同一条命令,零输出、退出码 0(log 最后两行),告警闭环。顺带的好处是这段代码从「仿真器专属写法」变成了真正可综合的 RTL——#10 在任何综合器里都是直接被忽略的,原来的写法即使过了 lint 也会在硅上行为全变。
实验环境
- 机器/系统:Linux 容器环境(docker 镜像 geek-eda:0.2)
- 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1)
- 日期:2026-10-29
- 完整命令:
verilator --lint-only -Wall initdly_bad.v(NEEDTIMINGOPT 错误,退出码 1);verilator --lint-only -Wall --no-timing initdly_bad.v(STMTDLY 告警);verilator --lint-only -Wall --no-timing initdly_nba.v(INITIALDLY 告警);verilator --lint-only -Wall initdly_fixed.v(修复后复验,退出码 0)
代码获取
复现用源码(initdly_bad.v 语句延时版、initdly_nba.v 带延时 NBA 版、initdly_fixed.v 修复版)与完整 lint 记录在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/verilator-warn-initialdly。装好 Verilator 后按目录 README 里的四条命令逐条复现,先报错再验证修复。
这一篇在知识体系里的位置
本篇是 EDA 报错排查系列中 Verilator lint 告警簇的一员:initial 块延时与 INITIALDLY 看本篇;通用 #delay 语句的 STMTDLY 泛用场景看 Verilator 报 STMTDLY;顺带的 clk 未使用告警看 %Warning-UNUSEDSIGNAL 排查;阻塞与非阻塞赋值语义混淆的另一面看 %Warning-BLKSEQ 排查。整个系列的目录在 EDA 报错排查系列。
总结
Verilator 对 initial 块里 # 延时的态度是「无处安放、必须表态」:5.020 默认报 NEEDTIMINGOPT 错误逼你在 --timing 与 --no-timing 间二选一,后者下语句延时被忽略报 STMTDLY、initial 里的非阻塞赋值退化为阻塞报 INITIALDLY——三族告警对应三种写法,按族名对号即可定位;可综合 RTL 的唯一正解是把「等 N 个时间单位」改写成计数器加时钟沿,initial 只留静态初值,testbench 激励走 --timing,复跑 -Wall 零告警才算闭环。