Verilog -- 阻塞与非阻塞的仿真与综合

Verilog -- 阻塞与非阻塞的仿真与综合

参考 Clifford E. Cummings, Sunburst Design, Inc. "Nonblocking Assignments in Verilog Synthesis, CodingStyles That Kill!"
前段时间为了探究阻塞和非阻塞的进行过简单测试,当时觉得对阻塞与非阻塞的理解已经可以了,直到发现了Sunburst的这篇论文(这个机构真的很nb,很多verilog,SV的经典教程都出自它),才算真的明白了阻塞和非阻塞的原理。

基本概念

RHS(right-hand-side): 指等式右边的表达式或者变量(RHS expression or RHS variable)
LHS(left-hand-side):指等式左边的表达式或者变量(RHS expression or RHS variable)

竞争条件:Verilog竞争条件发生在当两个或多个语句被安排在相同的仿真时间步长中执行时,当语句执行的顺序改变时,会产生不同的结果。

阻塞赋值:当没有其它的Verilog描述可以打断“阻塞赋值”时,操作将会估计RHS的值并完成赋值。“阻塞”即是说在当前的赋值完成前阻塞其它类型的赋值任务。

  • “阻塞赋值“可以看作一步进程(one-step process): 当没有其它可以打断赋值的描述时,估计等式右边(RHS)的指并赋予左边(LHS)。 在同一个always块里面,阻塞赋值结果将一直持续下去直到赋值结束。

非阻塞赋值:在一个时间步(time step)的开始估计RHS expression的值并在这个时间步(time step)结束时用等式右边的值更新取代LHS。在估算RHS expression和更新LHS expression的中间时间段,其它的对LHS expression的非阻塞赋值可以被执行。即是说“非阻塞赋值”从估计RHS开始并不阻碍执行其它的Verilog描述。

  • “非阻塞赋值”可以看作二步进程(one-step process):
      1. 在时间步开始估计RHS;
      1. 在时间步结束时更新LHS;

Verilog层积事件列(stratified event queue)

层积事件列(stratified event queue)是一个概念模型,每个仿真器都有不同的实现方式。“层积事件列”逻辑上划分为四个不同的队列,分别用于当前的仿真时间和未来的仿真时间。

活跃事件列(Active Events)是(最多的被预备执行的Verilog事件)包括:

  • 阻塞赋值
  • 连续赋值
  • $display命令
  • 估出“非阻塞赋值”的RHS expressions
  • 计算初原元件(primitive)的输入和更改实例(instance)的输出

注意“非阻塞赋值”的LHS不在“活跃事件列”里更新。

非活跃事件列(Inactive event): #0延时的阻塞赋值。

非阻塞赋值更新事件列(Thenonblocking assign updates event queue)即是“非阻塞赋值”的LHS expression被安排更新赋值的那些事件。在一个仿真时间步(simulation time step)的开始,“RHS expression 的估值”与其它被激活事件是以任意的次序进行的。

monitor事件列是由那些被安排的“\(strobe”和“\)monitor”显示命令带来的。$strobe 和 $monitor 用于显示一个仿真时间步结束时变量更新后的值(这时该仿真时间步里所有的赋值分配都已经完成)

注意:事件可以被加到任意的事件列里(由IEEE标准强制约束的),但是只可能从“活跃事件列”里被移出。其它事件列里的事件最终总是要成为“激活事件”的(或者提升为“活跃事件)。

示例:自触发的always块

---- Non-Self Triggered ------
module osc1 (clk);
output clk;
reg clk;
initial #10 clk = 0;
always @(clk) #10 clk = ~clk;
endmodule
----------------------------------------

------- Self Triggered ------
module osc2 (clk);
output clk;
reg clk;
initial #10 clk = 0;
always @(clk) #10 clk <= ~clk;
endmodule
---------------------------------------- 
  • 第一种写法使用阻塞赋值。这样的话RHS估值和LHS赋值是不被打断地执行,此时不允许其他语句的干扰。阻塞赋值必须在@(clk)边沿触发到来时刻之前完成。即在边沿事件之前,对clk的赋值已经完成。所以, 没有“触发事件”(@(clk))来触发always块里面的触发事件。因此不能自触发。
  • 第二种写法使用非阻塞赋值,在第一个@(clk)触发之后按照事件的执行顺序可以做如下排序:
    • 10ns时进入触发always块,执行always中语句
    • 非阻塞赋值的RHS expression被估值,并且LHS值被送入“非阻塞赋值更新”事件列。
    • RHS估值完毕,等待LHS进行
    • 非阻塞LHS的值更新,同时又遇到了@(clk)触发语句,always块再次对clk的值变化产生反应。@(clk)再一次触发。
    • 非阻塞赋值的RHS再次估值...
      因此可以进行自触发。

