安德鲁

[文档].艾米电子 - 浅析阻塞赋值与非阻塞赋值.[Verilog]

0
阅读(2992)

说明

翻译自:FPGA Prototyping By Verilog Examples: Xilinx Spartan-3 Version的第7章第一节

 

内容

阻塞赋值VS非阻塞赋值

有两种赋值语句被用在always块内:阻塞赋值与非阻塞赋值。关于阻塞与非阻塞复制有3条简单的准则:

  • 将电路分为两部分:寄存器电路和组合电路
  • 在寄存器电路中使用非阻塞赋值
  • 在组合电路中使用阻塞赋值

 

1 概览

阻塞赋值 基本语法如下:

[var] = [expression];

当该条语句被执行时,右手边的表达式将被赋给左手边的变量,期间不允许其他语句的干扰。因此,就阻塞了其他语句,直到该条语句执行完毕为止。阻塞赋值的行为与C语言中的变量赋值类似。

 

非阻塞赋值 基本语法如下:

[var] <= [expression];

非阻塞赋值的行为非常令人难以琢磨。当always块被激活(在time step的开始),右手边的表达式被赋初值。当运行到always块的结尾(即time step的结尾),运算所得的值被赋给左手边的变量。

以x变量执行非阻塞赋值为例。因为Verilog模型的实际流程比较复杂,我们将非阻塞赋值的行为翻译成一下几个步骤:

  • 在alway块的开始,x值传递给x_entry;
  • 右手边的变量x的值被x_entry取代;
  • 左手边变量x的值被x_exit取代;
  • 在always块的结束,x_exit的值传递给x。

 

在下面的代码片段内,上述四个步骤被呈现在代码的注释中。

always@*
begin               // x_entry = x
    y <= x & ...      // y       = x_entry & ...
    x <= ...          // x_exit  = ...
end                 // x       = x_exit

范例 为了了解阻塞赋值和非阻塞赋值的区别,我们用三输入的的电路来做讨论。

代码1 使用阻塞赋值的电路

module and_blocking
	(
	  input      a,
	  input      b,
	  input      c,
	  output reg y
	); 
	 
	always@*
	begin
	  y = a;
	  y = y & b;
	  y = y & c;
	end
	 
	endmodule

阻塞赋值的欣慰类似于C语言中的顺序赋值。y最终得到的值为a & b & c。注意,此代码仅用于示范,使用顺序语义学来描述电路是比较差劲的行为。

 

下面给出的代码,其中的阻塞赋值被替换为非阻塞赋值。代码注释详细说明了y的赋值动作。

代码2 使用非阻塞赋值的电路

module and_nonblocking
	(
	  input      a,
	  input      b,
	  input      c,
	  output reg y
	); 
	 
	always@*
	begin               // y_entry = y
	  y <= a;           // y_exit  = a
	  y <= y & b;       // y_exit  = y_entry & b
	  y <= y & c;       // y_exit  = y_entry & c
	end                 // y       = y_exit
 
	endmodule

注意always块内的前2条语句将不会产生任何效果。上述always块等价与:

always@*       
	  y <= y & c;

2 组合电路

上一个小节的范例属于极端的情况。除了缺省值,大部分的组合电路并不会多次赋值同一变量。阻塞赋值和非阻塞赋值都可以用于描述同一电路。然而,它们有一些微妙的区别。下面的范例用于解释这些不同。让我们以一位同或(异或非)电路为例。我们将详细列出敏感列表中的变量。

代码3 使用阻塞赋值的一位同或电路

module eq1_blocking
	(
	  input      i0,
	  input      i1,
	  output reg eq
	);
	 
	reg p0, p1;
	 
	always@(i1, i2)     // 只有i0和i1在敏感列表
	begin               // 语句的顺序非常重要
	  p0 = ~i0 & i1;
	  p1 = i0 & i1;
	  eq = p0 | p1;
	end
	 
	endmodule

