uchanのアイコン画像
uchan 2026年02月12日作成
セットアップや使用方法 セットアップや使用方法 閲覧数 25
uchan 2026年02月12日作成 セットアップや使用方法 セットアップや使用方法 閲覧数 25

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

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

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_datawr_buf はともにレジスタの下位 6 ビットを示します。したがって値域は 00~3F となります。

レジスタへの代入が失敗するときとしないとき

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でレジスタ代入が失敗するときの制御信号

代入成功時(「A」受信時)の制御信号が次図。
FPGAでレジスタ代入が成功するときの制御信号

制御信号の幅が多少違いますが、変化の順序は全く同じです。

rx_full(ソースコード上は uart_rx_full)と uart_rd は UART モジュールのポートに接続されています:

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 というハードウェア記述言語で書いています。

wr_buf(ソースコード上は scrn_wr_buf)への代入部分のソースコード:

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_fulluart_rd)は sys_clk(27MHz 水晶)で動作していて、wr_bufrx_ackpclksys_clk から PLL で作った、約40MHz 程度のクロック)で動作しています。つまりクロック境界をまたぐ回路になっています。そのため、一部の処理を unsafe (cdc) で囲んでいます。

最も核心の部分は次の 3 行です。

uart_rx_ack = 1; scrn_wr_buf = uart_rx_data; scrn_wr_valid = 1;

観測された信号を見る限り uart_rx_ackscrn_wr_valid はともに 1 になっているのにも関わらず、scrn_wr_buf に値が書かれていないように見えます。

問題究明のため、scrc_wr_buf へ書き込んだ 1 クロック後に、scrn_wr_bufuart_rx_data を比較して値が異なる場合にフラグ wr_buf_differ を 1 にするようにしてみた。そして、このフラグを LED に表示するようにしてみた。すると、何文字か入力して、初めてデータが化けたときに LED が点灯しました。scrc_wr_buf へ書き込めていないことは検出できているわけです。ではなぜ、書き込めなかったのでしょう。

長い文字列で実験

1234567890 の繰り返しを送信し、データ化けを観察しました。1 文字ずつではなく、長いデータを連続で送信してみます。

ディスプレイに描画された長い文字列とデータが化けた箇所の信号波形

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_ackwr_valid は必ず同時に 1 になるようにしています。ここ以外に rx_ack に 1 を書いている箇所はありません。

} 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_syncpclk に同期する信号に修正したところ、すっかり問題の現象が消えました。

修正前の記述:

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_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 文でした。

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 つの出力が何個の回路に供給されるかを「ファンアウト」と呼びます。

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

信号 cond が clk に同期していない信号であれば、中途半端なタイミングで信号が読み出されることがあります。信号の供給先が 1 つだけであれば、中途半端といっても 0/1 のどちらかになるはずで、あまり問題はないでしょう。しかし、ファンアウトが 2 以上の場合、その一部だけ 1 と判定され、残りが 0 になるということがあるかもしれません。

筆者はこのあたりにそれほど詳しくありませんが、おそらく「メタステーブル」などと呼ばれるものと関連があるかもしれません(参考記事:非同期クロック と 検証手法−2 - 半導体事業 - マクニカ)。

入力回路のばらつき

信号 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 のまま、というような現象が起きたのではないか、と推測しました。

uchanのアイコン画像
本業はプログラマですが、昔から電子工作は趣味でやってます。初めてのプログラミング言語はPICアセンブラです。 モジュールを買ってきて組み合わせるだけでなく、部品の動作原理をきちんと理解して回路を設計することに楽しさを感じます。 2021年より「uchanの電子工作ラボ」という施設を運営しています。はんだごてや測定器が使えます。 https://uchan.net/lab/ 2021年3月22日に「ゼロからのOS自作入門」を出版しました。Amazon→ https://amzn.to/2NP3FUj
  • uchan さんが 昨日の11:45 に 編集 をしました。 (メッセージ: 初版)
ログインしてコメントを投稿する