非同期信号を扱うための危ういVerilogライブラリ の履歴(No.15)

更新


公開メモ

ご注意

※ 以下は練習のために書いてみている回路です。
しっかりしたテストを経ていないので、鵜呑みにしないで下さい。
あまり役に立たない個人的なメモになってしまっていてすみません。

クロックに同期していない信号を扱う際の注意点

デジタル回路設計で、クロックに同期していない信号、 あるいは別のクロックに同期した信号を扱う必要があるときには、 十分な注意を払わないと、予想しない結果が生じて慌てることになります(なるそうです・・・)。

では何に注意すればよいかというと、

  • メタステーブル状態の伝搬を考慮する

です。

メタステーブル状態

メタステーブル状態とは、簡単に言うと あるラッチあるいはフリップフロップ(FF)の出力が1つの値に定まらず、 ふらついている状況を表す言葉です。

どうしてそんなことが起きるのか。

セットアップ時間とホールド時間

例えばDフリップフロップ(D−FF)は、 クロックの立ち上がりエッジ(あるいは立ち下がりエッジ)に 同期して入力信号を読み取り、次のエッジが来るまでその値を保持します。

理想的には、エッジの「直前の一瞬」の値を読み取って、 エッジの「直後」から正しい値を出力するのですが・・・。

実際のFFは「一瞬」で値を読み取れるわけではないため、 エッジの前後の短い時間の間に入力信号が変化してしまうと、 正しい値を読み取れず、出力が不定になってしまいます。

そこで、FFを正しく使うためにはクロックエッジ前後のある一定期間に 入力信号が変化しないよう回路を組みます。 一方FFは、その一定期間に入力信号が変化しないときに限って動作が保証されています。

FFが正しく動作するために、
クロックエッジの「直前」に設けられた入力信号の変化禁止時間を最小セットアップ時間、
クロックエッジの「直後」に設けられた入力信号の変化禁止時間を最小ホールド時間、
とそれぞれ呼びます。

入力信号がこれらの最小値よりも大きなセットアップ時間・ホールド時間を持つときにのみ FFが正しく動作する、と言い換えてもOKです。

FFに同期信号が入力される場合

下図で説明します。

metastable.png

上段部分はFFに入力されるクロック信号です。

中段はクロックの立ち上がりで動作する D−FF に、 クロックに同期した信号 (sync) が入力される状況を表しています。

入力信号はクロックの立ち上がりに同期して直前のラッチから出力されますが、 いくつかのゲートや配線を通るため、無視できない量の遅延時間の後にFFの入力に到達します。

この遅延時間は、信号経路に含まれる(複数の)ゲートを通過するのに掛かる時間と、 それらを繋ぐ配線を通過するのに掛かる時間の和になります。

クロック周期から遅延時間を引き算したものがセットアップ時間になります。 回路が複雑になり、あるいは信号経路が長くなりすぎて、遅延時間が大きくなると、 直後のFFのセットアップ時間が短くなり、もし最小セットアップ時間を下回ると、 素子の動作がおかしくなります。

一方でホールド時間については、入力が同期信号で限り通常あまり考慮する必要はありません。 というのも、ラッチの出力はクロックエッジと同時に変化する訳では無く、 内部に有限のゲート遅延を持つためです。FFに規定される最小ホールド時間は非常に短いため、 大抵FFの出力遅延だけでホールド時間の制約を満たしてしまいます。

FFに非同期信号が入力される場合

上図の下段はクロックに同期しない信号が入力される場合です。

この場合、入力信号はクロックタイミングとは無関係に変化します。 したがって、最小セットアップ時間あるいは最小ホールド時間の規定に違反して、 クロックエッジ付近で値が変化する状況が起きてしまいます。

セットアップ時間やホールド時間が守られず、エッジ付近で入力信号が変化すると、 FFが不安定な状況に陥り、出力信号の値が比較的長い時間(数 ns 程度)に渡り、 ばたばたと振動してしまうことがあります。

図では黄色の矢印の時刻にセットアップ時間の違反が起きており、 そのせいで次クロックの出力値が長い間発信している様子を書いているつもりです。 この振動は数 ns の時間を掛けて徐々に 0 に収まっていきます。

