阻塞赋值语句

串行块语句中的阻塞赋值语句按顺序执行,它不会阻塞其后并行块中语句的执行。阻塞赋值语句使用“=”作为赋值符。

  例子 阻塞赋值语句
  reg x, y, z;
  reg [15:0] reg_a, reg_b;
  integer count;
 
  // 所有行为语句必须放在 initial 或 always 块内部
  initial
  begin
          x = 0; y = 1; z = 1; // 标量赋值
          count = 0; // 整形变量赋值
          reg_a = 16'b0; reg_b = reg_a; // 向量的初始化
 
          #15 reg_a[2] = 1'b1; // 带延迟的位选赋值
          #10 reg_b[15:13] = {x, y, z} // 把拼接操作的结果赋值给向量的部分位(域)
 
          count = count + 1; // 给整形变量赋值(递增)
   end
 

在例子中,只有在语句x=0执行完成后,才会执行y=1,而语句count=count+1按顺序在最后执行。由于阻塞赋值语句是按顺序执行的,因此如果在一个begin-end块中使用了阻塞赋值语句,那么这个块语句表现的是串行行为。例子中,begin-end块中各条语句执行的仿真时间为:

  • x=0到regb = rega之间的语句在仿真0时刻执行;
  • 语句rega[2]=0在仿真时刻15执行; * 语句regb[15:13]={x,y,z}在仿真时刻25执行;
  • 语句count=count+1在仿真时刻25执行;
  • 由于前面的语句分别包含了15和10个时间单位的延迟,因此语句count=count+1将在第25个单位时刻执行。

注意,在对寄存器类型变量进行过程赋值时,如果赋值符两侧的位宽不相等,则采用以下原则:

  1. 如果右侧表达式的位宽较宽,则将保留从最低位开始的右侧值,把超过左侧位宽的高位丢弃;
  2. 如果左侧位宽大于右侧位宽,则不足的高位补0;

非阻塞赋值语句

非阻塞赋值语句允许赋值调度,但它不会阻塞位于同一个顺序块中其后语句的执行。非阻塞赋值使用“⇐”作为赋值符。读者会注意到,它与“小于等于”关系操作符是同一个符号,但在表达式中它被解释为关系操作符,而在非阻塞赋值的环境下被解释成非阻塞赋值。为了说明非阻塞赋值的意义以及阻塞赋值的区别,让我们来考虑将阻塞赋值例子中的部分阻塞赋值改为非阻塞赋值后的结果,修改后语句如下:

  例 非阻塞赋值语句
  reg x, y, z;
  reg [15:0] reg_a, reg_b;
  integer count;
 
  // 所有的行为语句必须写在initial 和 always块内
  initial
  begin 
          x = 0; y = 1; z = 1; // 标量赋值;
          count = 0; // 整形变量赋值
          reg_a = 16'b0; reg_b = reg_a; // 向量的初始化
 
          reg_a[2] <= #15  1'b1; // 带延迟的位选赋值
          reg_b[15:13] <= #10 {x, y, z} // 把拼接操作的结果赋值给向量的部分位(域)
 
          count <= count + 1; // 给整形变量赋值(递增)
   end
 

在这个例子中,从x=0到regb=rega之间的语句是在仿真0时刻顺序执行的,之后的三条非阻塞赋值语句在regb=rega执行完成后并发执行。

  • rega[2]=0被调度到15个时间单位之后执行,即仿真时刻为15; * regb[15:13]={x,y,z}被调度到10个时间单位之后执行,即仿真时刻为10;
  • count=count+1被调度到无任何延迟执行,即仿真时刻为0。

从上面的分析中可以看到,仿真器将非阻塞赋值调度到相应的仿真时刻,然后继续执行后面的语句,而不是停下来等待赋值的完成。一般情况下,非阻塞赋值是在当前仿真时刻的最后一个时间步,即阻塞赋值完成之后才执行的。
在上面的例子中,我们把阻塞和非阻塞赋值语句混合在一起使用,目的是想更清楚地比较和说明它们的行为。需要提醒大家注意的是,不要在同一个always块中混合使用阻塞和非阻塞赋值语句。


