[Verilog6]阻塞与非阻塞赋值

阻塞赋值,非阻塞赋值

Author:蛋蛋的忧伤,rommel,渣渣晖

  • 在Verilog语法讲解中,我们曾经提到,对于阻塞赋值与非阻塞赋值,我们通常有以下的使用习惯:
  1. 在描述组合逻辑的always块中用阻塞赋值,则综合成组合逻辑的电路结构
  2. 在描述时序逻辑的always块中用非阻塞赋值,则综合成时序逻辑的电路结构
  • 为什么一定要这样做呢?这是因为希望综合前仿真和综合后仿真一致的缘故。如果不按照上面两个要点来编写Verilog 代码,也有可能综合出正确的逻辑,但前后仿真的结果就会不会一致。具体原因将会在后文展开介绍。

阻塞赋值

阻塞赋值的定义

  • 阻塞赋值操作符用等号(即=)表示。为什么称这种赋值为阻塞赋值呢?这是因为在赋值时先计算等号右手方向(RHS)部分的值,这时赋值语句不允许任何别的Verilog语句的干扰,直到现行的赋值完成时刻,即把RHS赋值给LHS的时刻,它才允许别的赋值语句的执行。
  • 可综合的阻塞赋值操作在RHS不能设定有延迟(即使是零延迟也不允许)。从理论上讲,阻塞赋值语句与后面的赋值语句只有概念上的先后,而无实质上的延迟。若在RHS上加延迟,则在延迟期间会阻止赋值语句的执行,延迟后才执行赋值,这种赋值语句是不可综合的,在需要综合的模块设计中不可使用这种风格的代码。
    • 阻塞赋值的执行可以认为是只有一个步骤的操作,即计算RHS并更新LHS,此时不能允许有来自任何其他Verilog语句的干扰。所谓阻塞的概念是指在同一个always块中,其后面的赋值语句从概念上(即使不设定延迟)是在前一句赋值语句结束后再开始赋值的。

在时序逻辑中使用阻塞赋值

  • 如果在一个过程块中阻塞赋值的RHS变量正好是另一个过程块中阻塞赋值的LHS变量,这两个过程块又用同一个时钟沿触发.这时阻塞赋值操作会出现问题,即如果阻塞赋值的顺序安排不好,就会出现竞争。若这两个阻塞赋值操作用同一个时钟沿触发,则执行的顺序是无法确定的。下面的例子可以说明这个问题。
module fbosc1(
    output  y1, y2,
    input    clk,rst_n
);
    reg y1,y2;
    always@(posedge clk or negedge rst_n)begin
        if (!rst_n) y1 = 0;//reset
        else  y1 = y2;
    end
    always@(posedge clk or negedge rst_n)begin
        if(!rst_n) y2 = 1;//reset
        else y2 = y1;
    end
endmodule
  • 在上述代码中,两个always块是并行执行的,若复位信号从0到1,当上面y1的always块的有效时钟沿比下面y2的always块的时钟沿早几个皮秒(由时钟偏差造成)到达,则yI和y2都会取1;而若下面y2的那个always块的有效时钟沿比上面y1的always块早几个皮秒到达,则y1和y2都会取0。这清楚地说明这个模块是不稳定的,必定会产生冒险和竞争的情况。

非阻塞赋值

非阻塞赋值的定义

  • 非阻塞赋值操作符用小于等于号(即<=)表示。为什么称这种赋值为非阻塞赋值?这是因为在赋值操作时刻开始时计算非阻塞赋值符的RHS表达式,赋值操作结束时刻才更新LHS。在计算非阻塞赋值的RHS表达式和更新LHS期间,其他的Verilog语句,包括其他的Verilog非阻塞赋值语句都能同时计算RHS表达式和更新LHS。非阻塞赋值允许其他的Verilog语句同时进行操作。非阻塞赋值的操作过程可以看作两个步骤:
  1. 在赋值开始时刻,计算非阻塞赋值RHS表达式;
  2. 在赋值结束时刻,更新非阻塞赋值LHS表达式。
  • 非阻塞赋值操作只能用于对寄存器类型变量进行赋值,因此只能用在"initial"块和"always"块等过程块中,而非阻塞赋值不允许用于连续赋值。

