Verilator BLKANDNBLK 告警排查:同一变量混用阻塞与非阻塞赋值
更新时间:2026-10-29。FAQ 速查页,属于 EDA 报错排查系列。Verilator BLKANDNBLK 告警发生在同一变量被两种赋值方式混用时:一处写
cnt = 0(阻塞),另一处写cnt <= cnt + 1(非阻塞)。与「时序块里误用阻塞赋值」的 BLKSEQ 告警不同,BLKANDNBLK 在 Verilator 里是 %Error 级「Unsupported」——不是提醒,是直接拒绝编译;而事件驱动仿真器(如 Icarus Verilog)对这种写法往往静默放行,于是代码「在 iverilog 里跑得好好的,一上 Verilator 就报错」。反方向的坑——组合块里误用<=——见 Verilator COMBDLY 告警。
首屏答案:BLKANDNBLK 的全文是 %Error-BLKANDNBLK: Unsupported: Blocked and non-blocking assignments to same variable: 'cnt',意思是变量 cnt 在代码里既被 = 赋值又被 <= 赋值。最典型的案发现场是复位分支手滑写成 cnt = 8'd0; 而累加分支写 cnt <= cnt + 1;。修法一句话:时序逻辑里全量统一为 <=(包括复位),不要只改报出来的那一行。
本文要回答的问题
- BLKANDNBLK 和 BLKSEQ、COMBDLY 这三条告警分别管什么?
- 为什么 Verilator 把它当 Unsupported 直接拒编译,而 iverilog 一声不吭?
- 混用在事件驱动仿真器里到底会埋什么雷?(有实测)
- 怎么修、修完怎么确认闭环?
一、报错原文与典型现象
最小复现:一个带低有效复位的 8 位计数器,复位分支误用阻塞赋值:
module blkandnblk_bad(
input wire clk,
input wire rst_n,
output reg [7:0] cnt
);
always @(posedge clk) begin
if (!rst_n)
cnt = 8'd0; // 复位分支误用阻塞赋值
else
cnt <= cnt + 1; // 主逻辑用非阻塞赋值
end
endmodule跑 verilator --lint-only -Wall,真实输出如下:
读这段输出抓四点。第一,BLKANDNBLK 是 %Error 不是 %Warning:Unsupported: Blocked and non-blocking assignments to same variable: 'cnt'——Verilator 不支持这种写法,编译就此打住,不存在「忽略告警继续跑」的选项。第二,报错给了三个位置:变量声明处(第 5 行)、阻塞赋值处(第 9 行)、非阻塞赋值处(第 11 行),三方对质,定位零成本。第三,同一编译还顺带报了 %Warning-BLKSEQ:复位分支的 = 落在时序块里,本身就是 BLKSEQ 管的「时序块误用阻塞赋值」——混赋几乎总是由一次 BLKSEQ 笔误引发的,两条告警是母子关系。第四,退出码 1,CI 直接卡住。
收敛顺序一张图走完:
这张图的关键在第二步「判块性质」:赋值符跟着块性质走,不跟着手感走。带 posedge/negedge 敏感表的时序块里所有赋值都该是非阻塞;always @(*) 组合块里所有赋值都该是阻塞。BLKANDNBLK 的本质就是你把两个阵营的语法混进了同一个变量的生命周期。
二、根本原因:Verilator 为什么没有 NBA 队列
事件驱动仿真器(VCS、Icarus)按 IEEE 1800 的事件区域模型跑:阻塞赋值 = 在 Active 区域立即生效,非阻塞赋值 <= 把更新排到 NBA(Non-blocking Assignment)区域、本拍末统一落账。同一变量混用两种赋值,标准并未禁止——它只保证 <= 的更新最后发生,于是最终值取决于事件调度顺序,属于「合法但糟糕」的代码。
Verilator 是周期求值(cycle-based)模型:把 RTL 编译成 C++,每个时钟沿一次性算出下一拍状态,没有事件队列、没有 NBA 区域。要让混赋语义正确,就得在求值顺序里为每个变量维护「阻塞写立即生效、非阻塞写延迟生效」的两条路径,还要处理两者交叠时的先后顺序——这正是 Verilator 明文列为 Unsupported 的原因:支持它的代价是拖慢所有设计的仿真速度,而收益只是兼容一种公认的坏味道。所以 Verilator 的选择是硬错误拒编译,逼你把写法改干净。
伴随的 BLKSEQ 则是另一层考量:即便没有混赋,时序块里用 = 也会让同沿读该变量的其他进程看到「新值还是旧值」取决于进程调度顺序,产生竞争——BLKSEQ 只是警告(默认配置下 Reset 里的 = 甚至不报),而 BLKANDNBLK 直接是错。
三、行为差异实测:iverilog 静默放行,竞争就埋在复位沿
同一个混赋文件丢给 Icarus Verilog 13.0:编译零告警、仿真正常跑完。这就是这条告警最坑的地方——iverilog 不拦你,不代表代码是对的。为了把「静默」背后的风险摆到台面上,加一个对照 testbench:另一个 always 块与 DUT 在同一个时钟沿采样 cnt:
$ iverilog -g2012 -o tb_race.vvp blkandnblk_bad.v blkandnblk_fixed.v tb_race.v && vvp tb_race.vvp
t=5000 rst_n=0 bad_sampled=x fixed_sampled=x
t=15000 rst_n=0 bad_sampled=0 fixed_sampled=0
t=25000 rst_n=0 bad_sampled=0 fixed_sampled=0
t=35000 rst_n=1 bad_sampled=0 fixed_sampled=0
t=45000 rst_n=1 bad_sampled=1 fixed_sampled=1
t=55000 rst_n=1 bad_sampled=2 fixed_sampled=2
# exit_code: 0读这段波形输出抓 t=5000 那一拍:复位生效的第一个时钟沿,混赋版 bad_sampled 与修复版 fixed_sampled 都打到了 x。对修复版来说这是确定行为——<= 的复位更新排在 NBA 区域,同沿的采样块必然读到旧值 x,任何仿真器任何版本都一样。对混赋版来说这是撞大运:复位走的是阻塞赋值,在 Active 区域立即生效,采样块读到 x 还是 0 完全取决于仿真器这一拍先调度哪个进程——IEEE 1800 对同区域进程的执行顺序不做任何保证。iverilog 13.0 这次恰好先跑了采样块所以读到 x;换一个仿真器、甚至同一仿真器换了编译选项,结果就可能变成 0。这类「复位沿上的读-写竞争」正是混合赋值最经典的翻车点:testbench 在 A 仿真器上调通,搬到 B 仿真器上断言莫名失败,查半天发现是调度顺序不同。Verilator 的 BLKANDNBLK 硬错误就是在编译期把这种不确定性连根拔掉。
四、修复:全量统一 <=,复跑闭环
修复版把复位分支也改成非阻塞赋值:
module blkandnblk_fixed(
input wire clk,
input wire rst_n,
output reg [7:0] cnt
);
always @(posedge clk) begin
if (!rst_n)
cnt <= 8'd0; // 复位同样用 <=
else
cnt <= cnt + 1;
end
endmodule重跑同一条 lint 命令,零告警、退出码 0,连带 BLKSEQ 一起消失:
$ verilator --lint-only -Wall blkandnblk_fixed.v
# exit_code: 0两个注意点。其一,Verilator 对 BLKANDNBLK 按变量去重、只报第一个命中的变量——如果一个大模块里有多个变量混赋,修完一处复跑会冒出下一处,所以更稳的做法是先 grep 一遍:搜出报错的变量名,核对它在整个文件里所有赋值点是否统一;再扫一遍复位分支里所有 =,逐个改。其二,不要试图用 /* verilator lint_off BLKANDNBLK */ 消音——它是 Error 不是 Warning,lint_off 压不住 Unsupported;就算压得住,混赋的语义分歧依然存在,只是你看不见了。
五、辨析:BLKANDNBLK / BLKSEQ / COMBDLY 三条族谱
三条告警经常被搞混,一句话分工:
| 告警 | 触发写法 | 级别 | 修法 |
|---|---|---|---|
| BLKANDNBLK | 同一变量既有 = 又有 <= | %Error(Unsupported) | 按块性质全量统一赋值符 |
| BLKSEQ | 时序块里只有 = | %Warning | 时序逻辑统一改 <= |
| COMBDLY | 组合块里写了 <= | %Warning | 组合逻辑统一改 = |
记法:块性质定阵营,变量不许跨阵营。时序块(敏感表带时钟沿)里所有赋值走 <=;组合块(@(*) 或 always_comb)里所有赋值走 =。BLKSEQ 和 COMBDLY 是「块内写错符」,BLKANDNBLK 是「同一变量两边都占了」——它往往是一次 BLKSEQ 笔误撞上本来就正确的 <= 主逻辑。testbench 场景另说:tb 里对 DUT 内部信号的 force/probe 混赋同样会触发 BLKANDNBLK,处置原则是 tb 自己驱动的变量在 tb 内部统一赋值风格。
六、预防清单
- 时序
<=、组合=:写 always 块前先定块性质,复位分支也不例外。 - 复位分支重点自查:混赋案发现场九成在复位分支,code review 时把它当固定检查项。
- 修完 grep 变量名:告警按变量去重,报一处不代表只有一处。
-Wall进 CI:BLKSEQ 是 Warning、BLKANDNBLK 是 Error,-Wall 下全部在提交阶段拦死。- 跨仿真器跑同一套代码时尤其警惕:iverilog 静默放行的写法不等于安全,Verilator 的 Error 是分歧的最早信号。
实验环境
- 机器/系统:macOS 15(Verilator 在 docker 容器 geek-eda:0.2 内跑;iverilog 本机原生)
- 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1)(照抄
verilator --version输出);Icarus Verilog version 13.0 (stable) (v13_0) - 日期:2026-10-29
- 完整命令:
verilator --lint-only -Wall blkandnblk_bad.v(报错复现);iverilog -g2012 -o tb_race.vvp blkandnblk_bad.v blkandnblk_fixed.v tb_race.v && vvp tb_race.vvp(行为对照);verilator --lint-only -Wall blkandnblk_fixed.v(修复后复验)
代码获取
复现用源码(blkandnblk_bad.v 混赋版、blkandnblk_fixed.v 修复版、tb_race.v 竞争演示 testbench)在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/verilator-warn-blkandnblk。装好 Verilator 后跑 verilator --lint-only -Wall blkandnblk_bad.v 复现报错,再对 blkandnblk_fixed.v 跑同一命令验证修复;iverilog 对照实验按 README 一条命令即可复跑。
这一篇在知识体系里的位置
本篇是 EDA 报错排查系列 中 Verilator lint 告警簇的一员:同一变量混用两种赋值看本篇;时序块纯用 = 看 Verilator BLKSEQ 告警;组合块误用 <= 看 Verilator COMBDLY 告警;表达式位宽不符看 Verilator WIDTH 告警。
总结
Verilator BLKANDNBLK 告警报的是「同一变量既被阻塞赋值 = 又被非阻塞赋值 <=」,Verilator 因其周期求值模型没有 NBA 队列而把它列为 Unsupported 硬错误、直接拒编译;iverilog 等事件驱动仿真器静默放行,但混赋在同沿采样场景下会留下依赖进程调度顺序的读-写竞争。修法是按块性质全量统一赋值符——时序块(含复位分支)一律 <=,复跑 -Wall 零告警退出码 0 才算闭环;不要用 lint_off 消音,修完按变量名 grep 全文件防止去重漏网。