可综合 RTL 编码规范:组合/时序分离、锁存器规避与位宽匹配
更新时间:2026-09-05。本文是「集成电路大类 · 芯片设计」**进阶层(intermediate)**第一篇新正文(#03,#00–02 为旧枢纽页 逻辑综合、物理实现、片上总线)。入门层讲了 一行 always 变成什么门 和 组合/时序逻辑的区别,这一篇解决写代码时每天都要做的选择:怎么写,综合器才会生成你以为的硬件。
本文要回答的问题
- 为什么「仿真过了」不能证明 RTL 没问题?
<=和=到底什么时候用哪个?混用会怎样?- 什么写法会凭空造出锁存器?为什么锁存器是坑?
- 位宽不匹配、少写复位、用了
#delay分别会发生什么?
一、军规:你写的不是程序,是硬件的文字描述
软件思维写 RTL 最大的误区:把 always 块当函数、把 reg 当变量。综合器(DC/Genus)读到你的代码,逐句映射成门电路和触发器——它不会猜你的意图,只会严格按字面结构生成硬件。于是有三类经典事故:
- 仿真对、综合错:testbench 里行为正确,但综合出的网表多了锁存器、少了复位、位宽被截断;
- 综合过、时序炸:写法让综合器推断出又长又慢的组合链;
- 仿真器之间行为不一致:依赖未定义顺序(同一块里阻塞+非阻塞混用),VCS 和 Verilator 结果不同。
编码规范不是风格洁癖,是把「意图」无歧义地传给综合器的协议。
二、时序用 <=,组合用 =,一个块只干一件事
| 逻辑类型 | always 块 | 赋值 | 敏感列表 | 生成硬件 |
|---|---|---|---|---|
| 时序逻辑 | always_ff @(posedge clk ...) | 非阻塞 <= | 时钟沿(+复位) | 触发器 D→Q |
| 组合逻辑 | always_comb / always @(*) | 阻塞 = | 全部输入 | 与/或/MUX |
| 连续赋值 | assign x = ... | — | — | 连线/缓冲 |
反例与后果:在时序块里用 =(阻塞),多个寄存器赋值有先后依赖时仿真按顺序执行、综合是并行硬件,行为分叉;在组合块里用 <=,仿真值延后一拍更新,组合逻辑凭空多出「节拍」。铁律:一个 always 块只描述一类逻辑,块内只用一种赋值。
// 时序:非阻塞,所有寄存器共享同一个 clk/rst 模板
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) cnt <= 8'd0;
else if (en) cnt <= cnt + 8'd1;
end
// 组合:阻塞,开头先给默认值
always_comb begin
mux_out = 8'd0; // 默认值,见下一节
case (sel)
2'b00: mux_out = a;
2'b01: mux_out = b;
2'b10: mux_out = c;
default: mux_out = 8'd0;
endcase
end三、锁存器从哪来:组合逻辑里有「保持」分支
组合逻辑的语义是「输出完全由当前输入决定」。如果某条分支没有给输出赋值,综合器只能让输出「保持原值」——保持状态需要存储元件,于是推断出电平敏感锁存器(latch):
// 反例:en==0 时 q 怎么办?代码没说 -> 综合出锁存器
always_comb begin
if (en) q = d;
end锁存器之所以是坑:它对电平敏感(不是沿),不占时钟沿、不走时序分析的常规路径,STA 难约束、容易产生毛刺、DFT 扫描链插不进去、仿真与门级行为还容易不一致。现代设计里除极少数专用电路(如低功耗保持、高速 CML)外不应出现显式锁存器。
根治手法三板斧:① 组合块开头先给所有输出赋默认值;② if 必须配 else;③ case 必须配 default。always_comb(SystemVerilog)比 always @(*) 更好——编译器强制检查敏感列表完整性,漏信号直接报错。casez/casex 的 ? 通配会把 x/z 当任意值匹配,仿真与综合可能不一致,状态译码等场合慎用或加 default 兜底。
四、位宽匹配:截断和扩展都在静默中发生
Verilog 的位宽规则是「按上下文最大位宽、无符号/符号自动扩展」,写错了不报错、只告警:
logic [7:0] a, b;
logic [7:0] sum8;
logic [8:0] sum9;
assign sum8 = a + b; // 危险:加法按 8 位做,进位被丢弃
assign sum9 = a + b; // 正确:结果 9 位,进位保留实践要点:常量写全位宽(8'd0 而非裸 0,避免 32 位运算带来的意外符号扩展);比较运算两边位宽对齐;有符号运算显式 logic signed 且全程位宽一致;参数化位宽用 parameter int W=8 + logic [W-1:0]。lint 工具的 width 告警逐条清零,是比仿真更便宜的防线。
五、可综合子集:这些写法只能出现在 testbench
| 写法 | 仿真 | 综合 | 说明 |
|---|---|---|---|
initial 块(含 $dumpvars) | ✅ | ❌(除 ROM 初值个别工具支持) | 上电初值靠复位 |
#10 延迟、fork/join | ✅ | ❌ | 硬件没有「等 10 个时间单位」 |
while/for 无界或变量上界循环 | ✅ | ❌/⚠️ | 综合要求循环次数编译期可定(展开) |
动态数组、string、class(RTL 中) | ✅ | ❌ | class 留给 UVM 验证平台 |
/、%(除非常数除数或换 IP) | ✅ | ⚠️ | 综合成巨大除法器,时序/面积双输 |
$display/$finish | ✅ | ❌ | 调试用,自动忽略但别依赖 |
复位策略同样属于规范:异步断言、同步释放(见 时钟树与复位网络,进阶层 #12 有复位设计专篇);只复位必须复位的寄存器(控制态、指针、FSM 状态),数据通路 SRAM/大寄存器不复位——复位树本身要布线、占面积、耗功耗。
六、落地:把规范变成工具门禁
成熟团队不靠人记,靠工具卡:Lint(SpyGlass、Verilator -Wall)在提交前跑,锁存器、位宽、敏感列表、未驱动信号全报;CDC/RDC 检查跨时钟域/复位域单独扫;code review 对照 checklist。规范的最终目的只有一个:让 RTL、仿真、网表、硅片四者行为一致。
相关与延伸
下一篇:状态机设计:一段/两段/三段式与状态编码——FSM 是这套规范最大的受益者;底层直觉见 组合与时序逻辑、从代码到门电路;规范怎么被工具强制执行见 EDA 工具链地图,综合侧的对应物见 逻辑综合;进阶层还会讲 Lint 与 CDC/RDC 检查(#45)。
总结
写可综合 RTL 就三条:时序块用 <=、组合块用 = 且分支补全不给锁存器留机会、位宽显式对齐;仿真能跑的写法未必能综合,让 lint 工具替你站岗。