1. 层次化事件队列简介
详细地了解Verilog的层次化事件队列有助于我们理解Verilog的阻塞和非阻塞赋值的功能。所谓层次化事件队列指的是用于调度仿真事件的不同的Verilog事件队列。在IEEE Verilog标准中,层次化事件队列被看作是一个概念模型。设计仿真工具的厂商如何来实现事件队列,由于关系到仿真器的效率,被视为技术诀窍,不能公开发表。本节也不作详细介绍。
在IEEE 1364-1995 Verilog标准的5.3节中定义了: 层次化事件队列在逻辑上分为用于当前仿真时间的4个不同的队列, 和用于下一段仿真时间的若干个附加队列。
层次化事件队列顺序
-
动态事件队列(下列事件执行的次序可以随意安排)
阻塞赋值
计算非阻塞赋值语句右边的表达式
连续赋值
执行$display命令
计算原语的输入和输出的变化 -
停止运行的事件队列
#0 延时阻塞赋值
-
非阻塞事件队列
更新非阻塞赋值语句LHS(左边变量)的值
-
监控事件队列
执行 m o n i t o r 命 令 执 行 monitor 命令 执行 monitor命令执行strobe 命令
-
其他指定的PLI命令队列
(其他 PLI 命令)
以上五个队列就是Verilog 的“层次化事件队列”
层次化事件描述表格图
仿真器首先按照仿真时间对事件进行排序,然后再在当前仿真时间里按照事件的优先级顺序进行排序。
活跃事件是优先级最高的时间,在活跃事件之间,它们的执行顺序是随机的。
两个缩写:RHS(right-hand-side) 和LHS(left-hand-side)。
前者指等式右边的表达式或者变量(RHS expression or RHS variable),后者指等式左边的表达式或者变量(LHS expression or LHS variable)。
由上表可知,阻塞赋值属于活跃事件,会立刻执行,这就是阻塞赋值“计算完毕,立即更新”的原因。此外,由于在分层事件队列中,只有将活跃事件中排在前面的事件调出,并执行完毕后,才能够执行下面的事件,这就可以解释阻塞赋值的第二个特点。
同样由上表可知,非阻塞赋值的RHS计算属于活跃事件,而非阻塞赋值的更新事件排在非活跃事件之后,因此只有仿真队列中所有的活跃事件和非活跃事件都执行完毕后,才轮到非阻塞赋值更新事件,这就是非阻塞赋值必须分两拍完成的原因。
层次化队列简单分析
大多数Verilog事件是由动态事件队列调度的,这些事件包括阻塞赋值、连续赋值、$display命令、实例和原语的输入变化以及他们的输出更新、非阻塞赋值语句RHS的计算等。而非阻塞赋值语句LHS的更新却不由动态事件队列调度。
在IEEE标准允许的范围内被加入到这些队列中的事件只能从动态事件队列中清除。而排列在其他队列中的事件要等到被“激活”后,即被排入动态事件队列中后,才能真正开始等待执行。IEEE 1364-1995 Verilog 标准的5.4节介绍了一个描述其他事件队列何时被“激活”的算法。
在当前仿真时间中,另外两个比较常用的队列是非阻塞赋值更新事件队列和监控事件队列。细节见后。
非阻塞赋值LHS变量的更新是按排在非阻塞赋值更新事件队列中。而RHS表达式的计算是在某个仿真时刻随机地开始的,与上述其他动态事件是一样的。
strobe和monitor显示命令是排列在监控事件队列中。在仿真的每一步结束时刻,当该仿真步骤内所有的赋值都完成以后,strobe和monitor显示出所有要求显示的变量值的变化。
在Verilog标准5.3节中描述的第四个事件队列是停止运行事件队列, 所有#0延时的赋值都排列在该队列中。采用#0延时赋值是因为有些对Verilog理解不够深入的设计人员希望在两个不同的程序块中给同一个变量赋值,他们企图在同一个仿真时刻,通过稍加延时的赋值来消除Verilog可能产生的竞争冒险。这样做实际上会产生问题。因为给Verilog模型附加完全不必要的#0延时赋值,使得定时事件的分析变得很复杂。我们认为采用#0延时赋值根本没有必要,完全可用其他的方式来代替,因此不推荐使用。
在下面的一些例子中,常常用上面介绍的层次化事件队列来解释Verilog代码的行为。
2. 层次化事件队列分析例子
自触发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] 不正确地使用的阻塞赋值来描述移位寄存器。(方式 #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。上面的模块实际上被综合成只有一个寄存器的电路,这并不是当初想要设计的移位寄存器电路。
[例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]的模块中,阻塞赋值的次序是经过仔细安排的,以使仿真的结果与移位寄存器相一致。虽然该模块可被综合成想要的移位寄存器,但不建议使用这种风格的模块来描述时序逻辑。
[例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
[例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时序逻辑时要用非阻塞赋值的方式。
对同一变量进行多次赋值
在一个以上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,将产生两个触发器输出到一个两输入与门。其综合级前仿真与综合后仿真的结果不完全一致。
非阻塞赋值和$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=%b\n", $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=%b\n", $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