在组合逻辑中使用非阻塞赋值

  • 当在always语句块不论是使用非阻塞赋值还是使用阻塞赋值用于描述组合逻辑电路,编译器会自动将代码优化成对应的组合逻辑电路。若设计者使用非阻塞赋值进行电路设计,则最终生成的电路就有可能与设计的设计产生偏差。具体举例如下。
  • 使用阻塞赋值描述
module And_3to1(
    input   a,b,s,
    output  y
    );
    reg y,f1,f2;
    always @(a or b or s) begin
         f1 = a & ~s;
         f2 = b & s;
         y = f1 | f2;
    end
endmodule
  • 使用非阻塞赋值描述
module And_3to1(
    input   a,b,s,
    output  y
    );
    reg y,f1,f2;
    always @(a or b or s)begin
        f1 <= a & ~s;
        f2 <= b &s;
        y  <= f1 | f2;
    end
endmodule
  • 生成的RTL电路
  • 在上述代码中,无论是使用了阻塞赋值还是非阻塞赋值,经过综合器优化后最终都生成了一样的电路。对于采用非阻塞赋值写法的设计者而言,原先设计的功能是,当a或b或s中任意有一个值发生了改变,此时会对f1和f2的值进行更新,而y的值仍还是采用上一时刻f1和f2的值进行计算,等到下一次a或b或s的值发生改变时,才会对输出y的值进行再次更新。也就是说,开发者预想的功能,y值的更新会滞后于输入变化一个节拍。但最终生成的电路,与设计者的预期并不一致。
  • 但这是为什么呢?其根本原因在于,由于我们是对组合逻辑电路进行描述。在组合逻辑中,只包含门电路,而不包存储电路。即组合逻辑的任意时刻的输出仅仅取决于该时刻的输入,与电路原本的状态无关。所以是无法锁存上一时刻f1与f2的数值,也就无法做到y的值滞后于输入一个周期再进行输出。所以经过综合器优化后,采用阻塞赋值和非阻塞赋值生成的电路均一致,且采用非阻塞赋值描述的代码会给出警告信息,告诉设计者,部分功能无法被综合已被自动优化。

应用举例

简单移位寄存器

  • 描述一个移位寄存器,将输入的数据延迟3个时钟周期再进行输出。

使用非阻塞赋值描述

  • 使用非阻塞赋值语句对延时器进行描述,可随意改变赋值语句的先后顺序, 不会对生成的电路产生任何影响
  • Verilog代码
module DELAY(
    input clk,rst,in,
    output out);
    reg      out_r,in_rrr,in_rr,in_r;
    always @(posedge clk or negedge rst) begin
        if(! rst)
            out_r \<\= 0;
            in_rrr \<\= 0;
            in_rr \<\= 0;
            in_r \<\= 0;
        else begin
            in_r  \<\= in;
            in_rr \<\= in_r;
            in_rrr \<\= in_rr;
            out_r \<\= in_rrr;
        end
    end
    assign out = out_r;  
endmodule
  • RTL图

使用阻塞赋值描述

  • 使用阻塞赋值进行延时器的描述,需要关注赋值语句的先后顺序,不同的描述顺序,会对生成的电路产生不同的影响。下文采用两种不同的赋值顺序进行描述,进行对比分析。
  • 顺序1-Verilog代码
module DELAY(
    input   in,
    input   clk,rst,
    output  out
    );
    reg     out_r,in_rrr,in_rr,in_r; 
    always @(posedge clk or negedge rst) begin
        if(!rst)begin     
            out_r = 0;
            in_rrr = 0;
            in_rr = 0;
            in_r = 0;
        end
        else begin 
            out_r = in_rrr;
            in_rrr = in_rr;
            in_rr = in_r;
            in_r = in;
        end  
    end 
    assign out = out_r;    
endmodule
  • 顺序1-RTL图
  • 顺序2-Verilog代码
module DELAY(
    input  in,clk,rst,
    output out
);
    reg out_r,in_rrr,in_rr,in_r;
    assign out = out_r;
    always @(posedge clk or negedge rst) begin
        if(!rst) begin
            out_r <= 0;
            in_rrr <= 0;
            in_rr <= 0;
            in_r <= 0;
        end
        else begin
            in_r = in;
            in_rr = in_r;
            in_rrr = in_rr;
            out_r = in_rrr;
        end
    end
