iverilog 敏感表不完整:仿真不报错但输出不更新,与综合结果不一致的排查
更新时间:2026-10-29。本篇属于开源 EDA 排错系列,总入口见 开源 EDA 报错排错中心。这是一类「不报错」的 iverilog 仿真错误:组合逻辑
always块手写的敏感表漏了一个输入信号,仿真器照常编译照常跑、一行警告都没有,但漏掉的那个信号变化时输出纹丝不动——更要命的是综合器根本不看敏感表,综合出的电路行为是「正确」的,于是仿真和硬件各说各话。如果你刚写完 组合逻辑与时序逻辑 的入门练习就遇到「仿真波形和预期对不上、但下板反而是对的」这种灵异现象,多半就是它。
本文要回答的问题
- 敏感表漏信号时 iverilog 为什么不报任何错?输出为什么会「卡住」?
- 为什么综合器(Yosys)对同一份代码却给出正确的电路?
- 用什么样的测试台能在几分钟内复现并锁定这个问题?
- 修复该手写敏感表还是直接换
always @(*)?
一、现象:不报错,但输出不随输入更新
先建立直觉。敏感表(sensitivity list)是 always @(…) 括号里那份信号清单,它的语义是「这些信号中任意一个变化时,重新执行块内语句」。仿真器严格按这份清单调度:清单里没有的信号变了,块内的赋值语句根本不会被执行,输出寄存器保持上一次的计算结果。而 iverilog 不会检查「块内用到的信号是否都在敏感表里」——这在 IEEE 1364 里是合法代码,只是语义通常不是你要的。
下面这张概念卡把问题结构一次说清:左边是手写敏感表漏了 b 的组合块,中间是仿真器与综合器对它的两种不同解读,右边是两种修复路径。
读这张卡抓住一条主线:同一份代码,仿真器按「敏感表」执行、综合器按「表达式依赖」生成电路,两条路在敏感表完整时殊途同归,一旦漏信号就分道扬镳——这就是 sim/synth mismatch 的全部根源。
二、最小复现:一个 2-to-4 译码器
错误版本的译码器长这样,敏感表只写了 a:
// decoder_bug.v —— 敏感表漏了 b
module decoder_bug (
input wire a,
input wire b,
output reg [3:0] y
);
always @(a) begin // BUG: b 不在敏感表里
case ({a, b})
2'b00: y = 4'b0001;
2'b01: y = 4'b0010;
2'b10: y = 4'b0100;
2'b11: y = 4'b1000;
endcase
end
endmodule测试台的思路是「对照实验」:同一个激励同时喂给 bug 版和修复版(always @(*)),每个激励后对照参考真值表打分。激励序列故意先动 a(两版都触发,都正确),再只动 b——这一刻 bug 版就该露馅。
读这份输出抓 t=33 那一行:a=1 b=1 时参考输出应为 1000,修复版给出 1000 OK,bug 版却仍停留在 t=22 算出的 0100,被标记 STALE——b 从 0 变 1 没有触发任何重算。结尾的 BUG-VERSION MISMATCH count=1 与 FIX-VERSION PASS 把结论钉死。注意 t=44 行 bug 版显示 OK,那是「碰巧正确」:b 回到 0 后旧值恰好等于期望值,这种偶合正是此类 bug 在大设计里极难肉眼发现的原因。
三、根本原因:仿真语义与综合语义的分歧
iverilog 为什么沉默。 敏感表不完整在 Verilog 标准里是合法构造(某些场景故意用它做特殊建模),iverilog 既不报警也不报错,vvp 按清单机械调度。想知道块内实际用了哪些信号,工具不做这件「多余的事」。
综合器为什么正确。 日志后半段的 Yosys 对照实验更有说服力:把 bug 版和修复版分别送进 yosys synth,两版综合出的网表单元统计完全一致——2 个 ANDNOT、1 个 AND、1 个 NOR。综合器生成组合逻辑时按标准忽略敏感表、根据右值表达式自动推导全部依赖。也就是说,RTL 仿真里那个「卡住」的行为在真实硅片/FPGA 上根本不存在。
这就是最阴的 bug 形态。 仿真说错、硬件说对:你信仿真去改设计,把对的改错;你不信仿真直接流片,验证又形同虚设。唯一的正解是让仿真语义对齐综合语义——敏感表必须完整。
为什么偏偏是译码器、选择器这类结构高发。 组合块里被漏掉的信号,往往是在某个分支里才用到的输入:译码器的低位选择线、多路器的高位地址、带使能的门控信号。写代码时先搭好主分支骨架、敏感表随手一写,后补的分支引入了新的右值信号,却没人回头改清单。模块越「半成品迭代」越容易中招,一次写完的简单模块反而少见。这也解释了为什么漏敏感表在教科书里总是被点名、在真实项目里也持续有人踩——它不是知识盲区,是迭代过程的自然产物。
「碰巧正确」让问题隐蔽性翻倍。 日志里 t=44 行 bug 版打出 OK 不是修复了,而是旧值恰好等于新期望——漏掉的信号只要不改变输出(或改回去之后旧值仍对),测试就完全感知不到异常。在一个几十个信号的中等模块里,漏掉一个只在边角条件起作用的输入,随机激励可能跑几千拍都不暴露,直到某一天某个特定向量序列触发,仿真和芯片的行为差异才浮出水面。靠肉眼扫波形发现这类问题几乎不可能,必须靠对照机制——黄金参考模型、lint、或本文这种同激励双版本对照。
四、排查清单
遇到「输出不随某个输入更新、且不报任何错」时按此顺序过一遍:
| 检查项 | 动作 | 判据 |
|---|---|---|
| 敏感表完整性 | 对比 always 块右值信号与 @(...) 清单 | 每个右值信号都在清单里 |
换 @(*) 对照 | 复制一份改成 always @(*) 同跑 | 两版行为出现分叉即实锤 |
| 分叉点定位 | 测试台逐激励打 $display 对照 | 找到第一个 STALE 激励 |
| 综合对照 | yosys synth 两版看 stat | 网表一致=仿真侧问题 |
| lint 兜底 | Verilator --lint-only -Wall | 会报 SENSITIVITY 类警告 |
最后一行值得展开:iverilog 对漏敏感表沉默,但 Verilator 的 lint 会明确告警。同一份代码过一道 Verilator,很多「iverilog 不说」的问题会被提前翻出——这也是 iverilog 隐式线网定义 那篇的同款教训:iverilog 的宽松是它作为教学工具的优点,也是它作为验证工具的盲区。
五、修复与预防
修复就一行:把手写敏感表换成自动推导。
always @(*) begin // 工具按块内表达式自动补全敏感表
case ({a, b})
2'b00: y = 4'b0001;
// ... 同上
endcase
end复跑结果见前面日志:FIX-VERSION PASS,所有激励点全部 OK。三条预防纪律:
- 组合逻辑永远写
always @(*)(Verilog-2001 起)或 SystemVerilog 的always_comb,从第一天起不手维护敏感表。always_comb更优:它强制块内只能赋组合值、且被多块驱动的变量会直接报错。 - 时序逻辑写
always @(posedge clk),敏感表里只有时钟(和异步复位),绝不混入数据信号。 - 回归里加一道 Verilator lint,把 iverilog 的沉默区交给更严的工具兜底。
代码获取:本文全部源码(decoder_bug.v / decoder_fix.v / tb_decoder.v + README 复现说明)在公开仓 experiments/iverilog-sensitivity-list,克隆后 iverilog -g2012 -o sim_both decoder_bug.v decoder_fix.v tb_decoder.v && vvp sim_both 即可复现。
实验环境
- 机器/系统:Mac mini M4 / macOS 15
- 工具版本:Icarus Verilog 13.0(
iverilog -V首行Icarus Verilog version 13.0 (stable) (v13_0));Yosys 0.33(docker 镜像 geek-eda:0.2) - 日期:2026-10-29
- 完整命令:
iverilog -g2012 -o sim_both decoder_bug.v decoder_fix.v tb_decoder.v、vvp sim_both、yosys -p 'read_verilog decoder_bug.v; synth -top decoder_bug; stat'(fix 版同理)
这一篇在知识体系里的位置
本篇是开源 EDA 排错簇中「仿真语义」节点的长尾页:iverilog 其他「不报错但行为不对」的坑见 iverilog 隐式线网定义,更多工具报错入口从 开源 EDA 报错排错中心 分流;组合逻辑本身的建模规则回到 组合逻辑与时序逻辑 系统学习。
总结
iverilog 敏感表不完整是一类「零警告」仿真错误:always @(a) 漏了 b,仿真器按清单调度导致输出不随 b 更新,而综合器忽略敏感表按表达式补全,两者分叉形成 sim/synth mismatch。定位靠「@(*) 对照版 + 逐激励打分」的最小复现,修复就是把敏感表交给工具——组合块一律 always @(*) 或 always_comb,再配一道 Verilator lint 兜底,这类灵异现象就彻底绝迹。