lyricalmagicalのアイコン画像
lyricalmagical 2020年05月05日作成 (2021年12月29日更新)
セットアップや使用方法 セットアップや使用方法 閲覧数 24992
lyricalmagical 2020年05月05日作成 (2021年12月29日更新) セットアップや使用方法 セットアップや使用方法 閲覧数 24992

[FPGA]非同期設計の考え方

はじめに

PLD、みなさん使ってますか?
今回は実際に何かを作るのでは無く、RTLでの非同期設計について触れてみようと思います。
非同期設計はマルチクロックドメイン設計と言われることもあります。
非同期設計は、一見なにも問題無くシミュレーション上では動いてしまい、また、RTLをソフトウェアの延長線上で考えて設計すると失敗しやすいところです。
ですが、意外と設計技術についてあまり踏み込んだ解説をしているところは少ないと思います(少なくとも日本語では)。

おおよそ一般的に説明される内容としては、

  • 非同期はやめとけ
  • どうしても非同期設計をする場合は、シフトレジスタでメタステーブルを吸収しろ

というものがおおいですが、実際にどのような設計にしたらよいのかがわかりにくいかと思います。
というわけで、具体的に例を挙げて説明していきます。

※という内容なので、ある程度RTLを書ける人を前提とした内容です

そもそも非同期とは何か

まず、基本的にデジタル回路は、

  • 組み合わせ回路(combinational circuit)
  • フリップフロップ(以下FF、記事内では全てD-FFを想定)

で構成されます。
組み合わせ回路とは、ANDやORなどで構成された、状態を保持しない(入力状態のみによって出力状態が決まる)回路です。
これらの回路は、下記のように簡略化できます。
キャプションを入力できます

このとき、前段(図の左側)のFFと、後段(図の右側)のFFのクロックに、同期関係が無い場合、非同期構成となります。
同期関係がある、という状態とは、

  • そもそも同じクロックが接続されている
  • 分周やPLL等を使用した逓倍など、元クロックが同じ物が接続されている

などの場合です。それ以外の場合は非同期となります。
前段がFFではない外部入力(たとえばセンサーやスイッチなど)も、どのクロックとも関係なく変化しますので、非同期となります。
ここの考え方で気をつけるべきは、たとえば、10MHzの水晶発振器と20MHzの水晶発振器の2つを一つの回路中に用意した場合などでは、机上では2倍速の同期に見えますが、実際には誤差があるため、非同期の設計が必要となります。

非同期で何が起きるか

ではこの非同期構成の場合、実際には何が起こるのでしょうか。
結果としてはFFのタイミングエラーが発生します。
しかし、そもそもFFのタイミングエラーとはどういう現象でしょうか?FPGAやCPLDをさわり始めたばかりの人であれば、「エラー無し、よし!」だけで済ませていないでしょうか?
というわけで、まずFFのタイミングエラーについての説明が必要です。

FFのスペック

まず、FFには「セットアップタイム」と「ホールドタイム」というスペックが定義されています。
また、FFの動作としては、(pos clock FFの場合)クロックの立ち上がりで入力データを保持し、出力します。
この際、クロックの立ち上がりを基準として、

  • 立ち上がりの前に入力データが変化してはいけない時間をセットアップタイム
  • 立ち上がりの後に入力データが変化してはいけない時間をホールドタイム

と呼びます。図にすると、
キャプションを入力できます
となり、着色部分でINは変化してはいけないというスペックです。

また、直接セットアップホールドとは関係ありませんが、クロックの立ち上がりから実際にOUTが変化するまでの時間もスペックで定義されており、図でCLK to OUTと記載した箇所になります。delayなどと記載されることもあります。

同期構成の動作

さて、FFのスペックがわかったところで、非同期構成で何が起きるのかを説明したいところですが、その前に、同期構成ではどのような動作をしているかを説明する必要があります。

キャプションを入力できます
図の回路の場合、クロックの立ち上がりで前段のFFがINの状態を取り込み、OUT1に出力します。この際、前段のFFのCLK to OUT分だけ、OUT1の変化はクロック立ち上がりに対して遅れます。
その後、後段のFFのIN2に行く間に、インバーターが1つあります。インバーター自体も遅延時間があるので、OUT1が変化してからIN2が変化するまでにも時間がかかります。
このように、前段FFのクロックの立ち上がりからIN2が変化する時間、逆に言えばIN2が変化してから後段FFのクロックが立ち上がるまで時間というのは、チップ上の回路配置(配置配線、place and route)が決まった段階で一意に決定します。
ですので、遅延時間の最遅条件と、クロックの周期を計算することで、後段FFのセットアップ、ホールドタイムが満たせるかを検証することができます(※1)。
これが、タイミングレポートで検証している内容(の一部)です。

