跳转至

寄存器

本节中,我们要接触与组合逻辑相对的另外一种逻辑电路,时序逻辑电路。

相比组合逻辑来说,时序逻辑引入了状态的概念。寄存器在时钟边沿到来,改变完值以后,在本周期内的值都不会发生变化。而组合逻辑的信号线则会随着输入变化立即变化。

另一方面,由于寄存器在周期内的值保持不变,比起称寄存器为“内部元件”,它更像是一个外部输入。

本节将通过最简单的寄存器例子和对应的仿真,教大家如何使用硬件描述语言描述时序电路。

代码

本节使用的样例代码如下:

register_example.sv

`timescale 1ns / 1ps
module register_example (
    input wire clk,
    input wire rst,
    input wire a_i,
    input wire write_en_i,

    output reg b_wo,
    output reg c_ro,
    output reg d_ro
);

always_comb begin
    b_wo = a_i;
end

always_ff @(posedge clk) begin
    if(rst) begin
        c_ro <= 0;
    end else begin
        c_ro <= a_i;
    end
end

always_ff @(posedge clk) begin
    if(rst) begin
        d_ro <= 0;
    end else begin
        if(write_en_i) begin
            d_ro <= a_i;
        end
    end
end

endmodule

tb_reg.sv

`timescale 1ns / 1ps
module tb_reg;

reg clk;
reg rst;

initial begin
    forever begin
        clk = 1;
        #100;
        clk = 0;
        #100;
    end
end

reg a_i;
reg we_i;

initial begin
    rst = 1;
    #1;
    we_i = 1;
    forever begin
        a_i = 1;
        #300;
        rst = 0;
        a_i = 0;
        #300;
        we_i = ~we_i;
    end
end

wire b,c,d;

register_example reg_example (
    .clk(clk),
    .rst(rst),
    .a_i(a_i),
    .write_en_i(we_i),
    .b_wo(b),
    .c_ro(c),
    .d_ro(d)
);

endmodule

讲解

always_ff 语句块

在本节的代码当中,我们引入了一个新的语句块 always_ff。FF 的意思是 flip flop,也就是寄存器。表示在 always_ff 语句块中的逻辑,即被赋值的 reg,将会优先综合为寄存器。

与 always_comb 块不同,always_ff 块需要额外指明寄存器响应的时钟信号边沿,也就是这里的 @(posedge clk)。其中 posedge 表示上升沿,negedge 表示下降沿。

需要注意的是:

  1. 出现在 always_ff 的敏感信号列表里面的只能是时钟信号,以及在异步复位时可以加入复位信号。

  2. 没有双边沿响应的触发器——因此不能同时响应一个时钟信号的 posedge 和 negedge

  3. 我们的设计中不会出现跨时钟域的情况,因此不会响应两个时钟信号。只有当编写需要异步复位的逻辑时,需要在 always_ff 中响应两个信号,格式如下:always_ff @(posedge clk or negedge rst)

仿真观察与寄存器的工作逻辑

仿真结果如下图所示:

仿真结果观察:

  1. b 信号跟输入 a 永远相同。
  2. 只有当 clk 信号上升沿到来时,c 和 d 才会变化
  3. 当上升沿来临时,c 会变为当前输入 a 的值。
  4. 当上升沿来临时,如果 we 为高,d 会变为当前输入 a 的值,we 为低时不变化。

我们总结一下 system verilog 中寄存器的工作逻辑:

寄存器与信号线不同,其输入端和输出端被隔离开,信号值不一定相同。因此你可以把一个寄存器的输入和输出接在一起。

当响应边沿来临时,输出值会变为输入值。这意味着寄存器的值在一个周期中将保持不变。

即,当本周期寄存器输入为 x 时,下一周期中,寄存器的值将变为 x,但本周期中寄存器的输出值仍然保持不变。

有关寄存器的编程规范

使用非阻塞赋值号 <=

由于寄存器的输出改变不是立即发生的,因此应该使用语义更加接近的非阻塞赋值号 <=,而不是 =。

寄存器不必覆盖所有分支

与组合逻辑不同,寄存器带有存储功能,因此默认维持输出原值的逻辑没有问题,不需要 像组合逻辑那样覆盖全部可能的分支。

保证一个 reg 信号只在一个 always_ff 块中被赋值

寄存器的输入端与组合逻辑相同,如果有被两个信号同时赋值,则相当于将两个信号接到寄存器的输入端口上,当两个值不同时会发生短路。因此请保证一个 reg 信号只在一个 always_ff 块中被赋值,防止出现 multi driven。

寄存器信号名称请显示标出为寄存器

由于在 System Verilog 语法当中,取决于在哪种 always 中被赋值,reg 信号既可以被综合成物理连线(组合逻辑),又可以被综合为寄存器(时序逻辑),两种信号的行为完全不同。为了更方便地区分这两种信号,请在命名时显示标出该 reg 信号为寄存器还是物理连线。

请保证响应时钟信号的上升沿

时钟信号与普通信号有区别。一个响应普通信号的寄存器的工作可能不正常。请务必保证所有在 always_ff 被响应的信号为时钟信号。

另一方面,由于不存在双边沿触发器,一个时钟信号不能同时响应其上边沿和下边沿。因此我们在这里统一约定,在编写逻辑时,只响应时钟信号的上升沿。

请保证所有寄存器信号有合适的复位逻辑

在电路刚刚烧到 fpga 上时,所有的寄存器会处于一个未初始化的状态 X,其值可能为 0,也可能为 1。但当某些信号为特定值时,可能会发生不可逆的后果。因此,对于所有寄存器信号,只有在确认其初始值对设计没有影响时,才可以不编写复位逻辑。否则请向上述代码中一样,引入复位信号 rst,并进行复位处理。

复位值必须为常量

在复位的时候,对寄存器的赋值需要是常量。这是因为,在 FPGA 中,触发器的复位输入仅仅指明了“复位与否”,但没有指定“复位成多少”,因此仅支持复位成常量,这个常量是通过不同类型的触发器实现的,比如复位到零的触发器、复位到一的触发器。如果在复位逻辑中实现了复杂的逻辑,可能会导致 latch 的生成,综合器也会有警告。

到这里完成了 System Verilog 3 的讲述,可以完成计数器实验。


最后更新: 2023年4月23日
作者:cuibst (99.44%), Jiajie Chen (0.56%)