Menu

Verilog 阻塞与非阻塞赋值:= 和 <= 怎么选

Verilog 初学者最容易混淆的主题。always 块中的 = 和 <= 到底是什么意思,以及能避免大多数竞争条件的那条规则。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

两个运算符

在 always 和 initial 块中,Verilog 有两个赋值运算符:

  • = 是阻塞赋值。立即更新左侧,然后才进入下一条语句。
  • <= 是非阻塞赋值。现在计算右侧,把左侧的更新安排到当前时间步的末尾。

在过程块之外(assign 中)只有 =,assign a <= b 是语法错误。在过程块之内两者都合法,而选对其中哪一个,是 Verilog 初学阶段最重要的一个决定。

阻塞赋值做了什么

阻塞赋值就是你在软件语言中熟悉的方式:从上到下,逐行执行。第 N 条语句执行完后,第 N+1 条语句才开始:

这完全是顺序执行。每条语句都能看到它上面的语句产生的效果,与软件的直觉一致。

非阻塞赋值做了什么

非阻塞赋值是按硬件的方式工作的。所有右侧都使用时间步开始时的值计算,所有左侧都在时间步结束时更新。源码中语句的顺序不会改变信号之间的依赖关系:

这段代码一步就完成了 a → b → c → a 的三路轮换。用阻塞赋值的话,你需要一个临时变量,才能避免其中一个值被覆盖。用非阻塞赋值,交换是原子完成的,因为每个右侧读取的都是这一步之前的值。

这正是三个触发器在时钟沿上的行为:不管它们之间有什么依赖关系,它们都在同一时刻采样各自的输入。

能救你的那条规则

时钟驱动的块用 <=,组合逻辑块用 =。

就这么简单。记住它,写代码时不假思索地照做。大多数竞争条件、仿真与综合不一致,以及“我这里能跑,综合后就不行”的 bug,都来自违反这条规则。

这条规则有硬件上的原因:时钟驱动的块模拟的是同时采样输入的触发器,组合逻辑块模拟的是以最快速度传播的逻辑。赋值运算符的语义正好对应这两种行为。

每个 <= 读取源寄存器的当前值,并安排目标寄存器在时间步结束时取这个值。最终效果正是硬件移位寄存器的行为:每个触发器都在同一个时钟沿上采样相邻触发器的值,没有竞争。

再来看看如果用阻塞赋值会发生什么:

// WRONG - this is not a shift register!
always @(posedge clk) begin
    out[3] = out[2];   // out[3] becomes out[2]
    out[2] = out[1];   // out[2] becomes out[1], which we just set above
    out[1] = out[0];
    out[0] = in;
end

每条语句都会在下一条语句读取之前覆盖掉源值。在一个时钟周期内,in 会一路传到 out[3],因为每一行看到的都是上一行刚写入的值。真实硬件(使用非阻塞语义)的行为会与仿真器显示的完全不同。

组合逻辑块:阻塞赋值才对

对于 always @(*),阻塞赋值是正确的。这里没有触发器,没有需要遵守的同时采样规则,而且中间变量很有用:

先计算 sum,然后 result 使用刚算出的值。这段组合逻辑会被展平成一块硬件:result = ~(a + b)。因为没有时钟,所以不会出现触发器。

如果这里用 <=,仿真器仍然会在用 sum 计算 result 之前更新 sum(因为两次更新都发生在时间步末尾),但顺序会有微妙的不同,而且很多综合工具会报警告。不要混用,选择与块类型相匹配的运算符。

危害最大的错误

就是它:在时钟驱动的块中使用阻塞赋值。

// BUG: race condition waiting to happen
always @(posedge clk) begin
    a = b;
    b = c;
    c = a;
end

在仿真中,仿真器可能先给 a 赋值,再给 b,再给 c,得到一组值。硬件会得到另一组值,因为真实的触发器是同时采样的。两者悄无声息地出现差异,你会浪费一整天去找这个 bug。时钟驱动的块请用 <=。

为什么会有两个运算符

Verilog 的设计者本可以只选一种赋值语义,然后一直用下去。他们没有这样做,因为这门语言需要模拟两种截然不同的硬件行为:

  • 组合逻辑:信号持续传播,依赖关系很重要,“这个门计算的是什么”这个问题有意义。
  • 时序逻辑:时钟沿到来,每个触发器同时采样,触发器输入与输出之间的依赖是解耦的。

阻塞赋值用于前者,非阻塞赋值用于后者。运算符决定语义,剩下的交给仿真器。

接下来

现在你已经掌握了正确编写任何过程块的规则。下一章从单个块进一步深入到块内部的控制流结构:if/else、case 和 for 循环。阻塞与非阻塞赋值的规则在它们内部依然适用。

常见问题

Verilog 中阻塞赋值和非阻塞赋值有什么区别?

阻塞赋值(=)会立即更新目标,然后才执行下一条语句,它模拟的是顺序执行。非阻塞赋值(<=)把更新安排到当前时间步的末尾:每个非阻塞赋值的右侧都使用所有信号的旧值计算,然后所有左侧在一个统一的步骤中一起更新。触发器的实际行为就是非阻塞的。

在 Verilog 中什么时候用 =,什么时候用 <=?

规则:在时钟驱动的 always @(posedge clk) 块中用 <=,在组合逻辑 always @(*) 块中用 =。这一条规则就能避免混用赋值所产生的一整类竞争条件。在测试平台的 initial 块中,通常用 =;<= 在那里很少出现。

Verilog 为什么需要非阻塞赋值?

因为硬件中的触发器是在时钟沿上同时采样各自的输入的。如果在时钟驱动的代码中使用 =(阻塞赋值),文件中语句的顺序就会决定哪个信号看到另一个信号的新值,造成仿真与真实硬件之间的竞争条件。<= 先计算所有右侧,再统一完成所有更新,与硬件行为一致。

在同一个 Verilog always 块中混用 = 和 <= 会怎样?

会产生竞争条件。混用生成的硬件,其行为取决于仿真器的内部实现(事件调度顺序);更糟的是,仿真结果可能与综合结果不一致。大多数 lint 工具会把它标记为错误。解决办法是根据块的作用只用其中一种:时钟驱动的块用非阻塞赋值,组合逻辑块用阻塞赋值。

Coddy 编程语言插图

和 Coddy 一起学编程

开始