01signal.com

データを伴うクロックドメインクロッシング

このページは、クロックドメインに関する全 3 ページのシリーズの最後です。

1 ビットでは足りないとき

クロックドメインをまたぐ必要がある信号は、1 ビットではなく、データワードであることも珍しくありません。この場合の素直な解決策は、前のページで提案したとおり、FPGA ベンダーが提供するデュアルクロック FIFO です。しかし、それが使えない場面もあります。そもそも、その FIFO を最初に実装した人がいたはずです。

ここでの目標は、ベクトル信号が別のクロックドメイン上に正しく現れるようにすることです。まず、クロックドメインクロッシングの素朴で誤った例を見てみましょう。なぜそれほど簡単ではないのかを説明するためです。

reg [7:0] foo, bar, bar_metaguard;

always @(posedge clk1)
  foo <= foo + 1;

always @(posedge clk2)
  begin
    bar_metaguard <= foo; // This will fail sometimes!
    bar <= bar_metaguard;
  end

これは前のページの単純なメタステイビリティガードの例とまったく同じですが、レジスタが 8 ビットのベクトルであり、@foo は '0' と '1' の間で変化する代わりにカウンタになっています。

では、なぜこれが誤りなのでしょうか。問題は、@foo から @bar_metaguard に至るパス(path)の配線遅延の違いにあります。8 つのビットのそれぞれで遅延が異なるのです。@foo が変化したとき、これらのビットの変化の一部は @bar_metaguard のフリップフロップに正しいタイミングで届くかもしれませんが、ほかのビットは間に合わないかもしれません。

したがって、@bar_metaguard を構成する 8 つのフリップフロップのどれにもメタステイビリティが起きない場合でも、次のような状況が起こり得ます。@foo が変化したとき、@bar_metaguard の一部のビットだけが新しい値を取り込み、残りのビットは古い値のままになることがあります。もし @foo が 0xff から 0x00 に変化したら、@bar_metaguard の次の値は何でもあり得ます。なぜなら、一部のビットは変化前の @foo の値を取り込み、別のビットは変化後の値を取り込むからです。この誤った値は、@clk2 の 1 クロックサイクル後に @bar に現れます。

この問題を解決するには、まず要件を明確にする必要があります。@bar は常に有効な値を保持することが求められているのでしょうか。それとも、時々情報を一方のクロックドメインからもう一方へ渡すためのものなのでしょうか。この 2 つの選択肢を分けて説明します。

選択肢 1:連続サンプリング

宛先のワード(この例では @bar)が、もう一方のクロックドメイン上のワード(@foo)を連続的にサンプリング(sampling)し、常に正しく意味のある値を保持しなければならない場合、それを保証する方法は 1 つしかありません。上に示した素朴なクロックドメインクロッシングの方法を使うことですが、その際、@clk1 の各クロックサイクルで @foo のビットのうち 1 つだけが変化する(または変化しない)ことを確認しなければなりません。言い換えれば、ベクトル信号に対してメタステイビリティガードを使いつつ、同じクロックサイクルで複数のビットが変化することによる問題を避けるのです。

各クロックサイクルで変化できるビットは 1 つだけなので、個々の変化は @bar_metaguard にサンプリングされるか、取り逃がされるかのどちらかです。どちらにしても、それは @foo がかつて持っていた値のいずれかを反映しています。

したがって、上の例の @foo が通常のバイナリコードではなくグレイコードを使うカウンタであれば、これは完全にうまく機能します。グレイコードの本質は、カウントが進むときに毎回 1 ビットだけが変化することです。したがって、@bar には常に意味のある値が入ることが保証されます。

でも待ってください。@clk1 が @clk2 より高い周波数だったらどうなるのでしょう?@bar が @foo の値をいくつかスキップしても構わないのであれば、それは問題になりません。たとえば @foo がグレイコード形式のカウンタであれば、@bar を見たときにカウント値の一部が抜けていることがあります。それでも、@bar に現れるすべての値は、ある時点で @foo に確かに現れた値ですから、その意味では正しいのです。

この方法が最もよく使われるのはデュアルクロック FIFO の中です。FIFO の RAM 内のアドレスを 2 つのクロックドメイン間で渡すためにグレイコードが使われます。FIFO の書き込み側は、RAM に書き込んだ最後のワードのアドレスをグレイコードに符号化します。その符号化されたワードは、ワードに対してメタステイビリティガードを使うことによって、別のクロックドメインにある読み出し側へ渡されます。逆方向でも同じ方法が使われます。

