寄存器
本节中,我们要接触与组合逻辑相对的另外一种逻辑电路,时序逻辑电路。
相比组合逻辑来说,时序逻辑引入了状态的概念。寄存器在时钟边沿到来,改变完值以后,在本周期内的值都不会发生变化。而组合逻辑的信号线则会随着输入变化立即变化。
另一方面,由于寄存器在周期内的值保持不变,比起称寄存器为“内部元件”,它更像是一个外部输入。
本节将通过最简单的寄存器例子和对应的仿真,教大家如何使用硬件描述语言描述时序电路。
代码
本节使用的样例代码如下:
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 表示下降沿。
需要注意的是:
-
出现在 always_ff 的敏感信号列表里面的只能是时钟信号,以及在异步复位时可以加入复位信号。
-
没有双边沿响应的触发器——因此不能同时响应一个时钟信号的 posedge 和 negedge
-
我们的设计中不会出现跨时钟域的情况,因此不会响应两个时钟信号。只有当编写需要异步复位的逻辑时,需要在 always_ff 中响应两个信号,格式如下:
always_ff @(posedge clk or negedge rst)
。
仿真观察与寄存器的工作逻辑
仿真结果如下图所示:
仿真结果观察:
- b 信号跟输入 a 永远相同。
- 只有当 clk 信号上升沿到来时,c 和 d 才会变化
- 当上升沿来临时,c 会变为当前输入 a 的值。
- 当上升沿来临时,如果 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 的讲述,可以完成计数器实验。