注意到敏感列表仅包括i0和i1,。当其中之一变化时,always块被激活,p0、p1和ep被顺序运算,ep在第一个time step的结尾被更新。语句的顺序非常重要。假设性我们移动最后面的语句到最前面。

always@(i1, i2)    
	begin              
	  eq = p0 | p1;
	  p0 = ~i0 & i1;
	  p1 = i0 & i1;
	end

在第一条语句中,由于p0和p1还没有被指定新的值,因此先前被激活的值将会被用到。而先前的值将意味着锁存器的存在,故此代码是不正确的。

 

下面将使用非阻塞赋值替换阻塞赋值。

代码4 使用非阻塞赋值的一位同或电路

module eq1_nonblocking
	(
	  input      i0,
	  input      i1,
	  output reg eq
	);
	 
	reg p0, p1;
	 
	always@(i1, i2, p0, p1)       // p0、p1也在敏感列表中
	                              // 语句的顺序不重要     
	begin                         // p0_entry = p0; p1_entry = p1            
	  p0 <= ~i0 & i1;             // p0_exit = ~i0 & ~i1
	  p1 <= i0 & i1;              // p1_exit = i0 & i1
	  eq <= p0 | p1;              // eq_exit = p0_entry | p1_entry
	end                           // eq = eq_exit; p0 = p0_exit; p1 = p1_exit                         
	 
	endmodule

注意p0和p1也包括在敏感列表中。当i0或i1变化时,always块被激活;在第一个time step的结尾,p0和p1被赋以新值。既然ep取决于p0和p1(p0_entry和p1_entry)的旧值,那么其值在第一个time step的结尾保持不变。当当前的time step执行完毕,always块重新被激活,因为p0和p1发生了变化(这便于为何p0和p1也要置于敏感列表之中的原因)。注意语句的顺序不影响结 果。

 

3 存储单元

使用非阻塞赋值来引用存储器。例如,D触发器:

always@(posede clk)
	  q <= d;

当然也可以用阻塞赋值来引用D触发器,如下:

always@(posede clk)
	  q = d;

虽然在单个D-FF情况下面,上面的代码工作正常,但是当多个寄存器互相动作的时候,这里就出现许多微妙的问题。

考虑两个寄存器在每个时钟周期交换数据。使用阻塞赋值,代码为:

always@(posede clk)
	  a = b;
	   
	always@(posede clk)
	  b = a;

在clk的上升沿,两个always块都被激活,并行操作。这两个操作应该在同一time step结束。根据Verilog的标准,两个always块的执行可以以任何顺序列入。若第一个always块先执行,则由于阻塞赋值的缘故a立即得到 b的值。当第二个always块执行的时候,b得到a的刷新值,及b的原始值,因此b保持不变。类似的,若第二个always块先执行,a得到的也是其初 始值。这就是Verilog中的竞态条件(race condition)。从Verilog的角度看,两种结果都是有效的。

下面我们修改代码中的阻塞赋值为非阻塞赋值。

always@(posede clk)
	begin       // b_entry = b
	  a = b;    // a_exit  = b_entry
	end         // a       = a_exit
	   
	always@(posede clk)
	begin       // a_entry = a
	  b = a;    // b_exit  = a_entry
	end         // b       = b_exit

通过注释我们看到,因为输入(entry)值都被用于赋值,所以无论执行的顺序,a和b都得到正确的值。

因此为了避免竞态条件,我们使用非阻塞赋值来引用D-FF和触发器。

 

4 混用阻塞和非阻塞赋值的时序电路

在同一个always块内,有可能混用阻塞赋值和非阻塞赋值。下面我们使用简单的例程来解释不同组合的行为,以加强对赋值的理解。


图4.1 通过混合赋值来引用电路

 

考虑图4.1(b)所示的原理图。当时钟上升沿来临之时,a、b与运算所得的值被存入D-FF。基于前面的讲解,我们可以将存储和组合电路分配到两段代码中。如代码4.1所示。

代码4.1 两段实现

