編集履歴一覧に戻る
uchanのアイコン画像

uchan が 2026年02月12日11時45分03秒 に編集

初版

タイトルの変更

+

FPGA でレジスタ代入が失敗する

タグの変更

+

FPGA

+

Gowin

+

Veryl

+

Tang-Nano

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

セットアップや使用方法

本文の変更

+

Tang Nano 9K で 2 つのクロックをまたがってデータを受け渡す回路を作ってみたのですが、たまにデータの受け渡しに失敗する現象に出会いました。体感としては 5%~10% くらいの失敗率で、かなり頻度が高く、これを解決しないと使い物にならないため、原因と解決策を探りました。 ## 遭遇した現象 UART で受信した文字列を映像信号に変換する回路を作っていました。UART で受信した文字はいったんバッファに格納され、そのバッファを先頭から読み出しつつフォントデータを組み合わせることでピクセルデータを作り、それをさらに DVI 信号へと加工するという回路です。そのなかで、UART で受信した文字がバッファに記録されないことがある、という現象に遭遇しました。 以下、登場する主な信号を紹介します。 | 信号名 | クロックドメイン | 信号の役割 | |--------|------------------|------------| | `rx_data` | `sys_clk` | UART モジュールのデータ出力(8 ビット) | | `rx_full` | `sys_clk` | UART の受信バッファにデータがあることを示す | | `uart_rd` | `sys_clk` | UART モジュールに対し、受信バッファからデータを読み取ったことを伝える | | `wr_buf` | `pclk` | バッファへ格納する前段階の一時置き場(8 ビット) | | `rx_ack` | `pclk` | `rx_data` から `wr_buf` への読み込みが完了したことを示す | | `wr_valid`| `pclk` | `wr_buf` に有効なデータが入っていることを示す | まず `DEAD\n` を送信したときの波形を示します。`rx_data`、`wr_buf` はともにレジスタの下位 6 ビットを示します。したがって値域は 00~3F となります。 ![レジスタへの代入が失敗するときとしないとき](https://camo.elchika.com/35223ed47f2c83b1218c26d9bdfbfa42b091583d/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f39653039633461662d363265312d346233652d626162662d353863636137633039343733/) `wr_buf` が変化していない部分があります。これが、本記事のタイトルの代入失敗を表しています。なお、代入失敗の発生確率は体感で 5% から 1 割くらいでしょうか。 `DEAD\n` は16進数で書くと 44 45 41 44 0A です。下位 6 ビットは 04 05 01 04 0A。つまり、最初の文字 D は代入できているが、次の文字 E の代入が失敗しています。 代入失敗時(「E」受信時)の制御信号が次図。(`uart_rd` を間違えて `uard_rd` と書いてます) ![FPGAでレジスタ代入が失敗するときの制御信号](https://camo.elchika.com/830fc4337e07ff62ac03fb035288de7c812474a8/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f33303766623131322d613765362d346637392d613234332d623030356235393764623334/) 代入成功時(「A」受信時)の制御信号が次図。 ![FPGAでレジスタ代入が成功するときの制御信号](https://camo.elchika.com/a6c5bba9cedf5a8e2d6929a472839162ae14d9d3/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f39346432646435392d353032652d343530382d393837662d396363633066373733343736/) 制御信号の幅が多少違いますが、変化の順序は全く同じです。 `rx_full`(ソースコード上は `uart_rx_full`)と `uart_rd` は UART モジュールのポートに接続されています: ```verilog inst uart3b: Uart ( clk: sys_clk, rst_n: rst_n, rx: uart_rx, tx: uart_tx, rx_data: uart_rx_data, tx_data: uart_tx_data, rd: uart_rd, rx_full: uart_rx_full, wr: uart_wr, tx_ready: uart_tx_ready, ); var uart_rx_full_sync: bit; always_ff (sys_clk, rst_n) { if_reset { uart_rx_full_sync = 0; } else { uart_rx_full_sync = uart_rx_full; } } var uart_rx_ack_sys: bit; unsafe (cdc) { assign uart_rx_ack_sys = uart_rx_ack_dvi; } always_ff (sys_clk, rst_n) { if_reset { uart_rd = 0; } else { uart_rd = uart_rx_ack_sys; } } ``` なお、これは SystemVerilog をベースにした [Veryl](https://doc.veryl-lang.org/book/ja/) というハードウェア記述言語で書いています。 `wr_buf`(ソースコード上は `scrn_wr_buf`)への代入部分のソースコード: ```verilog unsafe (cdc) { always_ff (pclk, dvi_rst_n) { if_reset { uart_rx_ack = 0; scrn_wr_valid = 0; wr_buf_differ = 0; } else { if scrn_wr_valid & scrn_wr_ready { scrn_wr_valid = 0; } if uart_rx_full_sync { if uart_rx_ack { uart_rx_ack_dvi = 1; // 受信後、1クロック経過してから受信完了を通知 if scrn_wr_buf != uart_rx_data { wr_buf_differ = 1; // デバッグ用 } } else { uart_rx_ack = 1; scrn_wr_buf = uart_rx_data; scrn_wr_valid = 1; } } else { uart_rx_ack = 0; uart_rx_ack_dvi = 0; // uart_rx_full_sync が 0 になったことを確認し、受信完了信号をネゲート } } } } ``` 見ての通り、UART モジュール(や、そこに繋がる `rx_full`、`uart_rd`)は `sys_clk`(27MHz 水晶)で動作していて、`wr_buf` や `rx_ack` は `pclk`(`sys_clk` から PLL で作った、約40MHz 程度のクロック)で動作しています。つまりクロック境界をまたぐ回路になっています。そのため、一部の処理を `unsafe (cdc)` で囲んでいます。 最も核心の部分は次の 3 行です。 ```verilog uart_rx_ack = 1; scrn_wr_buf = uart_rx_data; scrn_wr_valid = 1; ``` 観測された信号を見る限り `uart_rx_ack` と `scrn_wr_valid` はともに 1 になっているのにも関わらず、`scrn_wr_buf` に値が書かれていないように見えます。 問題究明のため、`scrc_wr_buf` へ書き込んだ 1 クロック後に、`scrn_wr_buf` と `uart_rx_data` を比較して値が異なる場合にフラグ `wr_buf_differ` を 1 にするようにしてみた。そして、このフラグを LED に表示するようにしてみた。すると、何文字か入力して、初めてデータが化けたときに LED が点灯しました。`scrc_wr_buf` へ書き込めていないことは検出できているわけです。ではなぜ、書き込めなかったのでしょう。 ## 長い文字列で実験 1234567890 の繰り返しを送信し、データ化けを観察しました。1 文字ずつではなく、長いデータを連続で送信してみます。 ![ディスプレイに描画された長い文字列とデータが化けた箇所の信号波形](https://camo.elchika.com/eefe0603c5713d4526d1a6204350898c2a22d62c/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f31613335333562352d353132622d346666612d613436332d663364323235366335393432/) UART受信データ `rx_data` は正常に受信できています。一方 `wr_buf` に所々データ化けが見られます。同じ文字が連続する現象が 1 件、1 文字消失する現象が 2 件ありました。同じ文字が連続するケースでは `wr_valid` が出ていて、文字が消失するケースでは `wr_valid` が出ていないことが分かります。 ### 謎1 受信したデータが `wr_buf` に代入されない これは元々の問題意識です。`rx_data` には正常に値が入っていることから、UART からの受信は正常です。しかし `wr_buf` が更新されていない、あるいは古い値が再び代入されているように見えます。なぜでしょうか。 ### 謎2 `wr_valid` が出ていないのに `rx_ack` が出ている この謎は、長い文字列で実験して初めて気付いたものです。次に示すように、`rx_ack` と `wr_valid` は必ず同時に 1 になるようにしています。ここ以外に `rx_ack` に 1 を書いている箇所はありません。 ```verilog } else { uart_rx_ack = 1; scrn_wr_buf = uart_rx_data; scrn_wr_valid = 1; } ``` にもかかわらず、`rx_ack` は 1 になっているが `wr_valid` が 0 のまま、ということがなぜ起こるのでしょうか。 ### 信号間の時間の解析 正常と異常のケースそれぞれで、`rx_full` が 1 になってから `rx_ack` が 1 になるまでの時間(T)の統計を取ってみました。すると、異常の場合は `rx_full` から `rx_ack` までの時間が短いことが分かりました。異常のケースは 3 件だけなので、信頼性は低いかもしれませが。 | ケース | T の平均 | T の最大 | T の最小 | | --- | ------ | ----- | ----- | | 正常 | 50ns | 70ns | 40ns | | 異常 | 36.7ns | 40ns | 30ns | ※ロジックアナライザのデータを CSV で書き出し、それを Python スクリプトで解析して統計を取りました。値が 10ns 単位になっているのは、ロジックアナライザのサンプリングレートが 100MHz のため、元データが 10ns 単位になっているからです。 ## 解決 理論はよく分かっていないのですが、`uart_rx_full_sync` を `pclk` に同期する信号に修正したところ、すっかり問題の現象が消えました。 修正前の記述: ```verilog var uart_rx_full_sync: bit; always_ff (sys_clk, rst_n) { if_reset { uart_rx_full_sync = 0; } else { uart_rx_full_sync = uart_rx_full; } } ``` 修正後の記述: ```verilog var uart_rx_full_sync: 'dvi bit; unsafe (cdc) { always_ff (pclk, dvi_rst_n) { if_reset { uart_rx_full_sync = 0; } else { uart_rx_full_sync = uart_rx_full; } } } ``` `if` の条件式に指定する信号は、その `if` が動作するクロックに同期した D-FF の出力を使う、というのがポイントなのでしょうか。 ## 自分なりの考察 理論が分かっていないなりに考察してみようと思います。問題となったのは次のような if 文でした。 ```verilog always_ff (clk, rst) { if_reset { ... } else { if <clkに同期していない信号> { 文1; 文2; } } } ``` 特徴としては次のようになっています。 1. `always_ff` はクロック `clk` に同期して動く。 2. `if` の条件式に `clk` に同期していない信号を指定する。 3. `if` の中に複数の文がある。 私は、一般的なプログラミングの if 文を想像して上記の回路の動作を考えていました。一般的なプログラミン言語では、if 文の条件式は 1 回だけ評価されるため、文 1 と文 2 は両方とも実行されるか、実行されないかのどちらかです。片方だけが実行されることはあり得ません。しかし、今回はそれが起きました。 ### ファンアウト Verilog における if 文に含まれるノンブロッキング代入は、すべて同時に動作します。したがって、if 文の条件式に指定した信号(以降、信号 cond)が、複数の回路へと供給されることになるのです。このような、1 つの出力が何個の回路に供給されるかを「ファンアウト」と呼びます。 ![キャプションを入力できます](https://camo.elchika.com/ebb138a9feccbb7be0ea2111ed5ebb21a9fe1f95/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f63623236363463662d646236382d346535332d626636342d643561353665393736656130/) 信号 cond が `clk` に同期していない信号であれば、中途半端なタイミングで信号が読み出されることがあります。信号の供給先が 1 つだけであれば、中途半端といっても 0/1 のどちらかになるはずで、あまり問題はないでしょう。しかし、ファンアウトが 2 以上の場合、その一部だけ 1 と判定され、残りが 0 になるということがあるかもしれません。 筆者はこのあたりにそれほど詳しくありませんが、おそらく「メタステーブル」などと呼ばれるものと関連があるかもしれません(参考記事:[非同期クロック と 検証手法−2 - 半導体事業 - マクニカ](https://www.macnica.co.jp/business/semiconductor/articles/intel/2129/))。 ### 入力回路のばらつき 信号 cond が D-FF だと仮定します。その D-FF は別のクロックによって駆動されています。初期状態が 0 であり、とあるクロックの立ち上がりで 1 を取り込むとします。D-FF の内部回路が完全に 1 に落ち着くまで、しばらく時間がかかります。そのため、セットアップタイムやホールドタイムというものが規定されています。 受け側のクロック `clk` が立ち上がると、「文 1」の回路と「文 2」の回路が信号 cond の D-FF から値を取り込もうとします。D-FF の内部回路が 1 に落ち着く前にクロックが立ち上がってしまうと、1 が読まれるか 0 が読まれるか、不定になってしまうのだと思います。 それでも、2 つの回路の入力部分が全く同じ特性になっていて、完全に同じタイミング、完全に同じ閾値電圧を持っていれば、同じ値が取り込まれるのだろうと思います。もちろん現実の回路でそんなことはありませんから、2 つの回路はそれぞれ微妙に異なる判定基準によって、不安定な D-FF の出力を読みます。その結果、`rx_ack` は 1 になっているが `wr_valid` が 0 のまま、というような現象が起きたのではないか、と推測しました。