非同期構成だと何が起きるか

では次に、非同期の場合は何が起きるかですが、構成としては前段のFFと後段のFFに同期関係の無いクロックが入力される場合です。
同期構成では前段FFと後段FFのCLKの変化タイミングは同時になります。
厳密に言えばCLKの配線自体の遅延もあるので、同時ではないのですが、少なくともその関係が崩れることはありません(※2)。
逆に、非同期構成の場合、前段FFと後段FFのCLKに関係性が一切ありませんので、前段FFから見て後段FFがどういうタイミングでCLKが変化するのかは予想できません。
そのため、場合によっては以下のようなタイミングが起こりえます。
キャプションを入力できます
キャプションを入力できます
この場合、後段FFのSetup time中にIN2が変化してしまいスペック違反がおきてしまいます。
また、非同期構成を使用する限り、このスペック違反は絶対に発生します。

スペック違反で何が起きるか

では、実際にスペック違反が起きた場合、どのような現象が起きるかについてですが、上記の図に書いてあるとおり、「不定値」となります。
ただ、実際に論理回路を設計するうえで「不定値」とは2種類の意味を持ちます。

(1)0か1かわからないが、0か1のどちらかの値
(2)0でも1でも無く、中間の電圧の場合

微妙な違いですが、実際の回路として考えると明確に異なります。
5Vを基準とする回路であれば、(1)の場合、0Vか5Vかのどちらかの場合です。
それに対し、(2)の場合、2.5Vなど、どちらとも判断が付かない電圧の場合です。

スペック違反の場合、どちらの不定値になるのかと言うと、
  (2)の状態がしばらく続いた後、(1)に落ち着く
となります。
この、「(2)の状態がしばらく続く」という状態を、「メタステーブル」と呼びます。
メタステーブル状態がどれくらい続くのかという具体的な指標については、使用するデバイスメーカーが公開しています。
基本的にはだいたいのメーカーは確率で検討する内容となり、「xxxxという条件(電圧など)のとき何ns未満で収束する確率がどのくらい」というような資料になります。なので、「xxx nsで確実に収束する」という記載はあまり無いはずです。
ですので、実際にメタステーブルが起きる確率と、収束するまでの確率で検討することになります。

キャプションを入力できます

発生する確率は、基本的にクロックの1周期の時間と、setup time、hold timeから決まります。
図で入力データが着色部分で変化すればメタステーブル発生、それ以外の箇所で変化すればメタステーブル非発生です。
図だとだいたい50%弱の確率で発生することになります。

また、実際の回路として(2)の状態の、中間の電圧というのは、非常に厄介なものです。
CMOSの論理回路の特徴として、入力に中間電圧が加わると、急激に消費電力が増えます(貫通電流という)。そのため、チップ内で部分的に電圧降下などが発生し、誤動作の原因となります。

また、中間電位が入力される側の回路では、それを1か0かを正確に判断することができないため、どちらとして認識されるかは不定となります。なので、同じ信号源が接続されているにもかかわらず、その先の回路では場所によっては0として動いたり、別の場所では1として動いたりし、回路全体で矛盾した状態となるため、こちらも誤動作の原因となります。
キャプションを入力できます

このように、中間電位が入力されると、不具合の原因となるため、中間電位が入力される可能性のある箇所は、可能な限り減らさなくてはいけません。

ところで、RTLシミュレーションではsetup timeとhold timeは 0という理想的な状態のため、CLKの変化点と入力データの変化が同時でも何も起きません。
このとき、変化前と変化後のどちらの入力データが出力に反映されるかは、「デルタ遅延」というシミュレーション独自の考え方によって決まります。通常、変化前の入力データが出力されます。そのため、シフトレジスタなども正しく動作します。
デルタ遅延の挙動については非同期の本質ではないため詳細は割愛しますが、この動作により、RTLシミュレーションでは何も起きないため、シミュレーションでは非常に発見しにくいです。
setup timeやhold timeや出力遅延を正確にシミュレーションできるゲートレベルシミュレーションでは狙って起こすことは可能ですが、そもそも実施が難しいですし、狙わないとなかなか起きないのでどこで非同期検証が起きるかを事前に把握していないと発生させることは難しいです。
自分がどちらのシミュレーションをしているかわからない場合は、ほぼ間違いなくRTLシミュレーションでしょう。

