Menu Close

Verilog 中阻塞与非阻塞赋值语句的区别

在Verilog HDL语法中有阻塞赋值语句与非阻塞赋值语句之分,这两种赋值语句在Verilog语言程序设计中应用都比较广泛。这两者在用法上既有类似之处也有区别。下面就这两种赋值语句的定义,区别与联系以及使用中应注意的事项进行详细探讨,同时也注意在探讨过程中对已介绍知识点的回顾。

1.定义描述

  • 阻塞赋值语句:

阻塞赋值语句用”=” 表示,按照字面理解可以认为该语句在一个过程中阻止Verilog后续语句的执行,直到该语句赋值完成。通俗的讲就是该赋值语句在顺序过程中在满足延时条件后立即生效。

例:

reg [3:0] a , b ,c;

initial begin
    a = 0;
    b = 0;
    c = 0;
    #10

    a = 1;
    b = a;
    c = a + b;
    a = 2;
    c = c + a;
    #10;

end

 

下面我们来分析这个程序的执行结果:根据阻塞赋值语句的描述,变量赋值立刻生效;同时根据顺序语句的特点,程序执行具有顺序性,即语句的执行遵循从前到后,从上到下的特点,每条语句执行后的结果如下,

a = 1;     //a==1
b = a;     //b==1
c = a + b; //c==2
a = 2;     //a==2
c = c + a; //c==4; 因为在运算前c=2,a=2, 因此 c=2+2=4

可以看出c有两次赋值,结果也不同,分别为2和4, 那么最终结果为多少?我们知道在顺序赋值中,如果多次对同一个变量赋值,该变量的最终结果一定是最后一次赋值生效,即4是c的最终值。图1是上面语句的仿真结果,从图形中可以看出,中间结果c==2,在图形中并没显示出来,因为从时间的角度看,它们处在同一个时间点,只有最后的结果4,在图形中显示。但中间结果确实参与了运算。

%title插图%num

图1

  • 观察中间过程

如果想观察变量的中间结果可以使用$display函数显示, 可以修改程序如下:

`timescale 1 ns  /1 ps
 
module tb 
(
 
);
 
 
reg   [3:0]   a , b ,c;
 
initial begin
    a = 0;
    b = 0;
    c = 0;
    #10

    a = 1;
    b = a;
    c = a + b;
    $display(" c=  %d " ,c);
    a = 2;
    c = c + a;
    $display(" c= %d " ,c);
    #10;

end


endmodule

 

显示波形与图1一致,但在Modelsim的transcript窗口中可以清晰看到c的变化过程。如图2,

%title插图%num

图2

  • 分析顺序语句的最终结果

从上面的例子中已经可以看出,变量c的最终结果为4,修改程序如下,从$display和波形图对比可以更好的理解顺序语句的执行结果。

`timescale 1 ns  /1 ps


module tb 
(

);


reg   [3:0]   a , b ,c;

initial begin
    a = 0;
    b = 0;
    c = 0;
    #10

    a = 1;
    b = a;
    c = a + b;
    $display(" c=  %d " ,c);
    a = 2;
    c = c + a;
    $display(" c= %d " ,c);
    c = a + b;    //a==2, b==1; 因此 c==3
    $display(" c= %d " ,c);
    #10;

end


endmodule

 

通过已学习过的知识,可以分析出c的最终结果应该为3。 波形图和和$display显示如图3和图4,从图中可以看出仿真结果与分析结果一致。

%title插图%num

图3

%title插图%num

图4

  • 非阻塞赋值语句

顾名思义,非阻塞赋值语句就是在赋值时并不阻塞在整个顺序过程中其它语句的评估与执行,这一点与阻塞赋值语句不同,在执行结果上也有很大的差别。非阻塞赋值语句用”<=”表示。将上面的例子进行修改如下:

`timescale 1 ns /1 ps

module tb_non_block
(

);

reg [3:0] a , b ,c;

initial begin
    a <= 0;
    b <= 0;
    c <= 0;
    #10

    a <= 1;
    b <= a;
    c <= a + b;
    $display(" c= %d " ,c);
    a <= 2;
    c <= c + a;
    $display(" c= %d " ,c);
    c <= a + b;
    b <= 6;
    $display(" c= %d " ,c);
    #10;

end

endmodule

 

先尝试分析上面的程序,看看a,b,c三个变量的最终结果。

a <= 1;     //a==1吗?不一定,由于非阻塞,还要看后面赋值语句是否对a进行修改
b <= a;     //b==1吗? 不一定,由于非阻塞,还要看后面赋值语句是否对b进行修改
c <= a + b; //c==2吗?不是,由于非阻塞,a,b的值需要存在delta延迟,在整个initial过程中,当前a,b的值都应该为0

$display(" c= %d " ,c); //该如何显示?c=0;

a <= 2;                 //a==2吗?对,后面没有对a修改的赋值语句,但要delta延迟后才会生效,
c <= c + a;             //c等于多少?c==0;
$display(" c= %d " ,c); //该如何显示? c=0

c <= a + b;             //c==3吗? 不是,无论a,b的值如何修改,a,b的当前值都为0;
b <= 6;                 //b==6吗? 对,这是对b的最后一次修改,但需要delta延迟之后才会生效。
$display(" c= %d " ,c); //应该显示 c==8吗?似乎正确,但c的值实际为零;

#10;

 

通过分析,最终a==2,b==6, 那么c==8应该是正确的,然而实际结果c==0; 这个结果出乎意料,让人大跌眼镜。下面看看仿真结果。如图5和图6所示。从仿真结果看,a,b的变量值的确与我们分析的一致,但c的值不是8而是0,原因出在哪里?

原因是所有这些赋值语句都是在同一时间点对a, b ,c赋值,在评估所有的赋值语句结束时,a和b最终获得了2和6,然而c在这个时间点上只抓取到a==0,b==0的值进行计算,因此c=0; 如果后续时间点没有给c重新赋值,c的值将维持不变。

%title插图%num

图5

%title插图%num

图6

修改一下程序可以验证上面的结果。延时10单位重新显示c的值,看看结果。代码与仿真结果如下:

`timescale 1 ns /1 ps

module tb_non_block
(

);

reg [3:0] a , b ,c;

initial begin
    a <= 0;
    b <= 0;
    c <= 0;
    #10

    a <= 1;
    b <= a;
    c <= a + b;
    $display(" c= %d " ,c);
    a <= 2;
    c <= c + a;
    $display(" c= %d " ,c);
    c <= a + b;
    b <= 6;
    $display(" c= %d " ,c);
    #10;

    $display(" c= %d " ,c);
    #10;

end

endmodule

 

%title插图%num

图7

%title插图%num

图8

从图7,8的显示结果可以看出,C一旦得到了值0,在后续的顺序语句中没有给c继续赋值的话,c==0将保持不变。

继续修改程序,将c重新赋值,观察仿真结果。

`timescale 1 ns  /1 ps

module tb_non_block 
(

);


reg   [3:0]   a , b ,c;

initial begin
    a <= 0;
    b <= 0;
    c <= 0;
    #10

    a <= 1;
    b <= a;
    c <= a + b;
    $display(" c= %d " ,c);
    a <= 2;
    c <= c + a;
    $display(" c= %d " ,c);
    c <= a + b;
    b <= 6;
    $display(" c= %d " ,c);
    #10;

    $display(" c= %d " ,c);
    #10;

    c <= a + b;
    $display(" c= %d " ,c);
    #10;

end


endmodule

 

仿真结果如下:

%title插图%num

图 9

%title插图%num

图10

从图9可以看出,c的值的确在延迟一段时间后变成8了,但紧随其后的$display为什么还显示0呢?这就引入了另一个概念,赋值语句delta延迟的概念。

 

  • Delta延迟:

%title插图%num

Delta延迟的概念解释如下,非阻塞赋值语句的左边要得到右边表达式的值,一般会有一个delta延迟,这个delta是多大呢?从数学的角度讲,delta可以认为无穷小;如下面的语句,赋值语句“c<=a+b;”和显示$display都在同一时刻评估,赋值语句要等delta时间后才会生效,$display是抓取当前值进行显示,而c的当前值由于并没有更新(delta之后才更新),依然是0,因此$display显示c的值为0就不奇怪了。图9在波形上能够显示c==8,那是因为人的眼睛无法分辨delta延迟的原因造成的。

 

#10;
c <= a + b; //c延迟delta后为8
$display(" c= %d " ,c);//c的当前值依然为零,
#10;

继续修改程序看看$display的结果。修改后的程序如下:

`timescale 1 ns /1 ps

