SystemVerilog 基础

第 8 章 · 共 16 章

过程块:initial、always_comb、always_ff、always_latch

系统梳理 initial、传统 always、always_comb、always_ff、always_latch 这几种过程块各自的用途,并兑现前面章节的承诺——彻底讲清楚阻塞赋值 = 与非阻塞赋值 <= 的区别。

前面几章已经在例子里用过 initialalways_combalways_ff,但一直没有正式讲过它们各自的边界,也留了一个"欠账"——非阻塞赋值 <= 到底和阻塞赋值 = 有什么本质区别。这一章把这些过程块系统梳理一遍,并还清这笔账。

initial 块:只执行一次

initial 块在仿真时间 0 开始执行,从头到尾跑一遍就结束,不会再被重新触发。它不可综合,只存在于验证代码里,典型用途是产生激励、做一次性初始化:

initial begin
  clk = 0;
  a   = 1'b0;
  b   = 1'b1;
  #10 $display("initial block ran once");
end

传统的 always 块:什么都能塞,也因此容易出错

在 SystemVerilog 给出更明确的分类之前,Verilog 只有一种 always 块,靠手写的敏感列表(sensitivity list)决定它什么时候被触发:

// 传统写法:组合逻辑
always @(sel or a or b)
  y = sel ? b : a;

问题在于敏感列表是手写的——一旦漏写了某个信号,仿真行为和综合后的电路行为就会不一致(仿真里这个信号变化不会触发重新计算,但综合出来的组合逻辑电路必然会响应),这是非常经典也非常隐蔽的一类 bug。SystemVerilog 用三个更明确的关键字取代了这种"万能但危险"的 alwaysalways_combalways_ffalways_latch

always_comb:组合逻辑,敏感列表自动推导

always_comb begin
  y = sel ? b : a;
end

always_comb 会自动把块内读取到的所有信号(这里是 selab)都加入敏感列表,不需要、也不允许手写——从根源上消除了"漏写敏感信号"这类 bug。它还有一个和传统 always 不同的细节:always_comb 会在仿真一开始(时间 0)就先执行一次,确保输出从一开始就是正确值,而不是等到第一次触发之后才第一次计算。

always_ff:时序逻辑,只由时钟边沿驱动

always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    q <= 1'b0;
  else
    q <= d;
end

always_ff 明确表达"这是时序逻辑"的意图——必须由边沿触发(posedge/negedge),通常用来描述触发器/寄存器。这种明确的意图声明能让工具在你不小心写出不像时序逻辑的代码时(比如误用了阻塞赋值,见下一节)主动报警告。

always_latch:锁存器,通常是"我确实是故意的"

always_latch begin
  if (enable)
    q_latch = d;
end

always_latchalways_comb 一样自动推导敏感列表,但语义上表示"这里故意描述一个锁存器(电平触发的存储元件)"。锁存器在数字设计里大多数时候是意外产生的(比如 always_comb/case 里漏写了某个分支,工具就会推导出一个隐式锁存器),真正需要主动使用 always_latch 的场景并不算多;它的价值更多在于:如果你确实想要一个锁存器,用这个关键字明确表达意图,工具就不会把它当成"漏写分支导致的意外"来报警告。

阻塞赋值 vs. 非阻塞赋值:=<=

这是本章最重要的一节。两种赋值符号语义完全不同:

  • 阻塞赋值 =:语句按书写顺序立即、依次执行,下一条语句要等这一条完全执行完才开始——和普通编程语言里的赋值语句一样。用在 always_comb(以及 initial/testbench 代码)里。
  • 非阻塞赋值 <=:不会立即写入左边的变量,而是先记下"这个变量将来要变成什么值",等当前仿真时间步的所有非阻塞赋值都计算完右边表达式之后,再统一更新。用在 always_ff 里。

为什么时序逻辑必须用非阻塞赋值?看这个经典例子——用一个时钟沿把 ab 的值互换:

// 用非阻塞赋值:正确地实现了互换
always_ff @(posedge clk) begin
  a <= b;
  b <= a;
end

因为 <= 会先"记下"右边的值,两条语句读到的都是时钟沿之前ab,最后再同时更新——效果正是把两个寄存器的值互换。

// 如果误用阻塞赋值:不会得到互换的效果
always_ff @(posedge clk) begin
  a = b;  // a 立即变成 b 的值
  b = a;  // 这时候读到的 a 已经是刚更新过的新值(也就是原来的 b)!
end

第二行的 b = a 读到的 a 已经是第一行刚写入的新值,结果 ab 最终都变成了原来的 b——这不是互换,而是一个由赋值方式错误导致的经典 bug。

记住这条经验法则:always_comb 里用阻塞赋值 =always_ff 里用非阻塞赋值 <=;不要在同一个块里混用两种赋值,工具通常也会对混用发出警告。

该用哪个块?

过程块触发条件该用的赋值典型用途
initial仿真时间 0,只执行一次=(阻塞)验证代码:产生激励、一次性初始化
传统 always手写敏感列表视场景而定已被下面三种更明确的写法取代,不建议在新代码里使用
always_comb自动推导(读到的信号变化即触发)=(阻塞)组合逻辑
always_ff时钟边沿(posedge/negedge<=(非阻塞)时序逻辑(触发器、寄存器)
always_latch自动推导,电平触发=(阻塞)有意为之的锁存器(少见)

小结

  • initial 只执行一次,不可综合,是验证代码的基本工具。
  • 传统 always 依赖手写敏感列表,容易漏写导致仿真与综合行为不一致;always_comb/always_ff/always_latch 用更明确的关键字替代了它。
  • always_comb 自动推导敏感列表、描述组合逻辑;always_ff 描述时钟驱动的时序逻辑;always_latch 用于有意为之的锁存器。
  • always_comb 用阻塞赋值 =always_ff 用非阻塞赋值 <=——用错赋值方式(尤其是在时序逻辑里误用阻塞赋值)会导致像"互换值"这类经典 bug。

相比传统的 always @(...) 写法,always_comb 最主要解决了什么问题?

在下面的 always_ff 块里,如果把 a <= b; b <= a; 错误地写成 a = b; b = a;(阻塞赋值),会发生什么?

在 always_ff 时序逻辑块里,应该使用哪种赋值符号?(直接填写符号,如 = 或 <=)