対策

ではこのメタステーブルをどう扱うか、というところになりますが、基本的には冒頭に書いたとおり

  • シフトレジスタで吸収

となります。具体的にはこのような構成になります。
キャプションを入力できます

ここでメタステーブルが発生する可能性があるのは、中間のFFになります。
実際の動作としては、
キャプションを入力できます
となります。
図の通り、メタステーブルがCLK2基準で1クロック以内に収束し、0 or 1のどちらかの値に落ち着いたことを期待した場合、中間電位が入力されるのは中間のFFだけであり、OUTからは0 or 1のどちらかが出力されることになります。
これが、メタステーブル吸収にシフトレジスタ構成を使うという基本的な考え方になります。
可能性としては、メタステーブル状態が1クロック以内に収束しなかった場合、3つめのFFの出力もメタステーブル状態となることがあり得ます。この場合は、さらにCLK2同期のシフトレジスタを後段に繋いでいくことで、発生確率をさげることができます。
具体的にどのくらいの段数が必要になるかは、前述したとおりメタステーブルがどのくらいの時間で収束するかの確率と、CLK2の速度によって決まります。

もちろん正式なプロダクトであれば正しい検討をすべきですが、お試しレベルであれば、メーカー公称のデバイスの最高動作速度から以下のように概算で求めるのでいいと思います。これはあくまで個人的な経験則なので根拠は一切ありません!!
たとえばメーカー公称で最高動作周波数が100MHzのデバイスだとします。ざっくり3分の1程度の速度までで後段2つのシフトレジスタを組むならば、この回路図の通り後段のクロックは2つのFFを使ったシフトレジスタ1段構成で問題は起きないでしょう。
それよりも速いクロック、たとえば50Mhzで動作させるとすると、後段のシフトレジスタはもう一つ増やしたほうが良いと思います。
また、たとえばスペックギリギリの100MHzで動作させる場合、さらにもう一つ増やした方が良さそうに思います。
つまり、メタステーブルが発生するFFの入力から、シフトレジスタの出力までが、メーカースペック値の約3分の1程度以上になるようにシフトレジスタの段数を決める、でいいと思います。
あくまで個人的な見解であり、保証できる根拠は一切ありません。もちろん、今後プロセスが微細化されていくなど技術の進化に伴い、このような概算が当てはまらなくなる可能性は大いにあります。基本的には非同期構成はギリギリを攻めることはしないで、公称スペックに対しじゅうぶんに余裕のある速度で設計するのが安全です。

もう一つ考慮すべき事項:グリッチ

ここまで、非同期設計で発生するタイミングエラー、それに伴うメタステーブル、およびその吸収方法について説明しましたが、非同期設計をするうえで考えるべき課題がもう一つあります。それがグリッチと呼ばれる現象です。
グリッチ自体は同期回路でも発生しますが、同期回路の場合、FF間のデータ受け渡しで問題になることはほぼありません(※3)。
そこで、まず、FF間のデータ受け渡しで発生するグリッチとはどういう現象かを同期回路の例で説明します。

グリッチとは何か

キャプションを入力できます
キャプションを入力できます
ここで、回路真ん中のANDに注目します。
ANDの入力のAとBが、「A='0'、B='1'」から「A='1'、B='0'」に変化する瞬間の挙動になります。
FF1とFF2は同じクロックで動いているので、FF1とFF2の出力は理論上は同時に変化します。
しかし、実際にはCLK to OUTの遅延値(実際にはANDの入力までの遅延も含む)が多少異なるため、ANDの入力が同時に変化するわけではありません。
この場合、CLK→AよりもCLK→Bの方が時間がかかっており、その結果、ANDの入力が「A='0'、B='1'」から「A='1'、B='0'」に変化する間に、一瞬だけ、「A='1'、B='1'」となる瞬間が有り、ANDの出力が1になっている箇所があります。このように出力が一瞬だけ変化する現象を「グリッチ(glitch)」と呼びます。また、このような入力の状態変化が起きることを「レーシング」と呼びます。
この場合でも、FF3のsetup timeはグリッジ発生よりも後にあるため、FF3でこの一瞬だけ1となる信号を取り込むことはありません。
なので、前段FFのCLK→後段FFのD入力までの最大の遅延値(この場合はBの経路)がsetup timeを違反しない値(つまりタイミングレポートでエラーが無い状態)であれば、同期回路でグリッチが問題になることありません。

