Menu

Verilog 时序逻辑:触发器、寄存器与流水线

如何用时钟驱动的 always 块构建寄存器、计数器、移位寄存器和流水线,这是所有同步数字设计的核心模式。

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

基本单元:D 触发器

同步设计中的一切都可以归结为一个很小的硬件:D 触发器。它有一个时钟输入、一个数据输入和一个数据输出。在每个时钟上升沿,它采样 D 的值并保持到下一个时钟沿。仅此而已。

用 Verilog 写出来:

always @(posedge clk) begin
    q <= d;
end

三行代码,一个触发器。<= 是非阻塞赋值,这正是真实触发器的行为(详见阻塞赋值与非阻塞赋值)。posedge clk 敏感列表让它成为时序逻辑。

把许多这样的触发器和它们之间的组合逻辑堆叠起来,你就能构建任何想要的同步电路。

带复位的寄存器

实际设计总是需要复位,也就是在上电时或需要时把系统置于一个已知状态的方法:

这是同步复位:复位条件像其他输入一样在时钟沿上求值。综合工具会生成一个数据输入端带有 2 选 1 多路选择器的触发器:reset 为高电平时,多路选择器送入 0;否则送入 d。

对大多数设计来说,同步复位是正确的选择。它的时序更简单,在 FPGA 上表现良好,而且复位信号可以来自任何地方,不需要单独的与时钟对齐的信号源。

带使能的寄存器

你经常需要一个只在收到指示时才更新的寄存器。在时钟驱动的块内部使用 if,并依靠省略 else 时隐含的“保持原值”行为:

这就是标准的“带加载使能的寄存器”。它随处可见:配置寄存器、只在下游就绪时才前进的流水线级、可以暂停和继续的计数器。在时钟驱动的块中,省略最后的 else 是有意为之,而且是安全的:触发器本身就会记住之前的值,所以“什么都不做”就意味着“保持”。

在组合逻辑块中,同样的代码会推断出锁存器。语法相同,规则不同。

计数器

计数器就是下一个值等于当前值加一的寄存器:

当 enable 为高电平时,计数器每个时钟周期加一。16 个周期后它回绕到 0(因为我们声明了 4 位,15 + 1 溢出为 0)。这种回绕是 N 位算术固有的特性,真实的硬件计数器正是这样工作的。

移位寄存器

把触发器串起来就得到移位寄存器。诀窍是在一条非阻塞语句中完成所有移位:

块体是 out <= {out[WIDTH-2:0], in}:把当前 out 的低位与新的 in 拼接起来,再整体赋值。因为是非阻塞赋值,右侧读取的是左侧更新之前的旧 out。效果是在一个时钟周期内干净地完成 N 位移位。

这是移位寄存器模式最精简的形式。它可以推广到 LFSR、串行发送器、解串器,以及任何数据在每个时钟沿沿着一串触发器移动的设计。

流水线

流水线是一串由组合逻辑隔开的寄存器。每一级处理上一级送来的数据,再送给下一级:

三级流水线,延迟为三个周期,但流水线填满后每个周期都会输出一个新结果。延迟是 3 个时钟,因为数据要经过三个触发器;吞吐量是每个时钟 1 次运算,因为三级同时处理不同的输入。

高性能设计就是这样达到吞吐量目标的:让每一级保持简短,把流水线做得更深,让并行性来完成工作。

异步复位(需要时再用)

有时你不能等到时钟沿才让复位生效:芯片正在断电、时钟被门控、外部看门狗正在拉低复位线。在这些情况下:

always @(posedge clk or negedge reset_n) begin
    if (~reset_n) q <= 0;
    else          q <= d;
end

现在敏感列表中既有时钟沿,也有复位沿。触发器对两者中的任何一个都会立即响应。按照惯例,reset_n 是低电平有效(为低时有效),所以判断条件写成 ~reset_n。

异步复位有它的代价:更难做时序分析;撤销复位时如果处理不当可能导致亚稳态;不能在所有 FPGA 架构之间移植。默认使用同步复位,只有设计确实需要时才使用异步复位。

接下来

现在你可以构建任何同步数据通路了。下一篇文档有限状态机把时钟驱动的寄存器和 case 语句结合起来,形成标准的有限状态机写法:它是数字设计中每个控制器、协议引擎和决策模块的核心。

常见问题

Verilog 中的时序逻辑是什么?

由时钟信号控制何时更新的逻辑。标准写法是 always @(posedge clk) target <= next_value;:在 clk 的每个上升沿,target 采样 next_value。这一行就描述了硬件中的一个触发器。把许多触发器和它们之间的组合逻辑堆叠起来,就能构建计数器、移位寄存器、流水线,以及一切同步电路。

Verilog 中同步复位和异步复位有什么区别?

同步复位写成 always @(posedge clk) if (reset) ...:复位信号像其他输入一样在时钟沿上被采样。异步复位写成 always @(posedge clk or negedge reset_n) if (~reset_n) ...:时钟沿或复位信号有效都会触发这个块,所以复位立即生效。同步复位是默认选择;当必须保证即使时钟停止复位也能生效时,才使用异步复位。

如何在 Verilog 中构建流水线?

堆叠多级 always @(posedge clk) stageN_reg <= stageN_combinational;:每一级的组合逻辑输出送入下一级的寄存器,所有寄存器在同一个时钟沿上采样。结果就是一条流水线:每个周期都有新数据进入,N 个周期后输出,吞吐量为每个时钟一个结果。

Verilog 中的移位寄存器是什么?

一串触发器,每个触发器的输出接到下一个触发器的输入。每个时钟沿把所有位移动一个位置。标准的 Verilog 写法使用非阻塞赋值:out <= {out[N-2:0], in};,这一行就创建了一个 N 位移位寄存器,每个周期从 in 读入一位。

Coddy 编程语言插图

和 Coddy 一起学编程

开始