在 Verilog 中實作時脈邊界同步器

本文將探討時脈同步器的 Verilog 描述。在學習數位電子學入門知識時,您可能已經了解了亞穩態(metastability)的概念。這是一種不希望出現的情況,即數位訊號進入一種不確定狀態,既非 1 也非 0。時脈邊界同步器的作用就是緩解這種亞穩態。

為了更好地理解同步器的邏輯,讓我們先回答幾個常見問題:

什麼是時脈邊界?

許多邏輯設計都採用同步方式,這意味著所有邏輯都以相同的時脈週期運作。在 FPGA 中,這是透過一系列暫存器來實現的。資料通常在時脈正緣在暫存器之間傳輸。所有與該時脈同步的邏輯都位於同一時脈域中。FPGA 內部可能存在多個時脈域。FPGA 外部也可能存在時脈域,例如外部微控制器。

什麼是亞穩態(Metastability)?

保持清晰的時脈邊界的主要原因之一是防止亞穩態情況。暫存器(正反器)進入中間狀態的情況。對於單一時脈域來說,這種情況得到緩解,因為所有元素同時轉換(例如在時脈的正緣)。此時,在下一個正緣出現之前,所有元件都有一段時間穩定下來。如果閘極傳播延遲不過分,亞穩態的可能性就會降低。

什麼是同步器?

當訊號跨越時脈邊界時,就會出現棘手的情況。這些邊界就像兩件略微走音的樂器,它們的正緣會彼此滑動。有時正緣重合,有時則相距甚遠。當邊界之間的訊號彼此靠近時,就會出現亞穩態問題。這會帶來風險,因為接收時脈域中的暫存器在讀取訊號之前沒有足夠的時間穩定下來。在極端情況下,這種不穩定性會在整個時脈域中蔓延,導致不可預測的操作。

同步器方塊圖

圖 1 展示了同步器的 Verilog 實作方塊圖。這是一個經典的同步器設計,非同步訊號流經各個暫存器。每個暫存器都由新域的主時脈驅動。換句話說,非同步訊號在新域中被同步。暫存器的數量是設計上的一個權衡因素,取決於統計規律。更多的同步暫存器可以降低出現亞穩態的機率。然而,過於複雜的設計會消耗寶貴的 FPGA 資源,降低速度。

圖 1:同步鏈在訊號跨越時脈邊界時對其進行處理。

同步器模組的 Verilog 參數

接下來,我們將探討本文最後所附 Verilog 程式碼的一些重點。首先,您會注意到圖 2 所示的同步器 ASCII 藝術表示。隨後是一個範例實例和一個標頭,其中簡要描述了所有輸入和輸出訊號。

圖 2:同步器的 ASCII 藝術表示。

在圖 2 的中間部分,您會注意到「參數和預設值」部分。參數是 Verilog 的一大亮點。它們允許您在實例化 RTL(暫存器傳輸級)設計時修改其操作。每個參數都定義了一個預設值。如果需要,可以覆寫該預設值。在本例中,此參數決定了同步器中包含的正反器數量。它的預設值為 2,並且如本範例實例所示,它始終保持為 2。

//** Sample Instantiation ******************************************************
//
//    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip-flop
//    synchronizer(                        // Instance name
//        .clk(clk),                       // Your clock signal
//        .async_in(async_in),             // Asynchronous input signal
//        .sync_out(sync_out),             // Safe synchronized signal
//        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
//        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
//    );
//

這行 Verilog 程式碼宣告並初始化同步正反器:

reg [SYNC_STAGES-1:0] sync_regs = {SYNC_STAGES{1'b0}};  // init with zeros

這是一個寬度由 [SYNC_STAGES-1:0] 決定的單一暫存器。例如,當 SYNC_STAGES 參數設定為 2 時,該向量被理解為 [1:0]。{SYNC_STAGES{0’b0}}; 語句是 Verilog 複製運算子。它巧妙地將 sync_reg 正反器的每個元素初始化為二進位零。

這看起來可能不像一個正反器。然而,FPGA 綜合工具肯定會將此描述解釋為疊接正反器,如圖 3 所示的 VIVADO 綜合器原理圖所示。

在本例中,定義一個暫存器,然後使用移位操作(如下一個程式碼片段所示)可以輕鬆擴展電路。它使用一個參數來確定正反器的數量。

圖 3:VIVADO 產生的同步器電路原理圖。

同步鍊和上升/下降偵測器在 always @(posedge clk) 程式碼區塊中實作。請注意使用了非阻隔代碼。此 always 程式碼區塊中的所有操作同時執行。這種同步操作是此設計的關鍵考慮因素。

    always @(posedge clk) begin
        sync_regs <= {sync_regs[SYNC_STAGES-2:0], async_in};    // shift left
        sync_out <= sync_regs[SYNC_STAGES-1];
        rise_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == T) ? T : F; 
        fall_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == F) ? T : F; 
    end

