首页 > 代码库 > 阻塞赋值与非阻塞赋值

阻塞赋值与非阻塞赋值

 

     过程赋值:用于对reg型变量赋值,改变寄存器的值或为以后排定改变。

     语法

{阻塞性(blocking)赋值}

     RegisterLValue = http://www.mamicode.com/[ TimingControl] Expression;

{非阻塞性(non-blocking)赋值}

RegisterLValue <= [ TimingControl] Expression;

     阻塞:在本语句中“右式计算”和“左式更新”完全完成之后,才开始执行下一条语句;

     非阻塞:当前语句的执行不会阻塞下一语句的执行。

     过程赋值右边的表达式在赋值执行的时候算出。如果没有内部赋值延时,左边的寄存器由于阻塞性赋值将立即更新,而非阻塞性赋值则下一个仿真周期才更新左边的寄存器。如果有内部赋值延时,左边的寄存器只在发生内部赋值延时后更新。

     例如对于阻塞性赋值:

  • 当执行赋值语句时,算出右边的表达式的值,但左边的值不更新,直到产生定时控制事件或延时(称为“内部赋值延时”)。
  • 直到左边被更新后(即经过内部赋值延时后)阻塞性赋值才完成。begin-end块中的下一个语句直到此时才开始执行。

    结合编程语句区分如下:

    ? 非阻塞(non-blocking) 赋值语句(b<= a):     

       - 块内的赋值语句同时赋值;    

       - b的值被赋成新值a的操作, 是与块内其他赋值语句同时完成的;    

       - 建议在可综合风格的模块中使用非阻塞赋值。

     ? 阻塞(blocking) 赋值语句(b = a):    

       - 完成该赋值语句后才能做下一句的操作;    

       - b的值立刻被赋成新值a;    

       - 硬件没有对应的电路,因而综合结果未知。

     阻塞赋值和非阻塞赋值如果使用不当会存在冒险和竞争现象,必须按照下面两条准则:

     1)在描述组合逻辑的always块中使用阻塞赋值,则综合组合逻辑的电路结构;

     2)在描述时序逻辑的always块中使用非阻塞赋值,则综合时序逻辑的电路结构。

     在时钟沿触发的always块中,如果用非阻塞赋值语句对reg型变量赋值;或者当reg型变量经过多次循环其值仍保持不变,则会在综合中生成触发器。若不想生成触发器,而希望用reg型变量生成组合逻辑,则应使用电平触发。在组合逻辑中,阻塞赋值只与电平有关,往往和触发沿没有关系,可以将其看成并行执行的;在时序逻辑中,非阻塞赋值是并行执行的;因此,优秀的HDL设计,其内部语句也是并行执行的。

 

     非阻塞赋值与阻塞赋值示例:

     1. 非阻塞赋值方式

 1 module nonblocking(input clk, 
 2                    input a,
 3                    output reg c); 
 4 reg b;
 5 
 6 always @(posedge clk) begin
 7     b <= a;
 8     c <= b;
 9 end
10 
11 endmodule
non-blocking

     编译结果显示使用了两个寄存器。Clock "clk" Internal fmax is restricted to 420.17 MHz between source register "b" and destination register "c~reg0" (the specified clock operates at the specified fMAX between the specified source pin or register and the specified destination pin or register). 器件内部的fmax(Internal fmax)分析器件中同步元件(如寄存器)到同步元件之间的延时,然后计算出最高频率。

     RTL原理图如下所示:

 

     波形功能和时序仿真分别如下所示:

     非阻塞赋值在块结束时才完成赋值操作。c的值比b的值落后一个时钟周期(功能仿真表现为b→c,时序仿真表现为b→c~reg0)。时序仿真中,第一个时钟沿15.743ns后(时钟周期 + tco = 10ns+5.743ns)输出c。

 

     2. 阻塞赋值方式

 1 module blocking(input clk, 
 2                 input a,
 3                 output reg c); 
 4 reg b;
 5 
 6 always @(posedge clk) begin
 7     b = a;  //两句交换,则等效于非阻塞赋值方式
 8     c = b;
 9 end