module ab_dff_2seg
	(
	  input clk,
	  input a,
	  input b,
	  output reg q
	);
	 
	reg q_next;
	 
	// D-FF
	always@(posedge clk)
	  q <= q_next;
	 
	// 组合电路
       always@*
	  q_next = a & b;
	 
	endmodule

我们可以变换一下,将两段组合在一起,使用单个always块来描述电路。下面通过六次尝试,来描述阻塞和非阻塞赋值的不同组合的区别。如代码4.2所示。

代码4.2 混合赋值例程

module ab_dff_mix
	(
	  input clk,
	  input a,
	  input b,
	  output reg q0,
	  output reg q1,
	  output reg q2,
	  output reg q3,
	  output reg q4,
	  output reg q5
	);
	 
	reg ab0, ab1, ab2, ab3, ab4, ab5;
	 
	// 尝试0
	always@(posedge clk)
	begin
	  ab0 = a & b;
	  q0 <= ab0;
	end
	 
	// 尝试1
	always@(posedge clk)
	begin                   // ab1_entry = ab1; q1_entry = q1
	  ab1 <= a & b;         // ab1_exit = a & b
	  q1  <= ab1;           // q1_exit = ab1_entry
	end                     // ab1 = ab1_exit; q1 = q1_exit
	 
	// 尝试2
	always@(posedge clk)
	begin
	  ab2 = a & b;
	  q2  = ab2;
	end
	 
	// 尝试3(调换尝试1的顺序)
        always@(posedge clk)
	begin
	  q0 <= ab0;
	  ab0 = a & b;
	end
	 
	// 尝试4(调换尝试2的顺序)
	always@(posedge clk)
	begin                   // ab4_entry = ab4; q4_entry = q4
	  q4  <= ab4;           // q4_exit = ab4_entry
	  ab4 <= a & b;         // ab4_exit = a&b
	end                     // ab4 = ab4_exit; q4 = q4_exite
	 
	// 尝试5(调换尝试3的顺序)
	always@(posedge clk)
	begin
	  q5  = ab5;
	  ab5 = a & b;
	end
	 
	endmodule

在尝试0中,起初赋值给ab0和q0将引用两个寄存器,一个用于存储寄存器ab0,另一个用于存储寄存器q0。因为ab0在块赋值时被立即更新,所 以q0得到了a&b的值。对应的原理图如图4.1(a)所示。由于ab0在always块外没有被使用,因此寄存器ab0的输出就不是必需存在 的,即相应的寄存器可以被移除。这样,结果电路就如图4.1(b)所示,也就是所需的电路。
在尝试1中,对ab1使用了非阻塞赋值,对应的阐述写到了注释里面。注意q1得到的是ab1_entry而非ab1_exit。而 ab1_entry是先前存储的ab值,即对应一个寄存器的输出。相应的原理图如图4.1(c)所示。一个不确定的输入缓存被引用,同时a&b的 值延迟一个时钟周期后才被存储到q1中。

在尝试2中,ab2和q2都是用了阻塞赋值。该代码所引用的电路,与尝试1等同,如图4.1(a)和(b)所示。由于使用阻塞赋值来引用D-FF,有可能产生竞态条件,因此不推荐使用这种类型的代码。

出于演示的目的,让我们来测试一下调换尝试0、1和2的赋值顺序会发生什么。其结果代码如尝试4、5和6所示。在尝试3中,ab3未更新便被使用, 因此q3得到的是先前激活块所产生的值。所引用的电路如4.1(c)所示。而尝试4,交换语句的顺序不影响综合的效果,因此等同于尝试1。尝试5中,由于 ab5未更新值便被使用,因此q5得到的寄存器a&b的值,等同于尝试3。

简而言之,只有尝试0描述的电路正确且可靠。在尝试0中,我们可以将ab0移除,合并代码如下:

// 尝试0
	always@(posedge clk)
	begin
	  q0 <= a & b;
	end

推荐阅读

1 SNUG.Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!

 

参考

1 Pong P. Chu.FPGA Prototyping By Verilog Examples: Xilinx Spartan-3 Version.Wiley