行为级 Verilog 的主力
assign 描述单个等式的组合逻辑。一旦需要 if/else、case 或记忆功能,就要用 always。always 块是一段过程代码,每当特定信号变化时就重新执行。触发重新执行的这些信号就是敏感列表。
你最常见到的 always 有两种形式:
always @(*):块中读取的任何信号变化时重新执行,构建组合逻辑。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。