それぞれの側は、相手側の更新後のアドレスを(メタステイビリティガードの遅延後に)知ることができるので、FIFO 内にいくつの要素があるかも計算できます。その結果、empty(空き)や full(満杯)のような信号を生成できるのです。

より高い頻度でイベントを伝える

前のページで述べたように、1 ビットだけのメタステイビリティガードには、送信元クロックドメインのクロック周波数に関する制限があります。このビットの変化の一つ一つが、相手側に何らかのイベントを知らせる手段だとすると、イベントの発生頻度が高すぎる場合、受け取り側がイベントを取り逃がす恐れがあります。

解決策は、グレイコードで符号化したカウンタをクロックドメイン間で渡すことです。そうすれば、受け取り側はイベントがいくつ発生したかを知ることができ、情報を失うことはありません。このカウンタのビット数は、@clk1 のすべてのクロックサイクルでイベントが発生しても、受け取り側がイベントの発生数を正しく推定できるように選びます。

そういう意味では、1 ビットよりもベクトルの方が扱いやすいことがあります。1 ビットが変化しても、宛先のクロックが遅いためにその事実を取り逃がすと、「何かが起きたこと」自体を失うことになります。しかし、グレイコードなどを使って正しくクロックドメイン間で渡されるワードであれば、情報は失われません。

そして、この目的には 1 ビットで十分だという場合、グレイコードのカウンタは、イベントのたびに値を反転する 1 ビットレジスタにすぎません。言い換えれば、1 ビットの場合、グレイコードカウンタによる解決策は、単純なメタステイビリティガードと完全に同じになります。

パスのタイミングに関する余談

この節のタイトルが示すとおり、ここは読み飛ばして次の節へ進んでも構いません。

ベクトル信号に対するメタステイビリティガードには、暗黙の前提があります。それは、これらのパスの遅延の差が、送信元クロックの 1 周期を超えないということです。

この前提は、特別なことをしなくてもほぼ間違いなく満たされます。それでも、この理論的な例を考えてみましょう。@clk1 が 500 MHz だとします。@foo から @bar_metaguard へのパスの 1 つには配線遅延が 1 ns あるとします。また、別のパスには配線遅延が 4 ns あるとしましょう。もちろん、そんなことは現実にはまず起こりませんが、何が起こり得るかを見てみます。

一方のビットが値を変え、その変化は 4 ns かかる旅を始めます。次のクロックサイクル、つまり 2 ns 後に、もう一方のビットが変化し、1 ns で @bar_metaguard に届きます。しかし、それは最初のビットの到着より 1 ns 早いことになります。したがって、@bar_metaguard は、@foo が実際には取ったことのない値をワード全体としてサンプリングし得るのです。

配線遅延は通常、この例よりはるかに短いので、実際には起こらないと考えてよいでしょう。それでも、理論的にはどんな配線遅延も起こり得ます。この可能性を完全に排除するには、次のようなタイミング制約(timing constraints)を使うことができます(Vivado 形式)。

set_max_delay -datapath_only -from [ get_pins -hier -filter {name=~*/C} ] -to [ get_pins -hier -filter {name=~*_metaguard*/D} ] 1.5

この制約は、前のページで示した set_max_delay の制約に似ています。ただし、前のページではメタステイビリティガードが "-from" 側にあり、ここでは "-to" 側にあることに注意してください。つまり、これらの制約は同じパスに適用されるわけではありません。前のページの制約の目的は、メタステイビリティガードがメタステイビリティから回復する時間を確保することでした。したがって、その制約は同じクロック内のパスに適用されます。一方、上の制約はクロックドメインクロッシングそのものに関係しています。

そのため、この制約は別の書き方になります。関連するパスは無関係クロックのクロックドメイン間を接続しているので、それらのクロックのクロックスキュー(clock skew)やジッタを考慮に入れても意味がありません。-datapath_only はまさにそのことを表しています。クロックがフリップフロップに届くまでの時間は気にせず、パスそのものを測定しなさい、ということです。

この制約がわかりにくいのは、パスが送信元フリップフロップのクロックピンで始まり、送信先のデータ入力ピン D で終わる点です。つまり、ストップウォッチは送信元フリップフロップにクロックが届いたときにスタートし、更新された信号が送信先に届いたときに終わります。そして、その到着がセットアップ時間を満たすことが要求されます。したがって、このパスには両側のタイミング要件が含まれているのです。

このタイミング制約のように、これらのパスをすべて 1.5 ns に制限すれば、どのパスもこの制限時間を超えることはできません。したがって、パス遅延のばらつきもこの値以下に制限されます。たとえ @clk1 のクロック周期が 2 ns であっても、パスが誤った順序で届くことはあり得ません。繰り返しになりますが、そんなことは現実にはまず起こりませんが、これを保証する方法がこれなのです。

メタステイビリティガードへのパスにフォールスパス(false path)制約(set_false_paths や set_clock_groups など)が適用されている場合、set_max_delay はおそらく効果を持ちません。フォールスパス制約が優先される可能性が高いからです。したがって、常にタイミングレポートでパスを確認し、ツールが制約を意図どおりに解釈していることを検証してください。これについてはタイミングに関する別のページで説明しています。

選択肢 2:レジスタを時々更新する

各クロックサイクルで 1 ビットしか変化してはいけない、という制約は、しばしば厳しすぎます。データの更新が時々しか行われないのであれば、別の手法が使えます。次の例では、@do_update が数クロックサイクルに 1 回だけアクティブ(値 '1')になると仮定します。また、この信号は、@foo の値を @new_value で更新すべきことを示すために使われるとします。

reg [7:0] foo, bar;
reg       toggle, toggle_metaguard, toggle_a, toggle_b;
reg       new_bar;

always @(posedge clk1)
  if (do_update)
    begin
      foo <= new_value;
      toggle <= !toggle;
    end

always @(posedge clk2)
  begin
    toggle_metaguard <= toggle;
    toggle_a <= toggle_metaguard;
    toggle_b <= toggle_a;

    if (toggle_a != toggle_b)
      bar <= foo; // No metastability guard, because foo is stable
    new_bar <= (toggle_a != toggle_b); // Not necessary, just side info
  end

今は @new_bar のことは無視してください。後で説明します。

この仕組みは次のとおりです。@foo は @do_update がアクティブなときだけ更新されます。そのとき、@toggle は同じクロックサイクルで反転します。

@clk2 のクロックドメインでは、@toggle_metaguard がメタステイビリティガードとして @toggle の値を取得します。次のクロックサイクルで、この値は @toggle_a にコピーされます。その次のクロックサイクルで、@foo の値が @bar に直接コピーされます。これは、@toggle_a と @toggle_b の値がちょうど 1 クロックサイクルの間だけ異なるためです。

@bar と @foo が異なるクロックドメインにあることは問題になりません。@foo は、タイミング要件を満たすのに十分以上に長い間、安定しているからです。

なぜそんなに確信があるのでしょうか。今回はちゃんとした理由があります。手順全体は、@toggle が変化したために @toggle_metaguard が値を変えたときに始まります。もし @bar が同じ @clk2 クロックサイクルで @foo をサンプリングしていたなら、それは安全ではありませんでした。運がよければ問題なかったかもしれませんが。しかし、@toggle_metaguard の新しい値が @toggle_a に届くまでには、もう 1 クロックサイクルの @clk2 が必要です。そして、その時点でもまだ @bar は更新されず、次の @clk2 クロックサイクルになってようやく更新されます。

したがって、@foo が変化してから @bar にサンプリングされるまでには、@clk2 の少なくとも 2 クロックサイクルに相当する時間があります。どんなフリップフロップのセットアップ時間と比べても、これは永遠とも言えるほど長い時間です。とはいえ、前のページで示したような set_max_delay を @toggle_metaguard に適用するのは理にかなっています。@bar に至るパスについても同様に適用できますが、先ほど述べた「永遠」の時間があるため、必要になる可能性は極めて低いでしょう。

この手法のアキレス腱は、@do_update のアクティブ頻度が十分に低く、@bar にサンプリングされるときに @foo が安定していることを保証しなければならない点です。更新間隔の現実的な最小時間は、@clk2 の 4 クロックサイクルに相当する時間です。したがって、@clk2 の 4 クロックサイクルが @clk1 の何クロックサイクルに相当するかを計算し、切り上げます。@clk1 が @clk2 より 4 倍以上遅ければ、これはまったく制約になりません。そうでない場合は、@do_update が許容される頻度より多くアクティブにならないことを保証する仕組みをロジック内に設ける必要があります。

