本小节我们来理解一下阻塞赋值与非阻塞赋值。那么阻塞赋值与非阻塞赋值的相关概念解析,在 Verilog 基础语法当中我们已经说过了,这儿为什么要再讲解一遍呢?是因为阻塞赋值与非阻塞赋值的概念,一直是初学者比较头疼的一个问题。那么经过上次的了解,有的朋友可能还没有理解阻塞赋值与非阻塞赋值的概念,那么有的朋友可能理解了,但是他们不知道应该怎么使用,也就是说不知道什么时候使用阻塞赋值,什么时候使用非阻塞赋值。
在本小节当中,我们除了对概念进行深入的解析之外,还会结合我们的实验工程,通过仿真波形,进一步加深大家对阻塞赋值和非阻塞赋值的理解。
本小节主要内容分为两个部分:一部分就是理论学习,就是阻塞赋值与非阻塞赋值的概念解析;然后就是实战演练,就是结合我们的实验工程,深入理解阻塞赋值与非阻塞赋值。
首先是理论学习
1 理论学习
1.1 阻塞赋值
先来看一下阻塞赋值。
在开始学习阻塞赋值的概念之前,我们先编写一段 RTL 代码
a = 1;
b = 2;
c = 3;
begin
a = b + 1;
b = a + 2;
c = a - 1;
end
接下来我们就结合这段 RTL 代码,进行阻塞赋值的概念解析。
阻塞赋值的赋值号使用等号 =
来表示,对应的电路结构往往与触发沿没有关系,只与输入电平的变化有关系,它的操作认为只有一个步骤的操作就是:计算赋值号右边的语句,更新赋值号左边的语句。什么意思呢?就像 a = b + 1;
这条语句计算 b+1
然后更新 a
的值;b = a + 2;
这条语句就是计算 a+2
更新 b
的值;此时不允许来自任何其他 Verilog 语句的干扰,直到现行的赋值完成,才允许下一条赋值语句的执行。这句话怎么理解呢?比如说 a = b+1;
这条语句在执行赋值的过程中,其他语句是不能执行的,如果说要执行 b = a+2;
必须等到 a = b+1;
执行完成之后才能执行 b = a+2;
这条语句。
那么串行块中就是 begin-end 中,我们的 begin-end 中,各条阻塞赋值语句将以它们在顺序块中的排列顺序依次执行,也就是说 begin-end 当中的语句是顺序执行的。a = b+1;
这条语句执行完成之后才能执行 b = a+2;
才能执行 c = a-1;
。如果说 c = a-1;
排列在 a = b+1;
之后,也就是说 b = a + 2;
和 c = a - 1;
它俩的位置调换一下,那么这三条语句怎么执行呢?就是 a = b+1;
然后是 c = a-1;
然后是 b = a+2;
,它们执行的顺序是依照排列顺序而来的,而且每条语句执行完成之后才能执行下一条语句。
那么这三条语句执行完阻塞赋值之后 a
b
c
它们的值分别是什么呢?我们来依次看一下。
那么首先是 a = b+1;
,b+1
先进行运算,b
原来等于 2
,b
加 1
之后等于 3
,那么执行完 a = b+1;
这条语句之后 a
就等于 3
了;
接下来 b = a+2;
,那么 a
现在已经等于 3
了,3
加 2
等于 5
,那么 b
现在就等于 5
;
最后是 c = a - 1;
,a
已经是 3
了,a
减 1
等于 2
,2
的值就给了 c
,c
就等于 2
。
这个串行块执行完成之后,三者的值就是这样了
阻塞赋值,一定要记住:每条语句执行完成之后,才能进行下一条语句的执行,而且在 begin-end 串行块当中,各条语句是顺序执行。
以上就是阻塞赋值的概念解析
1.2 非阻塞赋值
接下来看一下非阻塞赋值,同样我们先编写一段代码
a = 1;
b = 2;
c = 3;
begin
a <= b + 1;
b <= a + 2;
c <= a - 1;
end
我们编写的 RTL 代码它和我们的阻塞赋值编写的 RTL 代码是差不多的,只是使用的赋值方式不同。
非阻塞赋值的赋值号使用的是小于等于号 <=
,它对应的电路结构往往是与触发沿有关系的,只在触发沿的时刻才能进行非阻塞赋值,这儿是与阻塞赋值有区别的。那么非阻塞赋值的操作可以看作是两个步骤的过程,在赋值开始时刻计算赋值号右边的语句,在赋值结束时刻更新赋值号左边的语句。也就是说,b + 1
、a + 2
、a - 1
它们计算完成之后不会直接赋值给左边的语句,而是说等到 end 之后,同时将左边的语句进行一个更新。
在计算非阻塞语句赋值号右边的语句和更新赋值号左边的语句期间,允许其他的 Verilog 语句同时进行操作。
在 begin 和 end 这个串行块之间,三条语句是并行执行的,那么不同于阻塞赋值是顺序执行。执行完 begin-end 串行块之后 a
、b
、c
它们的值分别是什么呢?我们来看一下。
在赋值开始时刻,首先是计算赋值号右边的语句。那么 b
加 1
是等于 3
;a
加 2
(1
加 2
)也是等于 3
;那么 a
减 1
是 1
减 1
等于 0
。然后结束之后才会将 3
、3
、0
对应的赋值给 a
、b
、c
。也就是说最后结束完成之后:a
等于 3
,b
等于 3
,c
等于 0
非阻塞赋值操作只能用于对寄存器类型变量进行赋值,因此只能用于 initial
语句和 always
语句当中,不允许使用于连续赋值 assign
语句。
以上就是理论部分对阻塞赋值与非阻塞赋值的解析,接下来开始实战演练
2 实战演练
在实战演练部分,我们会创建实验工程,编写 RTL 代码;然后对阻塞赋值、非阻塞赋值,进行仿真波形的查看。通过仿真波形,你可以更加深入的理解阻塞赋值与非阻塞赋值。
首先建立我们的文件体系
2.1 阻塞赋值
对于本实验工程不再进行波形图的绘制,直接进行代码的编写
首先是模块开始与模块名称,下面是端口列表,然后是模块结束。端口列表肯定包括时钟信号和复位信号,然后输入一个 2 位宽的输入信号 in;然后是一个 2 位宽的输出信号,因为使用 always 语句进行赋值,所以说它的变量类型是 reg 型
下面我们定义一个中间变量 in_reg
,定义这个中间变量是为了方便我们仿真波形的观察;接下来对变量进行赋值,我们使用 always 语句,我们这儿使用异步复位
那么这几条语句的意思是:当复位信号有效时给 in_reg
变量进行一个初值赋值,然后给输出信号 out
进行一个初值的赋值。那么两个变量使用的是阻塞赋值,因为是对两个变量进行赋值,所以说我们使用了 begin-end
这几条语句是表示:当复位信号无效时对两个变量进行赋值,将输入信号 in
赋值给中间变量 in_reg
,然后将 in_reg
的值赋值给输出信号 out
。
这样阻塞赋值的代码已经编写完成,我们保存
blocking.v
module blocking
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire [1:0] in ,
output reg [1:0] out
);
reg [1:0] in_reg;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
begin
in_reg = 2'b0;
out = 2'b0;
end
else
begin
in_reg = in;
out = in_reg;
end
endmodule
那么下面建立我们的实验工程。回到桌面。那么这儿大家应该很熟悉了
然后添加编写的代码,添加完成之后进行一次全编译,那么编译完成,点击 OK
我们来看一下 RTL 视图
那么阻塞赋值代码,综合出来的 RTL 视图,使用了一个寄存器。
那么为了进一步验证,我们编写仿真文件。那么首先是时间参数,然后是开始与模块名称、端口列表,结束
然后是声明时钟、复位和输入信号,然后是将输出信号引出
那么下面使用 initial 语句初始化系统时钟、全局复位信号和输入信号
初始化完成之后,生成我们的模拟时钟,时钟频率还是 50MHz;然后使用求模取余法模拟输入信号
我们的输入信号的位宽是 2 位宽,那么它能表示:00、01、10、11,所以说我们这儿是对 4 进行求余,刚好可以生成非负数:0、1、2、3。
下面开始模块的实例化
tb_blocking.v
`timescale 1ns/1ns
module tb_blocking();
reg sys_clk;
reg sys_rst_n;
reg [1:0] in;
wire[1:0] out;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
in <= 2'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
always #20 in <= {$random} % 4;
blocking blocking_inst
(
.sys_clk (sys_clk ),
.sys_rst_n(sys_rst_n),
.in (in ),
.out (out )
);
endmodule
到了这里,仿真模块编写完成,我们保存,回到实验工程,添加我们的仿真文件,然后进行仿真设置
接下来开始仿真
那么仿真编译完成之后,打开 sim 选项卡,添加我们的阻塞赋值模块;打开波形窗口,全选、分组、去除前缀;点击 Restart,接下来让仿真运行 500ns;那么运行 500ns 之后,进行全局视图
根据我们上一讲的内容我们知道,一个寄存器可以实现延迟一拍的一个效果。
由我们的 RTL 视图可知,代码最终生成了一个寄存器。所以说,我们的仿真波形与 RTL 视图是刚好对应的
我们发现输入信号 in
和中间变量 in_reg
、输出信号 out
,它们的关系刚好就是延迟一拍的效果。但为什么是延迟一拍呢?首先,我们的中间变量 in_reg
它一定要等到复位信号被释放后且第一个时钟沿到来后,才会将 in
的值赋值给他,所以说它比输入信号延迟一拍,这儿是没有问题的;而中间变量 in_reg
和输出信号 out
它俩却没有延迟一拍的一个关系了,而是在同一时刻变化的,这是因为我们使用的是阻塞赋值,也就是说赋值号右边的表达式它的值有变化,赋值号左边的表达式立刻发生变化。我们最终看到的结果就是,中间变量 in_reg
和输出信号 out
它的波形变化是一样的。
那么以上就是阻塞赋值的仿真波形。
2.2 非阻塞赋值
接下来对代码进行修改,我们这儿使用非阻塞赋值,然后保存
blocking.v
module blocking
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire [1:0] in ,
output reg [1:0] out
);
reg [1:0] in_reg;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
begin
in_reg <= 2'b0;
out <= 2'b0;
end
else
begin
in_reg <= in;
out <= in_reg;
end
endmodule
回到我们的实验工程,重新编译
然后查看 RTL 视图
我们这儿可以发现 RTL 视图使用了两个寄存器。那么第一个寄存器输入信号是 in
,输出信号是 in_reg
;这儿就可以猜测出,我们的变量 in_reg
延迟了输入信号 in
一个时钟周期。那么第二个寄存器,输入信号是 in_reg
输出信号是 out
;我们可以猜测:输出信号 out
延迟了变量 in_reg
一个时钟周期,延迟了输入信号 in
两个时钟周期。那么猜测是否正确呢?我们先来仿真一下。
回到 ModelSim,然后找到 Library 对我们修改的模块进行一个重编译,这儿显示编译成功,回到波形界面,点击 Restart,那么重新仿真 500ns,在仿真波形界面,我们可以选中任意波形,通过拖拽的方式来改变它们的排列顺序
我们观察仿真波形可以看出:in_reg
变量的确是延迟了输入信号 in
一个时钟周期;而我们的输出信号 out
也是延迟了变量 in_reg
一个时钟周期,延迟了输入信号两个时钟周期。这是为什么呢?首先是 in_reg
信号它一定要等待复位被释放后且第一个时钟上升沿到来的时候,才会将 in
的值赋值给它,所以说,它会延迟输入信号 in
一个时钟周期,这儿与阻塞赋值是相同的。但是后面就不一样了,输出信号 out
同样是延迟了变量 in_reg
一个时钟周期,是因为使用的是非阻塞赋值,也就是说,只要赋值号右边的表达式的值有变化了,赋值号左边的表达式的值不会立刻发生变化,需要等待下一个时钟沿到来时一起变化,所以说 out
延迟了 in_reg
信号一个时钟周期,就相当于输入信号打了两拍。
那么这儿就是非阻塞赋值的仿真波形。
那么经过对实验工程的仿真,我们对阻塞赋值与非阻塞赋值的概念,有一个深入的理解。
它们两个到底哪个比较好呢?当我们想对一个信号打两拍的时候,如果使用了阻塞赋值,得不到我们想要的结果,非阻塞赋值可以。非阻塞赋值和阻塞赋值,不能够随便乱用,否则可能造成难以预估的问题。
要想完全掌控我们所写的代码,就要尽可能地规范的设计代码。当编写组合逻辑电路时使用阻塞赋值,那么编写时序逻辑时使用非阻塞赋值,这是官方推荐的写法。
那么本小节主要讲解了阻塞赋值与非阻塞赋值它们的电路差异,重新理解了阻塞赋值与非阻塞赋值的原理与意义。
那么最后我们也总结一下,编写 RTL 代码时推荐的一些规范:
- 第一条就是:编写时序逻辑时我们要采用非阻塞赋值的方式,前面已经讲过了;
- 第二条就是:使用 always 语句块来编写组合逻辑时,要使用阻塞赋值的方式。这时要记住:敏感列表一定要使用电平触发的方式;
- 第三条要注意的是:在一个 always 语句块中,不要既使用阻塞赋值又使用非阻塞赋值,这样会造成不可预测的后果;
- 那么第四条就是:我们前面讲到了锁存器,那么锁存器我们是不推荐使用的。但是说如果你非要使用锁存器,一定要采用非阻塞赋值的方式,因为使用非阻塞赋值实现时序逻辑,进而实现锁存器是最为安全的;
- 最后一条就是:我们推荐一个 always 语句块只对一个变量进行赋值,这样方便我们后期的维护和修改
参考资料: