• [文档].艾米电子 阻塞赋值与非阻塞赋值,Verilog


    说明

    翻译自:FPGA Prototyping By Verilog Examples: Xilinx Spartan-3 Version的第7章第一节

    内容

    阻塞赋值VS非阻塞赋值

    有两种赋值语句被用在always块内:阻塞赋值与非阻塞赋值。关于阻塞与非阻塞复制有3条简单的准则:

    • 将电路分为两部分:寄存器电路和组合电路
    • 在寄存器电路中使用非阻塞赋值
    • 在组合电路中使用阻塞赋值

    1 概览

    阻塞赋值 基本语法如下:

    [var] = [expression];

    当该条语句被执行时,右手边的表达式将被赋给左手边的变量,期间不允许其他语句的干扰。因此,就阻塞了其他语句,直到该条语句执行完毕为止。阻塞赋值的行为与C语言中的变量赋值类似。

    非阻塞赋值 基本语法如下:

    [var] <= [expression];

    非阻塞赋值的行为非常令人难以琢磨。当always块被激活(在time step的开始),右手边的表达式被赋初值。当运行到always块的结尾(即time step的结尾),运算所得的值被赋给左手边的变量。

    以x变量执行非阻塞赋值为例。因为Verilog模型的实际流程比较复杂,我们将非阻塞赋值的行为翻译成一下几个步骤:

    • 在alway块的开始,x值传递给x_entry;
    • 右手边的变量x的值被x_entry取代;
    • 左手边变量x的值被x_exit取代;
    • 在always块的结束,x_exit的值传递给x。

    在下面的代码片段内,上述四个步骤被呈现在代码的注释中。

    always@*
    begin               // x_entry = x
      y <= x & ...      // y       = x_entry & ...
      x <= ...          // x_exit  = ...
    end                 // x       = x_exit

    范例 为了了解阻塞赋值和非阻塞赋值的区别,我们用三输入的的电路来做讨论。

    代码1 使用阻塞赋值的电路

    module and_blocking
    (
      input      a,
      input      b,
      input      c,
      output reg y
    );  
    
    always@*
    begin
      y = a;
      y = y & b;
      y = y & c;
    end
    
    endmodule

    阻塞赋值的欣慰类似于C语言中的顺序赋值。y最终得到的值为a & b & c。注意,此代码仅用于示范,使用顺序语义学来描述电路是比较差劲的行为。

    下面给出的代码,其中的阻塞赋值被替换为非阻塞赋值。代码注释详细说明了y的赋值动作。

    代码2 使用非阻塞赋值的电路

    module and_nonblocking
    (
      input      a,
      input      b,
      input      c,
      output reg y
    );  
    
    always@*
    begin               // y_entry = y
      y <= a;           // y_exit  = a
      y <= y & b;       // y_exit  = y_entry & b
      y <= y & c;       // y_exit  = y_entry & c
    end                 // y       = y_exit
    
    endmodule

    注意always块内的前2条语句将不会产生任何效果。上述always块等价与:

    always@*        
      y <= y & c;  

    2 组合电路

    上一个小节的范例属于极端的情况。除了缺省值,大部分的组合电路并不会多次赋值同一变量。阻塞赋值和非阻塞赋值都可以用于描述同一电路。然而,它们有一些微妙的区别。下面的范例用于解释这些不同。让我们以一位同或(异或非)电路为例。我们将详细列出敏感列表中的变量。

    代码3 使用阻塞赋值的一位同或电路

    module eq1_blocking
    (
      input      i0,
      input      i1,
      output reg eq
    ); 
    
    reg p0, p1;
    
    always@(i1, i2)     // 只有i0和i1在敏感列表
    begin               // 语句的顺序非常重要
      p0 = ~i0 & i1;
      p1 = i0 & i1;
      eq = p0 | p1;
    end
    
    endmodule

    注意到敏感列表仅包括i0和i1,。当其中之一变化时,always块被激活,p0、p1和ep被顺序运算,ep在第一个time step的结尾被更新。语句的顺序非常重要。假设性我们移动最后面的语句到最前面。

    always@(i1, i2)     
    begin               
      eq = p0 | p1;
      p0 = ~i0 & i1;
      p1 = i0 & i1;
    end

    在第一条语句中,由于p0和p1还没有被指定新的值,因此先前被激活的值将会被用到。而先前的值将意味着锁存器的存在,故此代码是不正确的。

    下面将使用非阻塞赋值替换阻塞赋值。

    代码4 使用非阻塞赋值的一位同或电路

    module eq1_nonblocking
    (
      input      i0,
      input      i1,
      output reg eq
    ); 
    
    reg p0, p1;
    
    always@(i1, i2, p0, p1)       // p0、p1也在敏感列表中 
                                  // 语句的顺序不重要      
    begin                         // p0_entry = p0; p1_entry = p1             
      p0 <= ~i0 & i1;             // p0_exit = ~i0 & ~i1
      p1 <= i0 & i1;              // p1_exit = i0 & i1
      eq <= p0 | p1;              // eq_exit = p0_entry | p1_entry
    end                           // eq = eq_exit; p0 = p0_exit; p1 = p1_exit                          
    
    endmodule

    注意p0和p1也包括在敏感列表中。当i0或i1变化时,always块被激活;在第一个time step的结尾,p0和p1被赋以新值。既然ep取决于p0和p1(p0_entry和p1_entry)的旧值,那么其值在第一个time step的结尾保持不变。当当前的time step执行完毕,always块重新被激活,因为p0和p1发生了变化(这便于为何p0和p1也要置于敏感列表之中的原因)。注意语句的顺序不影响结果。

    3 存储单元

    使用非阻塞赋值来引用存储器。例如,D触发器:

    always@(posede clk)
      q <= d;

    当然也可以用阻塞赋值来引用D触发器,如下:

    always@(posede clk)
      q = d;

    虽然在单个D-FF情况下面,上面的代码工作正常,但是当多个寄存器互相动作的时候,这里就出现许多微妙的问题。

    考虑两个寄存器在每个时钟周期交换数据。使用阻塞赋值,代码为:

    always@(posede clk)
      a = b;
      
    always@(posede clk)
      b = a;

    在clk的上升沿,两个always块都被激活,并行操作。这两个操作应该在同一time step结束。根据Verilog的标准,两个always块的执行可以以任何顺序列入。若第一个always块先执行,则由于阻塞赋值的缘故a立即得到b的值。当第二个always块执行的时候,b得到a的刷新值,及b的原始值,因此b保持不变。类似的,若第二个always块先执行,a得到的也是其初始值。这就是Verilog中的竞争冒险(race condition)。从Verilog的角度看,两种结果都是有效的。

    下面我们修改代码中的阻塞赋值为非阻塞赋值。

    always@(posede clk)
    begin        // b_entry = b
      a <= b;    // a_exit  = b_entry
    end          // a       = a_exit
      
    always@(posede clk)
    begin        // a_entry = a
      b <= a;    // b_exit  = a_entry
    end          // b       = b_exit

    通过注释我们看到,因为输入(entry)值都被用于赋值,所以无论执行的顺序,a和b都得到正确的值。

    因此为了避免竞争冒险,我们使用非阻塞赋值来引用D-FF和触发器。

    4 混用阻塞和非阻塞赋值的时序电路

    在同一个always块内,有可能混用阻塞赋值和非阻塞赋值。下面我们使用简单的例程来解释不同组合的行为,以加强对赋值的理解。

    图4.1 通过混合赋值来推断电路

    图4.1 通过混合赋值来引用电路

    考虑图4.1(b)所示的原理图。当时钟上升沿来临之时,a、b与运算所得的值被存入D-FF。基于前面的讲解,我们可以将存储和组合电路分配到两段代码中。如代码4.1所示。

    代码4.1 两段实现

    module ab_dff_2seg
    (
      input clk,
      input a,
      input b,
      output reg q
    );
    
    reg q_next;
    
    // D-FF
    always@(posedge clk)
      q <= q_next;
    
    // 组合电路
    always@*
      q_next = a & b;
    
    endmodule

    我们可以变换一下,将两段组合在一起,使用单个always块来描述电路。下面通过六次尝试,来描述阻塞和非阻塞赋值的不同组合的区别。如代码4.2所示。

    代码4.2 混合赋值例程

    module ab_dff_mix
    (
      input clk,
      input a,
      input b,
      output reg q0,
      output reg q1,
      output reg q2,
      output reg q3,
      output reg q4,
      output reg q5
    );
    
    reg ab0, ab1, ab2, ab3, ab4, ab5;
    
    // 尝试0
    always@(posedge clk)
    begin
      ab0 = a & b;
      q0 <= ab0;
    end
    
    // 尝试1
    always@(posedge clk)
    begin                   // ab1_entry = ab1; q1_entry = q1
      ab1 <= a & b;         // ab1_exit = a & b
      q1  <= ab1;           // q1_exit = ab1_entry
    end                     // ab1 = ab1_exit; q1 = q1_exit
    
    // 尝试2
    always@(posedge clk)
    begin
      ab2 = a & b;
      q2  = ab2;
    end
    
    // 尝试3(调换尝试1的顺序)
    always@(posedge clk)
    begin
      q0 <= ab0;
      ab0 = a & b;
    end
    
    // 尝试4(调换尝试2的顺序)
    always@(posedge clk)
    begin                   // ab4_entry = ab4; q4_entry = q4
      q4  <= ab4;           // q4_exit = ab4_entry
      ab4 <= a & b;         // ab4_exit = a&b
    end                     // ab4 = ab4_exit; q4 = q4_exite
    
    // 尝试5(调换尝试3的顺序)
    always@(posedge clk)
    begin
      q5  = ab5;
      ab5 = a & b;
    end
    
    endmodule

    在尝试0中,起初赋值给ab0和q0将引用两个寄存器,一个用于存储寄存器ab0,另一个用于存储寄存器q0。因为ab0在块赋值时被立即更新,所以q0得到了a&b的值。对应的原理图如图4.1(a)所示。由于ab0在always块外没有被使用,因此寄存器ab0的输出就不是必需存在的,即相应的寄存器可以被移除。这样,结果电路就如图4.1(b)所示,也就是所需的电路。
    在尝试1中,对ab1使用了非阻塞赋值,对应的阐述写到了注释里面。注意q1得到的是ab1_entry而非ab1_exit。而ab1_entry是先前存储的ab值,即对应一个寄存器的输出。相应的原理图如图4.1(c)所示。一个不确定的输入缓存被引用,同时a&b的值延迟一个时钟周期后才被存储到q1中。

    在尝试2中,ab2和q2都是用了阻塞赋值。该代码所引用的电路,与尝试1等同,如图4.1(a)和(b)所示。由于使用阻塞赋值来引用D-FF,有可能产生竞争冒险,因此不推荐使用这种类型的代码。

    出于演示的目的,让我们来测试一下调换尝试0、1和2的赋值顺序会发生什么。其结果代码如尝试4、5和6所示。在尝试3中,ab3未更新便被使用,因此q3得到的是先前激活块所产生的值。所引用的电路如4.1(c)所示。而尝试4,交换语句的顺序不影响综合的效果,因此等同于尝试1。尝试5中,由于ab5未更新值便被使用,因此q5得到的寄存器a&b的值,等同于尝试3。

    简而言之,只有尝试0描述的电路正确且可靠。在尝试0中,我们可以将ab0移除,合并代码如下:

    // 尝试0
    always@(posedge clk)
    begin
      q0 <= a & b;
    end

    推荐阅读

    1 SNUG.Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!

    参考

    1 Pong P. Chu.FPGA Prototyping By Verilog Examples: Xilinx Spartan-3 Version.Wiley

    另见

    [与艾米一起学FPGA/SOPC].[逻辑实验文档连载计划]

     安德鲁® / CC BY 2.5     FPGA Run!
  • 相关阅读:
    Tomcat之the jre_home environment variable is not defined correctly this environment variable is need
    java集合框架之聚合操作stream
    java集合框架之比较器Comparator、Comparable
    java集合框架之HashCode
    java集合框架之几种set(HashSet LinkedHashSet TreeSet )
    java集合框架之HashMap和Hashtable的区别
    java集合框架之Collections
    java集合框架之Collection
    java集合框架之HashSet
    java集合框架之HashMap
  • 原文地址:https://www.cnblogs.com/yuphone/p/1874465.html
Copyright © 2020-2023  润新知