リセットとクロックドメイン間乗り換えのパターン
リセット同期化回路、2段フロップ同期化回路、パルスおよびハンドシェイク同期化回路、グレイポインタFIFOと、それらを安全にする制約を、コンパイル済みVerilogとともに示します。
このガイドは、クロック領域をまたがなければならない信号や、すべてのフロップに安全に届かなければならないリセットを抱えていて、動くことが分かっているパターンを求めているエンジニアのためのものです。各パターンについて回路、適用できる場面、そしてタイミングツールがそのパスを失敗させたり黙って無視したりしないようにする制約を示します。VerilogはIcarus Verilogでコンパイルし、パルス、ハンドシェイク、グレイポインタの各モジュールはシミュレーションしています。
同期リセットか非同期リセットか
| 同期リセット | 非同期リセット | |
|---|---|---|
| 振る舞い | リセットは他のデータ入力と同様にクロックエッジでサンプルされる | リセットがアサートされた瞬間にフロップがクリアされ、クロックは不要 |
| アサートに動作中のクロックが必要か | はい | いいえ |
| タイミング | リセットは通常のデータパスで、セットアップとホールドを満たす必要がある | アサートは自由。解除はリカバリとリムーバルを満たす必要がある |
| グリッチ感度 | クロックでフィルタされる | グリッチでフロップがリセットされる |
| ファブリックのコスト | フロップの同期クリアを使うか、一部のファミリではLUT入力を1つ使う | 専用の非同期クリアピンを使う |
| 一般的な採用先 | AMD(UG949で推奨)、Lattice | Altera、Microchip、ASIC風のコード |
当サイトの既定は正論理の同期リセットです。すべてのパスを通常のタイミング検証対象のパスに保てるからです。どちらを選ぶにせよ、リセットの解除はクロックに同期していなければならず、それを提供するのが最初のパターンです。
リセット同期化回路
非同期にアサートし(クロックが止まっていてもシステムはリセット状態に保たれる)、同期的に解除します(すべてのフロップが同じサイクルでリセットを抜ける)。
module reset_sync (
input wire clk,
input wire arst_n, // asynchronous, active-low, from pin or PLL lock
output wire rst_n // asserts asynchronously, releases on clk
);
(* ASYNC_REG = "TRUE" *) reg [1:0] sync_n;
always @(posedge clk or negedge arst_n) begin
if (!arst_n) sync_n <= 2'b00;
else sync_n <= {sync_n[0], 1'b1};
end
assign rst_n = sync_n[1];
endmodule
これをクロック領域ごとに1つ置き、同じ非同期ソースから駆動します。領域が大きければ、その後ろにレジスタのファンアウトツリーを続けてください。ツールは最終段のフロップを複製してくれますが、それは許可した場合だけです(KEEPを付けないこと)。出力は同期リセットにも非同期クリアピンにも使えます。重要なのは解除がクロックに揃っていることです。
2段フロップ同期化回路
レベルが送り先クロックの少なくとも2周期の間安定している、単一ビットの信号向けです。
module sync2 (
input wire clk_dst,
input wire d_src, // a level that stays stable for several clk_dst cycles
output wire d_dst
);
(* ASYNC_REG = "TRUE" *) reg [1:0] ff;
always @(posedge clk_dst) ff <= {ff[0], d_src};
assign d_dst = ff[1];
endmodule
この図から導かれる規則:
| 規則 | 理由 |
|---|---|
| 2つのフロップの間に決してロジックを置かない | ゲートが1つ増えるごとに1段目のフロップが解決に使える時間が削られる |
| 複数ビットのバスをビットごとに同期化しない | 各ビットがそれぞれ別のサイクルで解決するので、バスとしての整合性が失われる |
| ソースは組み合わせ論理ではなくレジスタでなければならない | 入力のグリッチが本物の遷移としてサンプルされる |
| 約400 MHz以上、または非常に長いMTBF目標では3段のフロップを使う | MTBFの計算は/reference/timing-formulae/を参照 |
| 送り先の2周期より短いパルスは失われる | 後述のパルス同期化回路を使う |
パルス(トグル)同期化回路
ソース領域での1サイクルのパルスを送り先領域での1サイクルのパルスに変換します。パルスの間隔が送り先の少なくとも3周期空いていれば、クロック周波数の比は問いません。
module pulse_sync (
input wire clk_src,
input wire pulse_src, // single-cycle pulse in clk_src
input wire clk_dst,
output wire pulse_dst // single-cycle pulse in clk_dst
);
reg tog_src = 1'b0;
always @(posedge clk_src)
if (pulse_src) tog_src <= ~tog_src;
(* ASYNC_REG = "TRUE" *) reg [1:0] ff;
reg tog_prev;
always @(posedge clk_dst) begin
ff <= {ff[0], tog_src};
tog_prev <= ff[1];
end
assign pulse_dst = ff[1] ^ tog_prev;
endmodule
トグルレジスタが各パルスをレベルの変化に変え、2段のフロップがそのレベルを同期化し、遅延したコピーとのXORがエッジをパルスに戻します。送り先が吸収できるより速くパルスが到着しうる場合は、後述のハンドシェイクかFIFOを使ってください。
ハンドシェイク同期化回路
リクエストとアクノリッジを使って1ワードのデータを渡すので、ソースはいつデータを変えてよいかを知ることができます。スループットは低い(両領域合わせて1ワードあたり約6サイクル)ものの、常に正しく動きます。
module handshake_sync #(parameter W = 16) (
input wire clk_src,
input wire req_src, // pulse when data_src is valid
input wire [W-1:0] data_src,
output wire busy_src, // high until the far side has taken the data
input wire clk_dst,
output reg valid_dst = 1'b0,
output reg [W-1:0] data_dst
);
reg req_tog = 1'b0;
reg [W-1:0] data_hold;
always @(posedge clk_src)
if (req_src && !busy_src) begin
req_tog <= ~req_tog;
data_hold <= data_src;
end
(* ASYNC_REG = "TRUE" *) reg [1:0] req_ff = 2'b00;
reg req_prev = 1'b0;
always @(posedge clk_dst) begin
req_ff <= {req_ff[0], req_tog};
req_prev <= req_ff[1];
valid_dst <= req_ff[1] ^ req_prev;
if (req_ff[1] ^ req_prev) data_dst <= data_hold;
end
(* ASYNC_REG = "TRUE" *) reg [1:0] ack_ff = 2'b00;
always @(posedge clk_src) ack_ff <= {ack_ff[0], req_ff[1]};
assign busy_src = req_tog ^ ack_ff[1];
endmodule
データバスdata_holdは同期化回路なしで領域をまたぎます。これが安全なのは、リクエストがトグルする前からアクノリッジが戻った後まで安定していることが保証されており、最後の節の制約でそのことをツールに伝えるからです。
グレイポインタを使う非同期FIFO
ストリーミングデータには非同期FIFOが適切な道具です。データはデュアルポートのブロックRAMが保持します。各側は書き込みポインタまたは読み出しポインタを自分の領域で持ち、各ポインタは2段フロップ同期化回路を通して相手側に渡され、フルとエンプティの計算に使われます。
バイナリのポインタは同期化できません。複数のビットが同時に変化しうるので、各ビットが別のサイクルで解決するかもしれないからです。グレイコードはインクリメントごとにちょうど1ビットしか変わらないため、同期化されたグレイポインタは常に古い値か新しい値のどちらかであり、混ざった値になることはありません。ポインタはアドレスより1ビット広くし、フルとエンプティを区別できるようにします。
module gray_ptr #(parameter AW = 4) (
input wire clk,
input wire rst,
input wire inc,
output reg [AW:0] bin, // one extra bit to tell full from empty
output reg [AW:0] gray
);
wire [AW:0] bin_next = bin + {{AW{1'b0}}, inc};
wire [AW:0] gray_next = bin_next ^ (bin_next >> 1);
always @(posedge clk) begin
if (rst) begin
bin <= {(AW+1){1'b0}};
gray <= {(AW+1){1'b0}};
end else begin
bin <= bin_next;
gray <= gray_next;
end
end
endmodule
module gray_to_bin #(parameter AW = 4) (
input wire [AW:0] gray,
output wire [AW:0] bin
);
genvar i;
generate
for (i = 0; i <= AW; i = i + 1) begin : g_xor
assign bin[i] = ^(gray >> i);
end
endgenerate
endmodule
フラグはローカルのグレイポインタと同期化した相手側のポインタを比較します:
module fifo_flags #(parameter AW = 4) (
input wire [AW:0] wr_gray, // write pointer, write domain
input wire [AW:0] rd_gray_sync, // read pointer after two flops in write domain
input wire [AW:0] rd_gray, // read pointer, read domain
input wire [AW:0] wr_gray_sync, // write pointer after two flops in read domain
output wire full,
output wire empty
);
assign empty = (rd_gray == wr_gray_sync);
assign full = (wr_gray == {~rd_gray_sync[AW:AW-1], rd_gray_sync[AW-2:0]});
endmodule
エンプティはポインタが等しいことを意味します。フルは上位2ビットを反転した以外は等しいことを意味し、これがグレイコードで「1周先行している」ことを表す形です。相手側のポインタは2サイクル古いので、どちらのフラグも悲観的ですが、それは安全な側です。FIFOはやや早めにフルを報告することはあっても、決してオーバーフローしません。RAMのアドレス指定にはバイナリのポインタを、領域の乗り換えにはグレイのポインタを使ってください。ベンダーのFIFOジェネレータ(AMDのFIFO GeneratorとXPM FIFO、AlteraのDCFIFO、LatticeのFIFO_DC)はまさにこれを実装しており、量産では手書きのFIFOよりそちらを優先すべきです。
リセットツリー
| 手順 | 理由 |
|---|---|
| 非同期ソースを1つにする(電源投入、外部ピン、PLLロック) | 「システムが立ち上がった」ことの唯一の情報源にする |
| クロック領域ごとにリセット同期化回路を1つ置く | 解除を各クロックに揃える |
| すべてのPLLがロックを報告するまでリセットを保持する | 解除の前にクロックが安定していなければならない |
| 最も遅い領域を最後に解除するか、カウンタでリセットを順序付ける | 速い領域が初期化前の遅い領域を読むのを防ぐ |
| 大きなブロックごとに先頭でリセットをレジスタに通す | ファンアウトを分割してタイミングを収束させる |
| 必要なものだけをリセットする: 制御状態、カウンタ、validフラグ | データパスのレジスタとRAMにはリセットは不要で、付けると配線を消費する |
AMDのUG949はリセットを少なく、同期で、正論理にすることを推奨しています。AlteraとMicrochipのファブリックは非同期クリアが安価で、そのガイドはどちらも認めています。同じ信号で2つの方式を混ぜないでください。
制約すべきもの
| パターン | Vivado XDC | Quartus SDC | 理由 |
|---|---|---|---|
| 同期化回路のフロップすべて | set_property ASYNC_REG TRUE [get_cells u_s/ff_reg*](またはRTL内の属性) |
QSFにset_instance_assignment -name SYNCHRONIZER_IDENTIFICATION FORCED -to "u_s|ff*"(Quartusは階層を縦棒で区切る) |
2つのフロップを隣接させ、ツールによるロジック挿入を防ぎ、MTBFレポートを有効にする |
| 同期化回路に入る単一ビットの乗り換え | set_false_path -to [get_cells u_s/ff_reg[0]]またはset_max_delay -datapath_only |
set_false_path -to [get_registers u_s/ff[0]] |
このパスは設計上非同期。これがないとツールは失敗させるか、さらに悪いことに「修正」してしまう |
| ハンドシェイクのデータバス、グレイポインタのバス | set_max_delay -datapath_only T_dst -from [get_cells src_reg*] -to [get_cells dst_reg*]と、グレイバスにはset_bus_skew |
set_max_delayとバスへのset_net_delay -max |
安定ウィンドウの仮定が成り立つよう遅延を制限する。フォルスパスにするとツールはバスを任意に遅く配線してしまう |
| 全体として無関係なクロック | set_clock_groups -asynchronous |
同じ | グループ間に同期パスが存在しないことを宣言する |
| リセット同期化回路の入力 | set_false_path -from [get_ports arst_n] |
同じ | 非同期のアサートはタイミング検証対象のパスではない。解除はすでに同期している |
データを運ぶものにはset_false_pathよりset_max_delay -datapath_onlyを優先してください。フォルスパスは「決して検査しない」、遅延の上限は「緩く検査する」という意味で、後の変更で遅くなった乗り換えを捕まえられるのは後者だけです。大きな変更のたびにベンダーのCDCレポート(Vivadoのreport_cdc、Quartus Prime ProのTiming Analyzer CDCビューア)を実行してください。
出典
- C. Cummings, “Clock Domain Crossing (CDC) Design and Verification Techniques Using SystemVerilog”, SNUG Boston 2008
- C. Cummings and P. Alfke, “Simulation and Synthesis Techniques for Asynchronous FIFO Design”, SNUG San Jose 2002
- C. Cummings and D. Mills, “Synchronous Resets? Asynchronous Resets? I am so confused!”, SNUG San Jose 2002
- C. Cummings, D. Mills and S. Golson, “Asynchronous and Synchronous Reset Design Techniques, Part Deux”, SNUG Boston 2003
- UG949: UltraFast Design Methodology Guide, clocking and reset chapters
- UG903: Vivado Design Suite User Guide, Using Constraints
- Quartus Prime Pro Edition User Guide: Timing Analyzer (document 683243)
- 当サイトの関連ページ: /reference/xdc-sdc-constraints/、/reference/timing-formulae/、/reference/glossary/#metastability