Verilogによるクロック境界シンクロナイザを実装する

APDahlen Applications Engineer

この投稿では、クロックシンクロナイザの Verilog記述について説明します。デジタル電子回路の基礎において、メタスタビリティという概念を学んだことがあるかもしれません。これは、デジタル信号が1でも0でもない不定状態に入る望ましくない状態です。クロック境界シンクロナイザの目的は、このメタスタビリティを軽減することにあります。

シンクロナイザの論理をよりよく理解するために、まずいくつかの一般的な質問に答えることから始めます。

クロック境界とは何でしょうか?

多くの論理設計は同期式であり、すべてのロジックが同じタイミングで動作することを意味します。FPGA では、これは一連のレジスタとして実装されます。データは通常、クロックの立ち上がりエッジでレジスタ間の転送が行われます。この単一のクロックと同期して動作するすべてのロジックは、同一のクロックドメインに属していると言われます。FPGA内には複数のドメインが存在する場合があります。また、外部マイクロコントローラのようにFPGAの外部にドメインが存在する場合もあります。

メタスタビリティとは何でしょうか?

明確なクロック境界を維持する主な理由の1つは、メタステーブル状態を防ぐことにあります。これは、レジスタ(フリップフロップ)が中間状態に入る状態を指します。単一のクロックドメインでは、すべての要素がクロックの立ち上がりエッジなど同じタイミングで遷移するため、この問題は軽減されます。この時点で、次の立ち上がりエッジが発生するまでの間に、すべての要素が安定するための時間が与えられます。ゲート伝搬遅延が過大でない限り、メタスタビリティが発生する可能性は低減されます。

シンクロナイザとは何でしょうか?

信号がクロック境界をまたぐ場合、問題はより複雑になります。これらの境界は、わずかに調律のずれた2つの楽器のようなものです。エッジは互いに相対的にずれていきます。立ち上がりエッジが一致する場合もあれば、大きく離れる場合もあります。境界間の信号が互いに近いタイミングにある場合に、メタスタビリティの問題が発生します。これは、受信側クロックドメインのレジスタが読み出される前に安定する時間を持たないため危険です。極端な場合、この不安定状態がクロックドメイン内に連鎖し、予測不能な動作を引き起こす可能性があります。

シンクロナイザのブロック図

シンクロナイザのVerilog実装をブロック図として表したものを図1に示します。これは典型的な設計であり、非同期信号が複数のレジスタを通過します。各レジスタは、新しいドメインのマスタークロックによって駆動されます。言い換えると、非同期信号を新しいドメインにおいて同期信号へ変換しています。ここでのレジスタ数は設計上のトレードオフになります。これは統計的な問題です。同期用レジスタを増やすほどメタスタビリティの発生確率は低減します。しかし、過度に複雑な設計はFPGAの貴重なリソースを消費し、速度低下を招きます。

図1: クロック境界をまたぐ信号を整える同期チェーン

シンクロナイザモジュールのVerilogパラメータ

ここで、この記事の末尾に添付されているVerilogコードの重要なポイントをいくつか説明します。まず、図2に示されているシンクロナイザのアスキーアート表現に目が留まるでしょう。これに続いて、インスタンス化の例と、すべての入出力信号の簡単な説明を含むヘッダが示されています。

図2: シンクロナイザのアスキーアート表現

図2の中央には、parameters and defaultsセクションがあります。パラメータはVerilogにおける非常に有用な機能の1つです。これにより、RTL(Register Transfer Level)設計の動作を、インスタンス化する時に変更できます。すべてのパラメータにはデフォルト値が定義されており、必要に応じて上書きできます。この例では、パラメータはシンクロナイザに含まれるフリップフロップの数を決定します。デフォルト値は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{1’b0}} はVerilogのレプリケーション演算子であり、sync_regsの各フリップフロップ要素をバイナリ0で初期化するための簡潔な方法です。

一見するとフリップフロップには見えないかもしれませんが、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は、シフトレジスタの最上位ビットから生成されます。ここでもノンブロッキングコードが使用されている点に注意してください。この例では、2段のシンクロナイザと1つの出力レジスタがあります。sync_outの値は3回目のクロックパルスの立ち上がりエッジで確定します。

立ち上がり/立ち下がりエッジ検出ブロックは、最後の2行のコードで実装されています。ここでは三項演算子「?」を使用しています。これはif-else文を簡潔に表現するVerilogの記法です。これらの動作は図1を参照すると理解できます。エッジトリガブロックは、出力レジスタ(最後の出力)と、これから出力される値を監視しています。クロックの立ち上がりエッジにおいて、出力レジスタの値が変化したかを判定します。それらが異なり、かつ新しい出力がHighの場合、立ち上がりエッジが発生したと判断されます。立ち下がりエッジについても同様です。

