Verilator USERWARN USERERROR:elaboration 参数断言与退出行为
Verilator USERWARN USERERROR 常在检查模块参数时出现:例化者传入了零位宽、过小深度或非 2 的幂深度,模块作者希望问题在编译或 lint 期暴露,而不是等仿真跑到一半才失败。SystemVerilog 的 elaboration(细化)阶段会确定参数、展开 generate 并检查层次结构,正适合放这类守卫。它与 Verilator ASCRANGE 升序位选 一类声明检查不同:诊断文本和触发条件由模块作者自己写。
本文要回答的问题
- 为什么模块作用域里的
$warning和$error会在--lint-only时执行? - USERWARN 与 USERERROR 分别适合表达什么参数问题?
- 为什么两者默认都退出 1,加上
-Wno-fatal后又都退出 0? - 怎样写参数守卫,才能同时检查默认参数与命令行覆盖值?
一、Verilator USERWARN USERERROR 为什么出现在 elaboration 阶段
模块参数不是运行时才变化的普通信号。Verilator 读取顶层参数及 -G 覆盖值后,需要常量求值 generate 条件,决定哪些声明真正进入设计。把 $warning 或 $error 放在条件 generate 分支中,分支一旦成立,工具就能在生成仿真模型之前打印诊断。因此不需要 testbench、时钟或 C++ 主程序,verilator --lint-only 已足够复现。
两种系统任务表达了作者意图,但在 Verilator 5.020 中都带 %Warning 前缀,也都受警告是否致命的配置影响。不要只凭 $error 的名字推断它一定绕过 -Wno-fatal。
读这张对比卡要把“诊断语义”和“退出策略”分开。左栏不是可以忽略的提示,右栏也不是无条件终止;真正稳定的 CI 约束应保留警告致命,而不是假设 $error 能突破全局选项。
二、用两个最小模块分别触发诊断
先看 USERWARN。下面模块默认 DEPTH=3,位运算 (DEPTH & (DEPTH - 1)) != 0 判断正整数是否为 2 的幂。条件在 elaboration 时已经确定,所以命中的只有这个 generate 分支。实际项目若允许深度 1,还要单独处理 0 与负数,不能直接把这段位运算当作完整参数校验。
module userwarn_bad #(
parameter int DEPTH = 3
) ();
if ((DEPTH & (DEPTH - 1)) != 0) begin : gen_depth_check
$warning("DEPTH=%0d is not a power of two", DEPTH);
end
endmoduleUSERERROR 的反例更直接:WIDTH=0 不可能形成有效数据通路,应明确拒绝。消息里同时给出期望值和实际值,读者不必再猜是哪个实例的哪个参数出了问题。
module usererror_bad #(
parameter int WIDTH = 0
) ();
if (WIDTH <= 0) begin : gen_width_check
$error("WIDTH must be positive, got %0d", WIDTH);
end
endmodule同一次运行依次 lint 两个文件,输出坐标都落在系统任务所在行;In instance 指明实例,最后的 Exiting due to 1 warning(s) 则来自默认的警告致命行为。
终端记录中两个反例的退出码都是 1。这里的 %Error: Exiting... 不是额外的第二处 RTL 错误,而是在汇总“有一个警告被当作失败”。排查时应先读上面的 %Warning-USERWARN 或 %Warning-USERERROR,不要从最后一行反推根因。
三、退出码由 fatal 配置决定,不只由系统任务名决定
-Wno-fatal 的作用是允许警告存在时继续完成处理。它常用于暂时观察完整警告清单,却不适合作为参数错误的长期 CI 配置。对同两个反例追加该选项后,消息仍然打印,USERERROR 也没有例外,两个退出码都变成 0:
--- USERWARN with warnings made non-fatal ---
$ verilator --lint-only -Wall -Wno-fatal userwarn_bad.sv
%Warning-USERWARN: userwarn_bad.sv:5:9: DEPTH=3 is not a power of two
: ... note: In instance 'userwarn_bad'
5 | $warning("DEPTH=%0d is not a power of two", DEPTH);
| ^~~~~~~~
... For warning description see https://verilator.org/warn/USERWARN?v=5.020
... Use "/* verilator lint_off USERWARN */" and lint_on around source to disable this message.
exit code: 0
--- USERERROR with warnings made non-fatal ---
$ verilator --lint-only -Wall -Wno-fatal usererror_bad.sv
%Warning-USERERROR: usererror_bad.sv:5:9: WIDTH must be positive, got 0
: ... note: In instance 'usererror_bad'
5 | $error("WIDTH must be positive, got %0d", WIDTH);
| ^~~~~~
... For warning description see https://verilator.org/warn/USERERROR?v=5.020
... Use "/* verilator lint_off USERERROR */" and lint_on around source to disable this message.
exit code: 0| 命令配置 | USERWARN | USERERROR | 适合的场景 |
|---|---|---|---|
--lint-only -Wall | 打印诊断,退出 1 | 打印诊断,退出 1 | 默认 CI,问题必须处理 |
再加 -Wno-fatal | 打印诊断,退出 0 | 打印诊断,退出 0 | 临时收集完整告警,不宜据此判定参数合法 |
| 合法参数、默认配置 | 无诊断,退出 0 | 无诊断,退出 0 | 修复后的正常检查 |
这也解释了“日志明明有 error,流水线却成功”的现象:作业可能全局继承了 -Wno-fatal。若必须临时使用该选项,应另外扫描诊断并设白名单,不能只看进程退出码。长期做法仍是缩小非致命配置的范围,让 USERERROR 在默认配置下使检查失败。
四、修复参数,同时保留可复用的守卫
修复不是删掉系统任务,而是给当前实例传合法值,并把边界检查留在模块中。下面默认 WIDTH=8、DEPTH=4;位宽和最小深度属于硬约束,用 $error,非 2 的幂深度只是建议,用 $warning。给 generate 块命名还能让层次路径和诊断更易读。
module params_fixed #(
parameter int WIDTH = 8,
parameter int DEPTH = 4
) ();
if (WIDTH <= 0) begin : gen_width_check
$error("WIDTH must be positive, got %0d", WIDTH);
end
if (DEPTH < 2) begin : gen_depth_check
$error("DEPTH must be at least 2, got %0d", DEPTH);
end else if ((DEPTH & (DEPTH - 1)) != 0) begin : gen_depth_advisory
$warning("DEPTH=%0d is not a power of two", DEPTH);
end
endmodule合法默认值先得到干净的 lint 结果。随后用 -GWIDTH=0 覆盖顶层参数,守卫仍会触发,证明检查不是只对源码里的默认值有效:
--- fixed: legal parameter values ---
$ verilator --lint-only -Wall params_fixed.sv
exit code: 0
--- fixed guards still reject WIDTH=0 ---
$ verilator --lint-only -Wall -GWIDTH=0 params_fixed.sv
%Warning-USERERROR: params_fixed.sv:6:9: WIDTH must be positive, got 0
: ... note: In instance 'params_fixed'
6 | $error("WIDTH must be positive, got %0d", WIDTH);
| ^~~~~~
... For warning description see https://verilator.org/warn/USERERROR?v=5.020
... Use "/* verilator lint_off USERERROR */" and lint_on around source to disable this message.
%Error: Exiting due to 1 warning(s)
exit code: 1把这类断言放在通用模块里,比只在某个 testbench 中检查更有价值:每个例化都会携带自己的参数约束。消息至少包含模块期望、实际参数值和必要单位;条件则使用可在 elaboration 求值的常量表达式。不要用运行时 always 块替代这里的检查,否则零位宽等结构问题可能早已让后续编译失败。
也不要为了让日志安静而全局关闭 USERWARN 或 USERERROR。确有第三方代码需要局部处理时,豁免范围、原因和恢复位置应成对出现,做法见 Verilator lint_off 局部豁免;自己维护的参数守卫更应修参数,而非关闭诊断。
五、在真实层次中定位参数来源并接入检查
最小例子里顶层就是受检模块,真实设计却常有多层包装:顶层给互联模块配置位宽,互联模块再把参数传给存储或协议模块。诊断中的源码行告诉你“哪条守卫失败”,In instance 告诉你“哪个实例失败”,两者要一起读。若同一个通用模块被例化多次,只看文件名无法判断应该改哪一处连接;应从失败实例沿层次向上找第一次出现非法值的位置,而不是在底层模块里把约束改松。
命令行 -G 主要覆盖顶层参数,不能自动改写所有下层实例。下层值可能来自上层默认值、参数表达式或另一个参数的计算结果。例如 DEPTH = 1 << LOG_DEPTH 看似能避免任意深度,却仍需检查 LOG_DEPTH 是否为负或大到超出实现能力。守卫应尽量靠近真正依赖该约束的模块,同时让上层错误消息保留足够线索,这样就不必逐层手工打印参数。
模块作者还要避免相互矛盾的诊断。比如 WIDTH=0 已触发“必须为正”的 USERERROR,同一模块后续若又按该位宽声明向量,可能产生额外告警,掩盖最有用的第一条消息。可把参数检查集中放在模块前部,并保证错误文本能独立说明修复方向;但不要为了消除派生告警而给非法值悄悄套用安全默认值,那会让错误配置变成另一套悄悄运行的硬件。
接入持续检查时可以按下面清单走一遍。它既验证合法配置没有被误伤,也验证非法配置确实无法混入通过结果。
| 检查动作 | 预期证据 | 能发现的问题 |
|---|---|---|
| lint 默认参数 | 无 USERWARN/USERERROR,退出 0 | 默认值本身违反模块约束 |
| 覆盖一个合法边界值 | 退出 0,功能回归通过 | 边界条件写得过严 |
| 覆盖零、负数或越界值 | 对应守卫打印实际值,退出 1 | 守卫未执行或检查对象错误 |
| 多实例使用不同参数 | 诊断能区分失败实例 | 只按源码行修改、误伤其他实例 |
| 检查完整命令选项 | 没有意外继承 -Wno-fatal | 日志有错误但流水线误判成功 |
功能验证仍然不可省略。参数合法只说明模块允许这种结构,不代表协议、时序和数据结果都正确;特别是深度、位宽会影响测试数据规模和覆盖范围,修复后应让回归显式覆盖该配置。
实验环境
- 机器/系统:macOS 主机,Docker 容器
geek-eda:0.2。 - 工具版本:
Verilator 5.020 2024-01-01 rev (Debian 5.020-1)。 - 日期:2026-10-31。
- 完整命令:
docker run --rm -v "$PWD:/work" -w /work geek-eda:0.2 bash experiments/ts-verilator-userwarn-usererror/run.sh;脚本对两个反例分别执行默认 lint 与-Wno-fatallint,再检查合法参数和-GWIDTH=0覆盖。
代码获取:反例、带守卫的修复版和完整复跑脚本位于 geek-doc.cn 公开仓库,进入该目录后运行 bash run.sh 即可复现。
这一篇在知识体系里的位置
本篇解决 Verilator USERWARN USERERROR 的参数断言与退出行为,重点是 elaboration 期检查。声明方向问题可转到 Verilator ASCRANGE 升序位选;需要处理外部代码诊断时,再配合 Verilator lint_off 局部豁免 控制范围。
总结
Verilator USERWARN USERERROR 来自 elaboration 条件分支中的 $warning 与 $error,适合在编译或 lint 期拒绝非法参数、提示非理想配置。Verilator 5.020 默认让两者随警告致命而退出 1,-Wno-fatal 下却都可能退出 0,所以 CI 不能把 $error 当成绕过 fatal 配置的保证。正确修复是传入合法参数、保留带实际值的模块级守卫,并分别验证默认值和 -G 覆盖值。