说明
翻译自: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