Menu Close

阻塞与非阻塞赋值语句深度探讨及的使用技巧(1)

上节内容介绍了阻塞赋值语句及非阻塞赋值语句的描述、特点及区别,并用实例逐步深入探讨的方式逐一介绍给大家。本文将介绍在建模及实际设计应用中应注意的问题以及在处理某些问题时的使用技巧,特别是结合顺序语句以及在边沿驱动下always过程使用中应注意的问题。由于上节中的例子都是在testbench中进行的,为了考虑综合和仿真的普遍性,本节中的内容都是采用实体程序与仿真程序分开设计的方式,将顺序性,阻塞性,非阻塞性放在可综合实体中实现,仿真程序仅提供激励信号和格式显示。

1. 阻塞与非阻塞赋值语句在边沿驱动过程使用

下面结合60进制计数器为例子进行介绍:

1.1 非阻塞赋值程序如下:

  • 实体程序
`timescale 1 ns /1 ps

module non_block_counter
(
    input rst,
    input clk,
    output reg [3:0] out_a,
    output reg [3:0] out_b
);

always@(posedge clk or posedge rst)
if(rst) begin
    out_a <= 0;
    out_b <= 0;
end
else begin
    if(out_a == 9) begin
        out_a <= 0;
        if(out_b == 5)
            out_b <= 0;
        else
            out_b <= out_b + 1;
    end
    else
        out_a <= out_a + 1;
end

endmodule

 

  • 仿真程序(tb.v)
`timescale 1 ns  /1 ps
 
module tb_block 
(
 
);
 
parameter       PERIOD = 10;
 
reg      clk;
 
initial   begin
    clk = 0;
 
    forever 
        #5 clk = ~clk;
end
 
reg      rst;
 
wire    [3:0]   out_a ,out_b;
 
initial begin
    rst = 0;
    #5
 
    rst = 1'b1;
    #30
 
    rst = 0;
end
 
 
always@(posedge clk or posedge rst)
if(rst) begin
    $display(“time=%d, out_a=%h, out_b=%h”,$time, out_a,out_b);
end
else begin
    $display(“time=%d, out_a=%h, out_b=%h”,$time, out_a,out_b);
end
 
 
non_block_counter  non_block_counter_inst
(
    .rst        (rst),
    .clk        (clk),
 
    .out_a      (out_a),
    .out_b      (out_b)
);
 
 
endmodule

 

仿真结果如图1,2,3所示

                               图1

 

                                    图2

图3

图4

从图1,2,3,4的对比可以看出$display函数打印的总是计数器上次的值,也就是上升沿之前的值。这就是上面内容提到的delta延时。

 

1.2 利用非阻塞语句实现触发器模型

在介绍计数器的时候,我们曾经探讨过计数器可以实现触发器模型,当时采用D触发器作为参考模型实现的。当将reg型变量抽象成D触发器模型,该变量就有三个属性,分别为D,CLK, Q。可以用X.D,X.CLK,X.Q表示, 如out_a的三个属性为out_a.D,out_a.CLK, out_a.Q.

为什么非阻塞语句比较切合D触发器模型呢?因为它们的行为比较吻合,吻合点列举如下:

(1)都由reg型变量表示(触发器是寄存器的一种)。

(2)都采用了带有边沿的always过程。

(3)在边沿的时间点上,非阻塞赋值语句由于delta延时,被赋值的变量只有always过程结束后才被更新(如图1,2),而触发器的Q的值也是在上升沿结束后才被更新,理想的D触发器被认为上升沿的时间足够短,这与delta延迟的概念一致。因此非阻塞赋值语句的左边的值被认为(或等效为)触发器的Q端,而赋值语句的右边表达式被认为(或等效为)D端(或与触发器的D端相连),如图5。

(4)在引用变量值时,非阻塞赋值语句引用上次的值(上升沿之前的值,如图1,2, $display函数显示的内容),而在数字同步时序电路设计中对于D触发器值的引用也是引用D触发器Q端的值,例如:在if(out_a==9)中我们就可以用D触发器的模型来分析,if(out_a==9)的含义就是if(out_a.Q==9)。而“out_a<=out_a+1;”在赋值语句的左右两边都有out_a,那么如何区分D和Q呢?按照带边沿的always顺序过程非阻塞的理解,赋值左边的应为out_a.D,右边赋值表达式中的out_a应为out_a.Q。

 

1.3 阻塞赋值

将上面的实体程序改成阻塞赋值的情况,然后分析和观察仿真结果。

  • 实体程序