此操作(靈敏度)由接收域主時脈的正緣控制。Verilog 中的級聯運算子執行相當於微控制器左移操作的功能。最左側的位元被丟棄,最新的非同步輸入被放置在最低位。

請注意,每個向量和位元操作都依賴 SYNC_STAGES 參數的值。因此,無需修改模組即可輕鬆更改同步正反器的數量。這些變更在模組實例化時進行。按照設計,它們無法在運行時更改。

模組的 sync_out 值取自移位暫存器的最左側位元。再次強調,這是非阻隔代碼。本例中有兩個同步器和一個輸出暫存器。sync_out 值在第三個時脈的正緣鎖定。

正緣和負緣偵測器由最後兩行程式碼構成。這裡我們使用了三元運算子 ?:。這是 Verilog 中實作 if else 語句的簡寫方式。可以參考圖 1 來理解這些語句。注意,邊緣觸發區塊監視輸出暫存器(最後一個輸出)和即將輸出的值。在時脈正緣,此區塊判斷輸出暫存器的值是否有變化。如果值不同,且新輸出為正,則出現正緣。負緣的判斷方法相同。

測試平台

接下來,我們將重點轉向 RTL 的效能分析。這包括測試平台的結果以及實際應用效能。此硬體基於 VIVADO 平台開發,用於安裝在 Digilent Basys 3 上的 Xilinx Artix FPGA。關於這方面的更多內容,我們將在以後的文章中討論。

測試平台的結果如圖 4 所示。時脈 1 被視為同步時脈,時脈 2 被視為非同步時脈。它們的模擬頻率分別為 100MHz 和 59MHz。請注意,sync_out 訊號在 clk_1 的三個正緣之後才跟隨輸入訊號。這就像 sync_out 是一個副本,儘管它被延遲了三個 clk_1 的正緣。另請注意,rise_edge_tick 和 fall_edge_tick 訊號與 clk_1 的正緣同步。「tick」指的是持續一個時脈週期的高電平脈衝訊號。此正緣觸發或負緣觸發訊號可用於下游邊緣觸發電路。sync_out 訊號可用於電平敏感電路。

圖 4:同步器電路的測試台測試結果。

圖 5 嘗試使用 Digilent Analog Discovery(舊款)儀器捕捉實際訊號。由於 100MHz 訊號瞬時且難以捕獲,因此很難看清。但是,可以看到各種延遲和脈衝訊號。雖然在這張靜態圖中無法直接觀察到,但訊號之間有明顯的抖動(jitter)。這是因為 Analog Discovery 的訊號產生器(用於產生測試脈衝)與 BASYS 3 板的 100 MHz 時脈不同步。但這正是關鍵所在:我們跨越了時脈邊界 —— 同步器完成了它的工作。

圖 5:Digilent BASYS 3 FPGA 上同步器的實際運作。

結語

現代 FPGA「編譯器」旨在利用使用者指定的功耗、速度和結構消耗權重來最佳化您的程式碼。綜合工具刪除部分邏輯的情況並不罕見。例如,您可能需要 rise_edge_tick 和 fall_edge_tick 輸出。如果您實例化同步器而不連接這些輸出,它們將自動被調整。您將看到這是一個未使用的邏輯警告。

您通常可以忽略此類警告。但是,在開發邏輯時,您應該盡一切努力識別並消除警告。我在為該同步器構建正緣和負緣檢測器時遇到了這個問題。最初,我使用暫存器來記住 sync_out 的最後一個值。這是微控制器世界中一種非常自然的編碼方式:

如果(現在不等於最後)做某事

這是一個不錯的解決方案,而且確實有效,但綜合工具不斷彈出邏輯修剪警告。原先考慮的角度錯了。為了理解原因,請回顧圖 1。注意最後一個正反器很特殊。它本身並不是同步器的一部分,而是該模組的輸出暫存器。它是為其他 FPGA 邏輯提供同步穩定性的來源。這裡的「穩定」指的是它是一個暫存器,由主時脈控制,並封裝在 always@(posedge clk) 區塊中。

仔細想想,我用來捕捉「最後一個輸出」的暫存器是多餘的。我感興趣的信號保存在 sync_out 寄存器中。綜合工具檢測到了這一點,並警告了備援的存在。

這項觀察結果或許有助於解釋圖 1 中正緣和負緣區塊輸入的位置。注意,該區塊會監控最後一個正反器的輸入和輸出。如果訊號不同,該區塊會發送相應的脈衝。





synchronizer.v

