このページは、タイミングに関する連載ページの一部です。これまでのページでは、タイミング計算の背後にある理論を説明し、いくつかのタイミング制約(timing constraints)の書き方を示し、タイミングクロージャ(timing closure)の原則について述べました。このページでは、マルチサイクルパス(multi-cycle path)のタイミング制約について説明します。
はじめに
マルチサイクルパスについて最初に知っておくべきことは、それは通常、悪い考えだということです。このページではマルチサイクルパス制約の使い方を説明しますが、結論としては、この技法を完全に避けるべきです。ASIC の世界ではこの技法を使う理由がありますが、FPGA 設計では、通常は代わりにクロックを追加する方が良いのです。
とはいえ、この種のタイミング例外がどのような場合に関連するのか見てみましょう。
次の Verilog コードの例を考えます。
reg foo, bar;
reg en, pre_en;
always @(posedge clk)
begin
pre_en <= !pre_en;
en <= pre_en;
if (en)
begin
foo <= !foo;
bar <= foo;
end
end
この例は不完全であることに注意してください。おそらく @pre_en と @en に合成属性を追加する必要があります。そうしないと、合成ツール(synthesizer)の最適化によって予期しないことが起こり得ます。これについては後で詳しく説明します。
@pre_en は、すべてのクロックサイクルで '0' と '1' を交互に繰り返します。@en も、わずかな遅延を伴って同じことを行います。
「if (en)」という部分は、@en が「begin」と「end」の間にあるすべての処理に対してクロックイネーブル(clock enable)として機能することを意味します。@en が Low のとき、Verilog コードのこの部分では何も起こりません。言い換えれば、@foo と @bar は、@en が Low のときにはクロックエッジが存在しないかのように振る舞います。
この例では、@en は 2 クロックサイクルに 1 回 High になります。したがって、@foo と @bar は、実際のクロック周波数の半分のクロックで動作しているかのように振る舞います。したがって、タイミング要件もそれに応じて緩和できます。tsetup の計算は、クロック周期が 2 倍であるかのように行えます。
thold については、変更はありません。このタイミング要件の計算は、同じクロックエッジが両方のフリップフロップに到達することを前提としています。したがって、クロック周期は無関係です。これは、thold 解析の例で既に説明したとおりです。したがって、クロックが遅いという仮定は、thold に関しては何の違いも生みません。
クロックイネーブルを使うべき場合
クロックイネーブルを使う正当な理由は、次の 2 つだけです。
- すでにうまく動作している論理設計が、クロックイネーブルに基づいている場合。
- クロックリソースが不足している場合。これは一般的な状況ではありません。FPGA には、この目的に必要な量を超えるリソースが通常あるからです。
クロックイネーブルが FPGA の消費電力を改善するかどうかは明らかではありません。追加のクロックは電力を浪費すると言えるでしょう。しかし、クロックイネーブルもファンアウト(fan-out)の大きい信号です。平均的には、クロックイネーブルは、それが置き換えるクロックと同じ頻度で値が変化します。したがって、論理状態の変化(消費電力の主な要因)という観点では、違いはありません。
マルチサイクルパスを使わなければならない特別な理由がない限り、このページは読み飛ばしてかまいません。たとえ設計に技術的に適切なクロックイネーブルがあったとしても、関連するタイミング例外を使わない方が良いかもしれません。特に、そのタイミング例外なしでタイミング制約を簡単に達成できるなら、誤りを犯すリスクを負う価値はありません。
タイミング例外
上記の Verilog コードに関連して、Vivado 用のマルチサイクルパス制約は次のとおりです。
set en_regs [all_fanout -endpoints_only -only_cells -flat [get_nets en]] set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
Quartus でも同じです。
set en_regs [get_fanouts en] set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
Vivado と Quartus の違いは最初の行だけです。Vivado では「all_fanout」コマンドが使われ、Quartus では「get_fanouts」が使われます。SDC で動作する他のツールでも、制約は同様です。
最初の行は、@en から始まる任意のパスの終点であるすべての順序エレメント(セル)を見つけます。このセルオブジェクトのリストは $en_regs に格納されます。次の 2 行は、$en_regs 内のセルで始まり、かつ $en_regs 内のセルで終わるパスに対するタイミング要件を変更します。
これには多くの説明が必要です。なぜ $en_regs の定義をこのように書いたのでしょうか。なぜ set_multicycle_path コマンドが 2 つあるのでしょうか。thold のタイミング要件はマルチサイクルパスの影響を受けないと述べたのに、なぜ 2 番目のコマンドに「-hold」と書くのでしょうか。
まず、説明しやすい方である set_multicycle_path コマンドから始めましょう。
set_multicycle_path コマンド
上に示したとおり、コマンドは次のとおりです。
set_multicycle_path -setup -from $en_regs -to $en_regs 2 set_multicycle_path -hold -from $en_regs -to $en_regs 1
上の例を少し一般化して、次のことも考えてみましょう。クロックイネーブルが 8 クロックサイクルに 1 回アクティブになる場合、Verilog コードは次のようになります。
reg en;
reg [2:0] pre_en;
always @(posedge clk)
begin
pre_en <= pre_en + 1;
en <= (pre_en == 0);
end
@en はストローブのように動作し、毎回 1 クロックサイクルの間だけアクティブになることに注意してください。これはクロック分周器の MSB ではありません。
この場合のマルチサイクル制約は次のようになります。
set_multicycle_path -setup -from $en_regs -to $en_regs 8 set_multicycle_path -hold -from $en_regs -to $en_regs 7
これら 2 つの例から、最初の set_multicycle_path コマンドの数値は、単純にクロックイネーブルの分周比であることが明確です。
2 番目のコマンドについては、同じ分周比から 1 を引いたものです。つまり、常に N と N-1 です。
最初のコマンドについては、説明することはあまりありません。クロックイネーブルが N クロックサイクルに 1 回アクティブになる場合、許容遅延は N 倍になります。これは tsetup のタイミング要件に関連します。
しかし、なぜ 2 番目のコマンドがあるのでしょうか。なぜ thold について何か言う必要があるのでしょうか。答えは、最初のコマンドが最小遅延の要件も変更してしまうからです。言い換えれば、thold の計算も「-setup」オプションの付いたコマンドの影響を受けるのです。なぜでしょうか。おそらく良い説明はないでしょう。
2 番目のコマンドはこれを修正します。最小遅延の要件を元の値に戻します。したがって、2 番目のコマンドの後、thold の計算は以前と同じように行われます。
ドキュメントには、2 番目のコマンドで N-1 が使われる理由について長々とした説明があります。しかし、正直に言うと、その追加情報には何も興味深いものはありません。thold に関する set_multicycle_path の動作は奇妙であり、その数値が N-1 である理由を理解しても、奇妙さは変わりません。
さて、ここまでは set_multicycle_path は簡単な部分でした。ここからが本当の難しさです。$en_regs として使う正しいセルオブジェクトのリストを生成することです。
レジスタを選択する
$en_regs にリストすべきレジスタを選択するには、どのようなパスがマルチサイクルパスとして適格であるかを理解する必要があります。規則はこれです。クロックイネーブルが両側を制御している場合にのみ、そのパスのタイミング要件を緩和することが許されます。つまり、クロックイネーブルが非アクティブのとき、2 つの順序エレメントのどちらもクロックエッジ後に値が変化しないことが保証されます。
クロックドメイン(clock domain)の観点から考えてみましょう。クロックイネーブルによって制御されるすべての順序エレメントは、仮想的なクロックドメインに属します。この仮想的なクロックドメイン内のクロックは周波数が低いため、このクロックドメイン内のタイミング要件を調整できます。
しかし、パスの片側がこの仮想的なクロックドメインに属さない場合、これは 2 つの関連クロック(related clocks)の間の仮想的なクロックドメイン横断です。このようなパスは、既存のタイミング制約によって既に処理されているため、特別なことをする必要はありません。しかし、この種のパスにマルチサイクル例外を適用するのは誤りです。
上に示したタイミング制約はこの考え方を反映しています。@en によって制御されるすべての順序エレメントが、セルオブジェクトとして $en_regs にリストされます。次に、2 つの set_multicycle_path コマンドが、このリストに属する順序エレメントで始まり、かつこのリストに属する順序エレメントで終わるパスに適用されます。
マルチサイクルパスに関する最も難しい部分は、この順序エレメントのリストが正しいことを保証することです。このリストには、クロックイネーブルによって制御されるすべての順序エレメントを含める必要があります。しかし、他の順序エレメントをこのリストに含めてはいけません。
順序エレメントがこのリストから欠けている場合、関連するパスのタイミング適用は必要以上に厳しくなります。これは災害ではありませんが、タイミング例外の効果を減らします。
しかし、リストに含めるべきでない順序エレメントが誤って追加された場合、深刻な結果を招く可能性があります。それは、タイミング要件が十分に厳しくないパスを生み出すことになります。つまり、ツールが順序エレメントの正常動作のための要件を保証しなくなります。そしてタイミング要件が満たされないとき、奇妙なことが起こり得ます。
私はこの順序エレメントのリストを作成するために「all_fanout」コマンド(または「get_fanouts」)を使うことを選びました。これは常に正しく機能するとは限りません。これからそれについて説明します。その後、このリストを作成する他の選択肢について説明します。これらの他の選択肢は、FPGA ツールが「all_fanout」または同様のコマンドをサポートしていない場合に特に重要です。
all_fanout と get_fanouts で起こり得る問題
マルチサイクル制約で最も起こりがちな誤りは、クロックイネーブル自身(つまり @en)がリスト(つまり $en_regs)に含まれてしまうことです。これが起こると、マルチサイクル例外は @en 自身から、それが制御する順序エレメントへのすべてのパスに適用されます。これは事実上、これらのパスのタイミング要件が保証されないことを意味します。クロックイネーブルはファンアウトの大きい信号であることが多いため、これは目に見える影響を及ぼす可能性があります。
上の例では、これは @pre_en によって回避されています。@en が次のように定義されなかった理由を疑問に思ったかもしれません。
always @(posedge clk)
en <= !en; // Wrong!
もし @en がこのように定義されていたら、@en からそれ自身へのパスが存在していたでしょう。その結果、@en が $en_regs に含まれていたでしょう。
つまり、@pre_en がこの問題を解決しています。しかし、合成ツールが最適化のためにこのレジスタを削除しないことを確認することが重要です。たとえば、Quartus の合成ツールは、@pre_en の唯一の用途が @en に値を与えることであることを検出します(このページの冒頭の Verilog コードで)。したがって、合成ツールは @pre_en を削除し、「en <= !en」と書かれていたかのように続行します。その結果、@en が $en_regs に含まれます。考えられる解決策は、@pre_en を次のように宣言することです。
reg pre_en /* synthesis preserve */;
この単純な例は、合成ツールによる予期しない最適化が悲惨な結果をもたらし得ることを示しています。解決策は単純ですが、この最適化を防ぐ必要性を見落としがちです。
クロックイネーブルに関するもう 1 つの起こり得る事故は、この信号がしばしば高いファンアウトを持つという事実に関連しています。そのため、ツールはレジスタを自動的に複製し、各複製のファンアウトが特定の制限値を下回るようにすることがあります。しかし、これは $en_regs にどのような影響を与えるでしょうか。このリストへの包含基準は、特定のネットに基づいていました。したがって、@en の複製によって制御される順序エレメントは含まれません。
しかし、このような状況の結果は悲惨ではありません。既に述べたように、これは一部のパスに対するタイミング適用が必要以上に厳しくなることを意味するだけです。設計の信頼性には影響しません。
レジスタの複製については、高いファンアウトの文脈で既に説明しました。そこで述べたように、合成ツールが複製するのを待つよりも、@en を手動で複製する方が良いです。驚きを避けるために、このレジスタの複製を禁止する合成属性を常に追加してください。高いファンアウトが後でタイミングクロージャの問題を引き起こす場合、手動での複製で解決してください。そうすれば、問題の原因を理解しやすくなります。プロジェクトが成長したために合成ツールが突然 @en を複製した場合、なぜタイミング制約が達成されなかったのかを理解するのはそれほど簡単ではありません。
いずれにせよ、$en_regs を定義するコマンドは、@en の複製が含まれるように更新する必要があります。
ところで、$en_regs の定義はネットの名前に依存していることに注意してください。既に説明したように、これは合成ツールがネットの名前を「en」とは異なる名前に変えるだけで、$en_regs が空のリストになることを意味します。その結果、マルチサイクルパス制約は完全に役に立たなくなります。この可能性もまた災害ではありません。設計は信頼性を保ちますが、タイミング制約を達成するのが難しくなります。
もう 1 つの可能な問題は、$en_regs の定義方法のため、@en がクロックイネーブル以外の目的に使われてはならないということです。たとえば、次の Verilog コードを考えます。
reg [7:0] counter;
always @(posedge clk)
if (en)
counter <= counter + 1;
この例では、@en は明らかにクロックイネーブルとして使われています。したがって、@counter に関連するすべてのパスがマルチサイクルパスであることは問題ありません。しかし、これはどうでしょうか。
reg [7:0] counter;
always @(posedge clk)
if (en)
counter <= counter + 1;
else
counter <= counter - 1;
ここでは、@en は他のレジスタと同じように使われています。@counter の値はすべてのクロックサイクルで変化します。したがって、@counter はマルチサイクルパスの候補になるべきではありません。それにもかかわらず、@counter のすべてのフリップフロップが $en_regs に含まれます。@en からこれらすべてのフリップフロップへのパスが存在するからです。
これは、@en の複製を作成することで比較的簡単に解決できます。
reg [7:0] counter;
reg non_ce_en;
always @(posedge clk)
non_ce_en <= pre_en;
always @(posedge clk)
if (non_ce_en)
counter <= counter + 1;
else
counter <= counter + 2;
合成ツールが @en と @non_ce_en を 1 つのレジスタに統合するのを防ぐために、合成属性が必要であることに注意してください。
結論として、@en から始まるすべてのパスに基づく $en_regs の定義は、単純で簡潔です。しかし、この定義は地雷原でもあります。それでは、いくつかの代替案を見てみましょう。
$en_regs を作成する代替方法
マルチサイクルパスのタイミング例外用の順序エレメントのリストを作成する最も安全な方法は、設計階層に依存することです。クロックイネーブルによって制御されるすべてのロジックは、別のモジュール(およびおそらくサブモジュール)に入れるべきです。これにより、セルオブジェクトの完全な名前に基づいてセルを見つけることで、$en_regs を作成できます。たとえば、Vivado では次のようにします。
set all_sync [all_fanout -endpoints_only -only_cells -flat \ [get_nets -of_objects [get_clocks clk]]] set en_regs [filter $all_sync {name =~ module_ins/multicycle_ins/* }]
最初のコマンドは、@clk に接続されているすべての論理エレメントを、クロックバッファ自身を除いて見つけます(このクロックは「clk」という名前のクロックオブジェクトによって表されます)。結果は $all_sync に格納されます。これは、関連する可能性のあるすべての順序エレメントを含むリストを作成する 1 つの可能な方法です。2 番目のコマンドは、$all_sync のうち、前述の別モジュールの内部にあるすべての論理エレメントのリストを作成します。
この方法は、クロックオブジェクトの名前とインスタンシエーションの名前に依存していることに注意してください。これらは変更されることが予想されません。この方法では、クロックイネーブルが複製されたり、その名前が合成ツールによって変更されたりしても、問題ありません。
別モジュールを使うもう 1 つの利点は、Verilog コードの扱いが容易になることです。クロックイネーブルによって制御される順序エレメントと、そうでないものとの間で混乱する可能性が小さくなります。
しかし、クロックイネーブルに依存するロジックを分離して、別のモジュールに置くことが常に自然であるとは限りません。そのうえ、Verilog コードが既に書かれていて正しく動作することが分かっている場合、それを変更するのは良い考えではないかもしれません。
もう 1 つの代替案も述べておきます。これは状況によっては適しているかもしれません。それは、すべてのレジスタに対する命名規則です。たとえば、クロックイネーブルによって制御されるすべてのレジスタに「MC_」で始まる名前を付けることができます。この選択により、$en_regs を作成するコマンドは単純になります。名前によってセルオブジェクトを検索するだけだからです。他の論理エレメント(たとえばブロック RAM)も、そのインスタンシエーションの名前を適切に選ぶことで含めることができます。この方法は Verilog コードが見苦しくなると言う人もいれば、扱いやすくなると言う人もいるでしょう。完璧な方法はありません。
なぜ -of_objects を使わないのか
$en_regs を単純な基準で定義することは魅力的に見えるかもしれません。「en」という名前のネットを見つけ、このネットに接続されているすべてのレジスタを追加する、というものです。たとえば、Vivado では次のように書けます。
set en_regs [get_cells -of_objects [get_nets en]]
これが間違いである理由はいくつかあります。最初の理由は、これには @en 自身が含まれることです。したがって、マルチサイクル制約は、クロックイネーブル自身から、それが制御する順序エレメントへのすべてのパスに適用されます。前述のように、これは重大な誤りです。
2 番目の理由は、一部の順序エレメントが見落とされる可能性があることです。上のコマンドによれば、包含基準は、セルが特定のネット(@en)に接続されていることです。これは、このネットがフリップフロップの CE 入力に直接接続されている場合に機能します。しかし、多くの場合、合成ツールは代わりに @en に基づく組み合わせ関数を使うことを選択します。
たとえば、合成ツールは @foo を、あたかも Verilog コードが次のようであったかのように実装することを選択できます。
foo <= foo ^ en;
これは元の式と機能的に等価です。
if (en)
foo <= !foo;
この種の最適化は正当であり、期待されるべきです。NOT ゲートを実装するために、合成ツールはとにかく LUT を使わなければなりません。では、この LUT を使ってフリップフロップの次の値を直接得てはいけないのでしょうか。フリップフロップの CE 入力にもう 1 本配線を追加する必要があるでしょうか。
この最適化の副作用として、フリップフロップ自身は @en に直接接続されません。したがって、$en_regs に含まれません。このような状況の意味合いについては既に説明しました。
合成ツールに、@en を順序エレメントのクロックイネーブル入力としてのみ使うように指示することが、しばしば可能です。たとえば、一部の合成ツールは「direct_enable」またはそれに類する名前の合成属性をサポートしています。この機能を使うと、合成ツールのロジック最適化の自由が減ることに注意してください。したがって、ツールの技術的な問題を解決するために、設計の性能が悪影響を受ける可能性があります。
そのうえ、@en が複製またはリネームされた場合、前述と同じ問題が発生します。
ですから、これらすべての理由から、ネットへの直接接続を基準として使うのは悪い選択です。
リセットとの相互作用
上の Verilog の例に同期リセット(synchronous reset)を追加すると仮定します。
always @(posedge clk)
begin
pre_en <= !pre_en;
en <= pre_en;
end
always @(posedge clk)
if (reset)
begin
foo <= 0;
bar <= 0;
end
else if (en)
begin
foo <= !foo;
bar <= foo;
end
マルチサイクルタイミング例外の背後にある考え方は、すべての順序エレメントがあたかも仮想的なクロックドメインの一部であるかのように振る舞うことでした。このクロックドメイン内の仮想的なクロックは @clk の半分の周波数です。したがって、すべてのレジスタは、@en が Low のときに @clk を無視しなければなりません。これは最後の Verilog コードの例では当てはまりません。@reset は @en に関係なく効果を持ちます。
たとえば、@reset が次のように定義されている場合を考えます。
assign reset = foo;
これは、おそらく実際の用途はありませんが、正当な同期リセットです。しかし、この定義はマルチサイクルパス例外の問題を示しています。@en が High のとき、@foo は次のクロックサイクルで High になります。しかし、それにより @reset も High になります。したがって、次のクロックサイクルで @foo は再び Low になります。@foo はすべてのクロックサイクルで値が変化します。したがって、@foo からそれ自身へのマルチサイクルパスは、このパスに十分に厳しくないタイミング要件を引き起こします。
これは、@en に同期リセットも制御させることで簡単に解決できます。
always @(posedge clk)
if (en && reset)
begin
foo <= 0;
bar <= 0;
end
else if (en)
begin
foo <= !foo;
bar <= foo;
end
これは正しいですが、@reset は @en と一緒にアクティブでなければなりません。簡単な解決策は、@reset を複数のクロックサイクルの間 High に保持することです。
同じ原則が非同期リセット(asynchronous reset)にも当てはまります。非同期リセットに関するページに書かれているすべてがここにも関連しますが、マルチサイクルパス制約があるとさらに複雑になります。最も簡単な解決策は、おそらく別のページで提案されているように、同期化回路を使うことでしょう。
@en と非同期リセット
@pre_en の利点の 1 つは、@en のすべての複製が常に同じ論理レベルを持つことを保証することです。これは当然のことのように聞こえるかもしれませんが、非同期リセットが誤って使われた場合には保証されません。たとえば、元の Verilog コードが次のようであったとします。
reg en;
always @(posedge clk or posedge reset)
if (reset)
en <= 0;
else
en <= !en; // This is not recommended!
もし @en が複製されたら、結果は次と等価になり得ます。
reg en, en_1, en_2;
always @(posedge clk or posedge reset)
if (reset)
begin
en <= 0;
en_1 <= 0;
en_2 <= 0;
end
else
begin
en <= !en;
en_1 <= !en_1;
en_2 <= !en_2;
end
@en_1 の次の値は、@en の値ではなく、それ自身の値に依存することに注意してください。これはレジスタの複製の現実的な結果です。
非同期リセットが安全でない方法で非アクティブ化されたらどうなるでしょうか。一部の複製はリセット後の最初のクロックエッジに反応し、他の複製はこのクロックエッジを無視する可能性があります。その結果、複製の論理状態は(次のリセットまで)決して同じにならないでしょう。
@pre_en はこれを解決します。すべての複製が同じソースから次の値をコピーするからです。これにより、たとえ乱暴な始まりがあっても、長期的には正しい動作が保証されます。
まとめ
クロックイネーブルを使うことは簡単です。タイミング例外のコマンドとして set_multicycle_path を使うことも簡単です。しかし、これを確実に機能させることは、決して簡単ではありません。多くのことが誤る可能性があり、時にはその理由が、成長する論理設計に応じて合成ツールの動作が変わることにある場合もあります。
これらの予期しない問題の結果は、マルチサイクルパスがその目的を果たさないことかもしれません。緩和されたタイミング要件が一部のパスに適用されなければ、この方法の利点は疑わしいものになります。さらに悪いことに、誤りによって、影響を受けるべきではなかったパスにマルチサイクルパス例外が適用されると、信頼性のない論理設計につながる可能性があります。
したがって、関連するパスのタイミングレポートを読んで、予期しないことが起こっていないことを確認することが極めて重要です。残念ながら、これは将来の驚きを防ぐものではありません。設計が発展するにつれて、合成ツールの動作を予測することは困難だからです。
したがって、可能であれば、クロックイネーブルを使う代わりに、同じ PLL から追加のクロックを生成する方がはるかに優れています。この新しいクロックとのクロックドメイン横断は、2 つのクロックが関連クロックであるため、信頼性があります。この新しいクロックのタイミング制約はツールによって自動的に生成されます。この方法では、驚きが生じるリスクはありません。
もしあなたが、設計にクロックイネーブルとマルチサイクル例外を追加したいと思ってこのページを読んでいるなら、このページが何か考えるきっかけを与えたのであれば幸いです。
これで、FPGA 内部のパスに関するタイミング制約の部分は終わりです。では、I/O についてはどうでしょうか。それは次のページから説明し始めます。