このように出力信号がふらついている間に別のラッチがこの信号を読もうとすると、 その結果が1と読まれるか0と読まれるか定まらないことになります。 この状態が、悪名高い(?)メタステーブル状態です。

メタステーブル状態の引き起こす問題

一見すると、 「そもそも1つめのFFが0と1とを切り替わる最中に値を読んだのだから、 出力が0でも1でもどちらに解釈されたって大きな差は無いじゃん」 という結論になってしまいそうな所です。

しかし、実際にはメタステーブル状態は2つの意味で問題を引き起こします。

  • 後段に複数のFFがあると、それぞれが異なる値となる場合がある
  • 後段のFFにもメタステーブル状態が伝播する場合がある

後段に複数のFFがある場合

selector.png

例えばこの回路は、外部からの入力を1段のFFで受けて、 その出力で4ビットのレジスタAとBのどちらの値を出力するかを選ぶものです。

外部からの入力が非同期の場合、初段のFFでメタステーブル状態が発生する可能性があります。

初段FFがメタステーブル状態になると、4つのセレクタは同じ素子に繋がれているにもかかわらず、 配線遅延の微小な差違により、入力を1と読むものと0と読むものの両方が出現する可能性があります。

すると出力はAでもBでもなく、ビット単位でAとBとを混ぜ合わせた値となってしまいます。

この例から分かるように、下流に複数の素子が繋がれた場合、 本来一斉に入力値が変化することを期待して設計された素子に それぞれ別の値が入力されたかのように振る舞い、 論理が破綻してしまう危険性があります。

後段のFFにメタステーブル状態が伝播する

メタステーブル状態になりうるFFに1つの素子しか繋がっていなければ、 前項のような問題は起きません。しかし、次のFFまでの遅延が大きいとき、 後段のFFにまでメタステーブル状態が伝播する危険性があります。

前提として、あるFFがメタステーブル状態になったとしても、 その状態は比較的速やかに1か0の値に収束し、定常状態に戻る考えることができます。

しかし後段のFFから見ると、実際にメタステーブル状態が収束してから、 さらに経路の遅延時間が経過しないと入力が定まらないため、 配線遅延が大きいときにはメタステーブル状態が長続きしているように見えるわけです。

すると結果として最小ホールド時間違反が置き、 メタステーブル状態が伝播してしまう可能性が生じます。

非同期信号を2段FFで受ける

上記のような問題を克服するには定石があって、 非同期信号を入力するときはFFを2つ繋いでおけばよいそうです。

double-ff.png

LANG:verilog
module double_ff(
    (* IOB="FALSE", TIG="TRUE" *) input idata,
    input oclk,
    output odata
);
    (* IOB="FALSE", RLOC="X0Y0", ASYNC_REG="TRUE" *) reg temp1;
    (* IOB="FALSE", RLOC="X0Y0"                   *) reg temp2;
    always @(posedge oclk) begin
        temp1 <= idata;
        temp2 <= temp1;
    end
    assign odata = temp2;
endmodule

図にもあるように、1つ目のFFでメタステーブルが発生することはどうやっても防げませんが、 2つ目のFFを繋ぐ信号線を十分に短く、遅延時間を少なくしておけば、 2つ目のFFのセットアップ時間までにはメタステーブル状態が収束し、 入力値が定まっていることが期待できます。

この2段FFの形は非同期信号を受け渡す時の定石として様々な回路に現れます。

注意点

忘れてはいけないのは、2つのFF間の配線が長い場合、 この回路はメタステーブルを取り除く役には立たないという点です。 遅延時間が長ければ、1つ目のFFのメタステーブルが2つ目に伝播して、 2つ目もメタステーブルになってしまう可能性が高くなります。

「2重FF回路では、2つのFFを可能な限り近くに配置する」 を鉄則として覚えておかなければなりませんん。

このために入れてあるのが、上記コードの IOB 制約と RLOC 制約です。 2つのFFが必ず同じスライスに入るように設定しています。