阻塞和非阻塞的综合问题

一般都建议使用阻塞逻辑描述组合逻辑,非阻塞逻辑描述时序逻辑。
但阻塞赋值就一定不能描述时序逻辑吗?答案是否定的,如果仔细安排阻塞赋值的顺序,也是可以进行时序电路的描述,并能通过仿真和综合(或者可以通过综合)。

以一个三级流水线为例:

首先我们直到,采用非阻塞赋值,仿真和综合肯定没问题。那么如果使用阻塞赋值呢?

always @(posedge clk) begin
    q1 = d; 
    q2 = q1;
    q3 = q2;
  end

采用上面这种方式,肯定不是我们要的结果,而是将会只综合出一个寄存器。这肯定是不可行的。
而如果换一下顺序:

always @(posedge clk) begin
    q3 = d2; 
    q2 = q1;
    q1 = q;
  end

阻塞赋值被仔细地排序,以使仿真能够像寄存器一样正确地工作。这样的写法无论是仿真还是综合都是正确的,但是不建议这样做。
另外,如果将一个always块拆分成三个写,也就是:

always @(posedge clk) q1=d;
always @(posedge clk) q2=q1;
always @(posedge clk) q3=q2;

这样Verilog标准允许以任意的次序来仿真执行3个always块,这也许会使得该流水线仿真结果产生错误,因为这产生了Verilog竞争条件。由不同的always块执行顺序会产生不同的结果。尽管这样,它的综合结果将是正确的!这就意味着综合前仿真综合后仿真不匹配。

另外,在组合逻辑中,可以用非阻塞赋值来描述吗?
答案是肯定的,但是也需要一番波折,还是以一个例子来看:

always @(a or b or c or d) begin 
   tmp1 <= a & b; 
   tmp2 <= c & d; 
   y    <= tmp1 | tmp2; 
 end

上面这段组合逻辑采用非阻塞赋值来描述,可以发现,y的值没法被正确更新,因为当abcd改变时tmp与y赋值的RHS同时估值,所以y的LHS更新时使用的还是旧的tmp,导致功能不正确。要解决这一问题也不麻烦:

always @(a or b or c or d or tmp1 or tmp2) begin 
   tmp1 <= a & b; 
   tmp2 <= c & d; 
   y    <= tmp1 | tmp2; 
 end

在“非阻塞赋值更新事件队列”中当非阻塞赋值更新LHS变量时,always块将会“自触发”并使用最新的tmp1和tmp2来更新y输出。现在y输出值正确了,但时代价是因为增加了两条 passes贯穿整个always块。使用太多的pass来贯穿always块等于降低仿真器的性能。因此虽然能获得正确功能,但不建议使用。

非阻塞赋值和$display

非阻塞赋值在$display命令之后才被更新赋值,因此,display如果紧跟非阻塞赋值则无效

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

输出:
\(display: a = 0 \)monitor: a = 1
$strobe : a = 1

0延时

在层积事件列中可以发现,#0延时有一个专门的非活跃事件列,因此,零延迟#0 使得赋值事件处于“非激活事件列”,也就是非阻塞赋值LHS更新之前。

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); 
end

结果:
0ns: $display: a=0 b=1
0ns: #0 : a=0 b=1
0ns: $monitor: a=1 b=0
0ns: $strobe : a=1 b=0

从上面可以看到,使用#0延时后,display语句进入非活跃事件,在非阻塞赋值之前执行。

建议

  • 当为时序逻辑建模,使用“非阻塞赋值”。
  • 当为锁存器(latch)建模,使用“非阻塞赋值”。
  • 当用always块为组合逻辑建模,使用“阻塞赋值”
  • 当在同一个always块里面既为组合逻辑又为时序逻辑建模,使用“非阻塞赋值”。
  • 不要在同一个always块里面混合使用“阻塞赋值”和“非阻塞赋值”。
  • 不要在两个或两个以上always块里面对同一个变量进行赋值。
  • 使用$strobe以显示已被“非阻塞赋值”的值。
  • 不要使用#0延迟的赋值。
posted @ 2020-04-14 23:42  love小酒窝  阅读(1867)  评论(6编辑  收藏  举报