[FPGA 学习记录] 阻塞赋值与非阻塞赋值

本文详细解析了Verilog中的阻塞赋值与非阻塞赋值概念,通过理论学习和实战演练,结合实例代码和仿真波形,帮助读者掌握两者在赋值操作中的区别、使用场景和正确运用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阻塞赋值与非阻塞赋值


本小节我们来理解一下阻塞赋值与非阻塞赋值。那么阻塞赋值与非阻塞赋值的相关概念解析,在 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 原来等于 2b1 之后等于 3,那么执行完 a = b+1; 这条语句之后 a 就等于 3 了;

接下来 b = a+2;,那么 a 现在已经等于 3 了,32 等于 5,那么 b 现在就等于 5

最后是 c = a - 1;a 已经是 3 了,a1 等于 22 的值就给了 cc 就等于 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 + 1a + 2a - 1 它们计算完成之后不会直接赋值给左边的语句,而是说等到 end 之后,同时将左边的语句进行一个更新。

在计算非阻塞语句赋值号右边的语句和更新赋值号左边的语句期间,允许其他的 Verilog 语句同时进行操作。

在 begin 和 end 这个串行块之间,三条语句是并行执行的,那么不同于阻塞赋值是顺序执行。执行完 begin-end 串行块之后 abc 它们的值分别是什么呢?我们来看一下。

在赋值开始时刻,首先是计算赋值号右边的语句。那么 b1 是等于 3a2(12)也是等于 3;那么 a111 等于 0。然后结束之后才会将 330 对应的赋值给 abc。也就是说最后结束完成之后:a 等于 3b 等于 3c 等于 0

非阻塞赋值操作只能用于对寄存器类型变量进行赋值,因此只能用于 initial 语句和 always 语句当中,不允许使用于连续赋值 assign 语句。

以上就是理论部分对阻塞赋值与非阻塞赋值的解析,接下来开始实战演练

2 实战演练

在实战演练部分,我们会创建实验工程,编写 RTL 代码;然后对阻塞赋值、非阻塞赋值,进行仿真波形的查看。通过仿真波形,你可以更加深入的理解阻塞赋值与非阻塞赋值。

首先建立我们的文件体系

20231029204335_1v1Tq49P7E

2.1 阻塞赋值

对于本实验工程不再进行波形图的绘制,直接进行代码的编写

aF07Frp0AO

首先是模块开始与模块名称,下面是端口列表,然后是模块结束。端口列表肯定包括时钟信号和复位信号,然后输入一个 2 位宽的输入信号 in;然后是一个 2 位宽的输出信号,因为使用 always 语句进行赋值,所以说它的变量类型是 reg 型

20231029205209_WVAQVryY7k

下面我们定义一个中间变量 in_reg,定义这个中间变量是为了方便我们仿真波形的观察;接下来对变量进行赋值,我们使用 always 语句,我们这儿使用异步复位

20231029205553_1iyFTBCcHF

那么这几条语句的意思是:当复位信号有效时给 in_reg 变量进行一个初值赋值,然后给输出信号 out 进行一个初值的赋值。那么两个变量使用的是阻塞赋值,因为是对两个变量进行赋值,所以说我们使用了 begin-end

20231029205922_7s8Y18el7E

这几条语句是表示:当复位信号无效时对两个变量进行赋值,将输入信号 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

那么下面建立我们的实验工程。回到桌面。那么这儿大家应该很熟悉了

AZYuqSCWJu

然后添加编写的代码,添加完成之后进行一次全编译,那么编译完成,点击 OK

20231029210504_24wRCyPvRm

我们来看一下 RTL 视图

image-20231029210638897

那么阻塞赋值代码,综合出来的 RTL 视图,使用了一个寄存器。

那么为了进一步验证,我们编写仿真文件。那么首先是时间参数,然后是开始与模块名称、端口列表,结束

20231029210931_RbTfaTJcfL
然后是声明时钟、复位和输入信号,然后是将输出信号引出

20231029211144_bHKp2EWMrb

那么下面使用 initial 语句初始化系统时钟、全局复位信号和输入信号

20231029211336_ZaaNYx4MG8

初始化完成之后,生成我们的模拟时钟,时钟频率还是 50MHz;然后使用求模取余法模拟输入信号

20231029211549_ySdqFpSBNl

我们的输入信号的位宽是 2 位宽,那么它能表示:00、01、10、11,所以说我们这儿是对 4 进行求余,刚好可以生成非负数:0、1、2、3。

下面开始模块的实例化

20231029211742_D1skzaAzJG

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

到了这里,仿真模块编写完成,我们保存,回到实验工程,添加我们的仿真文件,然后进行仿真设置

20231029211952_N0oo0Pbdzl

接下来开始仿真

20231029212108_ypNt9cWr1O

那么仿真编译完成之后,打开 sim 选项卡,添加我们的阻塞赋值模块;打开波形窗口,全选、分组、去除前缀;点击 Restart,接下来让仿真运行 500ns;那么运行 500ns 之后,进行全局视图

