%Warning-CMPCONST: Comparison is constant due to limited range 排查:Verilator 恒真恒假比较告警
更新时间:2026-10-29。FAQ 速查页,属于 EDA 报错排查系列。
%Warning-CMPCONST: Comparison is constant due to limited range是 Verilator 开-Wall后对「比较结果在编译期就已经注定」的告警:一个 n 位无符号信号最大只能取到 2ⁿ−1,拿它跟等于或超出这个范围的常量比大小,条件要么恒真要么恒假,对应的分支就是死代码。它和 Verilator WIDTH 告警 是近亲——都源于位宽算错,但 WIDTH 报的是两侧位宽对不齐,CMPCONST 报的是比了也白比。
首屏答案:CMPCONST 的意思是「比较因取值范围受限而成为常量」。典型触发:8 位计数器 cnt(取值 0–255)写 if (cnt > 8'd255),cnt 最大就是 255,条件恒假,目标分支永远执行不到。Verilator 在 lint 阶段就把这件事钉出来,而仿真器和综合器往往一声不吭地让你带着死逻辑上线。修法永远是让比较条件可达:要么把信号位宽加宽到能取到常量,要么把常量/运算符改成范围内能成立的形式(比如 > 改 ==)。不要用 lint_off 把告警关掉——关掉的是告警,留下的恒假条件还是死代码。
本文要回答的问题
%Warning-CMPCONST报错串里的limited range到底指什么范围?- 8 位计数器跟 255 比大小,为什么
>恒假而==就合法? - 这条告警和 WIDTH、UNSIGNED 两条近亲告警怎么区分?
- 怎么修才算闭环,为什么不能 lint_off 了事?
一、报错原文与现象
最小复现是一个典型的「数满一圈拉起完成标志」写法,作者想用「大于最大值」表达溢出,结果条件恒假:
module cmpconst_bad (
input wire clk,
input wire rst_n,
output reg done
);
reg [7:0] cnt; // 8 位自由运行计数器,取值范围 0..255
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 8'd0;
done <= 1'b0;
end else begin
cnt <= cnt + 8'd1;
if (cnt > 8'd255) // cnt 最大就是 255,条件恒假
done <= 1'b1;
end
end
endmodule对这个文件跑 verilator --lint-only -Wall,真实告警如下:
读这段输出抓三个字段:第一,主行 cmpconst_bad.v:15:21 精确指向 > 运算符所在的列,文案 Comparison is constant due to limited range 里的 limited range 就是「cnt 的取值被位宽限制在 0–255」;第二,Verilator 把源码行原样回显并用 ^ 钉在比较运算符上,不用自己去找是哪一次比较;第三,末尾 %Error: Exiting due to 1 warning(s) 表示 lint 模式下告警被提升为退出错误,CI 里这条直接卡住构建。提示行给出的 lint_off CMPCONST 是消音开关,第四节讲为什么不能用它当修复。
二、根因:位宽决定了比较的可达范围
这条告警的本质不是语法问题,而是算术常识:n 位无符号信号的值域是 [0, 2ⁿ−1],常量一旦落在这个值域的边界上或边界外,比较运算符两侧的取值集合就没有交叠弹性,结果在编译期就定了。三种典型触发形态一张卡看全:
读这张卡按「常量落在值域的哪一侧」归类:落在上边界之外且用上侧运算符(>、>=),条件恒假;落在上边界且用下侧包容运算符(<=),条件恒真;而位宽算错是前两类共同的病根——设计者脑子里数的范围比信号实际能表达的范围大。三类的修复都指向同一件事:让比较两侧的取值集合真正有交集。值得多说一句的是,这类缺陷在仿真里极难暴露:恒假分支对应的波形特征不是「错了」,而是「从来没出现过」——done 永远为低,你盯着波形看一万拍也不会觉得异常,除非你本来就预期它该拉高。这正是 lint 类静态检查的价值:它不看波形,直接在编译期把「这个条件不可能成立」算给你看。
还要分清两条近亲告警,别在站内搜错页面:%Warning-UNSIGNED 报的是无符号信号跟 0 比大小(a >= 0 恒真、a < 0 恒假),根因是符号性;%Warning-WIDTHEXPAND(WIDTH 簇)报的是比较两侧位宽不齐需要隐式扩展,根因是宽度对齐,比如 cnt < 256 这种裸常量写法在 Verilator 5.020 上走的是 WIDTH 簇而非 CMPCONST。只有常量落在信号可达范围之外、比较被判定为常量结果时,才是本篇的 CMPCONST。
三、排查流程
拿到 CMPCONST 后的标准收敛路径如下,颜色按角色区分:红是告警入口与根因,蓝是排查动作,橙是修复动作,绿是闭环复验,紫是防复发沉淀。
direction: down
warn: "%Warning-CMPCONST\nComparison is constant"
read: "按行号找到比较表达式\n确认两侧:信号位宽与常量值"
judge: "判断:这个条件想表达什么"
op: "根因:运算符用错\n想要 == 写成了 >(或反之)"
width: "根因:位宽算错\n信号表达不到目标阈值"
fix1: "改运算符或改阈值常量\n让条件可达"
fix2: "加宽信号位宽\n常量用显式位宽写法"
retest: "重跑 verilator --lint-only -Wall\n零告警通过"
prevent: "评审盯边界比较\nCI 固化 -Wall"
warn -> read
read -> judge
judge -> op: "阈值合理但结果注定"
judge -> width: "阈值本身超出信号范围"
op -> fix1
width -> fix2
fix1 -> retest
fix2 -> retest
retest -> prevent
warn.style: { fill: "#FDECEC"; stroke: "#D9534F" }
op.style: { fill: "#FDECEC"; stroke: "#D9534F" }
width.style: { fill: "#FDECEC"; stroke: "#D9534F" }
read.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
judge.style: { fill: "#EAF2FE"; stroke: "#3B6FD4" }
fix1.style: { fill: "#FEF3E2"; stroke: "#E08A1E" }
fix2.style: { fill: "#FEF3E2"; stroke: "#E08A1E" }
retest.style: { fill: "#E9F8F0"; stroke: "#2E9E6B" }
prevent.style: { fill: "#F1ECFD"; stroke: "#7A5BD4" }读这张图抓住一个分岔:排查量不在「找」(告警行号已经把表达式钉出来了),而在「判」——判断是运算符写错了(想要「等于最大值」却写成「大于最大值」),还是位宽真算错了(想数到 256 却只给 8 位)。前者改运算符或常量即可;后者必须把信号加宽到能表达目标值,且常量写成 9'd256 这种显式位宽形式,避免再掉进隐式扩展的坑。判的时候有一个实用口诀:把信号位宽换算成十进制最大值写在注释里,再拿常量跟它对一眼——8 位对 255、9 位对 511、16 位对 65535,常量一旦大过这个数,先停手想位宽,而不是先改代码。
四、修复与验证
本例属于「运算符用错」分支:意图是数满一圈(cnt 到达 255)后拉起 done,正确写法是用 == 判断等于最大值,条件可达:
module cmpconst_fixed (
input wire clk,
input wire rst_n,
output reg done
);
reg [7:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 8'd0;
done <= 1'b0;
end else begin
cnt <= cnt + 8'd1;
if (cnt == 8'd255) // 等于最大值:条件可达
done <= 1'b1;
end
end
endmodule对修复后的文件重跑同一条 lint 命令,输出干净、零告警:
# command: docker run --rm -v .:/work -w /work geek-eda:0.2 verilator --lint-only -Wall cmpconst_fixed.v
# date: 2026-10-29
# host-toolchain: Verilator 5.020 (geek-eda:0.2 docker)
---
$ verilator --lint-only -Wall cmpconst_fixed.v
# exit_code: 0两点强调。第一,如果真实意图确实是「数到 256」,那就走「位宽算错」分支:reg [8:0] cnt 配 cnt < 9'd256,位宽与常量一起改,只改一边会换一条告警继续报——比如只把常量改成 256 而不加宽信号,Verilator 5.020 会改报 %Warning-WIDTHEXPAND,等于从一个坑爬进另一个坑。第二,不要用 /* verilator lint_off CMPCONST */ 把告警圈掉当修复——恒假比较对应的分支在硬件里就是死逻辑,消音只是让工具闭嘴,功能缺陷原样带到流片。告警该消的方式是让条件可达,不是让工具闭嘴。修复后除了 lint 零告警,还值得顺手跑一遍仿真确认 done 真的会拉高:静态检查证明「条件可达」,动态仿真证明「行为如预期」,两条腿都落地才算真正闭环。
五、预防清单
- 边界比较先心算值域:写
>、>=、<=、<时先问自己信号最大能取到几,常量有没有落在可达范围外。 - 阈值常量写显式位宽:
9'd256而不是裸256,让阅读者和工具都能立刻看出位宽意图。 - 数满一圈用 == 不用 >:自由运行计数器的「到达终点」判断写
== MAX,> MAX永远是恒假陷阱。 - CI 固化
-Wall:CMPCONST 在默认告警级别不开,-Wall才报;把它提升为退出错误,提交阶段就拦下死逻辑。 - 死分支回溯设计意图:修之前先确认这个分支本来该不该存在——有时恒假比较是需求变更后忘了删的残留,删分支比改条件更对。
实验环境
- 机器/系统:docker 容器(镜像 geek-eda:0.2)
- 工具版本:Verilator 5.020 2024-01-01 rev (Debian 5.020-1)
- 日期:2026-10-29
- 完整命令:
verilator --lint-only -Wall cmpconst_bad.v(报错复现,exit 1);verilator --lint-only -Wall cmpconst_fixed.v(修复后复验,exit 0)
代码获取
复现用源码(cmpconst_bad.v 报错版、cmpconst_fixed.v 修复版)与完整 lint log 在公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/verilator-warn-cmpconst。装好 Verilator 后跑 verilator --lint-only -Wall cmpconst_bad.v 复现告警,再对 cmpconst_fixed.v 跑同一命令验证修复。
这一篇在知识体系里的位置
本篇是 EDA 报错排查系列中 Verilator lint 告警簇的一员:比较结果注定看本篇;比较两侧位宽不齐看 Verilator WIDTH 告警;有符号与无符号混算的比较陷阱见 Verilator 位宽与符号性混算排查;信号有驱动却没人读看 %Warning-UNUSEDSIGNAL 排查。开源工具链报错的总入口见 开源 EDA 报错排查导航,整个系列的目录在 EDA 报错排查系列。
总结
%Warning-CMPCONST: Comparison is constant due to limited range 就是比较一侧的常量落在信号可达取值范围的边界上或之外,条件在编译期就恒真或恒假,对应分支沦为死代码;按行号定位表达式后先判是运算符写错(> 该写 ==)还是位宽算错(8 位信号想数到 256),前者改运算符或常量、后者加宽位宽并把常量写成显式位宽形式,修完重跑 -Wall 零告警才算闭环——永远不要用 lint_off 消音代替修复,恒假比较在硬件里就是实打实的功能缺陷。