RTL Lint自查25项清单:按告警族分组的提交前检查表
verilator --lint-only -Wall 一跑,几百条告警刷屏,新人最常见的反应是「先把 -Wall 关掉再说」。关掉告警等于把编译器白送的评审意见扔进垃圾桶——位宽截断、锁存器推断、敏感表缺失这些告警背后都是真实流片事故的常客。本站 Verilator Lint 告警误差矩阵 已经讲过哪些告警误报多、哪些必须清零,本篇再往前走一步:把 25 项最高频的检查点按告警族整理成一张提交前自查表,每项给出「检查点 → 对应告警码 → 怎么修」,对着表格逐条过一遍,比逐条看告警原文快得多。
本文要回答的问题
- Verilator 的几百条 lint 告警,能不能按「族」归并成少数几类根因?
- 每一族的典型检查点是什么,对应的告警码叫什么,修起来一句话怎么说?
- 提交代码前,这份 25 项清单按什么顺序扫效率最高?
一、五族分组概览
25 项检查按失效机理分五族:位宽族查「数据通路两侧宽度是否对得上」;锁存与组合族查「组合逻辑是否真的在所有路径都有赋值」;时序与阻塞族查「赋值语义与时钟语义是否匹配」;声明与连接族查「名字、端口、驱动关系是否干净」;敏感表与仿真族查「仿真模型和真实硬件是不是一回事」。先用这张图建立全貌,后面逐族展开。
这张图抓住一个主线就够了:五族从上到下是从「数值对不对」到「仿真像不像」的顺序——位宽错了功能直接错,锁存和敏感表错了则仿真和综合行为分裂。建议扫描顺序也按这个走:先清位宽族(量最大、机械性最强),再清锁存族(最伤功能),最后清仿真语义族。
二、位宽族(第 1-6 项)
位宽族是告警量最大的一族,WIDTH 一个码就能贡献一大半输出。根因只有一个:赋值、比较、运算两侧的位宽或符号性不一致。
| # | 检查点 | 告警码 | 怎么修 |
|---|---|---|---|
| 1 | 赋值左右两侧位宽一致 | WIDTH | 宽赋窄显式切片 a <= b[7:0],窄赋宽显式补零 |
| 2 | 比较运算两侧位宽一致 | WIDTH | 计数器和常量比较时把常量写成 8'd100 带位宽形式 |
| 3 | 有符号/无符号不混算 | WIDTH/SIGNED | 用 $signed() / $unsigned() 显式标注,不靠默认 |
| 4 | 移位位数不超过源位宽 | WIDTH | 移位量位宽按 log2(源宽) 声明 |
| 5 | 拼接结果宽度与目标一致 | WIDTH | {a, b} 宽度逐段数清,必要时补前导零 |
| 6 | 参数/常量传播后不截断 | WIDTH/CMPCONST | 参数化模块实例化后重跑一次 lint |
遇到 WIDTH 刷屏先看是不是同一个根因的连锁反应——一个模块端口径用错,下游十几条告警全是它的影子。位宽不匹配的完整排查路径见 Verilator WIDTH 告警怎么修。
三、锁存与组合族(第 7-11 项)
这一族的告警不多但条条伤功能:组合逻辑里漏了赋值分支,综合器就默默给你推一个锁存器出来,仿真时序、功耗、可测性全部变差。
| # | 检查点 | 告警码 | 怎么修 |
|---|---|---|---|
| 7 | always 组合块所有路径都赋值 | LATCH | 进块先给所有输出默认值,再覆盖 |
| 8 | case 语句有 default 分支 | CASEINCOMPLETE | 补 default,或确认完整后加 unique case |
| 9 | if-else 链条无缺口 | LATCH | 最后补一个兜底 else |
| 10 | 无意外锁存器被推断 | LATCH | 综合日志里搜 latch,逐条确认是本意 |
| 11 | case 分支不重叠 | CASEOVERLAP | 重叠分支合并,或用 priority case 标明意图 |
CASEINCOMPLETE 的判定细节和「什么时候故意不加 default」的边界,见 CASEINCOMPLETE 告警与 default 分支。锁存器的综合后果本站 为什么不手写锁存器 有专门讨论。
四、时序与阻塞族(第 12-15 项)
阻塞赋值(=)和非阻塞赋值(<=)用错地方,是「仿真能跑、上板就死」的经典来源。
| # | 检查点 | 告警码 | 怎么修 |
|---|---|---|---|
| 12 | 时序 always 块只用 <= | BLKSEQ | 时序逻辑里的 = 全部改成 <= |
| 13 | 组合 always 块只用 = | COMBDLY | 组合逻辑里的 <= 改成 = |
| 14 | 同一变量不被两种赋值混用 | BLKANDNBLK | 拆成两个变量,或统一赋值风格 |
| 15 | 无 #delay 残留在可综合代码 | COMBDLY | 可综合代码删除延迟,留给 testbench |
第 12 项的机理(非阻塞赋值在 NBA 区域统一更新、避免仿真顺序依赖)在 BLKSEQ 告警:时序块里的阻塞赋值 里讲透了。记住口诀就够:时序 <=、组合 =、testbench 随意。
五、声明与连接族(第 16-21 项)
这一族查的是「整洁度」,但整洁度背后是真实的集成风险——端口没连上的模块在仿真里可能侥幸工作,换一家工具就露馅。
| # | 检查点 | 告警码 | 怎么修 |
|---|---|---|---|
| 16 | 文件名与模块名一致 | DECLFILENAME | 重命名文件或模块,一文件一模块 |
| 17 | 例化端口全部显式连接 | PINMISSING | 用 .port(signal) 显式连,不用的接空 |
| 18 | 端口名拼写正确 | PINNOTFOUND | 例化时对照模块定义逐个核对 |
| 19 | 每个信号有且只有一个驱动 | UNDRIVEN/MULTIDRIVEN | 未驱动的补驱动,多驱动的仲裁或改名 |
| 20 | 无声明后从未使用的信号 | UNUSEDSIGNAL | 删除或加 lint waiver 注释 |
| 21 | 局部变量不遮蔽外层同名 | VARHIDDEN | 循环变量、块内变量改独特名字 |
UNDRIVEN 和 MULTIDRIVEN 是这一族里最值得花时间的两项:未驱动信号见 UNDRIVEN 告警排查,多驱动冲突见 MULTIDRIVEN 告警:多驱动总线。第 20 项的未使用信号清理见 UNUSEDSIGNAL 与 UNUSED 告警——清不干净时宁可加 waiver 注释,也不要假装没看见。
六、敏感表与仿真族(第 22-25 项)
最后一族管的是「仿真模型和真实硬件像不像」。这类问题最阴险:仿真全过,综合结果也对,但两者的行为已经悄悄分叉了。
| # | 检查点 | 告警码 | 怎么修 |
|---|---|---|---|
| 22 | 组合 always 敏感表完整 | ALWCOMBORDER | 直接用 always @(*) 或 always_comb,不手写列表 |
| 23 | 异步复位/置位不与同步混用 | SYNCASYNCNET | 同一信号只按一种语义用,跨域处做同步释放 |
| 24 | 时序块敏感表只含时钟和异步复位 | 敏感表缺失 | 数据信号不进时序块敏感表 |
| 25 | testbench 的 initial 延迟不进 RTL | INITIALDLY | 延迟只留在测试平台,RTL 干净 |
第 23 项 SYNCASYNCNET 的典型场景是同一个复位信号一处当异步复位、另一处当同步输入采样——复位树设计阶段的约定要一以贯之。自查完这 25 项还不算终点:lint 只是静态检查的第一道关卡,为什么「lint 全绿」离流片安全还有距离,见 RTL 通过不等于流片安全。
这一篇在知识体系里的位置
本篇是 lint 告警族的「总索引」:横向的告警覆盖面与误报率判断交给 Verilator Lint 告警误差矩阵,单条告警的机理深挖交给 eda/ 目录 下的 ts-verilator 系列深文(WIDTH、LATCH、BLKSEQ、UNDRIVEN 等),lint 之后的检查链路上游接 CDC 自查、下游接签核自查。三份清单一起用,覆盖 RTL 从提交到流片的静态检查主线。
总结
RTL Lint自查25项清单把 Verilator 高频告警归并成五族:位宽族管数据通路两侧对得上,锁存与组合族管每条路径都有赋值,时序与阻塞族管 <=/= 用在正确的 always 块,声明与连接族管端口和驱动关系干净,敏感表与仿真族管仿真模型不背离硬件。提交 RTL 前按族扫一遍,先清量大的位宽、再清伤功能的锁存、最后清仿真语义,把 lint 从「告警刷屏」变成「25 个勾」。