阻塞赋值与非阻塞赋值
过程赋值:用于对reg型变量赋值,改变寄存器的值或为以后排定改变。
语法
{阻塞性(blocking)赋值} RegisterLValue = [ 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
编译结果显示使用了两个寄存器。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
编译结果显示使用了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 <= 2'd0; 8 end 9 else 10 begin //begin…end中为非阻塞赋值 11 if (Data[0] <= Data[Lid_Min]) begin 12 Lid_Min <= 2'd0; 13 end 14 if (Data[1] <= Data[Lid_Min]) begin 15 Lid_Min <= 2'd1; 16 end 17 if (Data[2] <= Data[Lid_Min]) begin 18 Lid_Min <= 2'd2; 19 end 20 if (Data[3] <= Data[Lid_Min]) begin 21 Lid_Min <= 2'd3; 22 end 23 end 24 end 25 26 endmodule
【注】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 = 2'd0; i <= 2'd3; i = i + 2'd1) begin 3 if (Data[i] <= Data[Lid_Min]) begin 4 Lid_Min = i; 5 end 6 end 7 end
需要注意的是,for语句的电路功能比较难理解,其展开形式往往更具可读性。