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 となります。
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 と書いてます)
制御信号の幅が多少違いますが、変化の順序は全く同じです。
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_full、uart_rd)は sys_clk(27MHz 水晶)で動作していて、wr_buf や rx_ack は pclk(sys_clk から PLL で作った、約40MHz 程度のクロック)で動作しています。つまりクロック境界をまたぐ回路になっています。そのため、一部の処理を unsafe (cdc) で囲んでいます。
最も核心の部分は次の 3 行です。
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 文字ずつではなく、長いデータを連続で送信してみます。
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 を書いている箇所はありません。
} 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 に同期する信号に修正したところ、すっかり問題の現象が消えました。
修正前の記述:
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;
}
}
}
特徴としては次のようになっています。
always_ffはクロックclkに同期して動く。ifの条件式にclkに同期していない信号を指定する。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
さんが
昨日の11:45
に
編集
をしました。
(メッセージ: 初版)
ログインしてコメントを投稿する