非同期信号を扱うための危ういVerilogライブラリ の履歴(No.5)
更新- 履歴一覧
- 差分 を表示
- 現在との差分 を表示
- ソース を表示
- 電気回路/HDL/非同期信号を扱うための危ういVerilogライブラリ へ行く。
ご注意†
※ 以下は練習のために書いてみた回路です。
しっかりしたテストを経ていないので、鵜呑みにしないで下さい。
あまり役に立たない個人的なメモになってしまっていてすみません。
クロックに同期していない信号を扱う際の注意点†
デジタル回路設計で、クロックに同期していない信号を扱う必要があるとき、 十分な注意を払わないと、予想しない結果が生じて慌てることになります(なるそうです・・・)。
では何に注意すればよいかというと、
- メタステーブル状態の伝搬を考慮する
ほぼこれに尽きます。(その他はちょっと考えればすぐ分かりそうなので)
メタステーブル状態とは、簡単に言うと あるラッチあるいはフリップフロップ(FF)の出力が1つの値に定まらず、 ふらついている状況を表す言葉です。
どうしてそんなことが起きるのかというと、、、
例えばDフリップフロップ(D−FF)は、 供給されるクロック信号の立ち上がりエッジ(あるいは立ち下がりエッジ)に 同期したタイミングで入力信号を読み取り、次のクロックエッジが来るまで 出力にその値を出し続けるという動作をします。
このように書いて分かるように、理想的には、エッジの「直前の一瞬」の値を読み取って、 エッジの「直後」から正しい値を出力するのがD−FFの動作です。
しかし実際のFFは「一瞬」で値を読み取れるわけではないため、 エッジの前後の短い時間の間に入力信号が変化してしまうと、 正しい値を読み取れず、出力値が不定になってしまいます。
そこで、FFを正しく使うための指針として、 クロックエッジ前後の短い時間を入力信号の変化を禁止する時間として定め、 その間に入力信号が変化しないときに限ってFFの動作を保証することになっています。
クロックエッジの「直前」に設けられた入力信号の変化禁止時間を最小セットアップ時間、
クロックエッジの「直後」に設けられた入力信号の変化禁止時間を最小ホールド時間、
とそれぞれ呼びます。
入力信号がこれらの最小値よりも大きなセットアップ時間、 ホールド時間を持つときにのみ、FFが正しく動作する、と言い換えてもOKです。
下図で説明します。
上段部分にFFに入力されるクロック信号が表示されており、
中段はクロックの立ち上がりに同期して動作する D−FF に、
クロックに同期した信号 (sync) が入力される状況を表しています。
入力信号はクロックの立ち上がりにほぼ同期して直前のラッチあるいはFFから出力されますが、 いくつかのゲートや配線を通るため、無視できない量の遅延時間の後にFFの入力に到達します。
この遅延時間は、信号経路に含まれる(複数の)ロジック(ゲート)を通過するのに掛かる時間と、 それらを繋ぐ配線を通過するのに掛かる時間の和になります。 言い換えると、クロックエッジからこの合計の遅延時間の後に、入力信号の値が定まることになります。
図から明らかなように、クロック周期から遅延時間を引き算したものがセットアップ時間です。 回路が複雑になり、あるいは信号経路が長くなりすぎて、遅延時間が大きくなると、 直後のFFのセットアップ時間が短くなり、もし最小セットアップ時間を下回ると、 素子の動作がおかしくなります。
一方でホールド時間はと言うと、FFの出力はクロックエッジの直後に変化する訳では無く、 内部に有限のゲート遅延を持ちます。FFに規定される最小ホールド時間は非常に短いため、 大抵はFFの出力遅延だけでホールド時間の制約を満たしてしまいます。
すなわち、内部FF同士のデータのやりとりでホールド時間に気を遣う必要は、 ほとんどありません。(クロックの供給に、 クロック専用の高速配線リソースが利用できないときは、 遠く離れた位置でクロックに大きなスキューが生じるため、 実質的に同じクロックと見なせなくなってしまうこともあり、 結果としてホールド時間が問題になることもあります。)
このようなクロック同期信号の動作と比較するため、 上図の下段にはクロックに同期しない信号を入力した状況を描きました。 この場合、入力信号はクロックタイミングとは無関係に変化します。 したがって、最小セットアップ時間あるいは最小ホールド時間の規定に違反して、 クロックエッジ付近で値が変化する状況が起きてしまいます。
セットアップ時間やホールド時間が守られず、エッジ付近で入力信号が変化すると、 FFが不安定な状況に陥り、出力信号の値が比較的長い時間(数 ns 程度)に渡り、 ばたばたと振動してしまうことがあります。
図では黄色の矢印の時刻にセットアップ時間の違反が起きています。 そのせいで次クロックの出力値が長い間発信してしまい、 数 ns の時間を掛けて徐々に 0 に収まっていく様子を表しています。
このように出力信号がふらついている間に別のラッチがこの信号を読もうとすると、 その結果が1と読まれるか0と読まれるか定まらないことになります。 この状態が、悪名高い(?)メタステーブル状態です。
一見すると、 「そもそも0と1とを切り替わる最中に値を読んだのだから、 結果としてどちらに解釈されたって大きな差は無いじゃん」 という結論になってしまいそうな所ですが、 このメタステーブルな出力が複数の素子に入力されているとき、 メタステーブル状態の怖さが顕在化します。
例えばこの回路は、外部からの入力を1段のFFで受けて、 その出力で4ビットのレジスタ、 AとBとのどちらの値を出力するかを選ぶものです。
外部からの入力が非同期の場合、初段のFFでメタステーブル状態が発生する可能性があります。
初段FFがメタステーブル状態になると、4つのセレクタは同じ素子に繋がれているにもかかわらず、 配線の微小な違いにより、入力を1と読むものと0と読むものの両方が出現する可能性があります。
すると出力はAでもBでもなく、ビット単位でAとBとを混ぜ合わせた値となってしまいます。
この例から分かるように、メタステーブル状態の怖いところは、下流に繋がれた複数の素子が、 本来一斉に入力値が変化することを期待して設計されているにもかかわらず、 それぞれ別の値が入力されたかのように振る舞うために、論理が破綻してしまうところにあります。
非同期信号をFFで受ける†
ではどうするかというと定石があって、非同期信号はFFを2つ繋いで受取ります。
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", ASYNC_REG="TRUE" *) reg temp2; always @(posedge oclk) begin temp1 <= idata; temp2 <= temp1; end assign odata = temp2; endmodule
図にもあるように、1つ目のFFでメタステーブルが発生することはどうやっても防げませんが、 2つ目のFFを繋ぐ信号線を十分に短く、遅延時間を少なくしておけば、 2つ目のFFのセットアップ時間までにはメタステーブル状態が治まり、 値が定まっていることが期待できます。
メタステーブル状態は、ある程度の時間が経てば必ず治まるものらしく、 少し古い本、CQ出版「定本 ASICの論理回路設計」小林芳直著によれば、 そのころの XILINX 社の FPGA で、発生から 10 ns 経ってもまだ治まらない メタステーブルが発生する確率は 10 万年に1度程度だ、と言われていたそうです。
この2段FFの形は非同期信号を受け渡すときの大前提になっていて、 以下の回路の中にもたびたび現れ、非同期回路の定石になってます。
ただ忘れてはいけないのは、2つのFF間の配線が長い場合、 この回路はメタステーブルを取り除く役には立たないという点です。 遅延時間が長ければ、1つ目のFFのメタステーブルが2つ目に伝播して、 2つ目もメタステーブルになってしまう可能性が高くなります。
「2重FF回路では、2つのFFを可能な限り近くに配置する」 を鉄則として覚えておかなければなりませんん。
このために入れてあるのが、上記コードの IOB 制約と RLOC 制約です。 2つのFFが必ず同じスライスに入るように設定しています。 ISE の Language Templates にあるコードでは RLOC の代わりに HBLKNM で配置制約を掛けようとしているのですが、実際にやってみると WARNING:ConstraintSystem:119 が出てうまく行きません。 http://japan.xilinx.com/support/answers/34088.htm によれば、 「HBLKNM をネットに設定すると、パッドにしか伝搬されません」 とのことなので、ここでは HBLKNM ではなく RLOC で指定しています。
ASYNC_REG 制約については、まだその効用を正しく理解できていないのですが、 Language Template のまねをして入れてあります。
上記コードでは idata 自体に TIG が付いてますが、 できれば idata -> temp1 のパスだけを TIG にしたいところです。 Verilog コード中にそんな制約を入れ込めないか、調査中です。
重要な指針†
これまでの考察で、以下の重要な指針を得ました。
- 非同期信号をそのまま使うな
- 非同期信号を1段FFに通しただけの信号は後段のつなぎ方に気をつけろ
- 複数のラッチを繋ぐような使い方はNG
- 1つだけFFを繋ぐなら問題ない
- 1つだけFFを繋ぐときも遅延が長いとNG
- 非同期信号を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
外部からの信号が最小セットアップ・ホールド時間に違反するタイミングで切り替わると、 個々のビットがそれぞれ異なるタイミングで読まれるため、 全体として意味のある値を読み取ることができないためです。
このように複数の信号線を一括して受け渡すために、次の回路があります。
異なるクロック間で複数の信号線を受け渡す†
異なるクロック間で複数の信号線を受け渡すには、 ハンドシェイクと呼ばれる手法を用います。
基本的な手順は以下の通りです。
- 出力側のクロックドメインから入力側に信号送信要求 (req) を送る
- 入力側は送られた req をダブルFFで受取る
- 入力側で読み取った値を temp に入れる
- 入力側から出力側へ送信応答 (ack) を送る
- 出力側は送られた ack をダブルFFで受取る
- 出力側は temp から値を読み取り出力側へ流す
- 出力側は req を下ろす(読み取り確認を送る)
- 入力側は req の変化をダブルFFで受取る
- 入力側が ack を下ろす(次に備える)
- 出力側は ack の変化をダブルFFで受取る
- 1. へ戻る
以下の回路は、後半の req や ack を下ろすところで次のデータを送受信するため、 データ更新の頻度が単純なハンドシェークに比べて2倍に上がっています。
動作タイミング図†
- 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
これも、idata -> temp のパスに TIG を付けたいのですが、 Verilog コードに制約を埋め込む方法が分からなかったので、 idata に TIG を付けています。 このモジュールの外の回路に影響を与えてしまう可能性があるので 改善策を模索中です。
トリガ信号の伝達†
クロックを越えて、トリガ信号を確実に伝えるためのモジュールです。
itrig に iclk に同期した正論理のトリガが入ると、
otrig に oclk に同期した1クロック幅のトリガを生じます。
動作タイミング図†
やりたいことの単純さから考えると、あまりに重たい動作になってます。
もっと手軽な方法がありそうな予感・・・
Verilog コード†
LANG:verilog module interclock_trig( input iclk, input itrig, input oclk, output otrig ); (* HBLKNM="sync_reg" *) reg itrig_ex; (* TIG="TRUE" *) wire itrig_ex2; double_ff req_ff ( .idata(itrig_ex), .oclk(oclk), .odata(itrig_ex2) ); (* HBLKNM="sync_reg", 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 とに配置制約、 と覚えてしまうのが良さそうですね。
クロックを越えてステートマシンを起動し、終了も検知†
1つ上の方法でトリガ信号を送ることができますが、 そのトリガで開始したタスクが終了したことも分かるようにしたいです。
動作タイミング図†
のようにして、制御側への 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 信号を生成する
という具合です。
グレイコードとバイナリコードの相互変換アルゴリズムは:
http://blog.livedoor.jp/k_yon/archives/51619205.html
を参考にさせていただきました。
Verilog コード†
カウンタ比較をバイナリで行っているので、almost full や almost empty なども簡単に追加できます。
full / empty の出力はFFから直なので遅延が小さくなっています。
代わりに we や re へのセットアップ時間が多少きつめかもしれません。
NLANG: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; always @(posedge oclk) 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 ); always @(posedge iclk) if (rst) full <= 1'b0; else if (we) full <= ( ( iclk_rp0 ^ mask ) == wp1 ); else full <= ( ( iclk_rp0 ^ mask ) == wp0 ); always @(posedge oclk) if (rst) empty <= 1'b1; else if (re) empty <= ( rp1 == oclk_wp0 ); else empty <= ( rp0 == oclk_wp0 ); endmodule