10. 阻塞赋值与非阻塞赋值

10.1. 章节导读

“阻塞赋值(=)”和“非阻塞赋值(<=)”符号其实我们早就见过,而且还在前面的章节实例中使用过,大家可能存在疑问,有时候用阻塞赋值而有时候用非阻塞赋值,所以阻塞赋值和非阻塞赋值的概念一直是初学者较为头疼的一件事情,因为大多数人往往因为文字概念的描述理解不透彻,说白了就是看那些文字的解释反而让我们更难 理解,更容易被绕进去,产生懵的感觉。甚至有些很有经验的 逻辑设计工程师也不能完全正确地理解何时使用非阻塞赋值何时使用阻塞赋值才能设计出符合要求的电路,不明白在电路结构的设计中,即可综合风格的 Verilog模块的设计中,究竟为什么还要用非阻塞赋值,以及符合 IEEE 标准的 Verilog 仿真器究竟如何来处理非阻塞赋值的仿真。

本章的目的是尽可能地把阻塞和非阻塞赋值的含义和用法详细地解释清楚,并通过实例给出正确的用法,使之能够设计出符合我们意愿的的代码及功能。

10.2. 理论学习

阻塞赋值的赋值号用“=”表示。为什么称这种赋值方式为阻塞赋值呢?因为对应的电路结构往往与触发沿没有关系,只与输入电平的变化有关系。阻塞赋值的操作可以认为是只有一个步骤的操作,即计算赋值号右边的语句并更新赋值号左边的语句,此时不允许有来自任何其他Verilog语句的干扰,直到现行的赋值完成时刻,即把当 前赋值号右边的值赋值给左边的时刻完成后,它才允许下一条的赋值语句的执行。串行块(begin- end)中的各条阻塞型过程赋值语句将以它们在顺序块后的排列次序依次执行。阻塞型过程赋值语句的执行过程是:首先计算赋值号右边的值,然后立即将计算结果赋值给左边,赋值语句结束,变量值立即发生改变。阻塞的概念是指在同一个always块中,其后面的赋值语句从概念上是在前一句赋值语句结束后再开始下面的赋值。

非阻塞赋值的赋值号用“<=”表示。为什么称这种赋值方式为非阻塞赋值呢?这是因为对应的电路结构往往与触发沿有关系,只有在触发沿的时刻才能进行非阻塞赋值。非阻塞操作开始时计算非阻塞赋值符的赋值号右边的语句,赋值操作结束时刻才更新赋值号左边的语句,可以认为是两个步骤(赋值开始时刻和结束时刻)来完成非阻塞赋 值。在计算非阻塞语句赋值号右边的语句和更新赋值号左边的语句期间,其他的Verilog语句包括其他的Verilog非阻塞赋值语句都能同时计算赋值号右边的语句和更新赋值号左边的语句,允许其他的Verilog语句同时进行操作。非阻塞赋值的操作可以看作为两个步骤的过程:在赋值开始时刻,计算赋值号右边的语句。 在赋值结束时刻,更新赋值号左边的语句。注意:非阻塞操作只能用于对寄存器类型变量进行赋值,因此只能用于“initial”和“always”块中,不允许用于连续赋值“assign”。

10.3. 阻塞赋值

下面的例子可以说明这个问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module blocking
(
input wire sys_clk , //系统时钟50Mh
input wire sys_rst_n , //全局复位
input wire [1:0] in , //输入信号

output reg [1:0] out //输出信号
);

 reg [1:0] in_reg;

 //in_reg:给输入信号打一拍
 //out:输出控制一个LED灯
 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视图如图 16‑1所示,我们可以看到综合出的结果只有一组寄存器。

block002

图 16‑1 RTL视图(一)

为了进一步验证,我们做通过Testbench进行仿真验证一下结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
module tb_blocking();

//reg define
reg sys_clk;
reg sys_rst_n;
reg [1:0] in;

//wire define
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

 //sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50MHz
 always #10 sys_clk = ~sys_clk;

 //in:产生输入随机数,模拟按键的输入情况
 //取模求余数,产生非负随机数0、1、2、3,每隔20ns产生一次随机数
 always #20 in <= {$random} % 4;

 //------------------------blocking_inst------------------------
 blocking blocking_inst
 (
 .sys_clk (sys_clk ), //input sys_clk
 .sys_rst_n (sys_rst_n ), //input sys_rst_n
 .in (in ), //input [1:0] in

 .out (out ) //output [1:0] out
 );

 endmodule

打开ModelSim执行仿真,仿真出来的波形如图 16‑2所示,我们让仿真运行了500ns即可得到较好的观察效果。根据上一章的内容,我们知道一个寄存器就是“延一拍”,所以该仿真波形和前面的RTL视图刚好对应,发现输入信号in和中间变量in_reg、输出信号out的关系就是延迟一拍的关系,但是为什么只 是延迟一拍呢?首先中间变量in_reg一定要等待复位被释放后且第一个时钟上升沿来到时才会被赋值为输入信号in的值,所以会比输入信号in延迟一拍,而中间变量in_reg和输出信号out却没有延迟一拍的关系了,而是在同一时刻同时变化的,因为我们使用的是阻塞赋值,也就是说只要赋值号右边的表达式的值有变化, 赋值号左边的表达式的值也将立刻变化,所以我们最终看到的结果是中间变量in_reg和输出信号out是同时变化的。

block003

图 16‑2 仿真波形图(一)

10.4. 非阻塞赋值

