Menu

Verilog always 块:组合逻辑与时序逻辑

always 块如何工作,组合逻辑 always @(*) 与时钟驱动的 always @(posedge clk) 有什么区别,以及决定它们各自生成什么硬件的规则。

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

行为级 Verilog 的主力

assign 描述单个等式的组合逻辑。一旦需要 if/else、case 或记忆功能,就要用 always。always 块是一段过程代码,每当特定信号变化时就重新执行。触发重新执行的这些信号就是敏感列表。

你最常见到的 always 有两种形式:

  1. always @(*):块中读取的任何信号变化时重新执行,构建组合逻辑。
  2. always @(posedge clk):只在 clk 的上升沿重新执行,构建时钟驱动的时序逻辑(触发器)。

还有其他形式(@(a or b)、@(negedge clk)、@(posedge clk or negedge reset_n)),但你写的几乎所有可综合的块都是上面这两种。

组合逻辑 always @(*)

需要注意三点:

  • out 是 reg。 在 always 中赋值的任何信号都必须是 reg。这个关键字并不表示“这是一个触发器”,在这里它只表示“我由过程块写入”。
  • always @(*)。 * 的意思是“我读取的任何信号变化时就唤醒”。仿真器会自动推算出敏感列表。你也可以手动写出列表,比如 always @(sel),但 @(*) 更安全,因为漏掉一个信号是经典的 bug 来源。
  • 没有时钟。 这个块描述的是组合逻辑。综合工具会生成一段直接由 sel 计算出 out 的逻辑,没有触发器,也不需要时钟引脚。

default 分支在语法上是可选的,但在实践中不应省略。省略它之后,任何未列出的输入值都会让 out 保持之前的值,这会被综合成一个意料之外的锁存器。务必加上 default。

时序逻辑 always @(posedge clk)

与组合逻辑版本的主要区别:

  • always @(posedge clk)。 这个块只在 clk 的上升沿重新执行,两个时钟沿之间什么都不会发生。
  • 非阻塞赋值 <=。 在时钟驱动的块中,这是正确的运算符。它表示“安排 count 在当前时间步结束时取新值”,这正是触发器的行为方式。原因以及另一种写法请看阻塞赋值与非阻塞赋值。
  • 不需要 default。 if 覆盖了两个分支(复位和非复位),没有产生锁存器的风险。

综合工具看到这种形式(时钟敏感列表加非阻塞赋值),就会生成一个 4 位寄存器(四个触发器),外加计算 count + 1 的组合逻辑,以及在复位和加一之间做选择的多路选择器。

综合结果的区别

同一份模块源码,取决于 always 块的形式,可能描述两种完全不同的硬件:

块硬件
always @(*) y = expr;纯组合逻辑,没有记忆。
always @(posedge clk) y <= expr;触发器,每个时钟周期保存一次 expr。
always @(*) if (en) y = expr;锁存器,通常是 bug。“else”情况会保持旧值。
always @(posedge clk) if (en) y <= expr;带使能的触发器,只在 en 为高电平时保存。

第三种情况就是锁存器陷阱。锁存器是一种透明的存储单元,输入无效时保持输出不变。它在某些特定设计中有用,但意外产生时几乎总是 bug。大多数综合工具在推断出你没要求的锁存器时会发出醒目的警告。请把这个警告当作错误对待。

敏感列表的其他写法

你还会看到几种不太常见的敏感列表:

  • always @(a or b or c):显式列表。Verilog-2001 增加了 , 分隔符:always @(a, b, c)。两种写法都可以。
  • always @(posedge clk or negedge reset_n):异步复位。这个块在时钟上升沿或复位信号下降沿执行。当复位必须立即生效、不能等到下一个时钟时使用。
  • always @(negedge clk):下降沿时钟。很少见;有些设计用它实现在下降沿而不是上升沿采样的“下降沿触发”触发器。

对于新设计,组合逻辑优先使用 always @(*),时序逻辑优先使用 always @(posedge clk)。只有设计确实需要时才使用异步复位。

两个块就是两块硬件

同一个模块中的多个 always 块彼此独立,每个都会成为一块独立的硬件:

时钟驱动的块生成一个触发器寄存器,组合逻辑块生成一个异或门。它们并排存在,互不知晓。两个输出按完全不同的节奏变化。

always 块不能做的事

有几件事看起来很诱人,但并不允许:

  • 给 wire 赋值:赋值目标必须是 reg,编译器会强制检查。
  • 在两个不同的 always 块中给同一个 reg 赋值:仿真中会产生未定义行为,也无法综合。每个信号只能有一个驱动源。
  • 在同一个组合逻辑块中读写同一个信号,从而形成反馈环路:always @(*) x = x + 1; 是一个零延迟循环,仿真器无法解决。

前两种编译器能发现。第三种有时只会在仿真时表现为卡死。

接下来

下一篇文档 initial 块讲解 always 的兄弟:一个在仿真开始时只执行一次的块,它是测试平台(testbench)的主力。之后会讲阻塞赋值与非阻塞赋值的规则,它们决定了你的时钟驱动块是否按你的意图工作。

常见问题

Verilog 中的 always 块是什么?

always 引入一个过程块,每当它的敏感列表中的信号发生变化时就重新执行。它有两种形式:always @(*) 构建组合逻辑(任何输入变化时重新执行),always @(posedge clk) 构建时序逻辑(在 clk 的每个上升沿重新执行)。always 块的块体中可以包含 if、case、for 以及过程赋值。

always @(*) 和 always @(posedge clk) 有什么区别?

always @(*) 对块中读取的任何信号都敏感,生成没有记忆的组合逻辑。always @(posedge clk) 只对 clk 的上升沿敏感,生成每个时钟周期保存一次状态的触发器。前者既没有时钟也没有寄存器,后者两者都有。

Verilog 中的敏感列表是什么?

@ 后面的信号列表,它决定 always 块何时重新执行。@(*) 是“块中读取的所有信号”的简写。@(posedge clk) 只在 clk 的上升沿执行。@(posedge clk or negedge reset_n) 在这两个事件中任一发生时执行,用于异步复位。写错敏感列表是仿真与综合结果不一致最常见的原因之一。

可以在 always 块中给 wire 赋值吗?

不可以。always 块只能给 reg(SystemVerilog 中是 logic)赋值,编译器会强制检查这一点。如果你想让一个 wire 作为过程逻辑的输出,可以声明一个中间 reg,在 always 中驱动它,再在外面用 assign 把这个 reg 赋给 wire;或者直接把 wire 改成 reg。

Coddy 编程语言插图

和 Coddy 一起学编程

开始