Ryuz's tech blog

FPGAなどの技術ブログ

メタステーブルについて考えてみる

はじめに

もう専門の方から一直線にマサカリが飛んできそうなタイトルで怖いんですが、ちょうど今週末は非同期周りを整理していて X でも少し盛り上がったのでネタにしておきます。

組み込みやっていると、チャタリングとかシュミットトリガとかはよく聞くわりに、HDL 書かない限りはあまり聞かない メタステーブル ですが(偏見?)、FPGAでの非同期バグでは鉄板のネタなので触れておきたいと思います。

再現性がないのでやらかすとデバッグがとても厄介なのですよね。

ちなみに私がメタステーブルを最初に勉強したのは定本ASICの論理回路設計です。

メタステーブルとは

フリップフロップ(FF)に値を正しくラッチさせるためには、セットアップ時間、ホールド時間、VIL/VIHなどの電圧範囲を守る必要があります。

これらが守られてない場合、出力が 0 になっても1になっても文句は言えないわけですが、0でも1でもない第3の状態としてFFがメタステーブル状態に突入することがあります。

メタステーブル状態に入ると、1にも0にも確定していない不安定な状態が指数分布に従う時間だけ続いて、やがて0か1に倒れて安定状態になるそうです。

「このようなことが起こると困りますよね。定格はちゃんと守って使いましょう。」で済めばいいのですが、そうはいかないのでこの話になるわけです。

人間がボタンを押すとかの非同期な事象に加え、異なるオシレータからのクロックドメイン同士の通信だとこのメタステーブル状態の発生は基本的に避けられません。

同じ100MHz同士であったとしても、オシレータ は数 ppm 程度の誤差は常に持っていますので、100MHzもあれば1秒後には100サイクルとかのオーダーでずれてしまっているわけで、その間に 100回ぐらいメタステーブルになりうる条件を作ってしまいます。

ただし、メタステーブル状態はずっと続くわけではないので、一定時間で収まり、ダブルラッチなどしておけば、次のクロックまでには高い確率で収まります。

ChatGPT 曰く

メタステーブル状態の発生に関する MTBF(Mean Time Between Failures)の式は以下のようになります:

\text{MTBF} = \frac{1}{f_{\text{clock}} \cdot f_{\text{data}} \cdot T_{\text{window}}} \cdot e^{\frac{T_{\text{res}}}{\tau}}
  •  f_{\text{clock}} : クロック周波数
  •  f_{\text{data}} : データ入力の切り替わり頻度
  •  T_{\text{window}} : メタステーブルが発生する時間幅(セットアップ+ホールド)
  •  T_{\text{res}} : 解決のために使える時間
  •  \tau : フリップフロップ固有の時定数

だそうです。

確率は常にゼロにはなりませんが、通常はダブルラッチするだけで数万年オーダーの MTBF となるそうなので、ちゃんとあしらっておけば、確率的にはECCのないメモリのソフトエラーなどの方がよっぽど心配すべき事象になる程度には下げられるのだと思います。

非同期信号のラッチ

いろんな解釈があると思いますが、入力信号を綺麗にラッチするために、入力クロックの位相をPLLで調整していっていい感じの場所を探すなんてことはよくやるわけです。

で、そんなノリでクロックの方の時間をずらしながら図示してみると、

データとクロックの関係

すべてのラッチ条件が満たせていてスペック通り0や1が取り込める領域と、そうでない領域があります。

とはいえ、「非同期を受けるときはダブルラッチしなさい」とよく訓練されているプログラマだと、とりあえずダブルラッチしているので最終的に0が1に値が倒れるので、早い方か遅い方のどちらかをラッチしたように見えてしまいがちです。

しかし、0と1のどちらかに倒れる話と、メタステーブル状態に突入する話は全く別の話なのでこれは注意しておく必要があります。

FPGA での非同期の扱い

RTL で書いてちゃんと制約する

私は Verilator などの OSS のシミュレータを使う関係上、あまりベンダーの IP は使わずに RTL でいろいろなものを書いてしまいがちです。

その際 AMD の場合、楽に制約を設定するために ASYNC_REG というアトリビュートがあります。 UG912にこのアトリビュートの詳しい説明があります。

UG912の図

これは何者かというと、合成時に関して言えば、メタステーブルが伝搬しにくいように受け側のFFを複数並べてダブルラッチ/トリプルラッチなどを作るときにFF同士を最短経路で繋がるように配置配線を制約してくれるというものです。

