参考来源:《Verilog数字系统设计(夏宇闻)》
在介绍《》时得到下面几个原则:
介绍这几个原则之前,先了解Verilog的层次化事件队列。
Verilog的层次化事件队列
详细地了解Verilog的层次化事件队列有助于我们理解Verilog的阻塞和非阻塞赋值的功能。所谓层次化事件队列指的是用于调度仿真事件的不同的Verilog事件队列。在IEEE Verilog标准中,层次化事件队列被看作是一个概念模型。设计仿真工具的厂商如何来实现事件队列,由于关系到仿真器的效率,被视为技术诀窍,不能公开发表。本节也不作详细介绍。
在IEEE 1364-1995 Verilog标准的5.3节中定义了: 层次化事件队列在逻辑上分为用于当前仿真时间的4个不同的队列, 和用于下一段仿真时间的若干个附加队列。
1)动态事件队列(下列事件执行的次序可以随意安排)
2)停止运行的事件队列
3)非阻塞事件队列
4)监控事件队列
5)其他指定的PLI命令队列
以上五个队列就是Verilog 的“层次化事件队列”
大多数Verilog事件是由动态事件队列调度的,这些事件包括阻塞赋值、连续赋值、$display命令、实例和原语的输入变化以及他们的输出更新、非阻塞赋值语句RHS的计算等。而非阻塞赋值语句LHS的更新却不由动态事件队列调度。
在IEEE标准允许的范围内被加入到这些队列中的事件只能从动态事件队列中清除。而排列在其他队列中的事件要等到被“激活”后,即被排入动态事件队列中后,才能真正开始等待执行。IEEE 1364-1995 Verilog 标准的5.4节介绍了一个描述其他事件队列何时被“激活”的算法。
在当前仿真时间中,另外两个比较常用的队列是非阻塞赋值更新事件队列和监控事件队列。细节见后。
非阻塞赋值LHS变量的更新是按排在非阻塞赋值更新事件队列中。而RHS表达式的计算是在某个仿真时刻随机地开始的,与上述其他动态事件是一样的。
monitor显示命令是排列在监控事件队列中。在仿真的每一步结束时刻,当该仿真步骤内所有的赋值都完成以后,monitor显示出所有要求显示的变量值的变化。
在Verilog标准5.3节中描述的第四个事件队列是停止运行事件队列, 所有#0延时的赋值都排列在该队列中。采用#0延时赋值是因为有些对Verilog理解不够深入的设计人员希望在两个不同的程序块中给同一个变量赋值,他们企图在同一个仿真时刻,通过稍加延时的赋值来消除Verilog可能产生的竞争冒险。这样做实际上会产生问题。因为给Verilog模型附加完全不必要的#0延时赋值,使得定时事件的分析变得很复杂。我们认为采用#0延时赋值根本没有必要,完全可用其他的方式来代替,因此不推荐使用。
在下面的一些例子中,常常用上面介绍的层次化事件队列来解释Verilog代码的行为。时件队列的概念也常常用来说明为什么要坚持上面提到的8项原则。
自触发always块
一般而言,Verilog的always块不能触发自己,见下面的例子:
[例3] 使用阻塞赋值的非自触发振荡器
module osc1 (clk);
output clk;
reg clk;
initial #10 clk = 0;
always @(clk) #10 clk = ~clk;
endmodule
上例描述的时钟振荡器使用了阻塞赋值。阻塞赋值时,计算RHS表达式并更新LHS的值,此时不允许其他语句的干扰。阻塞赋值必须在@(clk)边沿触发到来时刻之前完成。当触发事件到来时,阻塞赋值已经完成了,因此没有来自always块内部的触发事件来触发@(clk),是一个非自触发振荡器。
而例4中的振荡器使用的是非阻塞赋值,它是一个自触发振荡器。
[例4] 采用非阻塞赋值的自触发振荡器
module osc2 (clk);
output clk;
reg clk;
initial #10 clk = 0;
always @(clk) #10 clk <= ~clk;
endmodule
@(clk)的第一次触发之后,非阻塞赋值的RHS表达式便计算出来,把值赋给LHS的事件被安排在更新事件队列中。在非阻塞赋值更新事件队列被激活之前,又遇到了@(clk)触发语句,并且always块再次对clk的值变化产生反应。当非阻塞LHS的值在同一时刻被更新时, @(clk)再一次触发。该例是自触发式,在编写仿真测试模块时不推荐使用这种写法的时钟信号源。
移位寄存器模型
下图表示是一个简单的移位寄存器方框图。
从例5至例8介绍了四种用阻塞赋值实现图2移位寄存器电路的方式,有些是不正确。
[例5] 不正确地使用的阻塞赋值来描述移位寄存器。(方式 #1)
module pipeb1 (q3, d, clk);
output [7:0] q3;
input [7:0] d;
input clk;
reg [7:0] q3, q2, q1;
always @(posedge clk)
begin
q1 = d;
q2 = q1;
q3 = q2;
end
endmodule
在上面的模块中,按顺序进行的阻塞赋值将使得在下一个时钟上升沿时刻,所有的寄存器输出值都等于输入值d。在每个时钟上升沿,输入值d将无延时地直接输出到q3。
显然,上面的模块实际上被综合成只有一个寄存器的电路(见图3),这并不是当初想要设计的移位寄存器电路。
[例6] 用阻塞赋值来描述移位寄存器也是可行的,但这种风格并不好。(方式 #2 )
module pipeb2 (q3, d, clk);
output [7:0] q3;
input [7:0] d;
input clk;
reg [7:0] q3, q2, q1;
always @(posedge clk)
begin
q3 = q2;
q2 = q1;
q1 = d;
end
endmodule
在上面[例6]的模块中,阻塞赋值的次序是经过仔细安排的,以使仿真的结果与移位寄存器相一致。虽然该模块可被综合成图2所示的移位寄存器,但我们不建议使用这种风格的模块来描述时序逻辑。
[例7] 不好的用阻塞赋值来描述移位时序逻辑的风格(方式 #3)
module pipeb3 (q3, d, clk);
output [7:0] q3;
input [7:0] d;
input clk;
reg [7:0] q3, q2, q1;
always @(posedge clk) q1 = d;
always @(posedge clk) q2 = q1;
always @(posedge clk) q3 = q2;
endmodule
在[例7]中,阻塞赋值分别被放在不同的always块里。仿真时,这些块的先后顺序是随机的,因此可能会出现错误的结果。这是Verilog中的竞争冒险。按不同的顺序执行这些块将导致不同的结果。但是,这些代码的综合结果却是正确的流水线寄存器。也就是说,前仿真和后仿真的结果可能会不一致。
[例8] 不好的用阻塞赋值来描述移位时序逻辑的风格(方式 #4)
module pipeb4 (q3, d, clk);
output [7:0] q3;
input [7:0] d;
input clk;
reg [7:0] q3, q2, q1;
always @(posedge clk) q2 = q1;
always @(posedge clk) q3 = q2;
always @(posedge clk) q1 = d;
endmodule
若在[例8]中仅把always块的次序的作些变动,也可以被综合成正确的移位寄存器逻辑,但仿真结果可能不正确。
如果用非阻塞赋值语句改写以上这四个阻塞赋值的例子,每一个例子都可以正确仿真,并且综合为设计者期望的移位寄存器逻辑。
[例9] 正确的用非阻塞赋值来描述时序逻辑的设计风格 #1
module pipen1 (q3, d, clk);
output [7:0] q3;
input [7:0] d;
input clk;
reg [7:0] q3, q2, q1;
always @(posedge clk) begin
q1 <= d;
q2 <= q1;
q3 <= q2;
end
endmodule
[例10] 正确的用非阻塞赋值来描述时序逻辑的设计风格 #2
module pipen2 (q3, d, clk);
output [7:0] q3;
input [7:0] d;
input clk;
reg [7:0] q3, q2, q1;
always @(posedge clk)
begin
q3 <= q2;
q2 <= q1;
q1 <= d;
end
endmodule
[例11] 正确的用非阻塞赋值来描述时序逻辑的设计风格 #3
module pipen3 (q3, d, clk);
output [7:0] q3;
input [7:0] d;
input clk;
reg [7:0] q3, q2, q1;
always @(posedge clk) q1 <= d;
always @(posedge clk) q2 <= q1;
always @(posedge clk) q3 <= q2;
endmodule
[例12] 正确的用非阻塞赋值来描述时序逻辑的设计风格 #4
module pipen4 (q3, d, clk);
output [7:0] q3;
input [7:0] d;
input clk;
reg [7:0] q3, q2, q1;
always @(posedge clk) q2 <= q1;
always @(posedge clk) q3 <= q2;
always @(posedge clk) q1 <= d;
endmodule
以上移位寄存器时序逻辑电路设计的例子表明:
虽然在一个always块中正确的安排赋值顺序,用阻塞赋值也可以实现移位寄存器时序流水线逻辑。但是,用非阻塞赋值实现同一时序逻辑要相对简单,而且线性反馈移位寄存器,非阻塞赋值可以保证仿真和综合的结果都是一致和正确的。因此我们建议大家在编写Verilog时序逻辑时要用非阻塞赋值的方式。
阻塞赋值及一些简单的例子
许多关于Verilog和Verilog仿真的书籍都有一些使用阻塞赋值而且成功的简单例子。例13就是一个在许多书上都出现过的关于触发器的例子。
[例13] module dffb (q, d, clk, rst);
output q;
input d, clk, rst;
reg q;
always @(posedge clk)
if (rst) q = 1'b0;
else q = d;
endmodule
虽然可行也很简单,但我们不建议这种用阻塞赋值来描述D触发器模型的风格。
如果要把所有的模块写到一个always块里,是可以采用阻塞赋值得到正确的建模、仿真并综合成期望的逻辑。但是,这种想法将导致使用阻塞赋值的习惯,而在较为复杂的多个always块的情况下可能会导致竞争冒险。
[例14] 使用非阻塞赋值来描述D触发器是建议使用的风格
module dffx (q, d, clk, rst);
output q;
input d, clk, rst;
reg q;
always @(posedge clk)
if (rst) q <= 1'b0;
else q <= d;
endmodule
养成在描述时序逻辑的多个always块(甚至在单个always块)中使用非阻塞赋值的习惯比较好,见例14所示。
现在来看一个稍复杂的时序逻辑——线性反馈移位寄存器或LFSR。
时序反馈移位寄存器建模线性反馈移位寄存器(Linear Feedback Shift-Register 简称LFSR)是带反馈回路的时序逻辑。反馈回路给习惯于用顺序阻塞赋值描述时序逻辑的设计人员带来了麻烦。见15所示。
[例15] 用阻塞赋值实现的线性反馈移位寄存器,实际上并不具有LFSR的功能
module lfsrb1 (q3, clk, pre_n);
output q3;
input clk, pre_n;
reg q3, q2, q1;
wire n1;
assign n1 = q1 ^ q3;
always @(posedge clk or negedge pre_n)
if (!pre_n)
begin
q3 = 1'b1;
q2 = 1'b1;
q1 = 1'b1;
end
else
begin
q3 = q2;
q2 = n1;
q1 = q3;
end
endmodule
除非使用中间暂存变量,否则用例15所示的赋值是不可能实现反馈逻辑的。
有的人可能会想到将这些赋值语句组成单行等式(如例16所示),来避免使用中间变量。如果逻辑再复杂一些,单行等式是难以编写和调试的。这种方法不推荐使用。
[例16] 用阻塞赋值描述的线性反馈移位寄存器,其功能正确,但模型的含义较难理解。
module lfsrb2 (q3, clk, pre_n);
output q3;
input clk, pre_n;
reg q3, q2, q1;
always @(posedge clk or negedge pre_n)
if (!pre_n) {q3,q2,q1} = 3'b111;
else {q3,q2,q1} = {q2,(q1^q3),q3};
endmodule
如果将例15和例16中的阻塞赋值用非阻塞赋值代替,如例17和例18所示,仿真结果都和LFSR的功能相一致。
[例17] 用非阻塞语句描述的LFSR,可综合其功能正确。
module lfsrn1 (q3, clk, pre_n);
output q3;
input clk, pre_n;
reg q3, q2, q1;
wire n1;
assign n1 = q1 ^ q3;
always @(posedge clk or negedge pre_n)
if (!pre_n) begin
q3 <= 1'b1;
q2 <= 1'b1;
q1 <= 1'b1;
end
else begin
q3 <= q2;
q2 <= n1;
q1 <= q3;
end
endmodule
[例18] 用非阻塞语句描述的LFSR,可综合其功能正确。
module lfsrn2 (q3, clk, pre_n);
output q3;
input clk, pre_n;
reg q3, q2, q1;
always @(posedge clk or negedge pre_n)
if (!pre_n) {q3,q2,q1} <= 3'b111;
else {q3,q2,q1} <= {q2,(q1^q3),q3};
endmodule
从上面介绍的移位寄存器的例子以及LFSR的例子线性反馈移位寄存器,建议使用非阻塞赋值实现时序逻辑。而用非阻塞赋值语句实现锁存器也是最为安全的。
组合逻辑建模时应使用阻塞赋值:
在Verilog中可以用多种方法来描述组合逻辑,但是当用always块来描述组合逻辑时,应该用阻塞赋值。
如果always块中只有一条赋值语句,使用阻塞赋值或非阻塞赋值语句都可以,但是为了养成良好的编程习惯,应该尽量使用阻塞赋值语句来描述组合逻辑。
有些设计人员提倡非阻塞赋值语句不仅可以用于时序逻辑,也可以用于组合逻辑的描述。对于简单的组合alwasys块是可以这样的,但是当always块中有多个赋值语句时,如例19所示的四输入与或门逻辑,使用没有延时的非阻塞赋值可能导致仿真结果不正确。有时需要在always块的入口附加敏感事件参数,才能使仿真正确,因而从仿真的时间效率角度看也不合算。
[例19] 使用非阻塞赋值语句来描述组合逻辑——不建议使用这种风格。
module ao4 (y, a, b, c, d);
output y;
input a, b, c, d;
reg y, tmp1, tmp2;
always @(a or b or c or d)
begin
tmp1 <= a & b;
tmp2 <= c & d;
y <= tmp1 | tmp2;
end
endmodule
例19中,输出y的值由三个时序语句计算得到。由于非阻塞赋值语句在LHS更新前,计算RHS的值,因此tmp1和tmp2仍是应进入该always块时的值,而不是在该步仿真结束时将更新的数值。输出y反映的是刚进入always块时的tmp1和tmp2的值,而不是在always块中经计算后得到的值。
[例20]使用非阻塞赋值来描述多层组合逻辑,虽可行,但效率不高。
module ao5 (y, a, b, c, d);
output y;
input a, b, c, d;
reg y, tmp1, tmp2;5
always @(a or b or c or d or tmp1 or tmp2)
begin
tmp1 <= a & b;
tmp2 <= c & d;
y <= tmp1 | tmp2;
end
endmodule
例20和例19的唯一区别在于,tmp1和temp2加入了敏感列表中。如前所描述,当非阻塞赋值的LHS数值更新时,always块将自触发并用最新计算的tmp1和tmp2的值计算更新输出y的值。将tmp1和tmp2加入到敏感列表中后,现在输出y的值是正确的。但是,一个always块中有多次参数传递降低了仿真器的性能,只有在没有其他合理方法的情况下才考虑这样做。
只需要在always块中使用阻塞赋值语句就可以实现组合逻辑,这样做既简单仿真又快是好的Verilog代码风格,建议大家使用。
[例21] 使用阻塞赋值实现组合逻辑是推荐使用的编码风格。
module ao2 (y, a, b, c, d);
output y;
input a, b, c, d;
reg y, tmp1, tmp2;
always @(a or b or c or d) begin
tmp1 = a & b;
tmp2 = c & d;
y = tmp1 | tmp2;
end
endmodule
例21和例19的唯一区别是,用阻塞赋值替代了非阻塞赋值。这样做可以保证仿真时经一次数据传递输出y的值便是正确的,仿真效率高。因此有以下原则;
原则3 :用always块描述组合逻辑时,应采用阻塞赋值语句。时序和组合的混合逻辑——使用非阻塞赋值
有时候将简单的组合逻辑和时序逻辑写在一起很方便。当把组合逻辑和时序逻辑写到一个always块中时,应遵从时序逻辑建模的原则,使用非阻塞赋值,如例22所示。
[例22 ] 在一个always块中同时实现组合逻辑和时序逻辑
module nbex2 (q, a, b, clk, rst_n);
output q;
input clk, rst_n;
input a, b;
reg q;
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= 1'b0; // 时序逻辑
else q <= a ^ b;// 异或,为组合逻辑
endmodule
用两个always块实现以上逻辑也是可以的,一个always块是采用阻塞赋值的纯组合部分,另一个是采用非阻塞赋值的纯时序部分。见例23。
[例23] 将组合和时序逻辑分别写在两个always块中
module nbex1 (q, a, b, clk, rst_n);
output q;
input clk, rst_n;
input a, b;
reg q, y;
always @(a or b)
y = a ^ b;
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= 1'b0;
else q <= y;
endmodule
原则4:在同一个always块中描述时序和组合逻辑混合电路时,用非阻塞赋值。其他将阻塞和非阻塞混合使用的原则
Verilog语法并没有禁止将阻塞和非阻塞赋值自由地组合在一个always块里。虽然Verilog语法是允许这种写法的,但我们不建议在可综合模块的编写中采用这种风格。
[例24] 在always块中同时使用阻塞和非阻塞赋值的例子。
(应尽量避免使用这种风格的代码,在可综合模块中应严禁使用)
module ba_nba2 (q, a, b, clk, rst_n);
output q;
input a, b, rst_n;
input clk;
reg q;
always @(posedge clk or negedge rst_n) begin: ff
reg tmp;
if (!rst_n) q <= 1'b0;
else begin
tmp = a & b;
q <= tmp;
end
end
endmodule
例24可以得到正确的仿真和综合结果,因为阻塞赋值和非阻塞赋值操作的不是同一个变量。虽然这种方法是可行的,但并不建议使用。
[例25] 对同一变量既进行阻塞赋值,又进行非阻塞赋值会产生综合错误。
module ba_nba6 (q, a, b, clk, rst_n);
output q;
input a, b, rst_n;
input clk;
reg q, tmp;
always @(posedge clk or negedge rst_n)
if (!rst_n) q = 1'b0; // 对 q进行阻塞赋值
else begin
tmp = a & b;
q <= tmp; // 对 q进行非阻塞赋值
end
endmodule
例25在仿真时结果通常是正确的,但是综合时会出错,因为对同一变量既进行阻塞赋值,又进行了非阻塞赋值。因此,必须将其改写才能成为可综合模型。
为了养成良好的编程习惯,建议:
原则5:不要在同一个always块中同时使用阻塞和非阻塞赋值。对同一变量进行多次赋值
在一个以上always块中对同一个变量进行多次赋值可能会导致竞争冒险,即使使用非阻塞赋值也可能产生竞争冒险。在例26中,两个always块都对输出q进行赋值。由于两个always块执行的顺序是随机的,所以仿真时会产生竞争冒险。
[例25] 使用非阻塞赋值语句,由于两个always块对同一变量q赋值
产生竞争冒险的程序:
module badcode1 (q, d1, d2, clk, rst_n);
output q;
input d1, d2, clk, rst_n;
reg q;
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= 1'b0;
else q <= d1;
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= 1'b0;
else q <= d2;
endmodule
当综合工具(如Synopsys)读到[例25]的代码时,将产生以下警告信息:
Warning: In design 'badcode1', there is 1 multiple-driver
net with unknown wired-logic type.
如果忽略这个警告,继续编译例26,将产生两个触发器输出到一个两输入与门。其综合级前仿真与综合后仿真的结果不完全一致。
原则6:严禁在多个always块中对同一个变量赋值。
常见的对于非阻塞赋值的误解
非阻塞赋值和$display
误解1:“使用$display命令不能用来显示非阻塞语句的赋值”
事实是:非阻塞语句的赋值在所有的$display命令执行以后才更新数值[例]
module display_cmds;
reg a;
initial $monitor("$monitor: a = %b", a);
initial
begin
$strobe ("$strobe : a = %b", a);
a = 0;
a <= 1;
$display ("$display: a = %b", a);
#1 $finish;
end
endmodule
下面是上面模块的仿真结果说明$display命令的执行是安排在活动事件队列中,但排在非阻塞赋值数据更新事件之前。
$display: a = 0
$monitor: a = 1
$strobe : a = 1
#0 延时赋值误解2:“#0延时把赋值强制到仿真时间步的末尾”事实是:#0延时将赋值事件强制加入停止运行事件队列中。
[例]
module nb_schedule1;
reg a, b;
initial
begin
a = 0;
b = 1;
a <= b;
b <= a;
$monitor ("%0dns: $monitor: a=%b b=%b", $stime, a, b);
$display ("%0dns: $display: a=%b b=%b", $stime, a, b);
$strobe ("%0dns: $strobe : a=%b b=%bn", $stime, a, b);
#0 $display ("%0dns: #0 : a=%b b=%b", $stime, a, b);
#1 $monitor ("%0dns: $monitor: a=%b b=%b", $stime, a, b);
$display ("%0dns: $display: a=%b b=%b", $stime, a, b);
$strobe ("%0dns: $strobe : a=%b b=%bn", $stime, a, b);
$display ("%0dns: #0 : a=%b b=%b", $stime, a, b);
#1 $finish;
end
endmodule
下面是上面模块的仿真结果说明#0延时命令在非阻塞赋值事件发生前,在停止运行事件队列中执行。
0ns: $display: a=0 b=1
0ns: #0 : a=0 b=1
0ns: $monitor: a=1 b=0
0ns: $strobe : a=1 b=0
1ns: $display: a=1 b=0
1ns: #0 : a=1 b=0
1ns: $monitor: a=1 b=0
1ns: $strobe : a=1 b=0
原则7:用$strobe系统任务来显示用非阻塞赋值的变量值
****对同一变量进行多次非阻塞赋值
误解3:“在Verilog语法标准中未定义可在同一个always块中对某同一变量进行多次非阻塞赋值”。
事实是:Verilog标准定义了在同一个always块中可对某同一变量进行多次非阻塞赋值但多次赋值中,只有最后一次赋值对该变量起作用。
引用IEEE 1364-1995 Verilog标准【2】,第47页,5.4.1节关于决定论的内容如下:
非阻塞赋值按照语句的顺序执行,请看下例:
initial begin
a <= 0;
a <= 1;
end
执行该模块时,有两个非阻塞赋值更新事件加入到非阻塞赋值更新队列。以前的规则要求将非阻塞赋值更新事件按照它们在源文件的顺序加入队列,这便要求按照事件在源文件中的顺序,将事件从队列中取出并执行。因此,在仿真第一步结束的时刻,变量a被设置为0,然后为1。
结论:最后一个非阻塞赋值决定了变量的值。
总结
本节中所有的原则归纳如下:
结论:遵循以上原则,有助于正确的编写可综合硬件,并且可以消除90-100%在仿真时可能产生的竞争冒险现象。
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: muyang-0410