fpga-register-assignment-failure

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 となります。

FPGAでレジスタへの代入が失敗するときとしないときの信号波形

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

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

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

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

Verilogのif文では条件式が複数の回路へ供給される

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



この記事を参照しているブログ内記事はありません。

作成:2026-02-12 11:28:34

最終更新:2026-02-12 11:28:34