8.1 可综合的分界线 本节摘要:综合器只认「能翻译成静态电路」的语言子集:延迟与初始化语句被忽略,不定长循环被拒收,漏分支的组合逻辑被推断成锁存器。分界线不是背诵清单,而是一条判据——这段描述能变成一组固定的门与触发器吗。 验证过堂之后,判决执行的第一步是确认代码「有受审资格」:综合器只能处理可综合子集。这一节把分界线讲成一条判据而非一张清单,再用一份病代码的逐行修复把边界划实。 一条判据,三类待遇 判据一句话:这段描述能否对应一组固定数量的门与触发器。按此判据,语言设施受到三种待遇。第一类,正常翻译:assign、always 两类过程块、if、case、常量边界的 for、function、generate、位运算算术运算——RTL 的全部主力。
本节摘要:综合器只认「能翻译成静态电路」的语言子集:延迟与初始化语句被忽略,不定长循环被拒收,漏分支的组合逻辑被推断成锁存器。分界线不是背诵清单,而是一条判据——这段描述能变成一组固定的门与触发器吗。
验证过堂之后,判决执行的第一步是确认代码「有受审资格」:综合器只能处理可综合子集。这一节把分界线讲成一条判据而非一张清单,再用一份病代码的逐行修复把边界划实。
判据一句话:这段描述能否对应一组固定数量的门与触发器。按此判据,语言设施受到三种待遇。第一类,正常翻译:assign、always 两类过程块、if、case、常量边界的 for、function、generate、位运算算术运算——RTL 的全部主力。第二类,无视:延迟控制、initial 块、display 监控系统任务——它们是仿真语法,综合器看见了当没看见。无视型设施最危险的地方在于「代码能跑、综合也不报错」,让人误以为它们参与了电路行为。第三类,拒收或变形:不定长循环(while、数据相关的边界)、文件操作、动态层次的递归调用——工具要么报错,要么以你不认识的方式变形处理。
module boundary_demo #( parameter N = 4 )( input wire clk, input wire en, input wire [N-1:0] din, output wire [N-1:0] dout ); // 可综合:常量边界的 for,展开成 N 份组合逻辑 wire [N-1:0] masked; genvar i; generate for (i = 0; i < N; i = i + 1) begin : mask_bit assign masked[i] = din[i] & en; end endgenerate assign dout = masked; // 不可综合(示意,已注释):边界是运行期数据 // 想找 din 里最低的 1 的位置,循环次数取决于输入值 // while 写法工具直接拒收,硬件正解是逐拍扫描的状态机 endmodule
while 那段为什么必须拒收?因为它要生成的电路规模取决于运行期输入——工具在编译期数不出需要多少份硬件。这类需求在硬件世界的对应物是状态机逐拍扫描:每拍检查一位,用若干拍换「结构固定」,这是软件算法搬进硬件时的通用改造思路。
组合 always 块里分支不全时,综合器为了满足「保持旧值」的语义,会生成锁存器——电平敏感的存储单元。它不是触发器:没有时钟沿,使能有效期间输出跟随输入,透明窗口里输入的毛刺直达输出;时序工具对它也更头疼(无法用标准的沿路径模型分析)。
// 病灶:sel 为 11 时 y 保持旧值 → 锁存器 always @(*) begin case (sel) 2'b00: y = a; 2'b01: y = b; 2'b10: y = c; endcase end // 处方一:补全 default always @(*) begin case (sel) 2'b00: y = a; 2'b01: y = b; 2'b10: y = c; default: y = 1'b0; endcase end // 处方二:块首默认值 always @(*) begin y = 1'b0; // 先给安全默认 case (sel) 2'b00: y = a; 2'b01: y = b; 2'b10: y = c; endcase end
两种处方等效,工程上更偏爱块首默认值——分支再多也不会漏,新加分支时默认行为已定义。综合报告里锁存器会以「推断出电平敏感单元」之类的字样列出,这条告警永远值得逐条排查:合法使用锁存器的场景极其罕见,出现即嫌疑。
第 3 章从仿真语义论证过赋值铁律,综合视角再补一刀。时序块用阻塞赋值,综合器可能推断出「块内语句有先后依赖」的结构,产生级联的寄存器或意外的组合路径——不同工具处理方式不同,结果不可移植。组合块用非阻塞赋值,综合器虽然通常也能「救回来」(都按组合逻辑出),但仿真行为已经错位,仿真验证的结论失去效力。铁律在两个视角下同向:仿真正确性与综合正确性都要求时序非阻塞、组合阻塞——一条规则被两个世界同时背书,没有借口违反。
时序块有另一套「分支不全」的陷阱要分辨:if (!rst_n) q <= 0; else if (en) q <= d; 是合法的——en 为假时「保持」正是触发器的原生能力,综合出带使能的触发器,毫无问题。真正要警惕的是跨块赋值:同一个 reg 在两个 always 块里被赋值,综合器直接报多驱动错误(这与仿真语义一致);而一个 reg 在时序块赋值、又在组合块读取再运算,则是正常结构。时序块分支不全(想保持)合法,组合块分支不全(想保持)出锁存器——两者别混淆。
案例展开:一份病代码的门诊记录。 背景:某初学者的代码综合报错三处、告警五条,仿真却一切正常。操作:逐条过堂——报错一是 while 循环边界引用输入信号,改为固定次数展开循环;报错二是 initial 块里给寄存器赋初值并指望上电生效,改为纳入复位逻辑;报错三是同一 reg 被两个 always 块驱动,合并为一个块;告警里最重要的一条是组合块推断出锁存器,补块首默认值消除;其余告警(位宽不匹配、未使用信号)登记到规范整改清单。结果:综合干净通过,仿真行为与整改前一致——因为整改都在「仿真看不出来」的区间,这正是问题潜伏的原因。解读:这个案例的普遍教训是「仿真正确不等于可综合正确」——两个工具链执行的是两套语义,交界处的设施(延迟、初始化、分支完整性)是问题高发带。变式:团队层面把「综合零锁存器、零多驱动、告警清零」设为入库门禁,比代码评审的人眼检查可靠得多——第九章的 Lint 一节就是这个思路的工具化。
分界线划清了,下一节走完两条实现路线:FPGA 与 ASIC 的完整流程对照。