3.3 分支与循环:if、case、for 落进硬件会变成什么 本节摘要:if 综合成优先级链,case 综合成并行选择器,for 循环在编译期展开成实际电路,不定长循环不可综合。分支写漏分支项就推出锁存器——这是组合逻辑翻车的头号现场。 赋值语义清楚了,这一节审陈述的组织形式。分支与循环在软件里是控制流,在硬件里是结构——每种写法都对应一种具体电路,选错写法就是选错电路。 if:有优先级的串联审讯 if-else 在综合器眼里是一棵串联的判断树:第一个条件最先被询问,命中就短路,不命中往下传。这叫优先级逻辑——离 if 越近的条件优先级越高、路径越短。 优先级是逻辑不是副作用:多个请求同时到达时,仲裁结果由链的顺序决定。
本节摘要:if 综合成优先级链,case 综合成并行选择器,for 循环在编译期展开成实际电路,不定长循环不可综合。分支写漏分支项就推出锁存器——这是组合逻辑翻车的头号现场。
赋值语义清楚了,这一节审陈述的组织形式。分支与循环在软件里是控制流,在硬件里是结构——每种写法都对应一种具体电路,选错写法就是选错电路。
if-else 在综合器眼里是一棵串联的判断树:第一个条件最先被询问,命中就短路,不命中往下传。这叫优先级逻辑——离 if 越近的条件优先级越高、路径越短。
// 中断仲裁:编号越小优先级越高 always @(*) begin if (irq[3]) begin grant = 2'd3; valid = 1'b1; end else if (irq[2]) begin grant = 2'd2; valid = 1'b1; end else if (irq[1]) begin grant = 2'd1; valid = 1'b1; end else if (irq[0]) begin grant = 2'd0; valid = 1'b1; end else begin grant = 2'd0; valid = 1'b0; end end
优先级是逻辑不是副作用:多个请求同时到达时,仲裁结果由链的顺序决定。需要「真的并行、互斥到达」的场景用 if 也能跑对(互斥时优先级不起作用),但综合出的选择器更慢更大;这种情况该换 case。
case 把所有分支项摆在同一张桌上并行比对,硬件形态是多路选择器,各分支路径等长:
// 状态译码:各状态互斥,用 case 而不是 if 链 always @(*) begin case (state) S_IDLE: next_cmd = CMD_NOP; S_FETCH: next_cmd = CMD_LOAD; S_EXEC: next_cmd = CMD_RUN; S_DONE: next_cmd = CMD_ACK; default: next_cmd = CMD_NOP; endcase end
case 的互斥假设有一个著名的陷阱:普通 case 只认 0、1 的精确比对,x 与 z 不匹配任何分支项。casex 把 x 与 z 都当通配符,casez 只把 z 当通配符。casex 通配能力最强也最危险——如果条件信号意外出现 x,它可能匹配到错误分支,把未知态「洗白」成看似正常的输出;工程惯例是需要通配时用 casez 并显式写出通配位,casex 能不用就不用。
分支结构最大的坑是分支项不全:组合 always 块里 if 没有 else、case 没有 default,工具只好「保持旧值」——保持旧值就是存储,锁存器就此诞生。

软件的 for 是「重复执行」,硬件的 for 是「编译期展开」。循环体里的每份硬件都要真实存在——循环八次就是八份电路,循环一千次就是一千份。因此可综合的 for 有硬性前提:循环边界必须是编译期常量(或由 parameter 推导),循环变量只做索引不做数据依赖。
// 参数化奇偶校验:循环在编译期展开为异或树 function parity(input [7:0] data); integer i; begin parity = 1'b0; for (i = 0; i < 8; i = i + 1) parity = parity ^ data[i]; end endfunction
这份代码展开后是一棵八输入异或树,与手写 data[0]^data[1]^... 完全等价。for 在硬件代码里的正当用途由此清晰:批量生成结构相同的逻辑,配合参数位宽,让代码跟配置走。想「循环执行、每拍做一步」的软件式迭代,硬件答案是状态机或流水线,不是 for。
while、repeat、forever 这类不定长控制在可综合世界基本出局:while 的次数取决于运行期数据,展开不了;repeat 与 forever 只有配合延迟控制才有意义,那是仿真语法。见到可综合代码里的 while,先默认它有问题。
案例展开:一个 default 引发的功耗悬案。 背景:某芯片流片后实测静态功耗偏高,排查锁定一块译码逻辑。操作:复查 RTL 发现组合译码的 case 少写 default,而状态编码有若干非法组合未穷尽;非法组合到来时锁存器保持旧值,后续工具在时序分析里为这段「保持」路径补了大量缓冲与保持修复单元。结果:补全 default 后重新综合,面积与功耗指标双双回落。解读:锁存器的代价不止「结构意外」——它把组合路径变成跨周期路径,时序工具与功耗工具都会为它买单;分支穷尽既是正确性要求,也是面积功耗要求。变式:状态位宽宽于实际状态数时(如三位编码只用五个状态),default 分支要把所有非法编码显式导向安全态,这是功能安全设计的标准条款。
组合块里的纪律可以压成三句话:if 必有 else,case 必有 default,赋值先给默认值再按条件覆盖。第三句是防御性写法——块的第一行先把所有输出赋成安全默认值,后续分支按需覆盖,即使漏了分支也不会推出锁存器。这条写法在状态机与译码逻辑里几乎是免检通行证,第六章的三段式状态机会正式采用它。
逻辑结果一样,电路结果不一样。if 链综合出优先级结构,即使条件互斥、优先级毫无意义,串联的判断结构也在那里——路径更长、面积更大。case 综合出并行选择器,条件互斥时这就是最优形态。所以「知道条件互斥」应该直接反映在写法上:用 case 告诉工具「这些分支不会同时成立」,工具就不用为不存在的优先级买单。
genvar 循环里,循环变量是编译期常量,可以参与任何表达式——这正是参数化生成的基础。但可综合的 for(过程块内那种)里,循环变量也只在展开期有意义,展开后它就消失了。任何「运行期依赖循环变量做数据流转」的想法都属于软件思维,硬件里对应的需求应该用状态机逐拍处理。
可以,先匹配先生效——这实际上就是用 case 写优先级逻辑的技巧。部分设计者更喜欢用 case 表达优先级而不是 if 链,因为分支项并排罗列、优先级一目了然。两种写法综合结果等价,选哪个看团队风格,但全册要统一——同一个状态机里两种写法混用,评审成本直线上升。
分支循环的边界清楚了,下一节看陈述的复用机制:function 与 task 各自的活动范围。