ISE の Language Templates にあるコードでは RLOC の代わりに HBLKNM で配置制約を掛けようとしているのですが、Spartan 3A DSP 向けに実際に コンパイルしてみたところ WARNING:ConstraintSystem:119 が出てうまく行きませんでした。 http://japan.xilinx.com/support/answers/34088.htm によれば、 「HBLKNM をネットに設定すると、パッドにしか伝搬されません」 とのことなので、ここでは HBLKNM ではなく RLOC で指定しています。

ASYNC_REG は、シミュレーション方法を規定するための制約です。 通常、シミュレーション中にセットアップ・ホールド時間違反が起きた場合、 FFの出力は不定値 X になります。その結果、多くの場合に下流の回路は X だらけとなり、以降のシミュレーションができなくなってしまいます。 ASYNC_REG が付いていると、セットアップ・ホールド時間違反が起きた場合、 書き込み前の値が保持されるため X は発生せず、シミュレーションに支障をきたしません。

ただこの場合にも、 セットアップ・ホールド時間違反が起きた旨のメッセージがログに出力されるため、、 シミュレーションの方針によってはこの種のエラーが数百も溜まってしまいます。 これでは他の重要なエラーが見づらくなってしまうので、 どうにかならないものか、まだ解決策を探しているところです。

困ったこと

  上記の double_ff を使っていると、map 中に以下のエラーが出ることがありました。

ERROR:Pack:2811 - Directed packing was unable to obey the user design
   constraints (MACRONAME=writer_tx_req_ff, RLOC=X0Y0) which requires the
   combination of the symbols listed below to be packed into a single SLICE
   component.

   The directed pack was not possible because: The two registers have different
   set/reset signals.
   The symbols involved are:
   	FLOP symbol "writer/tx_req_ff/temp1" (Output Signal = writer/tx_req_ff/temp1)
   	FLOP symbol "writer/tx_req_ff/temp2" (Output Signal = tx_req<1>)

Mapping completed.
See MAP report file "main_map.mrp" for details.
Problem encountered during the packing phase.

リセットを使っているわけではないので、エラーの出る理由が分からないのですが・・・