module tb_non_block ( );

reg [3:0] a , b ,c;

initial begin
    a <= 0;
    b <= 0;
    c <= 0;
    #10

    a <= 1;
    b <= a;
    c <= a + b;
    $display(” c= %d ” ,c);
    a <= 2;
    c <= c + a;
    $display(" c= %d " ,c);
    c <= a + b;
    b <= 6;
    $display(" c= %d " ,c);
    #10;

    $display(" c= %d " ,c);
    #10

    c <= a + b;
    $display(" c= %d " ,c);
    #1

    $display(" c= %d " ,c);
    #10;

end 
endmodule

 

仿真结果如下:

%title插图%num

图11

%title插图%num

图12

从修改的程序程序上看,仅仅延迟一个单位的刻度,$display函数就能正确显示c的新值8,可见delta延迟的概念的确存在。

 

  • 时间点:

从图11,12的仿真结果可以看出,延迟语可以将整个initial过程划分为若干时间点,非阻塞赋值语句的赋值结果是在该时间点上评估的,如

a<=0; 
b<=0; 
c<=0;
#10

a<=1;
b<=a; 
c<=a+b;
#10;

在0时刻,a, b, c都赋值为零,但由于非阻塞赋值语句使得a, b ,c的结果却是在离开该时间点后才得到。因此考察#10时间点时,a, b ,c值的确都已经被赋值为零了。同样在#10时间点,虽然a, b ,赋了新值,但在该时间点不会立即生效,因此c的结果是a,b上一个时间点的值之和,因此c的值在该时间点为零,即使到了下一个时间点也仍然为0. 在学习和使用非阻塞赋值语句时要应注意理解三个概念:(1)同一时间点(2)语句的顺序性(3)delta延迟。

 

2.always过程中如何体现delta过程的。

学过以上概念学习后,针对非阻塞赋值语句,我们对always过程再探讨,看看有没有新的认识。修改程序如下:

`timescale 1 ns /1 ps

module tb_non_block_always
( );

reg [3:0] a , b ,c;

initial begin
    a <= 0;
    b <= 0;
    c <= 0;
end

always@(*) begin
    a <= 2;
    c <= a + b;
    b <= 6;
    $display(" c= %d " ,c);
end

endmodule

 

首先分析一下程序,我们知道always过程由敏感量组成的敏感表作为过程的入口,当任意一个敏感量发生变化时,always过程执行一遍。那么程序第一次如何进入的呢?

首先当程序第一次运行时a,b,c的值都默认为’xxxx’,当执行initial过程后,变量a,b,c初始化为0。变量由x到0的变化,引起always过程的第一次进入。

运行完成后a==2,b==6,c==0,引起第二次进入;第二次进入后,a==2,b==6,c==8,此时$display函数显示c==0, 由于c的变化引起第三次进入,变量a,b,c的结果仍然是a==2,b==6,c==8,但此时$display会打印出c=8。之后a,b,c的值保持不变,always过程也就不再进入。仿真结果如下:

%title插图%num

图13

如果如下改成多次赋值的程序:

`timescale 1 ns /1 ps
module tb
(
);
reg [3:0] a , b ,c;
initial begin
    a <= 0;
    b <= 0;
    c <= 0;
end
always@(*) begin
    #10
    a <= 1;
    b <= a;
    c <= a + b;
    $display(" c= %d " ,c);
    a <= 2;
    c <= c + a;
    $display(" c= %d " ,c);
    c <= a + b;
    b <= 6;
    $display(" c= %d " ,c);
end
endmodule

 

同样三次之后的结果为a==2,b==6, c==8, 但由于变量有中间转换过程,而且变量的结果在退出always过程后才会得到,因此程序会循环进入,这一点在使用时应引起注意。如果进行仿真可以看到,$display 函数不断打印出”c=8″的输出结果。因此在always@(*)过程中使用非阻塞赋值语句时,如果该过程只有一个时间点或多个时间点的最后一个时间点,注意一定要不能使用迭代语句,如“c <= c + a;”,否则会引起电路不可综合或产生竞争与冒险的行为。分析下面的程序,看与上面程序比较,结果有何异同?