20231029212517_lKOuOKWVFl

根据我们上一讲的内容我们知道,一个寄存器可以实现延迟一拍的一个效果。

由我们的 RTL 视图可知,代码最终生成了一个寄存器。所以说,我们的仿真波形与 RTL 视图是刚好对应的

image-20231029213301383

我们发现输入信号 in 和中间变量 in_reg、输出信号 out,它们的关系刚好就是延迟一拍的效果。但为什么是延迟一拍呢?首先,我们的中间变量 in_reg 它一定要等到复位信号被释放后且第一个时钟沿到来后,才会将 in 的值赋值给他,所以说它比输入信号延迟一拍,这儿是没有问题的;而中间变量 in_reg 和输出信号 out 它俩却没有延迟一拍的一个关系了,而是在同一时刻变化的,这是因为我们使用的是阻塞赋值,也就是说赋值号右边的表达式它的值有变化,赋值号左边的表达式立刻发生变化。我们最终看到的结果就是,中间变量 in_reg 和输出信号 out 它的波形变化是一样的。

那么以上就是阻塞赋值的仿真波形。

2.2 非阻塞赋值

接下来对代码进行修改,我们这儿使用非阻塞赋值,然后保存

20231029213806_5FUDSzBRCX

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

回到我们的实验工程,重新编译

20231029213925_8mQ6l1Rosb

然后查看 RTL 视图

image-20231029214205110

我们这儿可以发现 RTL 视图使用了两个寄存器。那么第一个寄存器输入信号是 in,输出信号是 in_reg;这儿就可以猜测出,我们的变量 in_reg 延迟了输入信号 in 一个时钟周期。那么第二个寄存器,输入信号是 in_reg 输出信号是 out;我们可以猜测:输出信号 out 延迟了变量 in_reg 一个时钟周期,延迟了输入信号 in 两个时钟周期。那么猜测是否正确呢?我们先来仿真一下。

回到 ModelSim,然后找到 Library 对我们修改的模块进行一个重编译,这儿显示编译成功,回到波形界面,点击 Restart,那么重新仿真 500ns,在仿真波形界面,我们可以选中任意波形,通过拖拽的方式来改变它们的排列顺序

20231029214759_Y5k4J7t41Y

image-20231029215039680

我们观察仿真波形可以看出:in_reg 变量的确是延迟了输入信号 in 一个时钟周期;而我们的输出信号 out 也是延迟了变量 in_reg 一个时钟周期,延迟了输入信号两个时钟周期。这是为什么呢?首先是 in_reg 信号它一定要等待复位被释放后且第一个时钟上升沿到来的时候,才会将 in 的值赋值给它,所以说,它会延迟输入信号 in 一个时钟周期,这儿与阻塞赋值是相同的。但是后面就不一样了,输出信号 out 同样是延迟了变量 in_reg 一个时钟周期,是因为使用的是非阻塞赋值,也就是说,只要赋值号右边的表达式的值有变化了,赋值号左边的表达式的值不会立刻发生变化,需要等待下一个时钟沿到来时一起变化,所以说 out 延迟了 in_reg 信号一个时钟周期,就相当于输入信号打了两拍。

那么这儿就是非阻塞赋值的仿真波形。

那么经过对实验工程的仿真,我们对阻塞赋值与非阻塞赋值的概念,有一个深入的理解。

它们两个到底哪个比较好呢?当我们想对一个信号打两拍的时候,如果使用了阻塞赋值,得不到我们想要的结果,非阻塞赋值可以。非阻塞赋值和阻塞赋值,不能够随便乱用,否则可能造成难以预估的问题。

要想完全掌控我们所写的代码,就要尽可能地规范的设计代码。当编写组合逻辑电路时使用阻塞赋值,那么编写时序逻辑时使用非阻塞赋值,这是官方推荐的写法。

那么本小节主要讲解了阻塞赋值与非阻塞赋值它们的电路差异,重新理解了阻塞赋值与非阻塞赋值的原理与意义。

那么最后我们也总结一下,编写 RTL 代码时推荐的一些规范:

  1. 第一条就是:编写时序逻辑时我们要采用非阻塞赋值的方式,前面已经讲过了;
  2. 第二条就是:使用 always 语句块来编写组合逻辑时,要使用阻塞赋值的方式。这时要记住:敏感列表一定要使用电平触发的方式;
  3. 第三条要注意的是:在一个 always 语句块中,不要既使用阻塞赋值又使用非阻塞赋值,这样会造成不可预测的后果;
  4. 那么第四条就是:我们前面讲到了锁存器,那么锁存器我们是不推荐使用的。但是说如果你非要使用锁存器,一定要采用非阻塞赋值的方式,因为使用非阻塞赋值实现时序逻辑,进而实现锁存器是最为安全的;
  5. 最后一条就是:我们推荐一个 always 语句块只对一个变量进行赋值,这样方便我们后期的维护和修改

参考资料:

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

第十二讲-阻塞赋值与非阻塞赋值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值