LANG:verilog
module double_ff(
    input rst,
    (* IOB="FALSE", TIG="TRUE" *) input idata,
    input oclk,
    output odata
);
    wire temp;

    (* RLOC="X0Y0", ASYNC_REG="TRUE" *)
    FDRSE #(
        .INIT(1'b0)     // Initial value of register (1'b0 or 1'b1)
    ) FDRSE_temp0 (
        .Q(temp),       // Data output
        .C(oclk),       // Clock input
        .CE(1'b1),      // Clock enable input
        .D(idata),      // Data input
        .R(1'b0),       // Synchronous reset input
        .S(1'b0)        // Synchronous set input
    );

    (* RLOC="X0Y0" *)
    FDRSE #(
        .INIT(1'b0)     // Initial value of register (1'b0 or 1'b1)
    ) FDRSE_temp1 (
        .Q(odata),      // Data output
        .C(oclk),       // Clock input
        .CE(1'b1),      // Clock enable input
        .D(temp),       // Data input
        .R(1'b0),       // Synchronous reset input
        .S(1'b0)        // Synchronous set input
    );

endmodule

のように FF を直接インスタンス化してやるとエラーはなくなったのですが、 ちょっと腑に落ちないところです。また、これで思った通りの回路になっているのかどうか、 もう一度調べようと思っています。

もう1つ気づいた点

ISE の Language Template では、ダブル FF ではなく、トリプル FF になっているのですね。 

LANG:verilog
   (* ASYNC_REG="TRUE", SHIFT_EXTRACT="NO", HBLKNM="sync_reg" *) reg [1:0] sreg;                                                                           
   always @(posedge clk) begin
      sync_out <= sreg[1];
      sreg <= {sreg[0], async_in};
   end

async_int => sreg[0] => sreg[1] => sync_out の3段階。

その方が良いんですかね???

重要な指針

これまでの考察で、以下の重要な指針を得ました。

  1. 非同期信号をそのまま使うな
  2. 非同期信号を1段FFに通しただけの信号は後段のつなぎ方に気をつけろ
    • 複数のラッチを繋ぐような使い方はNG
    • 1つだけFFを繋ぐなら問題ない
    • 1つだけFFを繋ぐときも遅延が長いとNG
  3. 非同期信号を2段FFに通せば十分安定

複数の信号を一度に入力する場合

上記回路を次のように複数信号に拡張しても良いことはありません。

LANG:verilog
module double_ff #(           // この回路はバグの元
    parameter DATA_BITS = 8
) (
    input clk,
    input [DATA_BITS-1:0] idata,
    output [DATA_BITS-1:0] reg odata
);
    reg [DATA_BITS-1:0] temp;
    always @(clk) begin
        temp <= idata;
        odata <= temp;
    end
endmodule

外部からの信号が最小セットアップ・ホールド時間に違反するタイミングで切り替わると、 個々のビットがそれぞれ少しだけ異なるタイミングで読まれるため、 全体として意味のある値を読み取ることができないのです。

このように複数の信号線を一括して受け渡すために、次の回路があります。

異なるクロック間で複数の信号線を受け渡す

異なるクロック間で複数の信号線を受け渡すには、 ハンドシェイクと呼ばれる手法を用います。

方針は「値を読み取る間だけ書き込みをストップしておく」 という単純な話なのですが、「いつ書き込んで、いつ書き込みを止めるか」 をうまく制御するための機構が「ハンドシェイク」と呼ばれる 手旗信号のようなやりとりです。

最も基本的な手順は以下の通りです。

  1. 出力側で送信要求 (req) を上げる
  2. 入力側は req を2段FFで受取る
  3. 入力側は req に従い temp の値を更新する
  4. 入力側で送信応答 (ack) を上げる
  5. 出力側は ack を2段FFで受取る
  6. 出力側は ack を見て temp から値を読み取る
  7. 出力側は req を下げる(読み取り終了通知となる)
  8. 入力側は req を2段FFで受取る
  9. 入力側は req が下りたら ack を下げる(次に備える)
  10. 出力側は ack を2段FFで受取る
  11. 出力側は ack が下りたら 1. へ戻る

実際の回路は、7. 以降の req や ack を下げるところでもう1つデータを送受信するため、 単純なハンドシェイクに比べてデータ更新の頻度が2倍に上がっています。

動作タイミング図

synchronize.png

  • req -> req1 -> req2 の2段FFは req2 が確定信号なので、これを元に temp を更新する
  • temp 更新と同じタイミングで ack を立てる
  • ack -> ack1 -> ack2 の2段FFは ack2 が確定信号なので、これを元に odata を更新する
  • odata 更新と同じタイミングで req を下げる
  • req -> req1 -> req2 の2段FFは req2 が確定信号なので、これを元に temp を更新する
  • temp 更新と同じタイミングで ack を下げる
  • ack -> ack1 -> ack2 の2段FFは ack2 が確定信号なので、これを元に odata を更新する
  • odata 更新と同じタイミングで req を上げる

の繰り返しです。

iclk の3周期と oclk の3周期を足した程度の頻度に一回、 入力側から出力側へ値が渡されます。

Verilog コード

インスタンシエーション時にパラメータでバス幅を変えられるので、 いろんな用途に使えると思います。

LANG:verilog
module synchronize #(
    parameter DATA_BITS = 8
) (
    input rst,
    input iclk,
    (* TIG="TRUE" *) input [DATA_BITS-1:0] idata,
    input oclk,
    output reg [DATA_BITS-1:0] odata
);
    (* TIG="TRUE" *) reg [DATA_BITS-1:0] temp;

    reg req;
    reg ack;

    wire oclk_ack;
    double_ff ack_ff ( .idata(ack), .oclk(oclk), .odata(oclk_ack) );
    
    always @(posedge oclk)
        if (rst) begin
            req <= 1;
        end else begin
            if ( req == oclk_ack ) begin
                req <= !req;
                odata <= temp;  // interclock signal "temp"
            end
        end
        
    wire iclk_req;
    double_ff req_ff ( .idata(req), .oclk(iclk), .odata(iclk_req) );
    
    always @(posedge iclk)
        if (rst) begin
            ack <= 0;
        end else begin 
            if ( ack != iclk_req ) begin
                ack <= !ack;
                temp <= idata;
            end
        end
        
endmodule

トリガ信号の伝達

クロックを越えて、トリガ信号を確実に伝えるためのモジュールです。

itrig に iclk に同期した正論理のトリガが入ると、
otrig に oclk に同期した1クロック幅のトリガを生じます。

クロックドメイン境界の向こう側にあるステートマシンを起動する用途に使います。

動作タイミング図

interclock_trig.png

やりたいことの単純さから考えると、かなり重たい動作になってます。

もっと手軽な方法がありそうな予感・・・

Verilog コード

LANG:verilog
module interclock_trig(
    input iclk,
    input itrig,
    input oclk,
    output otrig
);
    (* RLOC="X0Y0" *) reg itrig_ex;
    (* TIG="TRUE" *) wire itrig_ex2;
    double_ff req_ff ( .idata(itrig_ex), .oclk(oclk), .odata(itrig_ex2) );
    
    (* RLOC="X0Y0", ASYNC_REG="TRUE" *) reg itrig_ex3;
    always @(posedge iclk)
        itrig_ex3 <= itrig_ex2;

    always @(posedge iclk)
        if (itrig)
            itrig_ex <= 1;
        else
        if(itrig_ex3)
            itrig_ex <= 0;

    reg otrig1, otrig2;
    always @(posedge oclk) begin
        otrig1 <= itrig_ex2;
        otrig2 <= itrig_ex2 & !otrig1;
    end
    assign otrig = otrig2;

endmodule

itrig_ex2 -> itrig_ex3 -> itrig_ex も2段FFの役割を果たしているので、 制約を加えて itrig_ex3 と itrig_ex とが近くに配置されるようにしています。

A -> B -> C の2段FFでは、A に TIG、B と C とに配置制約、 と覚えてしまうのが良さそうですね。

改良案

上記回路はクロック同期のFFだけを使っていたせいでフラグを下げるところの動作が複雑になっていました。 ラッチをうまく使うことで単純化できました。

interclock_trig2.png

まだ工夫の余地はありそうですね。

Verilog コード

LANG:verilog
module interclock_trig(
    input iclk,
    input itrig,
    input oclk,
    output otrig
);

    reg itrig1;
    always @(posedge iclk)
        itrig1 <= itrig;

    // SR ラッチ
    reg itrig_ex = 1;
    wire oclk_itrig_ex;
    always @(*)
        if (itrig1)
            itrig_ex = 0;
        else
        if (!oclk_itrig_ex)
            itrig_ex = 1;

    double_ff itrig_ff ( .idata(itrig_ex), .oclk(oclk), .odata(oclk_itrig_ex) );
    
    reg oclk_itrig_ex1;
    always @(posedge oclk)
        oclk_itrig_ex1 <= oclk_itrig_ex;

    assign otrig = !oclk_itrig_ex & oclk_itrig_ex1;

endmodule

クロックを越えてステートマシンを起動し、終了も検知

1つ上の方法でトリガ信号を送ることができますが、 そのトリガで開始したタスクが終了したことも分かるようにしたいです。

動作タイミング図

interclock_execute.png

のようにして、制御側への busy 信号は busy0 と busy5 との論理和で与えれば・・・

と、考えていたのですが、interclock_trig を2個使っちゃえば楽ですね。

Verilog コード

ほんのちょっとのリソースを省くために、 新しいモジュールを作ってテストするのはあまりメリットが無さそうなので、 start 信号の伝達と、done 信号の伝達とに、 それぞれ interclock_trig を使ってしまうことに決めました。

非同期 FIFO

2つのクロックドメイン間で連続するデータを次々と受け渡すのに使います。

通常の同期 FIFO と同様にリングバッファのようなものを作るのですが、 Full や Empty 信号を生成するのに書き込み・読み出しカウンタを クロックドメイン間で受け渡す必要があり、その部分の実装が大変だとのことです。

http://marsee101.blog19.fc2.com/?no=1085 で marsee さんが書かれているのと (恐らく)ほぼ同じ方針で作りました。

すなわち、

  • 通常の書き込み・読み出しはバイナリカウンタを使う
  • クロックドメイン間でのカウンタの受け渡しのためにカウンタのグレイコード表現も持っておく
  • グレイコード表現を2段FFで受け渡す
  • グレイコード表現をバイナリ表現に戻す
  • もう一方のカウンタと比較することで、empty / full 信号を生成する

という具合です。

グレイコードはインクリメント、 デクリメントで信号線の値が1本ずつしか変化しないので、 メタステーブルさえ防げば読み出しタイミングを気にする必要が無いという特徴を持っています。

グレイコードとバイナリコードの相互変換アルゴリズムは:
http://blog.livedoor.jp/k_yon/archives/51619205.html
を参考にさせていただきました。

Verilog コード

カウンタ比較をバイナリで行っているので、almost full や almost empty なども簡単に追加できます。

full / empty の出力はFFから直なので遅延が小さくなっています。
代わりに we や re へのセットアップ時間が多少きつめかもしれません。

LANG:verilog
module async_fifo #(
    parameter DATA_BITS = 8,
    parameter DEPTH_BITS = 11
) (
    input rst,
    input iclk,
    input [DATA_BITS-1:0] idata,
    input we,
    output reg full,
    input oclk,
    output reg [DATA_BITS-1:0] odata,
    input re,
    output reg empty
);

    // バイナリコード・グレイコードの相互変換

    function [DEPTH_BITS:0] binary2gray;
        input [DEPTH_BITS:0] binary;
        binary2gray = 
            { binary[DEPTH_BITS], binary[DEPTH_BITS-1:0] ^ binary[DEPTH_BITS:1] };
    endfunction

    function [DEPTH_BITS:0] gray2binary;
        input [DEPTH_BITS:0] gray;
        reg [DEPTH_BITS:0] temp, result;
        begin
            temp = gray;
            result = gray;
            while (|temp) begin
                temp = temp >> 1;
                result = result ^ temp;
            end
            gray2binary = result;
        end
    endfunction

    // メモリ

    reg [DATA_BITS-1:0] mem [0:2**DEPTH_BITS-1];
    
    // ポインタ変数
    
    // 深さを 2**DEPTH_BITS-1 ではなく 2**DEPTH_BITS とするために
    // 最上位ビットをフラグとして使っている。
    
    // epmty : rp == wp
    // full  : rp == wp ^ ( 1 << DEPTH_BITS )
    
    reg [DEPTH_BITS:0] rp0; // 現在値
    reg [DEPTH_BITS:0] rp1; // +1
    reg [DEPTH_BITS:0] rpg; // グレイコード
        
    reg [DEPTH_BITS:0] wp0; // 現在値
    reg [DEPTH_BITS:0] wp1; // +1
    reg [DEPTH_BITS:0] wpg; // グレイコード
    
    // ビット幅を指定した 0 と 1
    
    wire [DEPTH_BITS:0] zero = {DEPTH_BITS{1'b0}};
    wire [DEPTH_BITS:0] one  = { {DEPTH_BITS-1{1'b0}}, 1'b1 };
    
    // メモリの読み書き
    // full / empty のチェックは外部回路で行う
    
    always @(posedge iclk)
        if (!rst & we)
            mem[wp0[DEPTH_BITS-1:0]] <= idata;

    wire mem_ce;
    always @(posedge oclk)
        if (mem_ce)         // empty 時に読み出すと書き込みとかぶるので回避する
            odata <= mem[re ? rp1[DEPTH_BITS-1:0] : rp0[DEPTH_BITS-1:0]];

    // カウンタ操作

    always @(posedge iclk)
        if (rst) begin
            wp0 <= zero;
            wp1 <= one;
            wpg <= binary2gray(zero);
        end else 
        if (we) begin
            wp0 <= wp1;
            wp1 <= wp1 + one;
            wpg <= binary2gray(wp1);
        end

    always @(posedge oclk)
        if (rst) begin
            rp0 <= zero;
            rp1 <= one;
            rpg <= binary2gray(zero);
        end else 
        if (re) begin
            rp0 <= rp1;
            rp1 <= rp1 + one;
            rpg <= binary2gray(rp1);
        end
        
    // 2段FFによるポインタの受け渡し
    // 各FFに配置制約を付けるためビット毎に操作している

    wire [DEPTH_BITS:0] iclk_rpg;
    genvar rpg_i;
    generate
        for (rpg_i=0; rpg_i<=DEPTH_BITS; rpg_i=rpg_i+1) begin: rpg_for
            double_ff rpg_ff( .idata(rpg[rpg_i]), .oclk(iclk), .odata(iclk_rpg[rpg_i]) );
        end
    endgenerate
    
    wire [DEPTH_BITS:0] oclk_wpg;
    genvar wpg_i;
    generate
        for (wpg_i=0; wpg_i<=DEPTH_BITS; wpg_i=wpg_i+1) begin: wpg_for
            double_ff wpg_ff( .idata(wpg[wpg_i]), .oclk(oclk), .odata(oclk_wpg[wpg_i]) );
        end
    endgenerate
    
    // グレイコードをバイナリコードに変換
    
    reg [DEPTH_BITS:0] iclk_rp0;
    always @(posedge iclk)
        iclk_rp0 <= gray2binary(iclk_rpg);

    reg [DEPTH_BITS:0] oclk_wp0;
    always @(posedge oclk)
        oclk_wp0 <= gray2binary(oclk_wpg);

    // 次クロックのための full / empty 信号を計算

    wire [DEPTH_BITS:0] mask = ( one << DEPTH_BITS );
    
    wire next_full = we ? ( ( iclk_rp0 ^ mask ) == wp1 ) :
                          ( ( iclk_rp0 ^ mask ) == wp0 ) ;
    always @(posedge iclk)
        if (rst)
            full <= 1'b0;
        else
            full <= next_full;
           
    wire next_empty = re ? ( rp1 == oclk_wp0 ) :
                           ( rp0 == oclk_wp0 ) ;
    always @(posedge oclk)
        if (rst)
            empty <= 1'b1;
        else
            empty <= next_empty;

    // empty 時に読み出すと書き込みとかぶるので回避する
    assign mem_ce = !next_empty;
 
endmodule

テストベンチ

エラーメッセージの表示は、ISim では $display を ModelSim では $messagelog を使うようになっています。

LANG:verilog
`define SETUP 1:1:1
`define HOLD  0:0:0
`ifdef XILINX_ISIM
    `define ERROR $display
`else
    `define ERROR $messagelog
`endif

module async_fifo_test;

    localparam DATA_BITS = 8;   // データ幅
    localparam DELAY = 2;       // 信号遅延

    reg rst;
    reg iclk;
    reg oclk;
    
    wire                 #DELAY full;
    wire                 #DELAY we;
    wire [DATA_BITS-1:0] #DELAY idata;

    wire                 #DELAY empty;
    wire                 #DELAY re;
    wire [DATA_BITS-1:0] #DELAY odata;
    
    // Instantiate the Unit Under Test (UUT)
    async_fifo uut (
        .rst(rst), 
        .iclk(iclk), 
        .idata(idata), 
        .we(we), 
        .full(full), 
        .oclk(oclk), 
        .odata(odata), 
        .re(re), 
        .empty(empty)
    );

    // 書き込み側のモック
    tranceiver_mock #(.DATA_BITS(DATA_BITS)) tx (
        .rst(rst),
        .clk(iclk),
        .full(full),
        .data(idata),
        .we(we)
    );
    
    // 読み出し側のモック
    receiver_mock #(.DATA_BITS(DATA_BITS)) rx (
        .rst(rst),
        .clk(oclk),
        .empty(empty),
        .re(re),
        .data(odata)
    );
    
    // クロック生成

    integer CLKA = 10;
    integer CLKB = 10;
   
    always #CLKA iclk = !iclk;
    always #CLKB oclk = !oclk;
    
    initial begin
        // Initialize Inputs
        rst = 1;
        iclk = 0;
        oclk = 0;

        // Wait 100 ns for global reset to finish
        #100;
        
        // Add stimulus here

        #500; // 初期化時間
        
        // タイミングを測ってリセットを下ろす
        wait (iclk==1 && oclk==1)
            rst = 0;
        
        // いくつかのクロックの組み合わせに対してシミュレート
        
        CLKA = 10; CLKB = 10.0; #1000000;
        CLKA = 10; CLKB = 10.1; #1000000;
        CLKA = 11; CLKB = 10.3; #1000000;
        CLKA = 10; CLKB = 30.1; #1000000;
        CLKA = 30; CLKB = 10.1; #1000000;
        $stop;
    end

endmodule

// 送信側モック
module tranceiver_mock #(
    parameter DATA_BITS = 8
) (
    input rst,
    input clk,
    input full,
    output we,
    output reg [DATA_BITS-1:0] data
);
    // 入力信号タイミングチェック
    specify
        $setuphold (posedge clk, full, `SETUP, `HOLD);
    endspecify

    // ランダムなタイミングで
    // 0から1ずつ増やしながら書き込む

    reg rand;
    always @(posedge clk)
        rand <= ($random & 1) == 1;

    assign we = !rst & !full & rand;

    always @(posedge clk) 
        if ( rst ) 
            data <= 0;
        else
        if ( we ) 
            data <= data + 1;

endmodule

// 受信側モック
module receiver_mock #(
    parameter DATA_BITS = 8
) (
    input rst,
    input clk,
    input empty,
    output re,
    input [DATA_BITS-1:0] data
);
    // 入力信号タイミングチェック
    specify
        $setuphold (posedge clk, empty, `SETUP, `HOLD);
        $setuphold (posedge clk, data, `SETUP, `HOLD);
    endspecify

    // ランダムなタイミングで読み出し
    // 値が 0 から順に増えていくことを確認

    reg rand;
    always @(posedge clk)
        rand <= ($random & 1) == 1;

    assign re = !rst & !empty & rand;

    reg [DATA_BITS-1:0] expected = 0;
    always @(posedge clk) begin
        if (rst) begin
            expected <= 0;
        end else begin
            if ( re ) begin
                // 書き込んだデータが正しく読み出せているかチェック
                if ( data != expected )
                    `ERROR("*** ERROR@%0t : for odata, %d expected but %d found", $time, expected, data);
                expected <= expected + 1;
            end
        end
    end
endmodule

全体を通じての方針

通常、FPGA の外から来る信号は基本的にクロック非同期になるため、必ず double_ff モジュールを通してから内部で使うようにします。

FPGA の外から来る信号があるクロックに同期した信号であり、 そのタイミングを OFFSET IN 制約で記述した場合に限って、 クロック同期信号として通常通り扱うことができます。

FPGA 内部の異なるクロックドメイン間でデータの受け渡しをする場合、 上記のような非同期信号用のモジュールを使うようにします。

それらのクロックが1つのクロックから DCM で生成される場合には、 XST が正しくタイミング解析をしてくれるます。 「危うい」データの受け渡しにクロック周期制約違反が出ますので、 上記非同期信号用モジュールを挟むことで制約違反を消せば安全です。

http://marsee101.blog19.fc2.com/blog-entry-27.html のように、インタークロック信号すべてに一括で TIG 制約を掛ける方法は、制約違反を消すための手軽な方法ではあるものの、 危険な受け渡しを見過ごしてしまう可能性が高まるため、 余程自信があるときだけにしたほうが良さそうです。

複数のクロックが外部から供給されているとき、 それらの間の受け渡しには制約違反は出ませんので、 コードを丹念に調べて、危うい受け渡しがないことを確認する必要があるのだと思います。

コメント

  • ブログでは、非同期FIFOで異なるクロックから信号を受渡しているので、一括でTIG制約をかけても問題ないと思います。 -- [marsee]
  • そうですね。時と場合でいろいろな方法があって、何がよいのか常に迷っているような状況です・・・ -- [武内(管理人)]

Counter: 167262 (from 2010/06/03), today: 16, yesterday: 7