Verilator WIDTHEXPAND:零扩展与符号扩展的显式位宽修复
Verilator WIDTHEXPAND 指出赋值右值比左值窄,SystemVerilog 会在写入前隐式扩展高位。它不像截断那样必然丢数据,却可能让无符号数与有符号数走上不同的扩展路径;如果声明中的 signed 写漏或写多,告警背后就是真实的数值错误。本篇用 8 位到 16 位的 unsigned、signed 双用例复现专码,再用显式拼接固定扩展意图。反向的高位丢弃问题见 Verilator WIDTHEXPAND 的反向诊断 WIDTHTRUNC。
本文要回答的问题
WIDTHEXPAND与旧版WIDTH、WIDTHTRUNC分别指向什么?- 同样是
8'hFF扩到 16 位,为什么结果可能是16'h00FF或16'hFFFF? - 排查时应看左值、右值,还是整个表达式的有符号性?
- 怎样用显式位宽修复,并用真实退出码确认修复生效?
这张图的转折在第三步:位宽相等只消除语法层面的隐式动作,不能证明扩展结果正确。无符号数据应补零,有符号负数要复制符号位;若这一步判断反了,即使 lint 通过,负数也会被改成很大的正数。
一、完整报错:同一份 broken 同时出现两种隐式扩位
下面的模块把两个 8 位输入分别送到 16 位输出。unsigned_data 没有 signed 修饰,signed_data 则明确是有符号数;两条赋值都依赖语言规则自动生成高 8 位,因此都会触发 WIDTHEXPAND。
module widthexpand_broken (
input wire [7:0] unsigned_data,
input wire signed [7:0] signed_data,
output wire [15:0] unsigned_result,
output wire [15:0] signed_result
);
assign unsigned_result = unsigned_data;
assign signed_result = signed_data;
endmodule完整诊断会分别指出 ASSIGNW 需要 16 位,而右值 VARREF 只有 8 位,并把插入符放在对应右值下面。末尾 %Error: Exiting due to 2 warning(s) 不是第三个语法错误,而是 Verilator 在默认告警致错策略下停止;本例 broken 的进程退出码为 1。不要看到 %Error 就去查模块语法,真正需要处理的是前面两条位宽诊断。
二、工具版本:Verilator 5.020 使用独立专码
实验使用 Verilator 5.020。5.x 将扩位方向报告为 WIDTHEXPAND,截断方向则使用 WIDTHTRUNC,比 Verilator WIDTHEXPAND 原先所属的旧版 WIDTH 总告警 更便于精确处理。版本升级后专码变细,不等于同一份 RTL 的硬件行为突然改变;但旧的告警清单和消音配置需要按新名称重新审视。
记录中版本行锁定工具,两个 WIDTHEXPAND 主行分别对应 unsigned 和 signed 赋值。broken exit=1 与 fixed exit=0 构成对照,最后的 PASS 还说明脚本检查到了恰好两条专码;因此这不是只凭终端没有报错就判断成功,而是同时核对了诊断数量、内容和进程状态。
%Warning-WIDTHEXPAND: widthexpand_broken.sv:7:28: Operator ASSIGNW expects 16 bits on the Assign RHS, but Assign RHS's VARREF 'unsigned_data' generates 8 bits.
%Warning-WIDTHEXPAND: widthexpand_broken.sv:8:28: Operator ASSIGNW expects 16 bits on the Assign RHS, but Assign RHS's VARREF 'signed_data' generates 8 bits.
%Error: Exiting due to 2 warning(s)
broken exit=1
fixed diagnostics: none
fixed exit=0
PASS: broken reported unsigned and signed WIDTHEXPAND; fixed lint exited 0三、根因:零扩展与符号扩展补出的高位不同
扩位(expansion)发生在目标位宽大于表达式位宽时。对这个简单赋值,右值的无符号性决定新增高位:unsigned 右值补 0,signed 右值复制最高位,也就是符号位。判断重点是右值表达式,不是只看左值声明。
| 右值类型 | 8 位输入 | 扩到 16 位 | 十进制含义 |
|---|---|---|---|
| unsigned | 8'hFF | 16'h00FF | 255 仍是 255 |
| signed | 8'shFF | 16'hFFFF | -1 仍是 -1 |
两条路径都保持原来的数值解释,所以 WIDTHEXPAND 本身并不等于功能错误。它提醒的是一种审查负担:阅读者必须找到右值声明,才能推出高位是什么。如果 signed_data 本来应该是无符号字节,隐式符号扩展就会让 8'hFF 变成 16'hFFFF;此时正确修复是改声明或明确补零,而不是把符号位复制得更隐蔽。
复杂表达式还要多检查一步。比较、运算、位选和拼接可能改变结果的位宽与有符号性,不能拿某个操作数的 signed 直接代替整个右值的属性。遇到这类告警,应先把实际表达式化简或拆成有明确类型的中间信号,再决定扩展方式。
边界值能快速暴露扩展方向是否选错。对 8 位输入至少检查 8'h00、8'h7F、8'h80 和 8'hFF:无符号路径的高 8 位应始终为零;有符号路径在前两个非负值上补零,在后两个负值上补一。若测试只覆盖零和很小的正数,两种实现会得到相同结果,符号处理错误就会一直藏到负数输入出现。
还要注意,显式扩展只负责表达位宽规则,不负责修复错误的类型声明。例如一个表示字节计数器的信号误标成 signed,在数值超过 127 后会被当成负数。此时保留符号扩展只是延续错误,应该回到接口定义纠正类型;如果信号来自不可修改的第三方模块,则在边界处显式补零,并加注释说明为什么放弃源信号的符号解释。
四、排查:沿赋值两侧确认位宽、类型与设计意图
拿到专码后,先把告警中的「期望位数」和「实际位数」抄成对照,再检查声明来源。端口穿过多个模块时,还要确认是不是上游接口缩窄、下游接口扩宽,或者参数修改让原本等宽的连接失去平衡。
| 检查项 | 要确认的事实 | 常见误区 |
|---|---|---|
| 赋值位宽 | 左值 16 位、右值 8 位是否都是接口要求 | 为消告警随意把输出改成 8 位 |
| 右值类型 | unsigned 还是 signed,表达式是否改变类型 | 只看左值有没有 signed |
| 新增高位 | 应补零、复制符号位,还是其他变换 | 把符号扩展当成通用扩位 |
| 数值范围 | 负数和最大正数扩展后是否保持原意 | 只测试 0 和 1,漏掉 8'hFF |
若设计其实要求饱和、缩放或偏移,单纯扩位也不够。例如把 8 位采样值映射到 16 位定点数,可能需要左移 8 位,而不是补在高位。WIDTHEXPAND 只指出位数调整,目标数值公式仍要由接口协议决定。
五、修复与复跑:用拼接写出新增高位
本例的设计意图就是保持两种输入原本的数值语义,因此 unsigned 路径显式补 8 个零,signed 路径显式复制 8 次符号位。端口和位宽完全不变。
module widthexpand_fixed (
input wire [7:0] unsigned_data,
input wire signed [7:0] signed_data,
output wire [15:0] unsigned_result,
output wire [15:0] signed_result
);
assign unsigned_result = {8'h00, unsigned_data};
assign signed_result = {{8{signed_data[7]}}, signed_data};
endmodule两个拼接结果都天然是 16 位,不再依赖隐式扩位。复跑时 fixed 没有任何诊断,退出码为 0;同一运行记录中的 PASS 同时确认 broken 仍然失败,说明对照没有被误改成两边都通过。相比局部 lint_off WIDTHEXPAND,这种写法让补位策略可审查,也不会把附近真正可疑的扩位一起隐藏。若团队使用尺寸转换函数,也应另外确认转换是否保留 signed 属性,不能只验证「告警消失」。
实验环境
- 机器/系统:Intel x86_64 Mac / macOS 26.5.2,实验在
geek-eda:0.2容器中运行 - 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1)
- 日期:2026-10-30
- 完整命令:
docker run --rm -v "$PWD":/work -w /work geek-eda:0.2 make run;脚本内部执行verilator --lint-only -Wall widthexpand_broken.sv与verilator --lint-only -Wall widthexpand_fixed.sv,核对 broken 非零、两条专码及 fixed 零退出。
代码获取
broken/fixed 源码、Makefile、复现脚本与原始输出均在公开仓 experiments/ts-verilator-warn-widthexpand。进入该目录执行 make run,或沿用上面的容器命令,即可重复完整对照。
这一篇在知识体系里的位置
本篇属于 EDA 报错排查系列 的 Verilator 位宽诊断分支:窄右值扩到宽左值看本篇,宽右值赋给窄左值看 Verilator WIDTHEXPAND 的反向问题 WIDTHTRUNC,需要理解旧版统一诊断时看 Verilator WIDTHEXPAND 相关的 WIDTH 总码说明。其他开源工具报错的分类入口见 开源 EDA 报错排查导航。
总结
Verilator WIDTHEXPAND 的本质是窄右值写入宽左值前发生隐式扩位。Verilator 5.020 对照中,unsigned 与 signed 两条 8 位到 16 位赋值都触发专码并使 broken 退出 1;显式补零和复制符号位后,fixed 无诊断、退出 0。修复时先确认右值类型和数值意图,再写清新增高位,不能用全局消音代替零扩展、符号扩展与接口语义的判断。