`timescale 1 ns /1 ps

module non_block_counter
(
    input rst,
    input clk,

    output reg [3:0] out_a,
    output reg [3:0] out_b
);

always@(posedge clk or posedge rst)
if(rst) begin
    out_a = 0;
    out_b = 0;
end
else begin
    if(out_a == 9) begin
        out_a = 0;
        if(out_b == 5)
            out_b = 0;
        else
            out_b = out_b + 1;
    end
    else
        out_a = out_a + 1;
end

endmodule

 

  • 仿真程序(tb.v)
`timescale 1 ns  /1 ps
 
 
module tb_block 
(
 
);
 
 
parameter       PERIOD=10;
 
reg      clk;
 
initial   begin
    clk = 0;
 
    forever 
        #5 clk = ~clk;
end
 
 
reg      rst;
 
 
wire    [3:0]   out_a ,out_b;
 
 
initial begin
    rst = 0;
    #5
 
    rst = 1'b1;
    #30
 
    rst = 0;
end
 
 
always@(posedge clk or posedge rst)
if(rst) begin
    $display(“time=%d, out_a=%h, out_b=%h”,$time, out_a,out_b);
end
else begin
    $display(“time=%d, out_a=%h, out_b=%h”,$time, out_a,out_b);
end
 
 
 
block_counter  block_counter_inst
(
    .rst        (rst),
    .clk        (clk),
 
    .out_a      (out_a),
    .out_b      (out_b)
);
 
 
endmodule

 

仿真结果如下:

图5

    图6

从上面的程序的逻辑和时序分析看,阻塞赋值语句应该非阻塞语句有差别,如在time=40时应该输出”out_b=0,out_a=1”,但仿真结果图5,6却与图1,2的结果完全一致。这与预期的结果不一致。观察程序可以出,$display 函数与变量赋值不在同一个always过程中,这对于非阻塞赋值没有区别,但对于阻塞赋值语句是不同,由于$display 函数在不同的always过程中,很难确定$display函数与各个赋值语句的先后顺序,因此上面的仿真结果并不可信。修改程序如下:

  • 将$display 函数放实体程序的赋值语句后面,注意这种改动会引起程序的不可综合。
`timescale 1 ns  /1 ps
 
module block_counter
(
    input         rst,
    input         clk,
 
    output reg [3:0]      out_a,
    output reg [3:0]      out_b
);
 
always@(posedge clk or posedge rst)
if(rst) begin
    out_a = 0;
    out_b = 0;
    $display("time=%d, out_b=%h, out_a=%h",$time, out_b,out_a);
end
else begin
    if(out_a == 9) begin
        out_a = 0;
        if(out_b == 5)
            out_b = 0;
        else
            out_b = out_b + 1'b1;
 
        $display("time=%d, out_b=%h, out_a=%h",$time, out_b,out_a);
    end
    else begin
        out_a = out_a + 1'b1;
        $display("time=%d, out_b=%h, out_a=%h",$time, out_b,out_a);
    end
end
 
endmodule

 

  • 注释仿真程序的$display函数
`timescale 1 ns  /1 ps
 
module tb
(
);
 
 
parameter       PERIOD = 10;
 
reg              clk;
 
initial   begin
    clk = 0;
    #5
     
    forever  begin
        #5 clk = ~clk;
 
        if($time > 630)
            $stop;
 
    end
end
 
reg              rst;
 
wire    [3:0]   out_a,out_b;
 
initial begin
    rst = 0;
    #5
 
    rst = 1'b1;
    #30
 
    rst = 0;
 
end
 
//always@(posedge clk or posedge rst)
//if(rst) begin
//  $display("time=%d, out_b=%h, out_a=%h",$time, out_b,out_a);
//end
//else begin
//  $display("time=%d, out_b=%h, out_a=%h",$time, out_b,out_a);
//end
//  
 
 
block_counter  block_counter_inst
(
    .rst        (rst),
    .clk        (clk),
     
    .out_a      (out_a),
    .out_b      (out_b)
);
 
 
endmodule

 

将修改后的程序重新仿真,结果如图7:

图7

从图7的仿真结果可以看出,和上面的分析结果一致。即在time=40时输出”out_b=0,out_a=1”。

从上面的结果看,如果虽然整个循环结果相同,但每次的$display函数输出结果提前了10ns,虽然对于计数器来说没有区别,但对于其它应用程序,可能会带来严重错误。例如,如果存储器的地址和数据在生成时,地址如果用阻塞语句而数据用非阻塞语句则会出现地址数据不一致的情况。因此使用时要注意。

 

1.4 时钟边沿驱动下的阻塞赋值语句模型

我们知道在时钟边沿驱动下非阻塞赋值语句可以用D触发器进行建模,阻塞赋值语句是否也可以呢?下面我们将上例做简单修改,测试阻塞赋值语句在另一个边沿驱动的always过程中非阻塞赋值语句的捕获情况,修改的程序如下:

  • 实体程序
