Verilator COMBDLY 组合逻辑延时告警排查:组合 always 块里的非阻塞赋值
更新时间:2026-10-29。FAQ 速查页,属于 EDA 报错排查系列。Verilator COMBDLY 组合逻辑延时告警是开
-Wall做 lint 时的高频告警:组合逻辑块里写了非阻塞赋值<=,Verilator 会把它当成阻塞赋值=执行并大声提醒你。方向恰好相反的告警——时序块里误用阻塞赋值——见 Verilator BLKSEQ 告警;位宽类告警见 Verilator WIDTH 告警。
首屏答案:COMBDLY 的全文是 %Warning-COMBDLY: Non-blocking assignment '<=' in combinational logic process,意思是 always @(*)(或 always_comb)组合块里出现了 <=。组合逻辑是纯函数,没有「等本拍结束再更新」的概念,<= 写在这里是笔误级的语义错误——绝大多数情况是写时序逻辑的手滑。Verilator 会把这条 <= 按 = 执行,并明确警告「This will be executed as a blocking assignment」。修法一句话:组合块里把 <= 全改回 =。注意一个常见混淆:如果你写的是 #5 这类井号延时,Verilator 5.x 报的不是 COMBDLY,而是 NEEDTIMINGOPT / STMTDLY 告警族,第四节有实测辨析。
本文要回答的问题
%Warning-COMBDLY指向的到底是什么写法?- 组合逻辑里用
<=为什么危险,Verilator 会怎么处理? - 组合块写
#延时会报 COMBDLY 吗?(实测结论:不会) - 怎么修、修完怎么确认闭环?
一、报错原文与现象
最小复现:一个组合多路选择器,手滑把阻塞赋值写成了非阻塞赋值:
module combdly_real(
input wire a,
input wire b,
input wire sel,
output reg y
);
always @(*) begin
if (sel)
y <= a; // 组合逻辑里用 <=,Verilator 报 COMBDLY
else
y <= b;
end
endmodule跑 verilator --lint-only -Wall,真实告警如下:
读这段输出抓三点。第一,告警钉在 combdly_real.v:10:9,即 y <= a 这一行的赋值符号上,源码行原样回显,不用自己搜。第二,紧跟的解释行 This will be executed as a blocking assignment '='! 是关键——Verilator 不会拒绝编译,而是悄悄按阻塞赋值执行,这正是危险所在:同一个文件在事件驱动仿真器(VCS/Icarus)里 <= 是「本拍末更新」,在 Verilator 里是「立即更新」,某些写法下两边仿真结果会不一致,提示行里 else you may end up with different sim results 说的就是这个。第三,末尾 %Error: Exiting due to 1 warning(s):-Wall 下告警被提升为退出错误,CI 直接卡住。这份代码里 else 分支还有一条 y <= b,Verilator 对同族告警做了去重,只报一处——修的时候别只改报出来的那一行,整个组合块里的 <= 都要过一遍。
二、根本原因:组合块里没有「延迟更新」
组合逻辑建模的是一张真值表:输入定了,输出就定了,求值应发生在「当下」。阻塞赋值 = 的语义正是立即求值、立即生效;非阻塞赋值 <= 的语义是「把新值排进本拍结束时的更新队列」,这个队列是为时序逻辑(寄存器在时钟沿统一翻转)准备的。把 <= 写进组合块,等于向一个不存在的调度点委托更新——IEEE 1800 没禁止这种写法(合法但糟糕的代码),事件驱动仿真器会按 NBA 区域延迟执行,于是同一块逻辑里先读后写与先写后读的顺序差异就开始暴露,产生仿真与综合结果不一致的经典坑。Verilator 是周期求值模型,没有 NBA 更新队列,干脆把 <= 当 = 执行,并用 COMBDLY 告诉你「我替你改了语义,你确认一下」。所以这条告警的本质不是「风格不好」,而是「你的代码在不同仿真器之间可能行为不同」。
举一个会真实翻车的场景。假设组合块里连续两条赋值,第二条依赖第一条的结果:
always @(*) begin
t <= a & b; // 非阻塞:本拍末才更新
y <= t | c; // 读到的是 t 的旧值
end在事件驱动仿真器里,y 拿到的是 t 更新之前的值——整个块要等下一个 delta 周期才稳定到真值表结果,中间态还会顺着扇出传播;而 Verilator 把两条 <= 都当 = 执行,y 立即拿到新 t 的结果。同一份 RTL,两边仿出来的波形不一样,你在 Icarus 里调通的逻辑搬到 Verilator 就「莫名其妙地对了」或者反过来。这类不一致在纯组合网里表现为毛刺与多拍收敛,一旦这块组合逻辑的输出又喂给时序块采样,差异就被固化进寄存器,极难追。Clifford Cummings 在 SNUG 2000 的经典论文里把「组合块用非阻塞赋值」列为 Verilog 编码的头号坏味道之一,理由正是这个调度语义陷阱。SystemVerilog 的 always_comb 从语言层面进一步收紧了约束:块内变量不许被块外赋值、敏感表自动推导,配合阻塞赋值才是组合逻辑的正确打开方式。
三、排查与修复流程
拿到 COMBDLY 后的收敛路径很短,一张图走完:
这张图的关键分叉在第二步:先确认这个块本来就该是组合块。如果敏感表是 @(*)、always_comb,那 <= 就是笔误,改回 =;如果你本意是写寄存器逻辑却忘了在敏感表里放 posedge clk,那问题是块声明错了——补上时钟沿让它成为时序块,<= 反而是对的,此时再冒出来的是反方向的 BLKSEQ 告警。判定标准一句话:块里出现时钟沿,用 <=;否则用 =,两种赋值符不许跨阵营。
修复版代码:
module combdly_real_fixed(
input wire a,
input wire b,
input wire sel,
output reg y
);
always @(*) begin
if (sel)
y = a; // 组合逻辑用阻塞赋值
else
y = b;
end
endmodule重跑同一条 lint 命令,零告警、退出码 0:
$ verilator --lint-only -Wall combdly_real_fixed.v
# exit_code: 0不要用 /* verilator lint_off COMBDLY */ 消音——这条告警守护的是跨仿真器语义一致性,圈掉它等于默认接受「Verilator 按 = 跑、事件驱动仿真器按 <= 跑」的分叉。
四、辨析:组合块里写 # 延时报的不是 COMBDLY
很多人按名字猜 COMBDLY 管「组合逻辑里的 #5 延时」,实测不是。Verilator 5.020 对下面这种写法:
always @(*) begin
if (sel)
#5 y = a; // 组合块里的井号延时
else
#5 y = b;
end默认报 %Error-NEEDTIMINGOPT(要求你显式选 --timing 或 --no-timing);选 --no-timing 则报 %Warning-STMTDLY(延时被忽略);选 --timing 反而触发 BLKSEQ(含延时的过程被整体当时序过程处理)。连续赋值形态 assign #3 y = a & b; 在 5.020 配 --timing 下甚至能静默通过——门延时语义被吞掉,连提醒都没有。也就是说 # 延时走的是 NEEDTIMINGOPT / STMTDLY 告警族,处置方式见 Verilator STMTDLY 延时语句排查;<= 进组合块才是 COMBDLY。两条族谱记错了会修错方向:给 NEEDTIMINGOPT 的代码把 <= 改成 = 解决不了任何问题。
五、预防清单
- 组合
=、时序<=:写 always 块前先定块性质,赋值符跟着块性质走,不跟着手感走。 - 改完搜全块:COMBDLY 对同族告警去重,报一处不代表只有一处。
-Wall进 CI:告警升级为退出错误,笔误在提交阶段就被拦下。- 跨仿真器跑同一 TB 时尤其警惕:事件驱动与周期求值模型对
<=的解释不同,COMBDLY 是分歧的最早信号。
实验环境
- 机器/系统:macOS 15(docker 容器 geek-eda:0.2 内跑 lint)
- 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1)(照抄
verilator --version输出) - 日期:2026-10-29
- 完整命令:
verilator --lint-only -Wall combdly_real.v(报错复现);verilator --lint-only -Wall combdly_real_fixed.v(修复后复验);第四节辨析用verilator --lint-only -Wall [--timing|--no-timing] combdly_bad.v
代码获取
复现用源码(combdly_real.v 报错版、combdly_real_fixed.v 修复版、combdly_bad.v # 延时对照样)在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/verilator-warn-combdly。装好 Verilator 后跑 verilator --lint-only -Wall combdly_real.v 复现告警,再对 combdly_real_fixed.v 跑同一命令验证修复。
这一篇在知识体系里的位置
本篇是 EDA 报错排查系列中 Verilator lint 告警簇的一员:组合块误用 <= 看本篇;时序块误用 = 看 Verilator BLKSEQ 告警;井号延时类写法看 Verilator STMTDLY 排查;表达式位宽不符看 Verilator WIDTH 告警;信号有驱动没人读的方向见 EDA 报错排查系列 导航。
总结
Verilator COMBDLY 组合逻辑延时告警报的是「组合 always 块里写了非阻塞赋值 <=」,Verilator 会把它当阻塞赋值执行并警告跨仿真器结果可能不一致;修法是把组合块里的 <= 全部改回 =(注意告警去重,别只改报出的那一行),若本意是时序逻辑则是敏感表漏了时钟沿;# 延时属于 NEEDTIMINGOPT / STMTDLY 告警族,与 COMBDLY 不是一回事;修复后 -Wall 零告警、退出码 0 才算闭环,永远不用 lint_off 消音代替修复。