iverilog 除零:仿真得到 x 却不报错的常量与变量排查
更新时间:2026-10-30。属于 EDA 报错排查系列。iverilog 除零与 iverilog 数值截断排查都可能静默改变结果,但本篇关注的是四态未知值,不是位宽变窄。
首屏答案:在本机 Icarus Verilog 13.0 中,8'd100 / 8'd0 无论写成常量表达式还是变量除法,8 位商都显示为 0xxx,编译和 vvp 运行退出码也全是 0;商再加 1 仍为全 x。修复不能靠等待报错,而应在 valid 有效且除数非零时才执行除法,其余情况给出确定输出和状态位。
本文要回答的问题
- 常量除数为零与运行时才变为零,输出是否不同?
- 为什么编译、运行都成功,结果仍可能完全不可用?
- 怎样确认 x 已传播,而不是只看进程退出码?
- 除数保护如何既拦住零除数,又不改变正常除法?
两栏要比较的是「何时能发现零除数」,不是猜测哪一种写法会产生更严重的值。常量可以在设计展开阶段暴露问题,变量则要到对应激励执行时才出现;这次两者的实际结果与退出码相同。
一、完整实测:iverilog 除零没有让进程失败
坏例把常量商定义为 localparam logic [7:0] CONST_Q = 8'd100 / 8'd0,再在初始块中把 divisor 置 0,计算 dividend / divisor。为了区分「商本身未知」和「仅显示异常」,还将商加 1,并用四态相等检查所有位是否为 x。最后补做 100 除以 4 的正常对照。
日志第 4、5 行说明坏例编译退出 0,编译器没有打印 warning 或 error。第 7 行常量商为 0xxx,第 8 行变量商和加一结果也都是 0xxx,两次 is_x=1 来自测试平台的四态检查。第 9 行非零除数得到 0x19,即十进制 25,证明实验不是把所有除法都写坏了。
bad run exit=0 只表示仿真执行到 $finish 并正常退出。修复版的两次编译、运行同样退出 0,但最后多出两项关键证据:零除数时 quotient=0x00 safe=0,正常输入仍为 0x19 safe=1,最终打印 PASS。
二、根因:零除数产生未知商,下游继续使用就会传播
在这次 8 位整数实验中,除零没有对应可表示的商,Icarus 给结果的全部位填入 x。x 是四态逻辑中的未知值,不是数值 0,也不是一个可直接当成随机数使用的特殊整数。它进入加法后,结果仍为全 x;若进入地址、比较或控制条件,也必须按四态语义重新检查,不能假定它会自动变成某个安全值。
还要区分仿真进程状态与设计健康状态。vvp 的退出码描述程序是否正常结束,除非测试平台主动调用 $fatal 等失败机制,否则一个商为 x 的算术结果不会自动改变进程状态。因此,把回归写成「命令退出 0 就通过」会漏掉本文这类问题;断言负责把语义错误转换成非零退出,二者职责不同。
常量与变量的差别在求值时机:常量表达式可在编译和展开阶段处理,变量除法依赖仿真时的操作数。这个差别影响诊断出现的位置,却没有在本次实验中改变最终位型。也不能据此泛化到实数、不同位宽或其他仿真器,那些情况需要各自实测。
红色链路对应坏例,未知值从除法流向下游;橙色节点是新增的协议判定;绿色两支分别约束异常情况与正常情况。保护不是把 x 涂改成 0,而是让零除数根本不走除法分支。
| 观察点 | 实测值 | 能得出的结论 |
|---|---|---|
| 常量除零 | 0xxx | 本例展开后得到全 x |
| 变量除零 | 0xxx | 本次运行时结果与常量相同 |
| 商加 1 | 0xxx | x 已传播到下游算术 |
| 坏例编译、运行 | 均为 0 | 退出码不能代替数值检查 |
| 100 除以 4 | 0x19 | 正常除法对照为 25 |
本文没有运行综合实验,因此不声称某个综合器会如何处理除零。仿真里的 x 也不是综合后电路必然输出的固定 0 或 1;常量折叠、诊断和除法器实现都应在目标综合流程中另行验证。
三、排查清单:先定位未知值,再审查除法协议
- 同时记录除数、商和下游值:在除法前打印除数,在除法后打印商,再观察一个实际消费者。只盯最终状态会错过第一个变 x 的位置。
- 用四态检查确认 x:
===与!==会比较 x、z 位型;普通==遇到未知位可能得到未知结果,不适合作为唯一的回归判据。本例用quotient !== 8'hxx触发失败检查。 - 拆开常量与运行时来源:常量除数可直接审查参数和宏;运行时除数要沿寄存器、存储器读取、握手和复位路径追查,确认哪个周期允许它变成 0。
- 核对有效位契约:若请求由
valid限定,就明确无效周期是否允许输出 0、保持旧值或忽略输出。不要只加保护,却让下游继续无条件消费商。 - 让测试平台能失败:对异常与正常两组输入都写
$fatal(1, ...)断言。进程退出 0 不是 PASS,只有预期位型和状态全部匹配才是。
还应把保护后的 safe 接到使用端,而不是仅用于打印。如果系统更需要发现配置错误,也可以在 valid && divisor == 0 时断言失败;选择默认零输出还是立即终止取决于接口契约,两者都比静默传播 x 更容易排查。
四、修复代码:有效位与条件保护后复跑 PASS
下面的组合逻辑先判断 valid 和除数,再决定是否计算。两个分支都赋满 quotient 与 safe,避免引入锁存器;零除数和无效请求采用相同的确定输出,正常请求保留原除法。
always_comb begin
if (valid && (divisor != 0)) begin
quotient = dividend / divisor;
safe = 1'b1;
end else begin
quotient = 8'd0;
safe = 1'b0;
end
end测试平台依次覆盖 valid=1, divisor=0、valid=1, divisor=4 和 valid=0, divisor=4,每项都用 !== 核对商和状态。真实复跑摘录如下:
guarded valid=1 divisor=0 quotient=0x00 safe=0
guarded valid=1 divisor=4 quotient=0x19 safe=1
guarded valid=0 divisor=4 quotient=0x00 safe=0
PASS: zero divisor guarded and valid division preserved
fixed.sv:56: $finish called at 3000 (1ps)
fixed run exit=0
PASS: x behavior observed and guarded division verified这组输出同时证明修复没有「一律输出 0」:只有异常或无效路径输出 0,有效非零除法仍得到 25。若下游只应在成功时更新状态,还必须用 valid && safe 限定消费条件。
实验环境
- 机器/系统:macOS 26.5.2,x86_64。
- 工具版本:Icarus Verilog 13.0 (stable) (v13_0)。
- 日期:2026-10-30。
- 完整命令:
bash run.sh;脚本依次执行iverilog -g2012 -s tb -o bad_simv bad.sv、vvp bad_simv、iverilog -g2012 -s tb -o fixed_simv fixed.sv、vvp fixed_simv,标准输出与标准错误合并记录。四次命令及脚本最终退出码均为 0。
代码获取
坏例、保护版、复跑脚本和完整日志位于公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/ts-iverilog-divide-by-zero。进入目录执行 bash run.sh,即可重现实测结果。
这一篇在知识体系里的位置
本篇处理 iverilog 除零后的四态未知值与运行时保护。若问题表现为高位丢失而非全 x,继续看 iverilog 数值截断排查;若模块边界已经混用了不同位宽,则看 iverilog 端口位宽不匹配。返回 EDA 报错排查系列 可按编译诊断、运行值和协议断言继续分诊。
总结
iverilog 除零在本机常量与变量实验中都得到全 x,且编译、运行退出码均为 0,商加 1 后 x 继续传播。排查应使用四态断言定位首个未知值,并同时验证正常除法;修复用 valid 加除数非零条件隔离异常输入,零除数输出 0、safe=0,100 除以 4 仍为 25,复跑 PASS。