10 
11 endmodule
blocking

     编译结果显示使用了1个寄存器,所以时序列表中没有Clock Setup: ‘clk‘一项。

     RTL原理图如下所示:

 

     波形功能和时序仿真分别如下所示:

     阻塞赋值在该语句结束时就完成赋值操作。在一个块语句中,如果有多条阻塞赋值语句,在前面的赋值语句没有完成之前,后面的语句就不能被执行,就像被阻塞了一样,因此称为阻塞赋值方式。可以看到,b被优化掉了,因为这里c的值与b的值一样!

     在阻塞赋值语句中,赋值次序非常重要,而在非阻塞赋值语句中,赋值的次序并不重要。

     仿真器首先按照仿真时间对事件进行排序,然后再在当前仿真时间里按照事件的优先级顺序进行排序。活跃事件是优先级最高的事件。在活跃事件之间,它们的执行顺序是随机的。阻塞赋值(=)、连续赋值(assign)以及非阻塞赋值的右式计算等都属于活跃事件。

 

     下面再通过一个典型案例,进一步说明阻塞赋值和非阻塞赋值的区别。

    【例】数组Data[0]、Data[1]、Data[2]和Data[3]都是4bit的数据。找到它们当中最小的数据,并将该数据的索引输出到LidMin中(类似 “冒泡排序”),且在一个时钟周期内完成。首先将Lid_Min设置一个任意初始值,然后将Data[0]~Data[3]与Data[Lid_Min]进行比较,每比较一个数,就将较小的索引暂存在Lid_Min中,然后再进行下一次比较。当4组数据比较完成之后,最小的数据索引就会保留在Lid_Min中。例如,若4个数据中Data[2]最小,则LidMin的值为2。

 1 module Bubble_Up(input Rst_n,
 2                  input Clk,
 3                  input [5:0] Data [0:3],  //需要SystemVerilog extensions(通常端口不可用数组型)
 4                  output reg [1:0] Lid_Min );
 5 always @(posedge Clk or negedge Rst_n) begin
 6         if (~Rst_n) begin
 7                 Lid_Min <= 2d0;
 8             end
 9         else
10             begin    //begin…end中为非阻塞赋值
11                 if (Data[0] <= Data[Lid_Min]) begin
12                         Lid_Min <= 2d0;    
13                     end
14                 if (Data[1] <= Data[Lid_Min]) begin
15                         Lid_Min <= 2d1;
16                     end
17                 if (Data[2] <= Data[Lid_Min]) begin
18                         Lid_Min <= 2d2;
19                     end
20                 if (Data[3] <= Data[Lid_Min]) begin
21                         Lid_Min <= 2d3;
22                     end
23             end
24     end
25 
26 endmodule
Bubble_Up

    【注】SystemVerilog extensions设置:Settings --> Analysis & Synthesis Settings-->Verilog HDL Input --> Verilog Version--> SystemVerilog-2005。

     以上代码中使用了非阻塞赋值,但仿真波形结果并不正确,如下图所示。图中的Data[0]~Data[3]分别为11、3、10和12,Lid_Min的初始值为0。Lid_Min结果应为1,因为Data[1]最小。

     原因如下:

     在时钟上升沿到来后,且Rst_n信号无效时开始执行后续4个语句,假设此时Lid_Min为0,Data[0]~Data[3]分别为11、3、10和12:

     第一句的if为真,因此执行Lid_Min <= 2’d0,而此时Lid_Min并未立刻被赋值,而是调度到事件队列中等待执行,这是非阻塞赋值的特点。

     第二句的if为真,因此执行Lid_Min <= 2’d1,这是Lid_Min也没有立刻被赋值为1,而是调度到事件队列中等待执行。当前的Lid_Min还是0,没有发生任何变化。

     同样,第三句的if也为真,因此执行Lid_Min <= 2’d2,将更新事件调度到事件队列中等待执行。当前的Lid_Min还是0。

     而第四句的if为假,因此直接跳过Lid_Min <= 2’d3,这时跳出always语句,等待下一个时钟上升沿。

     在以上的always语句执行完成以后,仿真时间没有前进。这时存在于事件队列中当前仿真时间上的3个被调度的非阻塞更新事件开始执行,它们分别将Lid_Min更新为0、1和2。

     按照Verilog语言的规范,这3个更新事件属于同一仿真时间内的事件,它们之间的执行顺序随机,这就产生了不确定性。一般的仿真器在实现的时候是根据它们被调度的先后顺序执行的,事件队列就像一个存放事件的FIFO,它是分层事件队列的一部分,如图所示:

     这3个事件在同一仿真时间被一一执行,而真正起作用的时最后一个更新事件,因此在仿真的时候得到的最终结果时Lid_Min为2。

     然而我们想要得到的结果是,在每个if语句判断并执行完成以后,Lid_Min先暂存这个中间值,再进行下一次比较,也就是说在进行下一次比较之前,这个Lid_Min必须被更新,而这点正是阻塞赋值的特点。将代码段”else”内部改为阻塞赋值(<=→=),仿真波形如图所示:

 

     在仿真过程中,”else”段第二句的if为真,执行Lid_Min = 2‘d1,根据阻塞赋值的特点,Lid_Min被立刻赋值为1。执行第三句if时if为假,直接跳过Lid_Min = 2‘d2不执行,同样也跳过Lid_Min = 2‘d3不执行。Lid_Min被最终赋值为1,这正是我们想要的结果。

     仿真采用Cyclone II系列EP2C5F256C6器件。编译结果显示,非阻塞赋值占用39个LE(logic elements),阻塞赋值占用80个LE,两者均占用2个寄存器(registers)。

     为了使代码看起来更简洁,也可使用for语句改写”else”段代码如下:

1 begin
2     for (i = 2d0; i <= 2d3; i = i + 2d1) begin
3             if (Data[i] <= Data[Lid_Min]) begin
4                     Lid_Min = i;
5                 end
6         end
7 end
for loop

需要注意的是,for语句的电路功能比较难理解,其展开形式往往更具可读性。