//******************************************************************************
//
// Module: synchronizer
//
//  This code is derived from Xilinx UltraFast Design Methodology Guide for
//  the Vivado Design Suite UG949 (v2014.1) April 2, 2014
//
//  This RTL is subject to Term and Conditions associated with the 
//  DigiKey TechForum. Refer to:
//  https://www.digikey.com/en/terms-and-conditions?_ga=2.92992980.817147340.1696698685-139567995.1695066003
//
//  Should you find an error, please leave a comment in the forum space below. 
//  If you are able, please provide a recommendation for improvement.
//
//******************************************************************************
//           ______________________________________________
//          |                                              |
//          | synchronizer                                 |
//          |______________________________________________|
//          |                                              |
//          |    Parameters and defaults                   |
//          |        SYNC_STAGES = 2                       |
//          |                                              |
//      ----| async_in                            sync_out |----
//          |                               rise_edge_tick |----
//      ----| clk                           fall_edge_tick |----
//          |______________________________________________|
//
//** Description ***************************************************************
//
//  A synchronizer used to safely transition a signal across a clock domain.
//
//** Sample Instantiation ******************************************************
//
//    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip flop
//    synchronizer(                        // Instance name
//        .clk(clk),                       // Your clock signal
//        .async_in(async_in),             // Asynchronous input signal
//        .sync_out(sync_out),             // Safe synchronized signal
//        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
//        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
//    );
//
//** Parameters ****************************************************************
//
//  Parameter SYNC_STAGES identifies the number of Flip-Flops to be used in
//  the synchronizer. The default value is set to 2 which is generally 
//  considered adequate for most applications.
//
//** Signal Inputs: ************************************************************
//
//  1) clk: high speed clock (typically 100 MHz) for synchronous operation
//
//  2) async_in: The input signal to be synchronized to clk
//
//** Signal Outputs ************************************************************
//
//  1) sync_out: this is a "safe" signal synchronized to clk
//
//  2) rise_edge_tick: pulse on rising edge of synchronized signal
//
//  3) fall_edge_tick: pulse on falling edge of synchronized signal
//
//** Comments ******************************************************************
//
//  N/A
//
//******************************************************************************

module synchronizer #(parameter SYNC_STAGES = 2)(
    input async_in,
    input clk,
    output reg sync_out,
    output reg rise_edge_tick,
    output reg fall_edge_tick
);

//** CONSTANT DECLARATIONS *****************************************************

    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;
        
//** BODY **********************************************************************

    reg [SYNC_STAGES-1:0] sync_regs = {SYNC_STAGES{1'b0}};  // init with zeros

    always @(posedge clk) begin
        sync_regs <= {sync_regs[SYNC_STAGES-2:0], async_in};    // shift left
        sync_out <= sync_regs[SYNC_STAGES-1];

        rise_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == T) ? T : F; 
        fall_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == F) ? T : F; 
  
    end
endmodule

tb_synchronizer.v

//`timescale 1ns / 1ps
//*************************************************************
//
// Description:
//
//    This testbench provides a stimulus to the synchronizer module.
//    The graphical signal timing diagram serves as the main
//    debugging tool.
//
//*************************************************************
module tb_synchronizer();

    /* Module Inputs */
        reg clk_1;
        reg clk_2;
        reg in;

    /* Module Outputs */
        wire sync_out;
        wire rise_edge_tick;
        wire fall_edge_tick;

    /* Testbench Specific */


//** CONSTANT DECLARATION ************************************

    /* Local */

    /* Clock simulation */
        localparam clock_1_T_ns = 10;     // 100 MHz
        localparam clock_2_T_ns = 17;     // 59 MHz
    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;

//** SYMBOLIC STATE DECLARATIONS ******************************

//** SIGNAL DECLARATIONS **************************************

//** INSTANTIATE THE UNIT UNDER TEST (UUT)*********************

    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip flop
    synchronizer(                        // Instance name
        .clk(clk_1),                     // Your clock signal
        .async_in(in),                   // Asynchronous input signal
        .sync_out(sync_out),             // Safe synchronized signal
        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
    );


//** ASSIGN STATEMENTS ****************************************

//** CLOCK ****************************************************

    always begin
        clk_1 = T;
        #(clock_1_T_ns/2);
        clk_1 = F;
        #(clock_1_T_ns/2);
    end

    always begin
        clk_2 = T;
        #(clock_2_T_ns/2);
        clk_2 = F;
        #(clock_2_T_ns/2);
    end

//** UUT Tests ************************************************ 

    initial begin

        initial_conditions();

    /* Begin tests */
        delay_N_clk_2_cycles(5);
        in = T;
        delay_N_clk_2_cycles(5);
        in = F;
        delay_N_clk_2_cycles(5);
        in = T;
        delay_N_clk_2_cycles(5);
        in = F;
        delay_N_clk_2_cycles(5);
        
//        $monitor($realtime, " count = %d", cnt);

        $finish;
    end

//** FUNCTIONS ************************************************ 

//** Tasks **************************************************** 

    task initial_conditions; begin
        repeat(5) @(posedge clk_1)
        in = F;
        end
    endtask


    task delay_N_clk_1_cycles(input integer N); begin
        repeat(N) @(posedge clk_1);
        end
    endtask
    
       task delay_N_clk_2_cycles(input integer N); begin
        repeat(N) @(posedge clk_2);
        end
    endtask
    
endmodule


原文作者: Aaron Dahlen | MSEE | Senior Applications Engineer, DigiKey