非同期でのグリッチ

では、ほぼ同じ回路を非同期で作った場合にどうなるかですが、
キャプションを入力できます
キャプションを入力できます

この場合、CLK以外の回路は前述の同期回路と全く同じなのですが、グリッチが発生した瞬間に、後段のクロックの変化点がくると、本来ありえないはずの値を後段が取り込んでしまうことがあります。
後段FFのタイミングスペックを満たせば、メタステーブルは起きませんが、それでも本来起こりえないはずの値を取り込んでしまうため、誤動作の原因となります。
なので、このような回路設計は非同期構成では避けなくてはいけません。

より複雑なグリッチ

というわけで、グリッチが出る回路を非同期FFで受け取ってはいけないと言う事は理解していただけたかと思いますが、より複雑なパターンとして、一見絶対にグリッチが発生しなさそうで、実際には発生する可能性があるというのを触れておきます。
キャプションを入力できます
単純なセレクタ回路になります。SELが0の時OUTにはAの値が出力され、SELが1のときにOUTにはBの値が出力される組み合わせ回路です。
実際にはこの前後にFFが繋がっていて、同期回路や非同期回路が構成されますが、グリッチの発生というとこに着目するため、直接関係の無いFFについては記載を省いています。
実際にこのセレクタ回路がどのような回路が作られるのかというと、常識的に考えると、このような回路が作られると考えるでしょう。
キャプションを入力できます
この場合、SEL=0に固定すると、Bの変化はOUTには影響しません。
しかし、他の周辺回路も考えると、この回路が最適とは限りません。たとえば、

キャプションを入力できます
A、B、SELを使った別論理が別回路で必要で、回路図中の青色の回路が別回路で必須だったとします。
その状態でセレクタも必要な場合、先ほどの単純な回路を作成するよりも、このような構成としたほうが、合計の回路規模は小さくなります。
そのため、セレクタとしては冗長ですが、論理合成結果次第ではこのような回路が作成されることがあります。
実際にはFPGAではこのようにAND単体などといった構成ではありませんが、この回路図中のANDなどの回路要素が、1つのLUTと考えると同様のことは起こりえます。
この回路に対して、A=1固定、SEL=0固定でBが1から0に変化した場合、セレクタとして考えるとOUTは変化しないのですが、
キャプションを入力できます
BからORまでの遅延値が経路によって異なる場合、このようにOUTにグリッチが発生する可能性があります。

ちなみに、論理合成はあくまで「前段FF」と「後段FF」と「その間の論理回路」という塊で論理の最適化を行うため、FF間の論理について、RTL記述の際にIFの入れ子などで演算の優先順位が付く記述としても、その優先順位通りの論理合成になるとは限りません。
たとえば、

VHDL例1

