Verilator WIDTHCONCAT 拼接位宽:赋值与端口中的无位宽项修复
更新时间:2026-10-31。FAQ 速查页,属于 EDA 报错排查系列。
Verilator WIDTHCONCAT 拼接位宽检查的不是拼接结果够不够用,而是拼接项自身有没有明确位宽。{1, 2}写在赋值右侧或实例端口上都会触发;常量等待条件的另一类专业检查见 Verilator WAITCONST 常量等待。
首屏答案:把拼接里的无位宽数字或参数改成有明确宽度的写法,例如把 {1, 2} 改为 {32'h1, 32'h2}。赋值目标或端口较宽,不会反向替每个拼接项确定位宽。若端口表达式过长,先赋给宽度明确的中间信号,再连接端口。不要靠关闭告警或只给整个拼接加外层位宽来掩盖无位宽项。
本文要回答的问题
- WIDTHCONCAT 为什么指向
{1, 2},而不是只检查赋值目标的 64 位? - 赋值右侧与实例端口上的拼接,位宽规则有什么共同点?
- 它与普通拼接结果变宽时的 WIDTHEXPAND 有何区别?
- 逐项标宽和拆分中间信号后,复跑结果是否干净?
先区分两类问题:一类违法或可疑之处在拼接内部,另一类是合法拼接结果与上下文宽度不一致。WIDTHCONCAT 属于前者,所以只把左侧信号加宽并不能解决。
右栏的「总计 64 位」来自两个 32 位项相加,不是从 data[63:0] 倒推出来。这个顺序正是修复关键:拼接先形成自己的结果,随后才进入赋值或端口连接。
一、Verilator WIDTHCONCAT 拼接位宽的报错现场
最小复现让一个 64 位输出直接接收两个无位宽常量。这里没有额外算术,也没有复位和时序逻辑,告警只会落在拼接表达式上:
module widthconcat_assign_bad (
output logic [63:0] data
);
always_comb begin
data = {1, 2};
end
endmodule端口版本把同样的 {1, 2} 直接连接到 sink_64 的 64 位输入。对两个文件分别运行:
verilator --lint-only -Wall widthconcat_assign_bad.sv
verilator --lint-only -Wall widthconcat_port_bad.sv sink_64.sv实际结果如下。赋值版本的箭头分别指向 1 和 2,端口版本也给出同样的 Unsized numbers/parameters not allowed:
每个无位宽项各产生一条 WIDTHCONCAT;本次输出中第二条说明文字使用 replications,但箭头仍落在普通拼接的第二个常量上。两个错误用例都以退出码 1 结束。端口版本没有额外文件命名告警,便于确认剩余问题就是拼接项位宽。
二、为什么目标位宽不能救拼接内部
拼接表达式的结果宽度由各拼接项宽度相加,属于自决定位宽。写成 {a, b} 且 a、b 都是 4 位时,结果自然是 8 位;把它赋给 16 位信号时,才在赋值边界扩展为 16 位。16 位目标不会进入花括号,把两个操作数各改成 8 位。
无位宽数字更麻烦:1、2 没有写出设计者希望的项宽。在本次 Verilator 5.020 检查中,它们直接触发 WIDTHCONCAT。即使左侧恰好是 64 位,也不能把「目标宽度碰巧匹配工具对无位宽数的处理」当作合法依据。SystemVerilog 代码应让拼接项的大小明确、可移植。
| 表达式场景 | 先看什么 | 随后可能发生什么 | 正确处理 |
|---|---|---|---|
{1, 2} | 两项都没有显式位宽 | 赋值和端口位置均报 WIDTHCONCAT | 改成 {32'h1, 32'h2} 或实际所需宽度 |
| 两个 4 位信号拼成 8 位,再赋给 16 位 | 拼接项宽度合法,结果为 8 位 | 上下文扩展可能报 WIDTHEXPAND | 按意图补高 8 位,例如 {8'b0, a, b} |
| 已定位宽拼接连接到等宽端口 | 项宽与总宽都明确 | 不需要扩展或截断 | 保持直接连接或中间信号均可 |
WIDTHEXPAND 与 WIDTHCONCAT 因此不能混为一谈。前者面对的是一个已有确定位宽、但需要变宽的值;后者先质疑拼接内部的无位宽项。只消除扩展提示,不等于拼接内部已经合规。
三、两种修复:逐项标宽与中间信号
赋值版保留 64 位数据语义,把两个常量分别写成 32 位。高位是 32'h1、低位是 32'h2,位序与原意一致,总宽度也明确为 64:
always_comb begin
data = {32'h1, 32'h2};
end如果实际想表达的是两个 1 位值,就应写 {1'b1, 1'b0} 并重新核对目标宽度,而不是照抄 32 位。修复数字来自协议或字段定义,不来自「让告警消失」这个目标。未加位宽的参数也适用同一原则:给参数声明位宽,或在使用处选取明确的位段。
端口版除了给常量标宽,还把表达式拆成 pair。这样在原理图式的实例连接中,端口宽度与信号声明能直接对照:
logic [63:0] pair;
always_comb begin
pair = {32'h1, 32'h2};
end
sink_64 u_sink (
.data (pair),
.observed (observed)
);中间信号不是 WIDTHCONCAT 的必需品,真正消除告警的仍是逐项标宽;它的价值是让端口连接更易评审。复跑记录中两个修复版均无告警,退出码为 0:
$ verilator --lint-only -Wall widthconcat_assign_fixed.sv
exit_code: 0
$ verilator --lint-only -Wall widthconcat_port_fixed.sv sink_64.sv
exit_code: 0
PASS: assignment and port cases warn; explicit width and intermediate signal are clean四、评审时别再踩三个误区
不要写 64'({1, 2}) 作为首选修复。外层 cast 只表达整个结果的宽度,花括号内部仍然保留无位宽项;应把宽度写到 1、2 各自身上。也不要仅扩大 data 或端口声明,因为目标位宽不参与拼接项的定宽。
复制表达式也要检查花括号内的项。{4{1}} 的被复制值仍是无位宽常量,不能因为前面写了重复次数 4 就认为它变成 1 位或 4 位;按真实意图写成 {4{1'b1}},项宽与结果总宽才都明确。重复次数负责决定份数,不负责给每份内容定宽。
参数同样需要声明宽度。parameter P = 1 后直接写 {P, P},参数本身仍可能触发同类告警。若协议字段是 32 位,可声明 parameter logic [31:0] P = 32'h1;只把使用处左侧扩成 64 位无济于事。模块参数名里的 WIDTH 也只是用于定义其他信号的普通参数,不会自动约束常量 P。
第二个误区是用 lint_off WIDTHCONCAT 消音。它不会改变常量宽度,也不会改善跨工具一致性。第三个误区是把所有位宽告警都机械补零:若真实问题其实是高位截断,补零既不对症,还可能掩盖字段布局错误。
评审可沿四个问题走:每个拼接项是否显式定位宽;总宽度是否等于各项之和;赋值或端口需要扩展还是截断;扩展时高位应补零、补符号位还是补协议字段。回答完再修改,最后用相同的 -Wall 参数复跑。
实验环境
- 机器/系统:macOS 主机运行 Docker,容器为 Ubuntu 24.04.5 LTS。
- 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1),镜像
geek-eda:0.2。 - 日期:2026-10-31。
- 完整命令:
docker run --rm -v "$PWD:/work" -w /work geek-eda:0.2 bash run.sh;脚本依次对赋值错误版、端口错误版、赋值修复版和端口修复版执行verilator --lint-only -Wall,端口用例同时传入sink_64.sv,并记录退出码、核对 WIDTHCONCAT 与最终 PASS。
代码获取
两个错误用例、两个修复用例、64 位接收模块、复跑脚本与使用说明位于公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/ts-verilator-warn-widthconcat。进入该目录后运行 bash run.sh,可复现赋值与端口的 WIDTHCONCAT,并看到逐项标宽、中间信号版本零告警通过。
这一篇在知识体系里的位置
本篇处理 SystemVerilog 表达式定宽中的拼接内部问题。常量条件导致无意义等待看 Verilator WAITCONST 常量等待,声明生命周期问题看 Verilator IMPLICITSTATIC 静态声明。这些检查共同要求把表达式、驱动和进程语义写明确,而不是以 lint 消音代替修复。
总结
Verilator WIDTHCONCAT 拼接位宽 的根因是拼接或复制表达式中存在无位宽数字、参数;赋值目标和实例端口的宽度不会反向确定各项宽度。把 {1, 2} 改为符合实际字段定义的 {32'h1, 32'h2},端口复杂表达式可再拆成中间信号。Verilator 5.020 实测两个错误版各报两条 WIDTHCONCAT、退出码为 1,两个修复版同参复跑均为零告警、退出码为 0。