`timescale 1 ns /1 ps

module block_counter
(
    input rst,
    input clk,
    output reg [3:0] out_a,
    output reg [3:0] out_b,
    output reg [3:0] out_c,
    output reg [3:0] out_d
);

always@(posedge clk or posedge rst)
if(rst) begin
    out_a = 0;
    out_b = 0;
    $display("time=%d, out_b=%h, out_a=%h",$time, out_b,out_a);
end
else begin
    if(out_a == 9) begin
        out_a = 0;
        if(out_b == 5)
            out_b = 0;
        else
            out_b = out_b + 1'b1;
        
        $display("time=%d, out_b=%h, out_a=%h",$time, out_b,out_a);
    end
    else begin
        out_a = out_a + 1'b1;
    
        $display("time=%d, out_b=%h, out_a=%h",$time, out_b,out_a);
    end
end

always@(posedge clk or posedge rst)
if(rst) begin
    out_c <= 0;
    out_d <= 0;
end
else begin
    out_c <= out_a;
    out_d <= out_b;
end

endmodule

 

  • 仿真程序
`timescale 1 ns /1 ps

module tb
(

);

parameter PERIOD=10;

reg clk;

initial begin
    clk=0;
    #5

    forever begin
        #5 clk = ~clk;
        if($time > 630)
            $stop;
    end
end

reg rst;

wire [3:0] out_a,out_b;
wire [3:0] out_c,out_d;

initial begin
    rst = 0;
    #5

    rst = 1'b1;
    #30

    rst = 0;
end

block_counter block_counter_inst
(
    .rst   (rst),
    .clk   (clk),
    .out_a (out_a),
    .out_b (out_b),
    .out_c (out_c),
    .out_d (out_d)
);

endmodule

 

仿真结果如图8,

图8

图8中可以看到,在40ns时,out_a 的变化不仅能立马更新,而且out_c也能立即得到out_a的值。我们知道在功能仿真时时钟的上升沿是理想的,其数学模型为阶跃函数。相比非阻塞模型,阻塞模型的值相当于左极限,而非阻塞模型更像右极限。因此阻塞模型不符合标准边沿D触发器的模型。但是在边沿撤销后又能保持数据不变,因此又的确是触发器的模型。由于阻塞赋值语句不能用单一的边沿D触发器模型来分析,在同步时序电路设计中带来复杂性。因此建议在同步时序电路设计中尽量采用非阻塞式赋值语句。下面针对阻塞赋值语句常见的问题进行分析并给出解决办法。

 

2. 阻塞语句不完整的分支语句处理

在图1,2中可以看出,对于完整if分支语句(if…else…),阻塞与非阻塞在分支及边界条件处理方面差别不大,都能实现要求的循环计数。但不完整的分支语句结果不同,如将上面的60进制计数器加以修改得到另外一种形式的计数器(仅复制部分程序)。

非阻塞源程序:

if(out_a==9) begin
    out_a<=0;
    if(out_b==5)
        out_b<=0;
    else
        out_b<=out_b+1;
end
else
    out_a<=out_a+1;

修改后的程序:

 

out_a<=out_a+1;

if(out_a==9) begin
    out_a<=0;
    out_b<=out_b+1;

    if(out_b==5)
        out_b<=0;
end

可以看出第二段更简洁,充分顺序语句的特点,调换语句的前后顺序,实现第一段的功能,由于该部分内容经常被使用,大家都熟悉这两种写法,这里省略仿真程序及过程。

阻塞源程序:

if(out_a==9) begin
    out_a=0;
    if(out_b==5)
        out_b=0;
    else
        out_b=out_b+1;
end
else
    out_a=out_a+1;

修改后的程序:

 

out_a=out_a+1;
if(out_a==9) begin
    out_a=0;
    out_b=out_b+1;
    if(out_b==5)
        out_b=0;
end

阻塞程序修改后,经过仔细分析,发现第二段不能完全替代第一段,主要发生在边界的地方。由于阻塞赋值语句立马生效,当out_a==8时,赋值语句out_a=out_a+1;立马生效得到out_a==9,而if(out_a==9)条件成立又把out_a修改成0;因此计数器将会丢失out_a==9的机会,out_b也会出现同样的情况。下面看看第二段的仿真结果,如图9所示,

图9

从图9的结果可以看出在边界处的赋值与上面的分析结果一致。将程序修改如下可以恢复源程序的功能。

out_a=out_a+1;
if(out_a==10) begin
    out_a=0;
    out_b=out_b+1;

    if(out_b==6)
        out_b=0;
end

程序修改后的仿真结果如图10,

图10

从图10可以看出,经过修改后,计数器恢复正常。关于阻塞与非阻塞赋值语句

还有许多值得探讨的问题,我们将在下节内容进行深入探讨。

 

 

 

Posted in FPGA, FPGA 教材教案, Quartus II, Quartus II, 开发工具, 开发工具, 教材与教案, 文章

发表回复

相关链接