実際の設計では、更新頻度が非常に低い場合、@toggle のような保護を何も使わずに、いい加減にクロックドメインをまたぐことがあります。そのようにした場合、@foo は @bar に連続的にコピーされます。@foo がごくたまに変化するとき、@bar は 1 クロックサイクルの間、誤った値を含むかもしれませんが、誰が気にするでしょう?たいていの場合、この誤りはクロックドメインの問題自体を軽視した結果です。だって、動いているのですから。それが、たまに動かなくなるまでは

ずさんな話ついでに言うと、上の例では @toggle もその関連レジスタも、リセットも初期値の代入もされていません。これは通常は問題ありません。おそらくシンセサイザ(synthesizer)がすべて初期値 0 にするからです。また、万一これらのレジスタが最初に同じ値を持っていなくても、@foo を 1 回余分にサンプリングするだけで、それ以上の問題にはなりません。とはいえ、これらのレジスタをリセットしておくのは良い考えかもしれません。

より発展的なバリエーション

ここまで、3 つの単純な例を示しました。

これらの単純な例は、ほかのさまざまな仕組みの基礎になっています。

まず、上の例の @new_bar について説明すると約束しましたね。これは、@bar に新しい値が入ったときに、ちょうど 1 クロックサイクルだけ High になるレジスタです。特別なことは何もありません。ただし、@bar と @new_bar は、もう一方のクロックドメインの @foo と @do_update を反映していることに注意してください。つまり、これはコマンドやステータスメッセージをクロックドメイン間で渡す 1 つの方法です(可能なら FIFO を使うべきだと言ったのを覚えていますか?)。

最後の例を発展させた興味深い方法として、@foo と @bar というレジスタのペアの代わりにデュアルポート RAM を使う方法があります。これは、データのバッファをクロックドメイン間で渡す手法です。@clk1 のクロックドメインのロジックが RAM にデータを書き込み、しばらくすると RAM の半分を満たすとしましょう。ロジックが RAM の後半半分を満たし始めるとき、@toggle の値を変更します。このレジスタは、上の例とまったく同じように @clk2 のクロックドメインへコピーされます。しかし、例のように @bar を更新する代わりに、ロジックは RAM の前半半分のデータを消費します。

このようにして、この単純なレジスタがダブルバッファ機構を同期させることができます。この機構では、一方が RAM にデータを書き込み、もう一方がその RAM からデータを読み出します。実際、@toggle の役割は値を変えることだけではなく、現在 RAM のどちらの半分に書き込もうとしているのかを相手側に伝えることでもあります。

それでも、可能であれば FIFO を使うのが最善です。このダブルバッファ機構は魅力的に聞こえるかもしれませんが、もっと良い代替手段がない場合にだけ使うべきです。たとえば、RAM からのデータの読み出し順序が書き込み順序と異なる場合などです。

まとめ

結局のところ、次の 1 点に行き着きます。無関係クロックのクロックドメイン間の遷移には、常に再同期ロジックが関わります。この再同期を通るデータワードには制約があり、送信元クロック(例では @clk1)の各クロックサイクルで変化できるのは 1 ビットだけです。そうしないと、不正なデータが送信先に届く可能性があります。

一部の応用ではこれで十分ですが、その制約が厳しすぎる場合は、データ自体には再同期ロジックを使わずに、ベクトルレジスタや RAM を介してクロックドメイン間でデータを移動させることもできます。これは、書き込み操作と読み出し操作の間に最小限の時間間隔を保つロジックによって成立します。この時間間隔により、データワードが送信先でサンプリングされるときに安定していることが保証されます。ただし、このロジック自体も、同じクロックドメインクロッシングの技術に基づいています。したがって、この解決策にも、1 度に 1 ビットずつしか変化しない再同期ロジックが関わってきます。場合によってはグレイコードを使います。

つまり、無関係クロックが関わる限り、再同期ロジックと「1 ビットずつ」という規則は常に存在します。問題は、それらをどう適用するかだけなのです。

このページは英語から機械翻訳されたものです。不明な点があれば、原文を参照してください。
Copyright © 2021-2026. All rights reserved. (dcc38493)