折角ダブルラッチ構成にしてもFF同士の距離が離れてしまうと、せかっく元のFFでは1クロック時間たってメタステーブルが収まったのに、取り込む側が配線遅延でまだメタステーブル中だった信号をラッチしてしまい自分もメタステーブル状態に突入する という身もふたもないことが起こってしまいます。

ベンダーの用意したライブラリを使う

AMD の場合、例えば UG974 などを見ると、下記のようなマクロが用意されています。

  • XPM_CDC_ARRAY_SINGLE
  • XPM_CDC_ASYNC_RST
  • XPM_CDC_GRAY
  • XPM_CDC_HANDSHAKE
  • XPM_CDC_PULSE
  • XPM_CDC_SINGLE
  • XPM_CDC_SYNC_RST

これらはクロックドメインをまたいで信号をやり取りするときの、定石的な方法をブラックボックス化してくれているようです。

実は今まで使ったことなかったのですが、使えるときは使った方が安全 かなと心を入れ替えつつ、そうはいっても使えないときに困らないようにRTLで互換部品も書いておこうと、今日この辺りを書きながら勉強してました。

案外普段自分がやってる方法と同じものもあれば違うものもあり勉強になりました。

衝撃だったのが、FFの段数がデフォルトで4なのですね。 トリプルラッチどころじゃないですね。 いつもダブルラッチで済ませているので不安を覚えました。 段数を増やすほどにMTBFは増大していきますので、周波数にもよりますが、品質の問われる用途に応じて増減させるものと思われます。

身もふたもない話

そんなプリミティブなところ弄くらんでも とりあえず非同期FIFO使っておけばOK というのはその通りで、ほぼすべてのFPGAベンダーが非同期FIFOを提供していますね。

AMD も UltraScele 世代だと、IP 生成以外に XPM_FIFO_ASYNC などのマクロも使えるようですね。

逆に 非同期FIFO があまりにもうまく臭いものに蓋をしてくれているせいで、このような話を調べる必要も普段はあんまりなかったとも言えます。

グレイコードに関する疑問

ちなみに IP 使うの避けがちな私は、超昔に書いたグレイコード使うFIFOを未だによく使っていたりします。

とはいえグレイコードには少し疑問も残っていて、複数bitが同時にメタステーブルを起こしてしまう可能性、すなわち、クロック周期からメタステーブルに入る条件のマージン分を引いた値に、全bitの遅延ばらつきを納めておかないと、メタステーブルを起こすのは高々1bitで、それが1に倒れても0に倒れても前後どちらかの符号になるだけ というグレイコードFIFOの前提条件が壊れる件です。

よく

set_max_delay -datapath_only -from [get_clocks clk1] -to [get_clocks clk2]  2.00

のような制約を書くのですが、結局、遅延時間はいくつを書けばいいのかと。

もちろんこれがクロック周期以上ずれるとメタス以前の問題で符号が化けるので、クロック周期より小さい値ではないといけないとは思うのですが、具体的にいくつにするのが良いのかなかなかよくわからず。

もっと言うとバラつきが問題なので set_max_delay というのもそもそもなんか違うんじゃないかと思ってみたり。

XPM_CDC_GRAY の中身にいったいどんな制約が埋め込まれているのか興味津々なのですが、合成結果だけ見ても分からないのですよね。

どう書くのが正解なのでしょうね。いやむしろ XPM_CDC_GRAY を使うべきなのでしょうね。

おわりに

なんとなくとりとめもない記事になってしまいましたが、こんなことをしていた1日でした。

余談(VIH/VIL)の話

今回FPGAの中の話でしたが、FPGAの外だと、Hとみなせる電圧(VIH)、L とみなせる電圧(VIL)に規定があります。

で通常はあまり問題ないのですが、I2Cみたいにプルアップ抵抗からの給電でHレベルを作るようなバスだと、電圧の立ち上がりがとても長いケースがあります。

そうするとFPGAの数百MHzなんかでラッチすると、電位が VIH と VIL の間にいるときに何度もラッチしてしまって、中にはメタステーブル状態に入って 0 や 1 を行き来することがあります。

チャタリングとはまた違う現象で、電圧は単調増加しているのになぜかラッチ後のデジタル波形に髭が出てくるんですね。

対策は、例えば8サイクルに1回だけラッチするとか、通信している仕様にあわせてラッチ周期を間引くことのようです。

間引いてしまえば、偶然1個メタステーブルを起こすようなタイミングに来ても、前後は正しくラッチされますので、0と1どちらに倒れても I2C としてのプロトコルに不整合は起こらなくなるわけですね。

いやはや、非同期系はほんと面倒なことが多い。