關鍵字:競爭,冒險,書寫規範

產生原因

數字電路中,信號傳輸與狀態變換時都會有一定的延時。

  • 在組合邏輯電路中,不同路徑的輸入信號變化傳輸到同一點門級電路時,在時間上有先有後,這種先後所形成的時間差稱為競爭(Competition)。
  • 由於競爭的存在,輸出信號需要經過一段時間才能達到期望狀態,過渡時間內可能產生瞬間的錯誤輸出,例如尖峰脈衝。這種現象被稱為冒險(Hazard)。
  • 競爭不一定有冒險,但冒險一定會有競爭。
  • 例如,對於給定邏輯 F = A & A',電路如左下圖所示。

    由於反相器電路的存在,信號 A' 傳遞到與門輸入端的時間相對於信號 A 會滯後,這就可能導致與門最後的輸出結果 F 會出現幹擾脈衝。如右下圖所示。

    其實實際硬件電路中,隻要門電路各個輸入端延時不同,就有可能產生競爭與冒險。

    例如一個簡單的與門,輸入信號源不一定是同一個信號變換所來,由於硬件工藝、其他延遲電路的存在,也可能產生競爭與冒險,如下圖所示。

    判斷方法

    代數法

    在邏輯表達式,保持一個變量固定不動,將剩餘其他變量用 0 或 1 代替,如果最後邏輯表達式能化簡成

    Y = A + A'

    Y = A · A'

    的形式,則可判定此邏輯存在競爭與冒險。

    例如邏輯表達式 Y = AB + A'C,在 B=C=1 的情況下,可化簡為 Y = A + A'。顯然,A 狀態的改變,勢必會造成電路存在競爭冒險。

    卡諾圖法

    有兩個相切的卡諾圈,並且相切處沒有其他卡諾圈包圍,可能會出現競爭與冒險現象。

    例如左下圖所存在競爭與冒險,右下圖則沒有。

    其實,卡諾圖本質上還是對邏輯表達式的一個分析,隻是可以進行直觀的判斷。

    例如,左上圖邏輯表達式可以簡化為 Y = A'B' + AC,當 B=0C=1 時,此邏輯表達式又可以表示為 Y = A' + A。所以肯定會存在競爭與冒險。

    右上圖邏輯表達式可以簡化為 Y = A'B' + AB,顯然 B 無論等於 1 還是 0,此式都不會化簡成 Y = A' + A。所以此邏輯不存在競爭與冒險。

    需要注意的是,卡諾圖是首尾相臨的。如下圖所示,雖然看起來兩個卡諾圈並沒有相切,但實際上,m6 與 m4 也是相鄰的,所以下麵卡諾圖所代表的數字邏輯也會產生競爭與冒險。

    其他較為複雜的情況,可能需要采用 "計算機輔助分析 + 實驗" 的方法。

    消除方法

    對數字電路來說,常見的避免競爭與冒險的方法主要有 4 種。

    1)增加濾波電容,濾除窄脈衝

    此種方法需要在輸出端並聯一個小電容,將尖峰脈衝的幅度削弱至門電路閾值以下。

    此方法雖然簡單,但是會增加輸出電壓的翻轉時間,易破壞波形。

    2)修改邏輯,增加冗餘項

    利用卡諾圖,在兩個相切的圓之間,增加一個卡諾圈,並加在邏輯表達式之中。

    如下圖所示,對數字邏輯 Y = A'B' + AC 增加冗餘項 B'C,則此電路邏輯可以表示為 Y = A'B' + AC + B'C。此時電路就不會再存在競爭與冒險。

    3)使用時鍾同步電路,利用觸發器進行打拍延遲

    同步電路信號的變化都發生在時鍾邊沿。對於觸發器的 D 輸入端,隻要毛刺不出現在時鍾的上升沿並且不滿足數據的建立和保持時間,就不會對係統造成危害,因此可認為 D 觸發器的 D 輸入端對毛刺不敏感。 利用此特性,在時鍾邊沿驅動下,對一個組合邏輯信號進行延遲打拍,可消除競爭冒險。

    延遲一拍時鍾時,會一定概率的減少競爭冒險的出現。實驗表明,最安全的打拍延遲周期是 3 拍,可有效減少競爭冒險的出現。

    當然,最終還是需要根據自己的設計需求,對信號進行合理的打拍延遲。

    為說明對信號進行打拍延遲可以消除競爭冒險,我們建立下麵的代碼模型。

    實例

    module competition_hazard
        (
          input             clk ,
          input             rstn ,
          input             en ,
          input             din_rvs ,
          output reg        flag
        );

        wire    condition = din_rvs & en ;  //combination logic
        always @(posedge clk or negedge !rstn) begin
            if (!rstn) begin
                flag   <= 1'b0 ;
            end
            else begin
                flag   <= condition ;
            end
        end

    endmodule

    testbench 描述如下:

    實例

    `timescale 1ns/1ns

    module test ;
        reg          clk, rstn ;
        reg          en ;
        reg          din_rvs ;
        wire         flag_safe, flag_dgs ;

        //clock and rstn generating
        initial begin
            rstn              = 1'b0 ;
            clk               = 1'b0 ;
            #5 rstn           = 1'b1 ;
            forever begin
                #5 clk = ~clk ;
            end
        end

        initial begin
            en        = 1'b0 ;
            din_rvs   = 1'b1 ;
            #19 ;      en        = 1'b1 ;
            #1 ;       din_rvs   = 1'b0 ;
        end

        competition_hazard         u_dgs
         (
          .clk              (clk           ),
          .rstn             (rstn          ),
          .en               (en            ),
          .din_rvs          (din_rvs       ),
          .flag             (flag_dgs      ));

        initial begin
            forever begin
                #100;
                if ($time >= 1000)  $finish ;
            end
        end

    endmodule // test

    仿真結果如下:

    由圖可知,信號 condition 出現了一個尖峰脈衝,這是由於信號 din_rvs 與信號 en 相對於模塊內部時鍾都是異步的,所以到達內部門電路時的延時是不同的,就有可能造成競爭冒險。

    雖然最後的仿真結果 flag 一直為 0,似乎是我們想要的結果。但是實際電路中,這個尖峰脈衝在時間上非常靠近時鍾邊沿,就有可能被時鍾采集到而產生錯誤結果。

    下麵我們對模型進行改進,增加打拍延時的邏輯,如下:

    實例

    module clap_delay
        (
          input             clk ,
          input             rstn ,
          input             en ,
          input             din_rvs ,
          output reg        flag
        );

        reg                  din_rvs_r ;
        reg                  en_r ;
        always @(posedge clk or !rstn) begin
            if (!rstn) begin
                din_rvs_r      <= 1'b0 ;
                en_r           <= 1'b0 ;
            end
            else begin
                din_rvs_r      <= din_rvs ;
                en_r           <= en ;
            end
        end

        wire                 condition = din_rvs_r & en_r ;
        always @(posedge clk or negedge !rstn) begin
            if (!rstn) begin
                flag   <= 1'b0 ;
            end
            else begin

                flag   <= condition ;
            end
        end // always @ (posedge clk or negedge !rstn)

    endmodule

    將此模塊例化到上述 testbench 中,得到如下仿真結果。

    由圖可知,信號 condition 沒有尖峰脈衝的幹擾了,仿真結果中 flag 為 0 也如預期。

    其實,輸入信號與時鍾邊沿非常接近的情況下,時鍾對輸入信號的采樣也存在不確定性,但是不會出現尖峰脈衝的現象。對輸入信號多打 2 拍,是更好的處理方式,對競爭與冒險有更好的抑製作用。

    4)采用格雷碼計數器

    遞加的多 bit 位計數器,計數值有時候會發生多個 bit 位的跳變。

    例如計數器變量 counter 從 5 計數到 6 時, 對應二進製數字為 4'b101 到 4'b110 的轉換。因為各 bit 數據位的延時,counter 的變換過程可能是: 4'b101 -> 4'b111 -> 4'b110。如果有以下邏輯描述,則信號 cout 可能出現短暫的尖峰脈衝,這顯然是與設計相悖的。

    cout = counter[3:0] == 4'd7 ; 

    而格雷碼計數器,計數時相鄰的數之間隻有一個數據 bit 發生了變化,所以能有效的避免競爭冒險。

    好在 Verilog 設計時,計數器大多都是同步設計。即便計數時存在多個 bit 同時翻轉的可能性,但在時鍾驅動的觸發器作用下,隻要信號間滿足時序要求,就能消除掉 100% 的競爭與冒險。

    小結

    一般來說,為消除競爭冒險,增加濾波電容和邏輯冗餘,都不是 Verilog 設計所考慮的。

    計數采用格雷碼計數器,大多數也是應用在高速時鍾下減少信號翻轉率來降低功耗的場合。

    利用觸發器在時鍾同步電路下對異步信號進行打拍延時,是 Verilog 設計中經常用到的方法。

    除此之外,為消除競爭冒險,Verilog 編碼時還需要注意一些問題,詳見下一小節。


    Verilog 書寫規範

    在編程時多注意以下幾點,也可以避免大多數的競爭與冒險問題。

    • 1)時序電路建模時,用非阻塞賦值。
    • 2)組合邏輯建模時,用阻塞賦值。
    • 3)在同一個 always 塊中建立時序和組合邏輯模型時,用非阻塞賦值。
    • 4)在同一個 always 塊中不要既使用阻塞賦值又使用非阻塞賦值。
    • 5)不要在多個 always 塊中為同一個變量賦值。
    • 6)避免 latch 產生。

    下麵,對以上注意事項逐條分析。

    1)時序電路建模時,用非阻塞賦值

    前麵講述非阻塞賦值時就陳述過,時序電路中非阻塞賦值可以消除競爭冒險。

    例如下麵代碼描述,由於無法確定 a 與 b 阻塞賦值的操作順序,就有可能帶來競爭冒險。

    always @(posedge clk) begin
        a = b ;
        b = a ;
    end

    而使用非阻塞賦值時,賦值操作是同時進行的,所以就不會帶來競爭冒險,如以下代碼描述。

    always @(posedge clk) begin
        a <= b ;
        b <= a ;
    end

    2)組合邏輯建模時,用阻塞賦值

    例如,我們想實現 C = A&B, F=C&D 的組合邏輯功能,用非阻塞賦值語句如下。

    兩條賦值語句同時賦值,F <= C & D 中使用的是信號 C 的舊值,所以導致此時的邏輯是錯誤的,F 的邏輯值不等於 A&B&D。

    而且,此時要求信號 C 具有存儲功能,但不是時鍾驅動,所以 C 可能會被綜合成鎖存器(latch),導致競爭冒險。

    always @(*) begin
        C <= A & B ;
        F <= C & D ;
    end

    對代碼進行如下修改,F = C & D 的操作一定是在 C = A & B 之後,此時 F 的邏輯值等於 A&B&D,符合設計。

    always @(*) begin
        C = A & B ;
        F = C & D ;
    end

    3)在同一個 always 塊中建立時序和組合邏輯模型時,用非阻塞賦值

    雖然時序電路中可能涉及組合邏輯,但是如果賦值操作使用非阻塞賦值,仍然會導致如規範 1 中所涉及的類似問題。

    例如在時鍾驅動下完成一個與門的邏輯功能,代碼參考如下。

    實例

    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
            q <= 1'b0;
        end
        else begin
            q <= a & b;  //即便有組合邏輯,也不要寫成:q = a & b
         end
    end

    4)在同一個 always 塊中不要既使用阻塞賦值又使用非阻塞賦值

    always 涉及的組合邏輯中,既有阻塞賦值又有非阻塞賦值時,會導致意外的結果,例如下麵代碼描述。

    此時信號 C 阻塞賦值完畢以後,信號 F 才會被非阻塞賦值,仿真結果可能正確。

    但如果 F 信號有其他的負載,F 的最新值並不能馬上傳遞出去,數據有效時間還是在下一個觸發時刻。此時要求 F 具有存儲功能,可能會被綜合成 latch,導致競爭冒險。

    always @(*) begin
        C = A & B ;
        F <= C & D ;
    end

    如下代碼描述,仿真角度看,信號 C 被非阻塞賦值,下一個觸發時刻才會有效。而 F = C & D 雖然是阻塞賦值,但是信號 C 不是阻塞賦值,所以 F 邏輯中使用的還是 C 的舊值。

    always @(*) begin
        C <= A & B ;
        F = C & D ;
    end

    下麵分析假如在時序電路裏既有阻塞賦值,又有非阻塞賦值會怎樣,代碼如下。

    假如複位端與時鍾同步,那麼由於複位導致的信號 q 為 0,是在下一個時鍾周期才有效。

    而如果是信號 a 或 b 導致的 q 為 0,則在當期時鍾周期內有效。

    如果 q 還有其他負載,就會導致 q 的時序特別混亂,顯然不符合設計需求。

    實例

    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin  //假設複位與時鍾同步
            q <= 1'b0;
        end
        else begin
            q = a & b;  
        end
    end

    需要說明的是,很多編譯器都支持這麼寫,上述的分析也都是建立在仿真角度上。實際中如果阻塞賦值和非阻塞賦值混合編寫,綜合後的電路時序將是錯亂的,不利於分析調試。

    5)不要在多個 always 塊中為同一個變量賦值

    與 C 語言有所不同,Verilog 中不允許在多個 always 塊中為同一個變量賦值。此時信號擁有多驅動端(Multiple Driver),是禁止的。當然,也不允許 assign 語句為同一個變量進行多次連線賦值。 從信號角度來講,多驅動時,同一個信號變量在很短的時間內進行多次不同的賦值結果,就有可能產生競爭冒險。

    從語法來講,很多編譯器檢測到多驅動時,也會報 Error。

    6)避免 latch 產生

    具體分析見下一章:《避免 Latch》

    源碼下載

    Download