(1)

initial begin
    a <= 0;
    b <= 0;
    c <= 0;
end

always@(*) begin
    #10
    a <= 1;
    b <= a;
    c <= a + b;
    $display(" c= %d " ,c);
    a <= 2;
    c <= c + a;
    $display(" c= %d " ,c);
    c <= a + b;
    b <= 6;
    $display(" c= %d " ,c);
    #10;
end



endmodule

 

(2)

initial begin
    a <= 0;
    b <= 0;
    c <= 0;
end

always@(a ,b) begin
    #10
    a <= 1;
    b <= a;
    c <= a + b;
    $display(" c= %d " ,c);
    a <= 2;
    c <= c + a;
    $display(" c= %d " ,c);
    c <= a + b;
    b <= 6;
    $display(" c= %d " ,c);
end

endmodule

 

如果将上面的程序该为阻塞赋值语句可以看出重大差别。程序如下:

`timescale 1 ns  /1 ps

module tb_non_block_always 
(

);

reg   [3:0]   a,b,c;

initial begin
    a <= 0;
    b <= 0;
    c <= 0;
end 	

always@(*)  begin
    #10	
    a = 1;
    b = a;
    c = a + b;
    $display(" c= %d " ,c);
    a = 2;
    c = c + a;
    $display(" c= %d " ,c);
    c = a + b;
    b = 6;
    $display(" c= %d " ,c);	
    #10; //#10 是为了在波形上显示最后结果,注释该语句看运行结果是否有差别?
end

endmodule

 

仿真结果如下:

%title插图%num

图14

%title插图%num

图15

从图15可以看出,阻塞赋值语句可以立即得到结果,并能用$display函数显示计算后的结果。而且在退出always过程前得到所有值,因此always过程只进入一次,没有像非阻塞赋值语句那样循环进入的机会。

3.总结:

(1)非阻塞赋值语句有delta延时,被赋值变量在顺序过程退出后才获得赋值结果,而阻塞赋值语句没有delta延迟,赋值结果立马生效。

(2)由于delta延时,因此对于always过程会多次进入,对于同一变量多次赋值会引起always语句的循环进入;而阻塞赋值语句由于变量赋值立马生效,如果没有外部引起变量变化,always过程不会多次进入。

(3)在同一个时间点上评估,非阻塞赋值语句由于有delta延迟,此时如果引用变量的值,则是上次进入顺序过程的值。而阻塞赋值语句紧跟其后的引用可以立即使用已被更新的值。

(4)对同一个变量赋值的时候,不允许阻塞和非阻塞赋值同时使用。

(5)注意在时钟边沿过程中使用差别,在后续课程中会详细介绍。

练习题:

1.假设 三个reg型变量a, b, c的当前值为:1,2,3;试分析下面$display的输出。

(1) 非阻塞赋值

reg  [3:0]   a,b,c;

initial begin
    a = 1;
    b = 2;
    c = 3;
    $display("a=%d, b=%d, c=%d",a,b,c);
end

always@(*)
begin
    a <= 5;
    b <= a;
    c <= b;
    $display("a=%d, b=%d, c=%d",a,b,c);
end

 

(2)阻塞赋值

reg  [3:0]   a,b,c;

initial begin
    a = 1;
    b = 2;
    c = 3;
    $display("a=%d, b=%d, c=%d",a,b,c);
end

always@(*)
begin
    a = 5;
    b = a;
    c = b;
    $display("a=%d, b=%d, c=%d", a, b,c);
end

 

2. 试分析下面程序的运行结果

`timescale 1 ns /1 ps

module tb_non_block_always
(

);

reg [3:0] a , b ,c;

initial begin
    a = 0;
    b = 0;
    c = 0;
end

always@(a ,b) begin
    #10
    a <= 1;
    b <= 2;
    c <= a + b;
    $display("time=%t a=%d b=%d c= %d " ,$time,a,b,c);
    c <= c + a;
    $display("time=%t a=%d b=%d c= %d " ,$time,a,b,c);
    c <= c + b;
    $display("time=%t a=%d b=%d c= %d " ,$time,a,b,c);