endmodule
  • 顺序2-RTL图

总结

  • 可以看出,非阻塞赋值与阻塞赋值均可以描述出正确的延时电路,实现具体功能。但对于非阻塞赋值语句的描述方式,无需关注赋值语句书写的先后顺序,赋值语句均在always语句块结束时同时进行赋值。对于阻塞赋值语句,则必须关注代码的具体书写顺序,只有正确的赋值顺序,才能够实现预期的移位寄存功能,如书写顺序错误,就不能按照设计的设计生成对应电路。采用阻塞赋值顺序1的描述方式是可行的,但是这种风格是不好的。
  • 在阻塞赋值的顺序2代码中,看似能生成4个D触发器,但是由于按顺序进行的阻塞赋值将使得在下一个时钟上升沿时刻,所有的寄存器输入in,在每一个时钟上升沿,输入值in将无延迟的直接输出到out上。这也就是导致只生成一个D触发器的原因。

流水线式移位寄存器

  • 有时候,为了提高代码运行速率,我们会对always代码块中的代码进行拆分,让每个always块内的代码减少,使用多个always语句块实现对应功能。下面的例子分别使用阻塞赋值与非阻塞赋值实现流水线式以为寄存器

阻塞赋值

module DELAY(
    input   in,
    input   clk,rst,
    output reg out
    );
    reg in_rrr,in_rr,in_r;
    always @(posedge clk or negedge rst) begin
        if(!rst_n) in_r = 0;
        else in_r = in;
    end
   always @(posedge clk or negedge rst) begin
        if(!rst_n) in_rr = 0;
        else in_rr = in_r;
    end
    always @(posedge clk or negedge rst) begin
        if(!rst_n) in_rrr = 0;
        else in_rrr = in_rr;
    end
    always @(posedge clk or negedge rst) begin
        if(!rst_n) out = 0;
        else out = in_rrr;
    end
endmodule
  • 在上述代码中,阻塞赋值分别被放在不同的always块里。仿真时,这些块的先后顺序是随机的,因此可能会出现错误的结果,这是Verilog中的竞争冒险。按不同的顺序执行这些块将导致不同的结果。但是,这些代码的综合结果却是正确的流水线寄存器。也就是说,前仿真和后仿真的结果可能会不一致。

非阻塞赋值

module DELAY(
    input   in,
    input   clk,rst,
    output reg out
    );
    reg in_rrr,in_rr,in_r;
    always @(posedge clk or negedge rst) begin
        if(!rst_n) in_r = 0;
        else in_r = in;
    end
   always @(posedge clk or negedge rst) begin
        if(!rst_n) in_rr <= 0;
        else in_rr <= in_r;
    end
    always @(posedge clk or negedge rst) begin
        if(!rst_n) in_rrr <= 0;
        else in_rrr <= in_rr;
    end
    always @(posedge clk or negedge rst) begin
        if(!rst_n) out <= 0;
        else out <= in_rrr;
    end
endmodule
  • 相较于阻塞赋值,非阻塞赋值无需关注赋值语句的书写顺序,且仿真与综合的前后结果均一致,在时序电路中较为方便。
  • 虽然在一个always块中正确地安排赋值顺序.则用阻塞赋值可以实现移位寄存器时序流水线逻辑。但是,用非阻塞赋值实现同一时序逻辑要相对简单,而且,非阻塞赋值可以保证仿真和综合的结果都是一致和正确的。

阻塞赋值与非阻塞赋值的编程习惯

  1. 时序电路建模时,用非阻塞赋值。
  2. 锁存器电路建模时,用非阻塞赋值。
  3. 用always块建立组合逻辑模型时.用阻塞赋值。
  4. 在同一个always块中建立时序和组合逻辑电路时,用非阻塞赋值。
  5. 在同一个always块中不要既用非阻塞赋值又用阻塞赋值。
  6. 不要在一个以上的always块中为同一个变量赋值。(7)用$strobe系统任务来显示用非阻塞赋值的变量值。
  7. 在赋值时不要使用#0延迟

参考
《Verilog经典教程(第三版)》,Author:夏闻宇,14章