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

更新


公開メモ

ご注意

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

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

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

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

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

です。(あと、レーシングの問題も絡んできます)

メタステーブル状態

メタステーブル状態とは、簡単に言うと あるラッチあるいはフリップフロップ(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(linenumber)
// この回路は初期のもので下に問題点と改善案が書かれています
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(linenumber)
// この回路は初期のもので下に問題点と改善案が書かれています
module double_ff(
    (* 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)
    ) double_ff_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)
    ) double_ff_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(linenumber)
   (* 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段階。

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

動作が怪しいです

上記のようにプロジェクトのそこかしこで使うユーティリティモジュールを設計する場合、 コード中に RLOC や TIG などの制約を埋め込んでおけることは大いに助けになります。

そうでなければ、数十個のインスタンス全てに対して、対応する制約を .ucf ファイルに書き込まなければならないからです。

TNM などもこの方法で掛けられれば本当にいろいろ応用が広がるのですが、 現時点ではそれは許されていないようですね。

ただ、いろいろ試していると、上記のようにコード中に記載する制約は かゆいところまで手が届かないばかりでなく、 Xilinx のツールでは何かの拍子に正しく機能しなかったりすることもあるようです。

特に、上記 double_ff の TIG については ISE の最新版の 12.1 を使ってさえ なぜか無視される条件があるようなのです。
(後で時間を見つけて再現する条件を探ってみようと思います => コード中に制約を書くときの注意点 にまとめました)

しかたがないので、現時点ではソース中の TIG は取り除いて、

LANG:xlinix_xcf
INST "*/double_ff_temp0" TNM = "double_ff_temp0";
TIMESPEC TS_DOUBLE_FF = TO "double_ff_temp0" TIG;

のようにして、.ucf ファイル中で一括して制約を掛ける方法を選択しています。

インスタンス名に特徴的な名前付けをしておくことで、 アスタリスク "*" を用いた指定で上手に制約を掛けることができます。

.ucf で記述する場合、TIG を TO で指定できるのがいい感じで、 また、今のところこの書き方であれば「制約が効いていない(怒」となることはないようです。

ただ、恐らくこの方法では RLOC を掛けるのは無理、、、ですよね。

RLOCは制約が無視されていても目で見て分かるようなエラーにならないので、 本当に制約が効いているかどうか、節目毎に確認する必要がありそうです? ・・・すべてのインスタンスについて確認するのは気が遠くなりますね。

FROM-TO 制約で遅延量を指定しておいた方が良いのかもしれません。 それなら "*" でうまく制約を掛けられますし。

LANG:xilinx_xcf
INST "*/double_ff_temp0" TNM = "double_ff_temp0";
INST "*/double_ff_temp1" TNM = "double_ff_temp1";
TIMESPEC TS_DOUBLE_FF = TO "double_ff_temp0" TIG;
TIMESPEC TS_DOUBLE_FF = FROM "double_ff_temp0" TO "double_ff_temp1" ??ns;

のような具合です。

現時点での最善案をまとめました

電気回路/HDL/コード中に制約を書くときの注意点 に上記 double_ff に対する制約の与え方について考察した結果と、現時点での最善案をまとめました。

重要な指針

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

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

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

上記回路を次のように複数信号に拡張しても、 必ずしも正しくデータが受け渡されないことに注意が必要です。

LANG:verilog(linenumber)
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(linenumber)
module synchronize #(
    parameter DATA_BITS = 8
) (
    input irst,
    input iclk,
    input [DATA_BITS-1:0] idata,
    input orst,
    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 (orst) 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 (irst) begin
            ack <= 0;
        end else begin 
            if ( ack != iclk_req ) begin
                ack <= !ack;
                temp <= idata;
            end
        end
        
endmodule

このコードの idata についている TIG 制約は、TIG に関する問題点 に書かれているように上流のピンまで遡って効力を発揮してしまうため、かなり危険です。後でこの点を改善したコードを載せようと思います。

すみません、そもそも idata に TIG は必要なかったですね。 idata の TIG 制約を取り除いた上記のコードで問題は解決されていると思います。

トリガ信号の伝達

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

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

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

動作タイミング図

interclock_trig.png

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

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

Verilog コード

LANG:verilog(linenumber)
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(linenumber)
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

さらに改良できないか考察

コメント欄でアプロさんから ジョンソンカウンタ を用いた ステートマシンへの入力は同期化しなくても良い場合があるため、2ビットのジョンソン カウンタを使ったら回路を改善できるのではないか、というような(?)示唆をいただきました。

ジョンソンカウンタの通常遷移ではカウンタ値が1ビットずつしか変化しないため、 非同期信号を1つのFFで受ける形と見なすことができます。

さらに、その前後のステートで出力値が変化しない設計であれば、 次クロックでも1つのカウンタビットにしか影響が及ばないため、 結果として2重 FF で受けているのと同じ形になるわけです。

このようなわけで、ジョンソンカウンタを用いたステートマシンには 以下の3つの条件を満たす限り、非同期信号を直接入力できます。

  • その遷移と次の遷移が共に通常遷移(スキップなし)であること
  • その遷移の前後でステートから計算される出力値が変化しないこと
  • その遷移で変化するビット値から、次の遷移で変化するビット値までの 信号遅延が十分に小さいこと

そのあたりを踏まえて、上記 改良案 について少々考察してみました。

ジョンソンカウンタが含まれていた

ちょっと考えて気づくのは、タイミングチャートの { itrig_ex2, itrig_ex1 } は そのまま2ビットのジョンソンカウンタと同じ動作をしていることです。

S0S1S2S3
itrig_ex11001
itrig_ex21100

つまり、現状ですでに 「2ビットジョンソンカウンタによるステートマシンに非同期信号を入れる回路」 に非常に近い動作になっていますので、何か改善できるとすれば、 それはステートマシンとは別の部分になるのかもしれません?

さらに改善できる余地があるかどうか、 上記回路の各部の意味を考えながら考察してみました。

itrig1 について

itrig には iclk に同期した任意の信号を繋ぐことができるようにしたいです。

すなわち、iclk には スタティックハザード が載る可能性があるため、スタティックハザードで生じるごく短時間のパルスで otrig にトリガを生じないようにするためのクロック同期化回路が必要となり、 それが itrig1 の役割になっています。

itrig1 の出力はスタティックハザードフリーになるので、 これを oclk ドメインに伝えます。

itrig_ex について

itrig_ex は、

  • itrig1 で下がる
  • otrig が生じ、なおかつ itrig1 が上がったら上がる

という動作をします。

これには以下の2つの意味があります。

itrig1 のパルスが確実に oclk ドメインに伝わるようにする
iclk が oclk に比べて高い周波数で動く場合、itrig のパルスは短いので oclk 周期のサンプリングでは見逃してしまう可能性が高いです。 otrig が生じるまで itrig_ex を下がりっぱなしになることで、 確実にトリガを伝えることができます。

itrig1 の1つのパルスが oclk ドメインに複数のパルスを生じないようにする
逆に、iclk が oclk に比べて低い周波数で動く場合、itrig のパルスは長いので、 otrig にパルスを出力しても、まだしばらく itrig が上がっている可能性があります。 そのような場合に連続して otrig へパルスを出力してしまわないように、 itrig1 が上がるまで待ってから、次の otrig の起動準備に入る形になっています。

Spartan 3A DSP のプリミティブである LDCPE は SR ラッチとして使えるのですが、Reset が Set に勝つ仕様なので、 itrig1 を Reset に、!itrig_ex2 を Set に繋ぎます。

そうすることで、itrig_ex2 が下がっても、 itrig1 が下がるまでは itrig_ex が下がりっぱなしになります。

itrig_ex は当初、itrig extended のつもりで付けた名前でしたが、 今見ると非常に分かりにくいですね(汗

itrig_ex1 と itrig_ex2 について

これらは、非同期信号 itrig_ex を oclk で受ける2重 FF を構成すると同時に、 上記の通り2ビットのジョンソンカウンタステートマシンと見なすこともできます。

上図では出力の otrig を ( !itrig_ex1 & !itrig_ex2 ) から計算できそうに見えるのですが、 oclk に比べて iclk が遅い場合には itrig_ex2 が下がっても itrig1 がまだ上がりっぱなしであるため、数クロックに渡り { itrig_ex1, itrig_ex2 } == 2'b00 となる可能性があって、 うまくいきません。

itrig_ex3

このため、itrig_ex2 が下りるのと同時に立ち上がり、 正確に1クロックで下りるパルスを作る目的で itrig_ex3 が追加されています。

otrig は !itrig_ex2 & itrig_ex3 として計算されます。

改善できるか?

可能性があるのは itrig_ex1 と itrig_ex2 を本当のジョンソンカウンタとして構成し直して、 ストップモーション付きの2ビットステートマシンの形にしたときに itrig_ex3 が必要なくなるかどうか、と言う点ではないかと思います。

まだ形になっていないので、うまく動きそうな回路を作れたら追記しようと思います。

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

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(linenumber)
module async_fifo #(
    parameter DATA_BITS = 8,
    parameter DEPTH_BITS = 11
) (
    input irst,
    input iclk,
    input [DATA_BITS-1:0] idata,
    input we,
    output reg full,
    input orst,
    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{1'b0}};
    wire [DEPTH_BITS:0] one  = { {DEPTH_BITS{1'b0}}, 1'b1 };
    
    // メモリの読み書き
    // full / empty のチェックは外部回路で行う
    
    always @(posedge iclk)
        if (!irst & 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 (irst) 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 (orst) 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 (irst)
            full <= 1'b0;
        else
            full <= next_full;
           
    wire next_empty = re ? ( rp1 == oclk_wp0 ) :
                           ( rp0 == oclk_wp0 ) ;
    always @(posedge oclk)
        if (orst)
            empty <= 1'b1;
        else
            empty <= next_empty;

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

テストベンチ

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

LANG:verilog(linenumber)
`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 irst;
    reg iclk;
    reg orst;
    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 (
        .irst(irst), 
        .iclk(iclk), 
        .idata(idata), 
        .we(we), 
        .full(full), 
        .orst(orst), 
        .oclk(oclk), 
        .odata(odata), 
        .re(re), 
        .empty(empty)
    );

    // 書き込み側のモック
    tranceiver_mock #(.DATA_BITS(DATA_BITS)) tx (
        .rst(irst),
        .clk(iclk),
        .full(full),
        .data(idata),
        .we(we)
    );
    
    // 読み出し側のモック
    receiver_mock #(.DATA_BITS(DATA_BITS)) rx (
        .rst(orst),
        .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
        irst = 1;
        iclk = 0;
        orst = 1;
        oclk = 0;

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

        #500; // 初期化時間
        
        // タイミングを測ってリセットを下ろす
        @(posedge iclk) irst = 0;
        @(posedge oclk) orst = 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

性能

上記の回路では、

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

の部分がボトルネックになっているのですが、これを

LANG:verilog
    always @(posedge oclk)
        odata <= mem[re ? rp1[DEPTH_BITS-1:0] : rp0[DEPTH_BITS-1:0]];

としてしまえば、最適化によっては Spartan 3A DSP 上で幅 32bit、深さ 512 の FIFO を 200 MHz オーバーでも回せるみたいです?

リセット信号の扱い

ここに書いてあった内容は、大幅に改訂して 電気回路/HDL/リセットについての考察 にまとめ直しています。

上記の synchronize と 非同期 FIFO も

入力側と出力側とでそれぞれ異なる同期リセット信号を使うように書き直しました。

全体を通じての方針

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

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

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

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

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

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

コメント




2bit のジョンソンカウンタベースのステートマシン

  • 2bit のジョンソンカウンタベースのステートマシンなら、非同期の直接入力はOKかと -- [アプロ]
  • このままだと分かりにくくなりそうだったので、コメント欄をトピックで分けさせていただきました。 -- [武内(管理人)]
  • で、毎度のことながらアプロさんのご指摘の意味を一発で理解できないでいます。これは、トリガ信号の伝達 あたりの話でしょうか?(はずしている自信大です) -- [武内(管理人)]
  • 当たりです(笑) 詳しくは、『定本 ASICの論理回路設計』 にノウハウが(^^ゞ -- [アプロ]
  • おー、当たりましたか(笑 ASICの論理回路設計は持っているので、読み直してみます。 -- [武内(管理人)]
  • というか、上記回路 の { itrig_ex2, itrig_ex1 } はよく見るとそのままジョンソンカウンタになってますね。ちょこちょこっと手直しすると最適化できそうに思えてきました。 -- [武内(管理人)] あんまり関係ないですね(汗 もう少し落ち着いて考えてみます。
  • いろいろ考えてみましたが、結局改善できる点を見つけることができませんでした(泣 -- [武内(管理人)]
  • あれーっ(笑) ヒントは、『定本 ASICの論理回路設計』 の7,8章を読んでもらえれば(♪ 2bit のジョンソンカウンタベースのステートマシンのストップモーションを()v -- [アプロ]
  • あ、考えてみた生々しい(?)内容を、さらに改良できないか考察 に書いてみたのでした。最初に示していたコードは、よく見ると 2bit のジョンソンカウンタベースのステートマシンのストップモーションになってるんじゃないかと思うのです。気づいてなかったのですが(汗 -- [武内(管理人)]
  • で、その周りのコードは、ステートマシンを確実に1度だけ起動するためのラッチだったり、出力パルス幅を1クロック幅にするためのFFだったりで、私の力では縮小できなかったというわけです。 -- [武内(管理人)]
  • うう、もしかして本当にそのものずばりのコードとか本に載ってたりします?(おどおど -- [武内(管理人)]
  • ちゃんと見ると 初期バージョン も、iclk ドメインと oclk ドメインとでそれぞれ1つずつ 2bit ジョンソンカウンタをストップモーションで回している形になっているんですね。びっくりしました! -- [武内(管理人)]
  • 初期バージョンのコードは otrig を reg で作らずに assign otrig = !itrig_ex2 & otrig1 とすることで、少しだけ軽くて速い回路に改善できることに気づきました。 -- [武内(管理人)]
  • 回路図がずばり(笑) -- [アプロ]
  • 単純に2bitあればOKではないのかな S0, S1, S2, S3 戻って S0 と遷移するので、S0=>S1 の遷移のストップモーション"トリガ"H"待ち"、S3=>S0 の遷移のストップモーション"トリガ"L"待ち" どうにょろ? -- [アプロ]
  • はい、中央で動くステートマシンはほぼそれに近い動作になると思うのですが、上での怪しい考察 によると(かなり自信ないですが)、入出力のクロック周波数が大幅に異なる場合などにも動作する回路を作るには、中心となる2ビットステートマシンの周辺に少しだけ回路を追加する必要があるのではないかと思うのです。あとは、それらをどれだけうまく作れるかなのですが。。。 -- [武内(管理人)]
  • S2=>S3 の遷移にストップモーションを追加し、時間差分のタイマを入れれば、どーとでもなると思いますが(笑) -- [アプロ]
  • すみません、根本から確認で、ステートマシンは出力クロック同期を考えているのは正しいでしょうか?何か大きく勘違いしている気がしてきました。 -- [武内(管理人)]
  • 非同期の信号を受けて、自分のクロックに引き込む側を考えています。基本的に、S0=>S1,S3=>S0 のストップモーションでよいと思います。相手が速い場合は、S0=>S1のストップモーションだけでよい筈です。ただし、相手側でパルス幅を広くする必要があります。 -- [アプロ]
  • そうですよね。直接繋いだ場合、相手がパルスを広くする必要と、スタティックハザードを出さない必要があるんだと思います。 -- [武内(管理人)]
  • また、本当にストップモーションのステートマシンとするためには、非同期信号から数えて1つ目のFFと2つ目のFFとの間に LUT(?) を入れることになるので、mtbf 的に上記回路が(無視できる程度かもしれませんが)多少なりとも有利なのだと思っています。 -- [武内(管理人)]
  • S0="00", S1="01", S2="11", S3="10" になります。[1:0] jon_reg ; S0=>S1 は、[0] にいたずらする。S3=>S0 は、[1] にいたずらします -- [アプロ]
  • S0=>S1 は非同期入力を使っているため、[0] は短時間メタステーブルになる可能性があると思います。で、そのメタステーブル信号は次の S1=>S2 の遷移が jon_reg[0]==1'b0 && jon_reg[1]==1'b0 の結果として起きることから、AND を1回通って次の FF である [1] に到達すると考えられます(ANDはLUTに実装される?)。これに対して上記の案では、itrig_ex1 がメタステーブルになったとしても itrig_ex2 までのパスに何のロジックも入らないため、mtbf 的に有利ではないかと考えました。 -- [武内(管理人)]
  • jon_reg[0] がメタ・ステープルになっても、jon_reg[1] が受け付けなければ、jon_reg[0] の状態が変化しないと思います。S1=>S2 の遷移は、jon_reg[0]=1 の時に、次のクロックで、jon_reg[1]が"1"になることです -- [アプロ]
  • この小さなコメント欄での分かりづらい議論に突き合って下さりありがとうございます。しかも、上で私が書き間違えたために話がおかしくなってしまいました。どうもすみません。 -- [武内(管理人)]
  • S0=>S1 は非同期入力を使っているため、[0] は短時間メタステーブルになる可能性がある、というのは正しいとして、次の S1=>S2 の遷移は jon_reg[1:0]==2'b01 の条件で起きるので、プリミティブに直せば if (jon_reg[1]==1'b0 && jon_reg[0]==1'b1) jon_reg[1] <= 1'b1; という回路になりますね。先ほどは誤って 2'b00 としてしまいました。 -- [武内(管理人)]
  • jon_reg[0] のメタステーブルが伝わって jon_reg[1] までもがメタステーブルに陥ってしまう可能性の高さは jon_reg[0] から jon_reg[1] までの遅延時間に依存していて、この遅延が長いほど(低確率とはいえ)メタステーブルが伝播しやすくなってしまいます。 -- [武内(管理人)]
  • if (jon_reg[1]==1'b0 && jon_reg[0]==1'b1) jon_reg[1] <= 1'b1; の場合の遅延時間は AND が1つだけなので決して大きくはないのですが、上記の回路 では間に1つもロジックがなく、遅延は極小化できていると思ったのでした。 -- [武内(管理人)]
  • 「非同期信号は2段のFFで受けよ」という話でも、通常それら2つのFF間の遅延に関する注意は声高に言われないのですが、本当は非同期信号をFFで受けた場合には常に、1段目と2段目のFF間の遅延時間をできる限り短く保つ努力が必要なのかな、と、、、少し気にしすぎなのかもしれませんが。 -- [武内(管理人)]
  • うわ、コメント欄で書く分量じゃないですね(汗 -- [武内(管理人)]
  • しかも、これだけ書き込んでから、ストップモーションの無いステートではジョンソンカウンタを進めるのに AND を取る必要が無いことに今頃気づきました(滝汗 -- [武内(管理人)]
  • mtbf 云々については私の勘違いと言うことで、そっとしておいて下さい(乾笑 -- [武内(管理人)]
  • 後でもう一度、ステートマシンを本当のジョンソンカウンタで作ったときに itrig_ex3 を取り除けないかどうか、考えてみようと思います。 -- [武内(管理人)]

一括でのTIG制約

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

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