ハードウェアに合った制約を書く
データシートと回路図からXDCとSDCのタイミング制約を導く方法を、設定ポート付きのソース同期ADCインターフェースを例に、実際の数値で解説します
このレッスンで学ぶこと
- デザインに入ってくるクロックと内部で作られるクロックのすべてについて、create_clockとcreate_generated_clockコマンドを書く
- ADCのデータシートと基板レイアウトからset_input_delayとset_output_delayの値を導出する
- 無関係なクロックをset_clock_groupsで分離し、その制約が何を約束することになるかを説明する
- フォルスパスとマルチサイクルパスの例外を、RTLの構造が正当化する箇所にのみ適用する
- 同じ意図をVivado XDCとQuartus Prime SDCの間で相互に変換する
タイミングアナライザは、チップ内部のあらゆる遅延を知っていますが、チップの外の世界については何も知りません。このレッスンは、タイミングレポートを読んだことがあり、そのレポートに意味を持たせるための制約を書く必要があるエンジニア向けです。設定ポートを備えた12ビットのソース同期ADCインターフェースという小さなデザインをひとつ取り上げ、その制約ファイルをVivado XDCとQuartus Prime SDCの両方で組み立てていきます。すべての数値はデータシートの値または基板上の測定値に基づいており、アナライザがそれぞれの値をどう扱うかを見ていきます。セットアップ、ホールド、スラックの理論は静的タイミング解析に、計算式はタイミング計算式にまとめています。
ツールが知り得ないこと
配置後、ツールはすべてのネットとセルの遅延を計算できます。ツールに見えないのは、基板上の発振器、自身のクロックでデータを送り出すADC、そして2つのクロックが無関係な水晶振動子から来ているという事実です。制約ファイルは、まさにその欠けている情報を、それ以外は何も加えずに与えるものです。制約ファイルは3つの問いに答えます。
- どのクロックが存在し、どれだけ速いか。 アナライザは、すべてのクロックピンについて周期を、デバイス内部で分周または逓倍されるすべてのクロックについてその記述を必要とします。
- 外部データはいつ到着し、いつ出て行かなければならないか。 入力遅延と出力遅延は、各配線の向こう側にあるデバイスを記述します。
- どのパスを解析すべきでないか、あるいは別の方法で解析すべきか。 クロックグループ、フォルスパス、マルチサイクルパスは、RTLの構造上不要になるチェックを取り除いたり緩和したりします。
制約はハードウェアについての言明であって、ツールへの要望ではありません。言明が間違っていれば、アナライザは実機で動かないデザインを平然と「問題なし」と報告します。1行1行を、データシートのページやオシロスコープの波形で裏付けられる主張として扱ってください。
プライマリクロック: create_clock
このデザインにはピンに到着するクロックが2つあります。100 MHzの発振器がclk_sysを駆動し、ADCがそのデータクロック出力であるadc_dcoを100 MHzで駆動します。どちらもポート上で宣言され、アナライザはそれらをバッファを通して伝搬させ、到達するすべてのレジスタまで追跡します。
# XDC (Vivado): primary clocks
create_clock -name clk_sys -period 10.000 -waveform {0.000 5.000} [get_ports clk_sys]
create_clock -name adc_dco -period 10.000 [get_ports adc_dco]
# SDC (Quartus Prime): primary clocks
create_clock -name clk_sys -period 10.000 -waveform {0.000 5.000} [get_ports clk_sys]
create_clock -name adc_dco -period 10.000 [get_ports adc_dco]
derive_clock_uncertainty
この2つのコマンドは同一であり、それこそが要点です。XDCはSDCの方言です。-waveformオプションは1周期内の立ち上がりエッジと立ち下がりエッジの時刻を列挙するもので、デューティ比が50%でない場合にのみ意味があるため、2つ目のクロックでは省略しています。唯一の違いは最後の行です。Quartus Primeではすべてのチェックにジッタとスキューのマージンを加えるためにderive_clock_uncertaintyが必要ですが、Vivadoは指示しなくても同等のマージンを加えます。Quartusでこの行を忘れると、楽観的なレポートと見落としやすい警告が出るだけです。
クロックが直接PLLに入る場合でも、ポート上でクロックを宣言してください。PLLの出力は別途記述されますが、どちらのツールもPLLが何を生成するかを求めるために入力周波数を必要とします。
デザイン内部で作られるクロック: create_generated_clock
他のクロックから派生したクロックはすべて生成クロックです。これには2種類あり、ツールはそれぞれを異なる方法で扱います。
PLLとMMCMの出力。 Vivadoは、入力クロックが宣言されていれば、IPの設定からこれらを自動的に作成します。Quartus Prime Standard EditionではSDCファイルにderive_pll_clocksが必要です。Quartus Prime ProではPLLのIPが独自のSDC断片を同梱しており、本稿執筆時点ではそれが推奨される方法です。
ロジックから作られるクロック。 トグルするレジスタは2分周クロックであり、どちらのツールもそれを推測してはくれません。このデザインの設定ポートは、まさにそのようなレジスタからADCのSPIクロックを駆動しています。宣言がなければ、spi_sclkでクロックされるすべてのパスは制約されず、黙って無視されます。
# XDC: a divide-by-two clock from a toggling register
create_generated_clock -name spi_sclk -source [get_pins u_fe/spi_sclk_reg/C] \
-divide_by 2 [get_pins u_fe/spi_sclk_reg/Q]
# SDC: the same clock in Quartus Prime naming
create_generated_clock -name spi_sclk -source [get_pins u_fe|spi_sclk|clk] \
-divide_by 2 [get_pins u_fe|spi_sclk|q]
-sourceはレジスタのクロックピンであり、生成クロックはそこに到着するクロックの周波数と位相を継承します。ターゲットはレジスタの出力ピンで、ここから新しいクロックが始まります。2つのファイルの間で変わっているのは名前の付け方だけです。Vivadoは合成されたすべてのレジスタに_regという接尾辞を付け、階層をスラッシュで区切ります。QuartusはRTLの名前をそのまま使い、階層を縦棒で区切り、ピンをCとQではなくclkとqと呼びます。これらの慣例はXDCとSDCのチートシートに表としてまとめています。
制約の対象となるデザイン
以下のモジュールは意図的に小さくしています。サンプルはピンでレジスタに取り込まれ、短い履歴を経て、4つずつ合計されます。別のクロック領域にはSPI設定ポートの代わりとなるもの、すなわち分周クロックと、その立ち下がりエッジで変化するデータ線があります。実際のマスタではチップセレクトとフレームカウンタが加わりますが、それらは制約を変えません。
// adc_front_end.v
// Captures 12-bit samples from a source-synchronous ADC, sums them in
// groups of four, and provides a divided clock for the ADC's SPI port.
module adc_front_end #(
parameter integer W = 12
) (
// ADC data domain. clk_adc is the ADC's DCO after whatever buffering
// scheme you choose (see the text).
input wire clk_adc,
input wire rst_adc, // synchronous, already in the clk_adc domain
input wire [W-1:0] adc_d,
output reg [W+1:0] sum, // h0 + h1 + h2 + h3
output reg sum_valid,
// System domain. Only a stand-in for the configuration port is shown.
input wire clk_sys,
input wire rst_sys,
output reg spi_sclk, // clk_sys divided by two
output reg spi_mosi
);
// Stage 1: capture at the pins. One register with no logic in front
// of it, so the tools can place it in the I/O block.
reg [W-1:0] adc_q;
always @(posedge clk_adc) begin
adc_q <= adc_d;
end
// Stage 2: a short history and a tick every fourth cycle.
reg [W-1:0] s1, s2, s3;
reg [1:0] phase;
wire tick = (phase == 2'd3);
always @(posedge clk_adc) begin
s1 <= adc_q;
s2 <= s1;
s3 <= s2;
if (rst_adc) phase <= 2'd0;
else phase <= phase + 2'd1;
end
// Stage 3: a held bank and the sum. h0..h3 and sum all update on tick,
// so every path from h* to sum has four clock periods available.
// That is the multicycle path constrained in the text.
reg [W-1:0] h0, h1, h2, h3;
always @(posedge clk_adc) begin
if (rst_adc) begin
sum_valid <= 1'b0;
sum <= {(W+2){1'b0}};
h0 <= {W{1'b0}};
h1 <= {W{1'b0}};
h2 <= {W{1'b0}};
h3 <= {W{1'b0}};
end else begin
sum_valid <= tick;
if (tick) begin
h0 <= adc_q;
h1 <= s1;
h2 <= s2;
h3 <= s3;
sum <= h0 + h1 + h2 + h3;
end
end
end
// Configuration port stand-in: a divide-by-two clock and a data line
// that changes on the falling edge of that clock.
localparam [15:0] CONFIG_WORD = 16'h8A5C;
reg [15:0] cfg_shift;
always @(posedge clk_sys) begin
if (rst_sys) begin
spi_sclk <= 1'b0;
cfg_shift <= CONFIG_WORD;
spi_mosi <= 1'b0;
end else begin
spi_sclk <= ~spi_sclk;
if (spi_sclk) begin // sclk is about to fall
spi_mosi <= cfg_shift[15];
cfg_shift <= {cfg_shift[14:0], cfg_shift[15]};
end
end
end
endmodule
テストベンチは、データシートに基づいてADCの振る舞いをモデル化しています。データクロックの各立ち上がりエッジの2.0 ns後に新しいサンプルが現れます。連続する合計値が16ずつ異なることを確認します。これは、4つの連続するランプ値を4つ分ずらしたときに必ず得られる差です。
// tb_adc_front_end.v (testbench, not for synthesis)
`timescale 1ns/1ps
module tb_adc_front_end;
localparam integer W = 12;
reg clk_adc = 1'b0;
reg clk_sys = 1'b0;
reg rst_adc = 1'b1;
reg rst_sys = 1'b1;
reg [W-1:0] adc_d = {W{1'b0}};
wire [W+1:0] sum;
wire sum_valid;
wire spi_sclk, spi_mosi;
adc_front_end #(.W(W)) dut (
.clk_adc(clk_adc), .rst_adc(rst_adc), .adc_d(adc_d),
.sum(sum), .sum_valid(sum_valid),
.clk_sys(clk_sys), .rst_sys(rst_sys),
.spi_sclk(spi_sclk), .spi_mosi(spi_mosi)
);
always #5.0 clk_adc = ~clk_adc; // 100 MHz data clock from the ADC
always #4.0 clk_sys = ~clk_sys; // 125 MHz system clock
// ADC model: launch each sample 2.0 ns after the rising edge, inside
// the 1.0 to 2.5 ns window the datasheet promises.
integer n = 0;
always @(posedge clk_adc) begin
#2.0 adc_d <= n[W-1:0];
n = n + 1;
end
integer valid_count = 0;
integer errors = 0;
reg [W+1:0] last_sum = {(W+2){1'b0}};
always @(posedge clk_adc) begin
if (sum_valid) begin
valid_count = valid_count + 1;
if (valid_count > 2 && sum != last_sum + 16) begin
errors = errors + 1;
$display("FAIL at %0t: sum %0d, expected %0d", $time, sum, last_sum + 16);
end
last_sum = sum;
end
end
initial begin
repeat (3) @(posedge clk_adc);
rst_adc <= 1'b0;
repeat (3) @(posedge clk_sys);
rst_sys <= 1'b0;
repeat (60) @(posedge clk_adc);
if (errors == 0) $display("PASS: %0d sums checked", valid_count);
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule
ソース同期ADCの入力遅延
ADCは各サンプルを自身のクロックから送り出すため、該当するデータシートの数値はクロック・ツー・出力時間です。以下の値は100 MSPSのパラレル出力コンバータに典型的なもので、ここでは例として用いており、特定の部品の仕様ではありません。
| 項目 | 値 | 出典 |
|---|---|---|
| データクロック周期 | 10.0 ns | 100 MSPS |
t_co 最小 |
1.0 ns | ADCデータシート |
t_co 最大 |
2.5 ns | ADCデータシート |
| 配線スキュー(データ対クロック) | ±0.2 ns | 基板レイアウト |
入力遅延は、ADCでのクロックエッジからデータがFPGAのピンに到着するまでの時間として定義されます。最大値はデータが到着し得る最も遅い時刻で、セットアップチェックに使われます。最小値はデータが変化し得る最も早い時刻で、ホールドチェックに使われます。基板スキューはこのウィンドウを両方向に広げます。
# XDC: data arrives 0.8 to 2.7 ns after each rising edge of adc_dco
set_input_delay -clock adc_dco -max 2.700 [get_ports adc_d[*]]
set_input_delay -clock adc_dco -min 0.800 [get_ports adc_d[*]]
# SDC: identical in Quartus Prime
set_input_delay -clock adc_dco -max 2.700 [get_ports adc_d[*]]
set_input_delay -clock adc_dco -min 0.800 [get_ports adc_d[*]]
ADCが両エッジでデータを送り出すダブルデータレートのインターフェースでは、-clock_fall -add_delayを付けた2つ目の制約ペアを追加します。-add_delayがないと、2つ目のペアは1つ目に加わるのではなく、置き換えてしまいます。
アナライザはこれらの数値をどう扱うか
この制約は正しく、そしてこのデザインはホールドに失敗します。チェックを手計算すると、その理由と、この種のインターフェースが人をつまずかせがちな理由が分かります。ピンからキャプチャレジスタまでのデータパスを0.8 ns、ピンからグローバルバッファを経て同じレジスタに至るクロックパスを3.0 ns、レジスタのセットアップ時間を0.3 ns、ホールド時間を0.2 nsとします。
| イベント | 時刻 |
|---|---|
| ADCでの送り出しエッジ | 0.0 ns |
| キャプチャレジスタでデータ確定 | 2.7 + 0.8 = 3.5 ns |
| キャプチャレジスタでデータが次に変化 | 10.0 + 0.8 + 0.8 = 11.6 ns |
| キャプチャエッジがレジスタに到着 | 10.0 + 3.0 = 13.0 ns |
| セットアップスラック | 13.0 − 0.3 − 3.5 = 9.2 ns |
| ホールドスラック | 11.6 − (13.0 + 0.2) = −1.6 ns |
クロックは、データをキャプチャするはずだったエッジの3.0 ns後に到着し、その時点でADCはすでに次のサンプルを送り出しています。セットアップには余裕があり、ホールドは破綻しています。これは、クロックが遠回りをするソース同期入力に典型的な兆候です。通常の対処法は3つあり、いずれも制約の変更ではなくハードウェア上の判断です。
- キャプチャクロックをずらす。
adc_dcoをゼロ遅延モードのPLLまたはMMCMに通し、180度位相シフトした出力を使います。キャプチャエッジは5.0 nsに移り、同じデータウィンドウに対してセットアップスラック1.2 ns、ホールドスラック6.4 nsが得られます。 - データを遅らせる。 I/Oブロック内の入力遅延素子を使い、データがクロックより後に到着するようにします。入力遅延制約が設定されていれば、ツールがタップ数を決めてくれます。
- 立ち下がりエッジでキャプチャする。 キャプチャレジスタを
negedgeで書くと、PLLなしでキャプチャ点が5.0 nsに移ります。その後に続く半周期のパスは、より高いレートでは注意が必要です。
いずれの場合も、入力遅延制約は書いたままで変わりません。制約はADCを記述しており、ADCは変わっていないからです。
設定ポートの出力遅延
設定ポートは逆方向に動きます。FPGAがspi_mosiを送り出し、ADCがspi_sclkの立ち上がりエッジでそれをキャプチャします。このようなポートのデータシートは、そのエッジを基準としたセットアップ時間とホールド時間を示しており、ここでは4.0 nsと2.0 nsとします。この低速インターフェースの配線スキューは最大0.3 nsです。
# XDC and SDC: data must be stable 4.3 ns before and 1.7 ns after each rising sclk edge at the ADC
set_output_delay -clock spi_sclk -max 4.300 [get_ports spi_mosi]
set_output_delay -clock spi_sclk -min -1.700 [get_ports spi_mosi]
最大値はADCのセットアップ時間にスキューを加えたものです。ツールはデータをエッジのそれだけ前に到着させなければならないからです。最小値はホールド時間にスキューを加えた値の符号を反転したものです。負の最小値は正常であり、クロックエッジがADCに達する前にデータが変化してもよいことを意味します。ただし、エッジの1.7 ns後より早くてはいけません。生成クロックをピンを駆動するレジスタ上で宣言しているため、どちらのツールもclk_sysとspi_sclkの関係を把握しており、正しいエッジの組み合わせをチェックします。
クロックグループ
clk_sysとadc_dcoは異なる発振器から来ています。制約がなければ、アナライザはすべてのクロックの組が関連していると仮定し、両者の間のパスをエッジの最悪の重なりに対してチェックします。位相関係のない2つの100 MHzクロックでは、これは修正しようのない失敗パスを生みます。
# XDC and SDC: the two oscillators are unrelated
set_clock_groups -asynchronous -group [get_clocks {clk_sys spi_sclk}] \
-group [get_clocks {adc_dco}]
構文はどちらのツールでも同じです。Vivadoはさらに-include_generated_clocksを受け付け、PLL出力が親クロックに自動的に従うようにできますが、Quartus Primeでは各グループのすべてのクロックを名前で列挙します。このコマンドはグループ間のすべてのタイミングチェックを取り除きます。これは、すべての乗り換えが同期化回路(シンクロナイザ)、非同期FIFO、またはハンドシェイクを使っているという約束です。これらのパターンはリセットとCDCのパターンに、FIFOの場合はメモリ、ブロックRAM、FIFOで扱っています。約束が偽であっても、ツールは何も教えてくれません。
例外: フォルスパスとマルチサイクルパス
例外は、パスが存在するが別の方法でチェックすべきであることをアナライザに伝えます。どちらの種類も使いすぎになりがちです。原則は、例外はRTLの構造によって正当化されなければならず、緑にしたいレポートによって正当化されるものではない、ということです。
フォルスパス
フォルスパスは一切チェックされません。正当なケースは限られています。同期化回路に入る押しボタン、クロックが始まる前に一度だけ書かれる設定レジスタ、決して使われないテスト信号などです。このデザインではリセットボタンはトップレベルで同期化されるため、ピンから最初の同期化段までのパスにはタイミング要件がありません。
# XDC and SDC
set_false_path -from [get_ports btn_rst_n]
すでに別のグループにあるクロック間の乗り換えを黙らせるためにフォルスパスを使ってはいけません。それはグループ制約がすでに行っています。レイテンシが重要になる同一グループ内の乗り換えでは、Vivadoのset_max_delay -datapath_onlyがクロックの関係を無視しつつ遅延の上限を保ちます。
マルチサイクルパス
保持レジスタh0からh3とレジスタsumはすべてtickで更新され、tickは4サイクルに1回ハイになります。あるtickでh0から送り出された値は、40 ns後の次のtickでsumにキャプチャされます。アナライザはそれを知らず、加算器を10 nsの周期に対してチェックします。100 MHzでは12ビット4入力の加算器は余裕で収まりますが、同じ構造を300 MHzで動かしたり、加算器を広げたりすると、実際には問題がないのに失敗します。
# XDC: four periods for setup, hold stays at the launch edge
set_multicycle_path 4 -setup -from [get_cells {u_fe/h0_reg[*] u_fe/h1_reg[*] u_fe/h2_reg[*] u_fe/h3_reg[*]}] \
-to [get_cells u_fe/sum_reg[*]]
set_multicycle_path 3 -hold -from [get_cells {u_fe/h0_reg[*] u_fe/h1_reg[*] u_fe/h2_reg[*] u_fe/h3_reg[*]}] \
-to [get_cells u_fe/sum_reg[*]]
# SDC: same intent, Quartus Prime naming
set_multicycle_path 4 -setup -from [get_registers {u_fe|h0[*] u_fe|h1[*] u_fe|h2[*] u_fe|h3[*]}] \
-to [get_registers u_fe|sum[*]]
set_multicycle_path 3 -hold -from [get_registers {u_fe|h0[*] u_fe|h1[*] u_fe|h2[*] u_fe|h3[*]}] \
-to [get_registers u_fe|sum[*]]
この2つの数値の組み合わせが人をつまずかせる部分です。デフォルトでは、ホールドチェックはセットアップエッジの1周期前のエッジを使います。セットアップを4番目のエッジに移すと、ホールドは3番目のエッジ、すなわち送り出しの30 ns後に引きずられ、どんな加算器もこれを満たせません。2つ目のコマンドはホールドを3周期分引き戻し、本来あるべき送り出しエッジでチェックされるようにします。どちらのツールでも、セットアップのマルチサイクルNには、ほぼ常にホールドのマルチサイクルN−1を組み合わせます。
例外に含まれていないものに注目してください。phaseからsumのイネーブル入力へのパスは通常の単一サイクルパスであり、adc_qからh0へのパスも同様です。送り元と送り先が同じイネーブルを共有するパスだけが対象になります。
制約が効いていることの確認
何にも一致しない制約は黙って無視されます。レジスタ名のタイプミスが、制約されていないのに合格するデザインを生むのはこのためです。変更のたびに、ツールが解析しなかったものを報告するチェックを実行してください。
| 目的 | Vivado | Quartus Prime Timing Analyzer |
|---|---|---|
| ツールが認識しているすべてのクロックを列挙 | report_clocks |
report_clocks |
| 未制約パスと欠けているI/O遅延を見つける | check_timing |
report_ucp |
| どのクロックの組が解析されているかを確認 | report_clock_interaction |
report_clock_transfers |
| 例外が何かに一致したことを確認 | ログ中の警告 | report_exceptions |
2つのフォーマットの違いは、1つの表にまとめられる程度に小さいものです。
| 項目 | Vivado XDC | Quartus Prime SDC |
|---|---|---|
| ファイルの役割 | タイミング、ピン配置、I/O規格をまとめて記述 | タイミングのみ。ピンはプロジェクト設定ファイルに記述 |
| PLL出力クロック | IPから自動的に導出 | Standardではderive_pll_clocks、ProではIP付属のSDC |
| クロック不確実性 | 自動的に適用 | derive_clock_uncertaintyが必要 |
| レジスタ名 | RTL名に_reg接尾辞、ピンはCとQ |
RTL名、ピンはclkとq |
| 階層の区切り | スラッシュ | 縦棒 |
| 制約の衝突 | 後に読まれたものが優先 | 後に読まれたものが優先 |
どちらのツールも制約ファイルを指定した順に読むため、後のファイルが前のファイルを上書きできます。クロック定義を最初に、I/O遅延を2番目に、例外を最後に置けば、ファイルはアナライザが必要とする順序どおりに読めます。
演習
adc_front_endのキャプチャレジスタをclk_adcの立ち下がりエッジを使うように変更し、それ以外はすべて立ち上がりエッジのままにします。入力遅延の節の手計算を、同じ0.8 nsのデータパスと3.0 nsのクロックパスを使って新しいキャプチャ点に合わせてやり直し、セットアップスラックとホールドスラックを示してください。次に、adc_qからs1への半周期のパスが100 MHzで安全かどうかを判断し、そのパスに2.0 nsのロジックと配線が必要だとしたら、どのクロックレートで安全でなくなるかを求めてください。テストベンチを実行して、合計値が依然として正しいことを確認してください。
まとめ
- 制約はチップ外部のハードウェアと、内部で作られるクロックを記述します。制約は主張であり、誤った主張は壊れたデザインに対してきれいなレポートを与えます。
- すべてのクロックピンには
create_clockが、ロジックから作られるすべてのクロックにはcreate_generated_clockが必要です。PLL出力はVivadoでは自動ですが、Quartus PrimeではコマンドまたはIPのファイルが必要です。 - 入力遅延と出力遅延は、相手側デバイスのデータシートから直接得られ、基板スキューの分だけ広げます。ソース同期入力は、クロック挿入遅延のために、セットアップは容易に通るがホールドに失敗することがよくあります。
set_clock_groupsは無関係なクロック間のすべてのチェックを取り除き、すべての乗り換えが正しく同期化されているという約束になります。- マルチサイクルの例外にはセットアップとホールドの両方の数値が必要で、フォルスパスにはレビュアーに説明できる理由が必要です。