非阻塞赋值语句的应用

描述了非阻塞赋值的行为之后,理解究竟为什么要在硬件设计中使用非阻塞赋值是很重要的。非阻塞赋值可以被用来为常见的硬件电路行为建立模型,例如当某一事件发生后,多个数据并发传输的行为。在下面的例子中,当时钟信号的上升沿到来之后,执行三个数据的并发传输:

  always @(posedge clock)
  begin 
     reg1 <= #1 in1;
     reg2 <= @(negedge clock) in2 ^ in3;
     reg3 <= #1 reg1; // reg1 的“旧值”
  end
 

每当一个时钟上升沿到来时,其中的非阻塞赋值语句按下面的顺序执行:

  1. 在每个时钟上升沿到来时读取操作数变量in1, in2, in3和reg1,计算右侧表达式的值,该值由仿真器临时保存;
  2. 对左值的赋值由仿真器调度到相应的仿真时刻,延迟时间由语句中内嵌的延迟值确定。在本例中,对reg1的赋值需要等一个时间单位,对reg2的赋值需要等到时钟信号下降沿到来的时刻,对reg3的赋值需要等待一个时间单位;
  3. 每个赋值操作在被调度的仿真时刻完成。注意,对左侧变量的赋值使用的是由仿真器保存的表达式“旧值”,因此赋值完成的实际顺序并不重要。在本例中,对reg3赋值使用的是reg1 的“旧值”,而不是在此之前对reg1赋予的新值,reg1的“旧值”是在赋值事件调度时由仿真器保存的。

由上面的分析可见,reg1,reg2和reg3的最终值与赋值完成的顺序无关,体现了非阻塞赋值并行的特点。
为了进一步理解阻塞和非阻塞赋值,让我们来看以下例子。这个例子的目的是在每个时钟上升延处交换a和b这两个寄存器变量的值,其中使用了两个always语句。

  例 使用非阻塞赋值来避免竞争
  // 说明1:使用阻塞语句的两个并行的always块
  always @(posedge clock)
             a = b;
 
  always @(posedge clock)
             b = a;
 
  // 说明2:使用非阻塞语句的两个并行的always块
  always @(posedge clock)
             a <= b;
 
  always @(posedge clock)
             b <= a;
 

在第一种描述中我们使用了阻塞赋值,这样就产生了竞争的情况:a=b和b=a,具体执行顺序的先后取决于所使用的仿真器。因此这段代码达不到交换a和b值的目的,而是使得两者具有相同的值(在时钟上升沿到来之前a或b的值),具体是哪一个值与使用的仿真器有关。
在第二种描述中,我们通过使用非阻塞赋值语句来避免竞争:在每个时钟上升沿到来的时候,仿真器读取每个操作数的值,进而计算表达式的值并保存在临时变量中;当赋值的时候,仿真器将保存的值赋予非阻塞赋值语句的左侧变量。这样就将读和写分开来了,达到了交换数据的目的,并且不受语句执行顺序的影响。以下例子介绍了如何使用阻塞赋值实现说明2中使用非阻塞语句才能实现的数值交换。

  例 使用阻塞赋值来达到非阻塞赋值的目的
  // 使用临时变量和阻塞赋值来模仿非阻塞赋值的行为
  always @(posedge clock)
  begin
      // 读操作
      // 把右侧表达式的值放在临时变量中
      temp_a = a;
      temp_b = b;
      // 写操作
      // 把临时变量的值放到左侧变量中
      a = temp_b;
      b = temp_a;
   end
 

在数字电路设计中,如果某事件发生后将产生多个数据的并发传输,我们强烈建议读者使用非阻塞赋值来描述这种情形。如果我们使用阻塞赋值来描述这种情形,由于最终结果依赖于语句的具体执行顺序,有可能引起竞争风险;而非阻塞赋值语句的执行结果是与执行顺序无关的,因此它能够准确地描述这种情况。非阻塞赋值的典型应用包括流水线建模和多个互斥(mutually exclusive)数据传输的建模。使用非阻塞赋值所带来的问题是,它会引起仿真速度的下降以及内存使用量的增加。