テストベンチ

ここからは視点を変え、RTLの性能について確認します。ここではテストベンチの結果および実機での動作を含みます。このハードウェアは、DigilentのBasys 3に搭載されたXilinxのArtix FPGAを対象として、VIVADOプラットフォーム上で開発されました。この点については別の機会に説明します。

テストベンチの結果は、図4に記録されています。クロック1は同期クロック、クロック2は非同期クロックです。シミュレーション周波数はそれぞれ100MHzおよび59MHzです。sync_outがin信号をclk_1の3回の立ち上がりエッジ分だけ遅れて追従していることが確認できます。これはsync_outが入力信号の複製であるかのように見えますが、clk_1の3回の立ち上がりエッジ分だけ遅延し、その期間にわたって保持された複製となっています。また、rise_edge_tickおよびfall_edge_tickがclk_1の立ち上がりエッジと同期していることにも注目してください。「tick」という用語は、1クロック周期のみHighとなるパルス信号を意味します。これらのエッジトリガ信号は後段のエッジトリガ回路で使用できます。一方、sync_out信号はレベル検出回路で使用できます。

図4: シンクロナイザ回路のテストベンチ結果

図5は、DigilentのAnalog Discovery(旧モデル)を使用して実際の信号の観測を試みた結果です。100MHzの信号は非常に短時間でありキャプチャが難しいため、明確には見えません。しかし、さまざまな遅延やパルス信号は確認できます。この静止画では確認できませんが、信号間にはジッタが観測されました。これは、テストパルスを生成したAnalog Discoveryの信号発生器が、Basys 3 ボードの100MHzクロックと同期していなかったためです。しかし、それこそが本質です。クロック境界をまたいでいますが、シンクロナイザは期待どおりに動作しています。

図5: Digilent BASYS 3 FPGA上におけるシンクロナイザの実動作

おわりに

最新のFPGA「コンパイラ」は、消費電力、速度、リソースの消費量に対してユーザーが指定した重み付けに基づきコードを最適化するよう設計されています。そのため、合成ツールがロジックの一部を削除することは珍しくありません。例えば、rise_edge_tickやfall_edge_tick出力を接続せずにシンクロナイザをインスタンス化した場合、これらは自動的に削除されます。これは未使用ロジックの警告として表示されます。

一般的には、この種の警告は無視しても問題ありません。しかし、ロジックの開発時には警告の原因を特定し、可能な限り解消するべきです。私はこのシンクロナイザの立ち上がり/立ち下がり検出ブロックを作成している時にこの問題に思いがけず直面しました。当初は、sync_outの直前値を保持するためにレジスタを使用していました。これはマイクロコントローラの世界では非常に自然な書き方です。

If (present != last) do something(値に変化があれば何らかの処理を行う)

これは妥当な解決策であり動作もしましたが、合成ツールは常にロジック削除警告を出力しました。結果として、問題の捉え方が誤っていたことが分かりました。その理由を理解するには図1を見直してください。最後のフリップフロップが特別な存在であることに気付きます。これは厳密にはシンクロナイザの一部ではなく、モジュールの出力レジスタです。FPGA内の他のロジックに対して同期安定性を提供する信号源となります。ここでいう安定とは、always @(posedge clk)ブロック内でマスタークロックにより制御されるレジスタであることを意味します。

この観点で考えると、「直前の出力」を保持するために追加していたレジスタは冗長でした。必要な信号はすでにsync_outレジスタに保持されていたためです。合成ツールはこの冗長性を検出し、警告を出していました。

この点は、図1に示された立ち上がり/立ち下がり検出ブロックの入力配置の理由を理解する助けになります。このブロックは最後のフリップフロップの入力と出力を監視しています。両者が異なる場合、適切なパルスを生成します。

ご健闘をお祈りします。

APDahlen

著者について

Aaron Dahlen氏、LCDR USCG(退役)は、DigiKeyでアプリケーションエンジニアを務めています。彼は、技術者およびエンジニアとしての27年間の軍役を通じて構築されたユニークなエレクトロニクスおよびオートメーションのベースを持っており、これは12年間教壇に立ったことによってさらに強化されました(経験と知識の融合)。ミネソタ州立大学Mankato校でMSEEの学位を取得したDahlen氏は、ABET認定EEプログラムで教鞭をとり、EETプログラムのプログラムコーディネーターを務め、軍の電子技術者にコンポーネントレベルの修理を教えてきました。彼はミネソタ州北部の自宅に戻り、このような記事のリサーチや執筆を楽しんでいます。




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




オリジナル・ソース(English)