下面的例子可以说明这个问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module non_blocking
(
input wire sys_clk e ,//系统时钟50Mh
input wire sys_rst_n ,//全局复位
input wire [1:0] in ,//输入按键

output reg [1:0] out //输出控制led灯
);

 reg [1:0] in_reg;

 //in_reg:给输入信号打一拍
 //out:输出控制一个LED灯
 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视图如图 16‑3所示,我们可以看到综合出的结果有两组寄存器,这是和使用阻塞赋值所综合的RTL视图所不同的。

block004

图 16‑3 RTL视图(二)

为了进一步验证,我们通过Testbench进行仿真验证一下结果,并和阻塞赋值(=)的波形进行对比。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
module tb_non_blocking();

//reg define
reg sys_clk;
reg sys_rst_n;
reg [1:0] in;

//wire define
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

 //sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50MHz
 always #10 sys_clk = ~sys_clk;

 //in:产生输入随机数,模拟按键的输入情况
 //取模求余数,产生非负随机数0、1、2、3,每隔20ns产生一次随机数
 always #20 in <= {$random} % 4;

 //------------------------blocking_inst------------------------
 blocking blocking_inst
 (
 .sys_clk (sys_clk ), //input sys_clk
 .sys_rst_n (sys_rst_n ), //input sys_rst_n
 .in (in ), //input [1:0] in

 .out (out ) //output [1:0] out
 );

 endmodule

打开ModelSim执行仿真,仿真出来的波形如图 16‑4所示,我们让仿真运行了500ns即可得到较好的观察效果。同样该仿真波形和其对应的RTL视图也是刚好对应的,我们发现输入信号in和中间变量in_reg是延迟一拍的关系,而中间变量in_reg和输出信号out也是延迟一拍的关系,也就是输入信号in 和输出信号out一共是延迟两拍的关系,为什么会这样呢?首先中间变量in_reg一定要等待复位被释放后且第一个时钟上升沿来到时才会被赋值为输入信号in的值,所以会比输入信号in延迟一拍,这是和阻塞赋值过程相同的,但是接下来就不一样了,因为我们使用的是非阻塞赋值,也就是说只要赋值号右边的表达式的值有变化 ,赋值号左边的表达式的值也不会立刻变化,需要等待下一次时钟沿到来时一起变化,所以我们最终看到的结果是输出信号out相对于输入信号是打了两拍的关系。

block005

图 16‑4 仿真波形图(二)

那么到底谁对谁错呢?显而易见,当我们想对一个信号打两拍的时候如果使用了阻塞赋值,那得到的结果明显不是我们想要的,如果乱用阻塞与非阻塞赋值其结果就不是我们可以预判的了,会出现各种问题,要想完全掌控我们所写的代码就要尽可能规范的设计代码。所以在描述逻辑电路时使用阻塞赋值,在描述时序逻辑电路时要用非阻塞赋 值,这也是官方的推荐写法。

10.5. 章末总结

本章主要讲解了阻塞赋值(=)与非阻塞赋值(<=)会给设计的电路所产生的差异,重新理解阻塞赋值与非阻塞赋值的原理和意义,能够使我们在以后的设计中正确设计出符合我们所想要表达的电路。

阻塞赋值(=):该语句结束时就完成赋值操作,前面的语句没有完成前,后面的语句是不能执行的。在一个过程块内多个阻塞赋值语句是顺序执行的。

非阻塞赋值(<=):一条非阻塞赋值语句的执行是不会阻塞下一条语句的执行,也就是说在本条非阻塞赋值语句执行完毕前,下一条语句也可开始执行。非阻塞赋值语句在过程块结束时才完成赋值操作。在一个过程块内的多个非阻塞赋值语句是并行执行的。

最后我们总结在编写RTL代码时推荐的一些规范,详细如下:

(1)在编写时序逻辑的代码时采用非阻塞赋值的方式

计算赋值号右手边的信号时,所有的变量值均是触发沿到来前的值,更新的赋值号左手边的信号作为触发沿后的值,并且保持到下一个触发沿到来时候,等待更新。这样,就可以使得在同一个块中非阻塞赋值语句不必要求出现的顺序,都是在全部进行赋值号右手边的信号计算后同时更新赋值号左手边的信号的值。非阻塞赋值可以简单的认为 是赋予下一状态的值。

(2)使用always块来编写组合逻辑的代码时要用阻塞赋值的方式

使用always块建立组合逻辑电路模型时不要忘记always块中的敏感列表一定要使用电平触发的方式,然后在always块中使用阻塞赋值语句就可以实现组合逻辑,这样做既简单且方针又快又好,这样的风格是值得推荐的。

(3)在同一个always块中不要既要用非阻塞赋值又用阻塞方式赋值

在同一个always块中对同一个变量既进行阻塞赋值又进行非阻塞赋值会产生综合不可预测的结果,不是可综合的Verilog风格。

(4)虽然锁存器电路建模是我们不推荐的,但是如果使用到要采用非阻塞赋值的方式。

使用非阻塞赋值实现时序逻辑,实现锁存器是最为安全的。

(5)一个always块只一个变量进行赋值

因为always块是并行的,执行的顺序是随机的,综合时会报多驱动的错误,所以严禁在多个always块中对同一个变量赋值;当然也不推荐一个always对多个变量进行赋值,虽然这种方式是允许的,但如果过多会导致代码的混乱且不易后期的维护和修改(本例之所以写在一起时因为变量不多,其次是为了进行对比得出本章 的实验效果),不是本教程推荐的设计方法。