end

endmodule

 

欢迎阅读及评价。

Posted in FPGA, FPGA, Quartus II, Verilog, 开发工具, 教材与教案, 文章, 编程语言

11 Comments

  1. wanglong30

    我想在使用modelsim仿真时输出dispaly 函数输出 结果 ,可是运行报错

    Cannot execute undefined system task/function ‘$dispaly’ 不知道要怎么解决

  2. 柳俊俊

    通过这篇文章,进一步认识阻塞赋值与非阻塞赋值区别,特别是由与非阻塞赋值存在延迟引起always块敏感量变化,导致循环进入。
    非常感谢老师的分享,还存在一些疑虑,需要老师帮忙解答一下,如下:
    1、从实际硬件电路上考虑,给寄存器赋值,不管是阻塞还是非阻塞都应该存在延迟;在阻塞赋值后面用$display函数观察,确实可以观察到变量立即改变,我认为这个不足以说明没有延迟。因为阻塞赋值成功后才执行display,当然可以观察变量立即改变。
    2、可不可以认为阻塞赋值延时比非阻塞赋值延时小得多。
    3、延时时间长短与器件有关吧,延时时间长短有方法衡量吗?

    • tzhuang

      这个是两个概念,完全不是一回事。其实在always过程中delta延时只是一个数学概念,就像数学的阶跃函数。这里delta仅仅表示赋值语句是在always过程退出之前还是退出之后,也可以认为是无穷小的概念。真正器件延时是指组合逻辑电路输入端到组合逻辑电路稳定的时间加上布线延迟时间。时序上看一般分为setup time和hold time。setup time 一般指在上升沿来临之前组合逻辑已经稳定的时间,hold time 是指上升沿之后还需要保持的的时间。后续会有时序约束与时序收敛的内容,敬请关注。

    • tzhuang

      问题:可不可以认为阻塞赋值延时比非阻塞赋值延时小得多?
      这句话是不对的,延时是组合逻辑和布线延迟造成的,与阻塞、非阻塞赋值语句无关。阻塞与非阻塞是verilog中建模的两种模型。

  3. wangff

    本篇文章通过modelsim仿真和$display 函数打印的方式,详细讲解了阻塞赋值与非阻塞赋值在赋值过程中的差别。通过在initial 语句模块与always模块中反复尝试对非阻塞赋值(<=)的赋值过程做对比分析,得出“delta延时”现象的真实存在性,深刻理解了非阻塞赋值(<=)的全过程。

  4. 张洪泉

    这算是看到解释阻塞和非阻塞赋值区别的最给力的文章了,谢谢分享,不过在文章下面的关于always里多次非阻塞赋值会因此循环进入过程不是很明白,请老师或者大佬们指教。。下面不明白的地方:
    “同样三次之后的结果为a==2,b==6, c==8, 但由于变量有中间转换过程,而且变量的结果在退出always过程后才会得到,因此程序会循环进入,这一点在使用时应引起注意。如果进行仿真可以看到,$display 函数不断打印出”c=0″的输出结果”

    • tzhuang

      我们可以这样理解多次进入的问题:在always过程中,阻塞赋值的结果是在always过程结束时获得所有变量的值,退出后所有的值稳定且不再变化(除非有些变量由外部引起),因此由always内部引起的变化不会再触发always过程的进入。而非阻塞赋值语句的变量更新是在always过程退出后更新(退出时刻,我们会经常以阶跃函数为例做比喻),但在always过程中局部变化和最终的结果不同,造成了always再次进入,甚至循环进入,如本文中的例子。这是从仿真角度观察到的结果,如果用FPGA根据综合后的结果,由于最终输出结果一致,硬件上看不到该变化。

    • tzhuang

      为了仿真与综合的一致性,我们会推荐使用规则如下:
      (1) 一般组合电路设计采用阻塞赋值语句
      (2)时序电路设计采用非阻塞赋值语句
      当然深刻理解后,充分发挥各自特点可以写出非常巧妙的程序。

发表评论

相关链接