非同期信号を扱うための危ういVerilogライブラリ のバックアップ差分(No.4)

更新


  • 追加された行はこの色です。
  • 削除された行はこの色です。
[[公開メモ]]

#contents

* ご注意 [#f663e998]

&color(red,white){&size(25){※ 以下は練習のために書いてみた回路です。};};~
&color(red,white){&size(15){しっかりしたテストを経ていないので、鵜呑みにしないで下さい。};};~
&color(red,white){&size(15){あまり役に立たない個人的なメモになってしまっていてすみません。};};

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

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

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

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

これに尽きます。

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

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

例えばDフリップフロップ(D−FF)は、
供給されるクロック信号の立ち上がりエッジ(あるいは立ち下がりエッジ)に
同期したタイミングで入力信号を読み取り、次のクロックエッジが来るまで
出力にその値を出し続けるという動作をします。

このように書いて分かるように、理想的には、エッジの瞬間の値を読み取って、
エッジの直後から正しい値を出力するのがD−FFの動作です。

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

そこで、FFを正しく使うための指針として、
クロックエッジ前後の短い時間を入力信号の変化を禁止する時間として定め、
その間に入力信号が変化しないときに限ってFFの動作が保証されています。

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

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

下図で説明します。

&attachref(metastable.png);

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

信号はクロックの立ち上がりにほぼ同期して直前のFFから出力されますが、
いくつかのゲートや配線を通るため、無視できない量の遅延時間の後にFFの入力に到達します。
この遅延時間は、信号経路に含まれる(複数の)ロジック(ゲート)を通過するのに
掛かる時間と、そっらを繋ぐ配線を通過するのに掛かる時間の和になります。
言い換えると、クロックエッジからこの合計の遅延時間の後に、入力信号の値が定まることになります。

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

一方でホールド時間はと言うと、FFの出力はクロックエッジの直後に変化する訳では無く、
内部に有限のゲート遅延を持ちます。FFに規定される最小ホールド時間は非常に短いため、
大抵はFFの出力遅延だけでホールド時間の制約を満たしてしまいます。
すなわち、内部FF同士のデータのやりとりでホールド時間を考える必要は、
ほとんどありません。(クロックの供給に、クロック専用の高速配線リソースが利用できない
ときには、同じクロックを表すネットでも、遠く離れた位置では大きなスキューが生じるため、
実質的に同じクロックと見なせなくなってしまうこともあり、
結果としてホールド時間が問題になることもあります。)

このような通常のクロック同期信号の動作と比較するため、上図の下段には、
クロックに同期していない信号をFFの入力に繋いだ状況を想定しました。
入力信号がクロックタイミングとは無関係に変化するため、
最小セットアップ時間あるいは最小ホールド時間の規定に違反して、
クロックエッジ付近で値が変化する状況がしばしば起きてしまいます。

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

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

このように出力信号がふらついている間に別のラッチがこの信号を読もうとすると、
その結果が1と読まれるか0と読まれるか定まらないことになります。

そのようにうわべだけを考えると、
「そもそも0と1とを切り替わる最中に値を読んだのだから、
結果としてどちらに解釈されたって大きな差は無いじゃん」
という結論になってしまいそうな所ですが、
このメタステーブルな出力が複数の素子に入力されているとき、
メタステーブルの怖さが顕在化してきます。

&attachref(selector.png);

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

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

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

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

この例から分かるように、メタステーブル状態の怖いところは、下流に繋がれた複数の素子が、
本来一斉に入力値が変化することを期待して設計されているにもかかわらず、
それぞれ別の値が入力されたかのように振る舞うために、論理が破綻してしまうところにあります。

* 非同期信号をFFで受ける [#te90b3d6]

ではどうするかというと定石があって、非同期信号はFFを2つ繋いで受取ります。

&attachref(double-ff.png);

 LANG:verilog
 module double_ff(
     (* IOB="FALSE", TIG="TRUE" *) input idata,
     (* IOB="FALSE" *) input idata,
     input oclk,
     output odata
 );
     (* IOB="FALSE", HBLKNM="sync_reg", ASYNC_REG="TRUE" *) reg temp1;
     (* IOB="FALSE", HBLKNM="sync_reg", ASYNC_REG="TRUE" *) reg temp2;
     (* IOB="FALSE", HBLKNM="sync_reg", ASYNC_REG="TRUE", TIG="TRUE" *) reg temp1;
     (* IOB="FALSE", HBLKNM="sync_reg", 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度程度だ、と言われていたそうです。

このダブルFFの形は非同期信号を受け渡すときの大前提になっていて、
以下の回路の中にもたびたび現れます。

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

これを回避しようと入れてあるのが、上記の IOB 制約と HBLKNM 制約です。
2つのFFが必ず同じスライスに入るように設定しています。このあたりは
ISE の Language Templates から取っているので間違いないと思います。

** 重要な指針 [#cd725725]

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

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

** 注意 [#jaf5ebfe]

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

 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

外部からの信号が微妙なタイミングで切り替わると、
個々のビットがそれぞれ異なるタイミングで読まれるため、
全体として意味のある値を読み取ることができないためです。

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

* 異なるクロック間で複数の信号線を受け渡す [#m28494e7]

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

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

+ 出力側のクロックドメインから入力側に信号送信要求 (req) を送る
+ 入力側は送られた req をダブルFFで受取る
+ 入力側で読み取った値を temp に入れる
+ 入力側から出力側へ送信応答 (ack) を送る
+ 出力側は送られた ack をダブルFFで受取る
+ 出力側は temp から値を読み取り出力側へ流す
+ 出力側は req を下ろす
+ 入力側は req の変化をダブルFFで受取る
+ 入力側が ack を下ろす
+ 出力側は ack の変化をダブルFFで受取る
+ 1. へ戻る

ここでは、後半の req や ack を下ろすところで再度データを送受信することで、
データ更新の頻度を上げるよう工夫しています。

** 動作タイミング図 [#oece2ebd]

&attachref(synchronize.png,,50%);

- 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 コード [#m77b9d4a]

インスタンシエーション時にパラメータでバス幅を変えられるので、
いろんな用途に使えると思います。(正しく動けば、ですが・・・)

 LANG:verilog
 module synchronize #(
     parameter DATA_BITS = 8
 ) (
     input rst,
     input iclk,
     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

* トリガ信号の伝達 [#hb75dbc2]


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

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

** 動作タイミング図 [#xfaa5dec]

&attachref(interclock_trig.png);

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

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

** Verilog コード [#ra58aed2]

 LANG:verilog
 module interclock_trig(
     input iclk,
     input itrig,
     input oclk,
     output otrig
 );
     (* HBLKNM="sync_reg" *)
     reg itrig_ex;
     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


* 非同期 FIFO [#b9208ec0]

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

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

http://marsee101.blog19.fc2.com/?no=1085 の情報を元に、
書き込み・読み出しカウンタをグレイコードで実装し、
必要な下図の FF を入れて受け渡す回路にしようと思っています。

が、まだ未稿。

* コメント [#kd328739]

#comment_kcaptcha


Counter: 163763 (from 2010/06/03), today: 8, yesterday: 0