process (CLK) begin if (CLK'event and CLK = '1') then if (A = '0' and B = '0') then OUT <=C; end if; end if;

VHDL例2

process (CLK) begin if (CLK'event and CLK = '1') then if (A = '0') then if (B = '0') then OUT <= C; end if; end if; end if;

で、論理合成結果に違いはありません。なので、A、B、CがOUTに対して非同期の場合は、VHDL例2の記載でも、A='1'ならBやCが変化してもOUTに絶対にグリッチを取り込まない取り込まないという保証はありません。

なので、一見絶対にグリッチが発生しないようなRTL記述であっても、意図しないグリッチが発生する可能性があるので、基本的に非同期構成とする場合、前段FFと後段FFの間には論理を入れないのが鉄則です。
論理合成結果がこのような冗長な構成になっていないかを確認するのは非常に困難です。

実装例

それでは、上記内容を踏まえ、具体的な実装例を挙げてみたいと思います。

エッジ検出(遅→速)

入力信号が変化したことを検出する回路です(この例ではCLK1に同期した信号の立ち上がりエッジを検出)。
ここでは、遅いクロックで動作する回路が出力したものを速いクロックで動作する回路が受け取る場合です。
送られてきたパルスの数を数えるなどに使用できます。
キャプションを入力できます
ここでは、CLK1同期の信号①をCLK2に乗せ替える為、①を非同期吸収の為に2段シフトしています。ここまでは非同期設計の定番です。
その後、さらにもう一段シフトし、2段目='1'、3段目='0'となったことを検出しています。
このような設計にすると、信号①の変化点を検出して、CLK2で1パルス幅の信号をすることが可能です。

また、この回路では、メタステーブル後に0に収束するか、1に収束するかでタイミングが変わります。
この回路によらず、基本的に非同期回路ではメタステーブル後にどちらに収束するかにより、タイミングが変わるので、どちらの場合でも正しく動作するように設計する必要があります。

メタステーブル後、0に収束した場合
キャプションを入力できます

メタステーブル後、1に収束した場合
キャプションを入力できます
※FFにのみdelayが存在するという前提で記載しています。実際にはANDにもdelayは存在します。

エッジ検出(速→遅)

こちらも同様に入力信号が変化したことを検出する回路ですが、今度は速いクロックで動作する回路が出力したものを、遅いクロックで動作する回路で受け取る場合です。
考え方としては(遅→速)と同じですが、速いクロック基準での1クロック幅のパルスは、遅いクロックでは取り込めない可能性があるので、確実に遅いクロック換算で1クロックより十分に長い幅に引き延ばす必要があります。
(遅いクロック1クロック幅ギリギリの場合、メタステーブル後の収束状態によっては検出できません)

キャプションを入力できます
回路が大きくなりましたが、回路上半分(CLK1同期部)と下半分(CLK2同期部)に分かれます。

まず上半分についてです。
キャプションを入力できます
各FFのCLK to OUTのdelayは青矢印で示しています。
CLKを乗せ替えたい信号を①として、これが最短でCLK1の1周期分の幅の'1'だとします。
まず、これをCLK2に乗せ替える為には、CLK2の1周期分以上の幅に引き延ばさなくてはいけません。今回、CLK2はCLK1の半分弱ですので、3倍に引き延ばす設計とします。
そのためには、CLK1で2段シフトし、その全ての出力をORします。
すると、④の信号がCLK1基準で3CLK(以上)の幅となります。
この時点で④を非同期で乗せ替えたいところではありますが、この信号は組み合わせ回路の出力のため、前述した「非同期でのグリッチ」に該当する構成となるため、そのままでは乗せ替えできません。
そのため、④信号をさらにもう一回CLK1のFFで取り込み直し、グリッチが発生しない綺麗な信号を作る必要があります。
これで、非同期乗せ替えを行う信号⑤を作ることができました。

次に、下半分の動作についてです。
キャプションを入力できます
こちらについては、「エッジ検出(遅→速)」の ①以降の回路と全く変わりはありませんので収束条件は片方のみ記載しております。
⑤の立ち上がりを検出し、1パルス幅のOUT信号を作成します。動作の詳細については「エッジ検出(遅→速)」を参照してください。

つまり、(当然ですが)速いCLKから遅いCLKの乗せ替えは、遅いクロックより高速で動作させることはできません。
たとえば、CLK1基準で1クロック幅のパルスが連続で来たとして、それを遅いクロック側に乗せ替えてパルス数を数えるということはできません。

req-ackハンドシェイク

クロックの速度が異なる回路同士で、requestとackでハンドシェイクをするような例です。
エッジ検出の応用パターンです。
そろそろ回路図イメージだと辛くなってくるので、RTLのソースにします。

非同期req-ack例

-- CLK1 FF signal trig : std_logic; signal req : std_logic; signal ack_c1_shift : std_logic_vector(2 downto 0); -- CLK2 FF signal req_shift : std_logic_vector(2 downto 0); signal ack_c2_shift : std_logic_vector(1 downto 0); signal ack : std_logic; -- combinational circuit signal req_rise : std_logic; signal ack_ext : std_logic; signal ack_rise : std_logic; begin ---------------------------------------- --CLK1 ---------------------------------------- process(CLK1) begin if (CLK1'event and CLK1 = '1') then if (trig = '1') then req<='1'; elsif (ack_rise = '1') then req<='0'; end if; end if; end process; process(CLK1) begin if (CLK1'event and CLK1 = '1') then ack_c1_shift<=ack_c1_shift(1 downto 0) & ack; end if; end process; ack_rise <= (not ack_c1_shift(2)) and (ack_c1_shift(1)); ---------------------------------------- --CLK2 ---------------------------------------- process(CLK2) begin if (CLK2'event and CLK2 = '1') then req_shift<=req_shift(1 downto 0) & req; end if; end process; req_rise <= (not req_shift(2)) and (req_shift(1)); process(CLK2) begin if (CLK2'event and CLK2 = '1') then ack_c2_shift<=ack_c2_shift(0) & req_rise; end if; end process; ack_ext <= ack_c2_shift(1) or ack_c2_shift(0) or req_rise; process(CLK2) begin if (CLK2'event and CLK2 = '1') then ack<=ack_ext; end if; end process;

キャプションを入力できます

CLK1側でreqを発行する契機(図中ではtrig信号)があると、reqが'1'になります。
このreq信号を、前述の「エッジ検出(遅→速)」と同様の仕組みで、CLK2側に取り込んでいます。
CLK2側ではreq信号の立ち上がり検出信号としてreq_rise信号を作成し、これを契機としてCLK2側の回路を動作させます。
また、この設計では、ackは即返しとしています。
次に、前述の「エッジ検出(速→遅)」と同様に、CLK2基準のackを3クロック幅まで引き延ばし、CLK1側に乗せ替えをし、CLK1側で立ち上がり検出をし、ack_rise信号を作成します。
そして、CLK1側では、ack_rise信号を契機に、req信号をクリアします。
このようにすることで、非同期回路間でのreq-ackの受け渡しができます。

バスブリッジ(非同期載せ替え)

違うクロックで動作する回路同士で、バス信号を載せ替える場合です。
速い回路から遅い回路の例になります。
こちらもRTLソースで説明します。

バスブリッジ例

-- CLK1 FF signal VALID : std_logic; signal DATA : std_logic_vector(15 downto 0); signal data_tmp : std_logic_vector(15 downto 0); signal valid_shift : std_logic_vector(1 downto 0); signal valid_ext_s : std_logic; -- CLK2 FF signal en_shift : std_logic_vector(2 downto 0); signal en_rise_s : std_logic; signal data_tmp2 : std_logic_vector(15 downto 0); signal data2 : std_logic_vector(15 downto 0); -- combinational circuit signal valid_ext : std_logic; signal en_rise : std_logic; begin ---------------------------------------- --CLK1 ---------------------------------------- process(CLK1) begin if (CLK1'event and CLK1 = '1') then if (VALID = '1') then data_tmp<=DATA; end if; end if; end process; process(CLK1) begin if (CLK1'event and CLK1 = '1') then valid_shift<=valid_shift(0) & VALID; end if; end process; valid_ext <= VALID or valid_shift(0) or valid_shift(1); process(CLK1) begin if (CLK1'event and CLK1 = '1') then valid_ext_s <= valid_ext; end if; end process; ---------------------------------------- --CLK2 ---------------------------------------- process(CLK2) begin if (CLK2'event and CLK2 = '1') then en_shift <= en_shift(1 downto 0) & valid_ext_s; data_tmp2 <= data_tmp; data2 <= data_tmp2; en_rise_s <= en_rise; end if; end process; en_rise<=en_shift(1) and (not en_shift(2));

キャプションを入力できます

まず、乗せ替え元のバスはVALID='1'の時に、DATA[15:0]に有効データが乗っている物とします。
VALIDを契機に、乗せ替え用のテンポラリとして、DATA[15:0]をdata_tmp[15:0]に保持し、この信号をクロック乗せ替え用に使用します。
VALIDを遅いクロックに乗せ替える為には信号の引き延ばしをする必要があり、実際に遅いクロックで受け取るのはVALID区間よりも後ろになるため、DATA[15:0]の有効データがVALID期間(1クロック幅)しかない場合は、一旦CLK2側で保持する必要があるためです。
次に、引き延ばしたVALID信号(valid_ext)をCLK2に乗せ替えるのですが、valid_extは組み合わせ回路の出力のため、これをこのままCLK2で受け取る事はできないため(「非同期でのグリッチ」参照)、valid_extをCLK2で一度シフトして、valid_ext_sをCLK2で取り込みます。
これで、valid_ext_s→en_shift(0)へ、FF→FFの単純シフト構成で非同期乗せ替えができます。この信号をシフトし、en_shift(1)、en_shift(2)を作り、これを元に立ち上がり検出をし、en_riseを作成しています。
また、CLK1で保持したdata_tmp[15:0]を、CLK2のdata_tmp2[15:0]に取り込みます。ここではメタステーブルが発生する可能性があります。また、各ビットごとにメタステーブル後にどの値に収束するかが不明のため、メタステーブル後の値はバスとしては無意味な値(不定)となります。
ですが、CLK1側のdata_tmp[15:0]はその後(新たにVALIDが来ない限り)変化はしないため、その次のCLK2の立ち上がりでは、メタステーブルは発生せず、確実にdata_tmp[15:0]の値をdata_tmp2[15:0]に取り込む事ができます(図中:data_tmp2[15:0]のValid部)。
ここで、CLK2側でen_riseの1の期間は、data_tmp2[15:0]に確実に有効データがあることが保証できます。
さて、ここでen_rise='1'のときにdata_tmp2[15:0]の値を使って色々処理をすれば良さそうですが、data_tmp2[15:0]はその1クロック前にメタステーブル状態にあります。
「スペック違反で何が起きるか」で説明した通り、実際にはその状態を使用しない場合でも、可能な限りメタステーブル状態は回路内部に伝搬しないように設計しなくてはいけません。
そのため、data_tmp2[15:0]を単純に1クロックシフトし、data2[15:0]を作ります。ここはビット単位でみれば単純なシフトレジスタとなります。
また、タイミングを合わせるため、en_riseもシフトし、en_rise_sを作成します。
ここで、安全な(メタステーブルなどが起きない)en_rise_sとdata2[15:0]が作成できますので、その後の処理ではこのタイミング(図中赤丸)の値を使用します。
つまり、DATA[15:0]の非同期吸収用シフトレジスタという観点では、1段目がdata_tmp2[15:0]で、2段目がdata2[15:0]になります。
「対策」で説明した通り、デバイススペックに対してギリギリの周波数で非同期回路を設計する場合、非同期吸収のシフトレジスタの段数を増やす必要があります。

これがバスブリッジの考えかたとなりますが、FPGAであれば、回路規模に余裕があればこんな面倒な設計をするよりは、元々用意されている非同期FIFOを使う方が安全で簡単でしょう。

まとめ

全体として、非同期設計をする場合に注意する点をもう一度挙げます。

やってはいけない実装

  • メタステーブル状態の分配
    メタステーブル状態になる可能性のある信号は回路内で複数箇所に分配してはいけません。1対1のシフトレジスタとしましょう。
  • 冗長論理によるグリッジ
    非同期構成のFF間に論理入れてはいけません。非同期の箇所はシフトレジスタ構成としましょう。
  • クロック以外のエッジ検出
    エッジ検出をする信号はグリッチを出力しない回路であることは保証されてますか?
    →ついでに、自分が出力する側になる場合、グリッチを出力しない回路とするよう心がけましょう。つまり、出力信号はFFから直出力するような構成にし、回路の最後がFFではなく組み合わせ回路となることは可能な限り避けましょう。

結論としては、非同期設計と同期設計の混在は、設計が難しい上に、回路規模や処理速度的にも無駄が多くなりますので、できる限り避けた方が賢明ですね。

最後に

RTLで記載する非同期回路は非常に設計が難しく、また、不具合の再現性がないため、原因の特定も難しいと思います。
わりと長めの記事ですが、この記事を読んで頂き、どういう原因で不具合が発生するのかの理解と、原因を理解した上での不具合箇所の特定のお役に立てればと幸いです。

脚注コメント

※1 厳密には、実際には配線そのものにも遅延があり、それはクロックの配線にも存在しますし、温度条件、電圧条件、1→0の変化なのか0→1の変化なのかでも遅延時間は変化しますので、計算はここまで単純ではありませんが、この説明では簡略化しています。
※2 さらに厳密にはチップ内の場所により温度が違うなどの条件で微妙に崩れることはありますが、最悪条件で計算すると検証できます。
※3 クロックの配線にグリッチが発生すると問題になりますが、それは今回の本質とは別件なので割愛します。

6
lyricalmagicalのアイコン画像
電子デバイスはとってもりりかるなの
ログインしてコメントを投稿する