9.3 错案清单:高频错误的结案汇编 本节摘要:锁存器、多驱动、位宽截断、阻塞误用、时钟当数据、复位漏洞、符号混算——这几类错误贡献了 RTL 工程的大半返工。每条错案给症状、根因与根治手法,用途有三:设计自查、代码评审提问、Lint 规则来源。 三案侦破手法练完,最后一站是结案汇编:把全书散落各章的错误按案卷格式归档。这份清单的排序依据不是严重度,而是「案发率乘以排查成本」——排在前面的,就是工程里最常发生、又最耗人时的那些。 案卷一:锁存器意外推断 症状:综合面积偏大、时序报告出现无法归类的电平敏感路径、组合输出在某些输入组合下保持旧值。根因:组合块分支不全,工具为满足「保持」语义推断出锁存器。根治:块首默认值加 case 补 default,双保险写法在 8.1 节给过样板。
本节摘要:锁存器、多驱动、位宽截断、阻塞误用、时钟当数据、复位漏洞、符号混算——这几类错误贡献了 RTL 工程的大半返工。每条错案给症状、根因与根治手法,用途有三:设计自查、代码评审提问、Lint 规则来源。
三案侦破手法练完,最后一站是结案汇编:把全书散落各章的错误按案卷格式归档。这份清单的排序依据不是严重度,而是「案发率乘以排查成本」——排在前面的,就是工程里最常发生、又最耗人时的那些。
症状:综合面积偏大、时序报告出现无法归类的电平敏感路径、组合输出在某些输入组合下保持旧值。根因:组合块分支不全,工具为满足「保持」语义推断出锁存器。根治:块首默认值加 case 补 default,双保险写法在 8.1 节给过样板。自查一问:每个组合 always 块,任何输入组合下所有输出是否都有明确的新值?这案卷在 8.1 节有完整门诊记录,此处归档备查。Lint 项「锁存器推断」直接对应,门禁应设为零容忍。
症状:综合直接报错(这是好情况),或仿真结果随调度漂移(这是坏情况)。根因:同一个 reg 被两个 always 块赋值,或 wire 被多处 assign 且并非有意的三态设计。根治:一个信号一个驱动者;需要「多个来源汇入」时,显式写优先级选择器,或用 5.3 节的三态建模(仅限总线场景)。这案卷的隐蔽形态是「通过 generate 或条件编译间接产生的双驱动」——源码里各自看着干净,特定配置下撞车,参数化工程的配置矩阵要覆盖到驱动者检查。
症状:数值偶发偏差、计数器提前回卷、拼接结果高段恒为零。根因:赋值上下文宽度小于表达式真实需要,第 2 章讲过竞赛规则与字面量默认 32 位的坑。根治三件套:结果位宽预留(N 位加 M 位,和 N 加一位,积 N 加 M 位)、字面量一律带位宽、拼接显式计数。自查一问:这份代码里每一个算术结果,最大值是多少位?答案与左值位宽逐一对照。
// 截断案的典型现场与两种写法 reg [7:0] cnt; reg [15:0] total; // 写法一:依赖上下文竞赛,cnt 乘 4 的中间结果在 16 位上下文里计算,此处恰好安全 total = total + cnt * 4; // 写法二:显式放宽,不依赖读者的位宽推算 total = total + ({8'd0, cnt} * 4'd4); // 事故现场示意:和需要 9 位,左值只有 8 位 // cnt = cnt + 9; // 循环到头回卷,静默截断
症状:仿真换工具结果不同、移位链一拍穿透、流水线寄存器「合并」。根因与机理在 3.2 节与 9.2 节双重立案:语义依赖与调度依赖双重灾难。根治:铁律加 Lint 门禁,时序块禁阻塞、组合块禁非阻塞、块内禁混用。这案卷的特殊性在于「简单场景测不出」——两三行的小模块怎么跑都对,规模一大、块间交互一多才爆雷,所以它只能靠纪律预防,不能靠测试兜底。
症状:板上偶发假脉冲、假复位,仿真完全正常。根因:比较器、译码器等组合输出被当作时钟沿或异步复位使用,毛刺直接被放大成事件。根治:统一时钟加使能表达同样逻辑;复位入口一律过同步器。自查一问:全设计里时钟与复位信号的驱动者是谁——答案必须永远是「时钟树或复位同步器」,出现任何组合逻辑名即为违例。这案卷在 4.2 节有完整的正反对照代码。
症状:上电后部分寄存器输出 x、状态机初始行为不确定、仿真前期波形一片灰。根因:控制状态寄存器未纳入复位,或复位条件漏分支。根治:复位策略逐寄存器登记(哪些复位、复位值、同步异步),4.3 节的登记清单直接套用。判别标准:控制类寄存器(状态机、握手、使能)必须复位,数据通路中间寄存器可以豁免——豁免是设计决策,要显式写进文档而不是默认遗漏。
症状:比较结果与数值直觉相反、负数判成大正数、移位后符号丢失。根因:表达式中混入无符号操作数,整式按无符号计算(2.3 节的传染规则)。根治:混算处显式转换符号(用语言提供的符号化系统函数包裹),比较两侧类型一致。自查一问:这份代码里所有带符号运算,操作数是否全部有 signed 声明?流水账式的检查枯燥但有效——这案卷的错误在仿真里能复现,属于「肯查就查得出」的一类。

这份清单的正确打开方式是三种角色。其一,设计自查:模块写完,七条案卷逐条过——每条的「自查一问」就是检查动作,成本是十几分钟。其二,评审提问:评审别人的代码时,案卷转化为固定问题(这个组合块所有分支都赋值了吗?这个信号的驱动者唯一吗?),机器与人工各查各的。其三,规则来源:每条案卷对应的 Lint 检查项前面都标了覆盖度,门禁配置照着设——仿真抓不到的案子(组合进时钟端、阻塞误用),恰好都是 Lint 能拦的,机器分工的合理性就摆在这张对照表里。
最后是清单的维护:每接一个新项目,把上一项目的调试记录翻出来,凡符合现有案卷的补细节,新病种立案。清单的厚度就是经验本身——这本教程的所有章节,说到底都是在给这份清单写「案卷的背景材料」。
以评审一个中等规模模块为例演示清单的用法。第一步,结构扫描:数出模块里的 always 块数量,逐个标注组合还是时序,组合块查敏感列表写法与分支完整性,时序块查赋值符号与复位覆盖。第二步,信号扫描:每个输出信号找驱动者,验证唯一性;每个跨模块信号确认位宽两侧一致。第三步,边界扫描:找所有算术表达式核对结果位宽,找所有比较表达式核对符号属性。三步走完通常十几分钟,覆盖七条案卷的全部自查一问。
这套流程的价值在于「评审可以委托」。机器能查的(锁存器、多驱动、混用赋值)交给 Lint 门禁,人只查机器查不了的(位宽意图、复位策略、跨域设计决策)——人机分工清楚后,评审的质量不再依赖评审者的状态与经验厚度,新成员拿到清单也能做出合格的评审。
全书九章至此结卷。愿这份清单在你的项目里不断增厚,而每一条新增的案卷,都来自别人已经替你踩过的坑。