3.2 always 与 initial:过程块的两种身份 本节摘要:always 是被敏感事件反复唤醒的常设岗位,initial 是仿真开场只走一遍的一次性程序。阻塞赋值按顺序当场生效,非阻塞赋值先采样、沿后统一更新——分清这两套语义,就守住了 RTL 设计的第一生存线。 assign 只能表达纯函数,带状态的电路要靠过程块。这一节审过程块的两种身份与两套赋值语义——后者是全语言事故率最高的知识点,值得用一整节的篇幅对质。 always:常设岗位 always 块在仿真开始后反复「待命—触发—执行」:敏感列表里的事件一发生,块内语句从头到尾走一遍,然后回到待命状态。它有两种敏感形式,对应两种电路: 电平敏感形式「任何输入变化就重算」,与组合电路的行为同构;
本节摘要:always 是被敏感事件反复唤醒的常设岗位,initial 是仿真开场只走一遍的一次性程序。阻塞赋值按顺序当场生效,非阻塞赋值先采样、沿后统一更新——分清这两套语义,就守住了 RTL 设计的第一生存线。
assign 只能表达纯函数,带状态的电路要靠过程块。这一节审过程块的两种身份与两套赋值语义——后者是全语言事故率最高的知识点,值得用一整节的篇幅对质。
always 块在仿真开始后反复「待命—触发—执行」:敏感列表里的事件一发生,块内语句从头到尾走一遍,然后回到待命状态。它有两种敏感形式,对应两种电路:
// 形式一:电平敏感,综合成组合逻辑 always @(*) begin y = a & b | c; // 任何输入变化立即重算 end // 形式二:边沿敏感,综合成时序逻辑 always @(posedge clk or negedge rst_n) begin if (!rst_n) q <= 1'b0; else q <= d; end
电平敏感形式「任何输入变化就重算」,与组合电路的行为同构;边沿敏感形式只在时钟沿行动,与触发器同构。综合器正是靠敏感形式来推断你想要的电路类型——写错敏感形式,得到的电路就不是你想要的那个,第四章会专门审敏感列表的完整性问题。
@(*) 是 2001 版的通配写法:工具自动扫出块内所有右值信号构造敏感列表,根治手写漏信号的老毛病。看见手写 @(a or b or c) 的老代码,改造时优先换成 @(*)。
initial 块在仿真时间零点开始执行,走完即止,不再触发。它只活在仿真世界:测试台用它生成激励、初始化信号;可综合设计里不该有它(个别 FPGA 流程支持用 initial 给寄存器上电值,属于厂商扩展,不可移植)。把初始化逻辑写进 initial 期望「硬件上电也这样」,是新手最常见的认知错位——硬件的「上电」行为由复位电路决定,不由 initial 决定。
过程块内的赋值分两套语义,符号不同,行为截然不同。阻塞赋值 =:语句立刻求值、立刻生效,下一句看到的是新值,像软件的顺序执行。非阻塞赋值 <=:语句先计算右值并「记账」,本时间步内所有非阻塞赋值在延迟到时间步末尾统一生效,下一句看到的还是旧值。
// 阻塞版:三行全部看到同一个旧值? 不,逐行更新 always @(posedge clk) begin b = a; c = b; // 看到的是 b 的新值 d = c; // 看到的是 c 的新值 end // 结果:一个时钟沿内 a 的值穿透三级,等效于 d = a,移位寄存器消失 // 非阻塞版:标准的移位寄存器 always @(posedge clk) begin b <= a; c <= b; // 记账时 b 还是旧值 d <= c; // 记账时 c 还是旧值 end // 结果:每个时钟沿各寄存器同步前移一级,真移位寄存器

时序逻辑用非阻塞,原因在于硬件触发器的行为就是「沿上采样旧值、沿后统一更新」——非阻塞语义与它同构。换成阻塞赋值,块内语句的书写顺序就参与了语义:语句一换序,电路就变样,而 Verilog 并没有承诺块间执行顺序,多个 always 块都用阻塞赋值驱动彼此的信号时,结果依赖工具的调度顺序,仿真换个工具都可能变脸。
组合逻辑用阻塞,是因为组合块要求「块内像一条顺序化的表达式」——先算中间量再汇总,阻塞语义正好提供这个直观。用非阻塞写组合逻辑,同一时间步内信号更新滞后一拍,多块联动时出现「差一拍」的诡异波形。
混用两套语义是最坏的选项:同一块里先 = 后 <=,执行顺序依赖工具实现,标准不做保证。Lint 工具会把块内混用直接列为违例,见到就改。
案例展开:消失的移位寄存器。 背景:某团队把一段验证过的移位寄存器代码「顺手改成风格统一」,把非阻塞全改成了阻塞,仿真立刻发现输出提前一拍且中间级恒等于首级。操作:比对两种写法的波形,确认阻塞版三行语句在同一个时间步内逐句穿透,等效于一根导线直连;恢复非阻塞写法,波形回到每级一拍延迟。结果:代码恢复,同时在团队规范里加入「过程块内禁混用、时序禁阻塞」的 Lint 规则。解读:阻塞写法没有语法错误,综合器甚至可能给出「等效但结构不同」的电路,真正的危害在仿真与综合的解释空间变大——你不再能确定工具会怎么理解你的顺序。变式:需要「一拍内多级联动更新」的场合(比如流水线冲刷),正确写法是把每级仍然用非阻塞表达,联动关系放在控制条件里,而不是靠阻塞赋值拉齐时序。
always 与 initial 都可以命名(begin : block_name ... end),命名块里可以声明局部变量。多个 always 块天然并行,各自守着自己的敏感列表,这正是硬件并行性的直接表达——两个块驱动同一个 reg 是非法的(综合报多驱动),但一个块驱动信号、另一个块读取它是常态,这构成了 RTL 的「生产者—消费者」网络。块间通信只能靠信号,没有锁、没有线程切换的概念,时序协调全部交给你设计的握手协议。
直觉理解可以借软件的写时复制概念:非阻塞赋值执行时不改目标,只把「目标加新值」登记进一个本时间步末尾统一兑现的清单。这份清单的存在解释了两个现象——同刻内多次对同一目标非阻塞赋值,清单里后面的覆盖前面的,最终只生效最后一次;不同块对同一目标的多次赋值,生效结果取决于兑现顺序,这正是它不能用于块间通信的原因。理解了清单模型,非阻塞的行为不再是需要背诵的规则,而是可推导的机械结果。
锁存器来自「分支不全」,与用哪种赋值无关——但两种赋值下「分支不全」的表现不同。阻塞赋值下分支漏了,输出保持旧值,综合推断出锁存器,这与第 8 章的结论一致。非阻塞赋值下更糟:输出永远慢一拍,仿真结果直接错误。所以组合块的纪律是双重的:必须用阻塞赋值,且必须穷尽分支。两条规定各防各的病,缺一条都可能出事。
@(*) 防漏敏:通配敏感列表交给工具构造,手写列表只在特殊场合保留;赋值语义落定,下一节审陈述的修辞结构:分支与循环落进硬件会变成什么。