01signal.com

FPGA のリセット:同期、非同期、それとも使わない?

これは、FPGA のリセットに関する連載の第 2 回目です。前のページで、非同期リセットが多くの人の考えとは違うという話を説明したうえで、このページでは FPGA のリセットと初期化に関するさまざまな選択肢を扱います。

まず大前提:リセットとは何か?

もう、もちろん、リセットが何かは誰でも知っていますよね? コンピュータのあのボタンのようなもので、押せば全部がまっさらな状態から始まります。チップに入るあの信号であり、それ以前に何が起きていたとしても、今後はすべてうまくいくことを保証するものです。リセットという言葉を、「システムを既知の状態に導く信号」と説明する人もいます。

FPGA 設計者にとって、リセットはしばしば、すべてのモジュールに追加し、同じコードパターンの中で使う「おまけの入力」に過ぎません。何も考えずにやっていることで、このリセット信号が実際に何を保証するのか(そもそも保証するのか)、あるいはまったく省略できるのかどうかには、必ずしも注意を払っていません。

最もよくある誤解は、リセット信号がシステムを既知の状態に導くという点に注目してしまうことです。もちろんそれは事実ですが、重要なのはリセット解除後に何が起きるかです。リセット解除後、ロジックが予測可能な形で動作し始めることを保証しなければなりません。少なくとも、正しく動作することが保証できる程度には予測可能である必要があります。リセットを解除したあとにシステムがどう振る舞うか確信が持てないのなら、リセットをかけること自体が無意味です。

このテーマを特に難しくしているのは、運が絡むという点です。一般に、リセットがアクティブになったときのシステムの状態は未知かつランダムであり、リセット解除のタイミングも同様にランダムです。したがって、リセット信号の扱いを誤ると、ランダムな機会に現れるまれな誤動作を引き起こす可能性があり、それはまったく別種の問題のように見えるかもしれません。同様に、この問題を無視しても目に見える結果が現れないこともあります。ただし、通常は呪術で対処されるような、たまに起きる問題は別ですが。

タイミング制約(timing constraints)をきちんとかけること、クロックとクロックドメイン(clock domain)を適切に扱うことと同様に、FPGA のウェイクアップとリセットを適切に扱うことも、FPGA を確実に動作させるために必須です。これらすべてに共通するのは、無視しても何とかやり過ごせることがあり、実際にそうしている技術者も少なくないのですが、残念ながらその代償として、FPGA がときどき取り憑かれたように振る舞うことになります。

シミュレーションとハードウェア

このページでは、リセットに関する決定が、デザインを FPGA に書き込んで実際に動かしたときにどのような影響を与えるかに焦点を当てます。もちろん、これらの決定はロジックのシミュレーションにも影響します。

シミュレータは、特に動作シミュレーション(behavioral simulation)において、すべてのレジスタに X(不明)を初期値として割り当てます。そして、これらの X は、その論理関数の中で X に依存するレジスタへ伝播します。そのため、たった 1 個のレジスタに X が入るだけでデザイン全体が X だらけになり、シミュレーションが役に立たなくなることがあります。

この問題に対するよくある間違った解決策は、すべての X を取り除くために、デザイン内のすべてのレジスタに非同期リセット(asynchronous reset)を付けることです。これは通常、シミュレーションの開始時にリセットを少しの間アクティブにすることで行われます。その結果、すべてのレジスタが既知の値を持ち、すべてが完璧に見えます。残念ながら、この間違った解決策は、前のページで説明した問題を隠してしまい、きれいに初期化されたかのような錯覚を与えることがよくあります。

たとえリセットを正しく使っている場合でも、シミュレーションで X の発生源を追跡するのを避けるために、すべてのレジスタをリセットするのは怠惰な選択です。これは単にリソースを無駄にし、タイミング制約を満たすのを難しくするかもしれません。それだけでなく、不要なレジスタまでリセットするとバグを隠してしまうこともあります。というのも、X の洪水は、あるレジスタから別のレジスタへの意図しない依存関係に起因することがあるからです。したがって、この X の洪水は、デザインに何か問題があることを示す警告になり得ます。

シミュレーションと比べると、ハードウェアはリセットを使わないことに対してはるかに寛容です。しかし、リセットの使い方が間違っていたり、必要なのにまったく使わなかったりすると、ハードウェアはかなり予期しない動作をすることがあります。

結論として、リセットはハードウェアのことを考えて使うべきであり、シミュレーション中に厄介な X を取り除くために使うべきではありません。そして、焦点はハードウェアに置くべきなので、シミュレーションについて私が言うことはこの程度です。

リセットの戦略

レジスタにリセットをかけるかどうか、またどのようにかけるかという判断は、レジスタごとに個別に検討する必要があります。こうすることで、リセット信号の不要に高いファンアウト(fan-out)を避けられるだけでなく、以前の状態がどうであれ、リセット後にロジックが確実に正しく動き始めるかどうかを考える良い機会にもなります。

原則として、4 つの選択肢があります。

私の意見

各選択肢については後で詳しく説明します。まず、私が正しいと考える方法を示し、そのあとで詳しく述べます。

これらの提案は、現在 FPGA ベンダーが推奨している内容とほぼ一致しています。

もうひとつ、非常に一般的な指針を追加します。制御ロジックは常に明示的にリセットし、データパスは初期のゴミデータを自然に流してしまいましょう。

それでは、各選択肢について長々と説明します。

選択肢 1:初期値不明

リセットも初期値も必要としないレジスタがあります。これは特にシフトレジスタや類似の論理要素に当てはまります。一般的に言って、データパスはこの選択肢に当てはまることが多いです。

次のコードを見てみましょう。

reg [31:0] d0, d1, d2, d3, d4;

always @(posedge clk)
  begin
    d4 <= d3;
    d3 <= d2;
    d2 <= d1;
    d1 <= d0;
    d0 <= orig_data;
  end

これは明らかに、32 ビットの遅延レジスタが 5 段あるものです。一部の FPGA(特に Xilinx)では、最後の値(@d4)しか使われず、これらのレジスタのどれにもリセットが付いていなければ、合成ツールはこれをシフトレジスタとして認識します。これにより、ロジックの消費量を大幅に削減できます。

これらの遅延レジスタの出力に接続されているロジックは、初期のランダムなデータをある程度許容できる必要があることは明らかです。これが問題にならない典型的なケースは、適切にリセットされた別のレジスタやステートマシンがあって、未初期化の値が届いてもそれを無視するようにしている場合です。たとえば、これらの遅延線がパイプライン(pipeline)に関係しているなら、パイプラインの制御ロジックが自然に無効なデータを無視します。

もっと一般的に言えば、レジスタをリセットも初期化もしなくてよいかどうかは、その有効性を示すフラグや状態が一緒にある場合に簡単に見分けられます。あるいは、最初にレジスタへ値を代入し、そのあとでその値を消費するという明確な順序がある場合です。要するに、正しい値が代入されるまでは、そのレジスタの値が無視されることが明確である場合です。

初期値が不明となるもうひとつのタイプは、合成ツールに依存する場合です。例:

reg val;

always @(posedge clk)
  val <= 1;

合成ツールは、@val が定数 1 を持つワイヤだと判断するかもしれません。あるいは、初期値を 0 にしたレジスタを割り当てて、最初のクロックで 1 に変化させる可能性もあります。実際にどうなるかは合成ツール次第です。つまり、最初のクロックサイクルを除けば @val の値は常に既知ですが、初期値は不明と見なすべきです。

選択肢 2:FPGA の初期値

FPGA の基本的な同期要素(通常はフリップフロップ、シフトレジスタ、メモリ)の初期値は、コンフィグレーション用のビットストリーム(bitstream)で与えられます。この機能のよく知られた使い方は、ブロック RAM に初期値を設定し、書き込みを行わないことで ROM を作る方法です。

この機能のもうひとつのよく知られた側面は、FPGA がコンフィグレーションから起動するとき、通常はすべてのレジスタがゼロになっているように見えることです。これは、合成ツールが通常すべてのレジスタにゼロを初期値として割り当てるためですが、初期値を明示的に設定していないと思いがけない結果になることがあります。

一部の同期要素、特にシフトレジスタや専用 RAM ブロックは、Verilog または VHDL でその動作を記述することによって(推論によって)作成できます。合成ツールは、コードが遅延線のように見える場合、通常シフトレジスタを作成します。同様に、配列(array)が記述されれば RAM の論理要素が作成されます。しかし、これらのレジスタにリセット(同期リセットまたは非同期リセット)を使うと、合成ツールはそのようなロジックリソースの使い方をできなくなります。シフトレジスタも RAM も、内部メモリの値を設定するリセット入力を持たないからです。

では、初期値はどのように設定されるのでしょうか。コンフィグレーション中、FPGA がアクティブになろうとする直前、つまり同期要素がクロックや非同期リセット入力に応答し始める直前に、すべての同期要素へ初期値が与えられます。Xilinx デバイスの場合、これはグローバルセットリセット(Global Set Reset: GSR)信号によって実現され、すべての同期要素が初期状態に置かれます。その後、グローバルライトイネーブル(Global Write Enable: GWE)がアクティブになり、同期要素が通常どおり動作し始めます。

コンフィグレーション処理は、FPGA のアプリケーションロジックが使うクロックとは無関係に実行されるため、FPGA が動作状態へ移行するタイミングは、これらのどのクロックに対しても非同期です。その結果、同期要素は、あたかも非同期リセットがどのクロックとも無関係に解除されたかのように振る舞います。言い換えれば、動作状態への移行後に最初に到着するクロックエッジに応答する同期要素もあれば、タイミング違反のために応答しない同期要素もある可能性があります。これは、この連載の最初のページで説明したように、厄介なバグを引き起こす可能性があります。

FPGA が起動するとき、クロックが必ずしも安定しているとは限らないことに注意が必要です。クロックが FPGA 自身の PLL によって生成されている場合、タイミング制約を大きく逸脱する可能性があります。上で説明したように、クロックが安定するまで無視される(たとえばクロックイネーブル(clock enable)によって)か、FPGA 起動時に安定していることが分かっているなら、これは問題になりません。また、クロックが安定するまでどの同期要素も値を変更する理由がないことをデザインが保証している場合も問題ありません。そうでない場合、初期値を設定してもあまり保証にはなりません。

初期値を設定する方法には限界があるものの、多くのシナリオでは十分機能し、明示的なリセットは必要ありません。また、リセット信号が利用できないため、選択の余地がない場合もあります。たとえば、FPGA が起動した直後に FPGA 用のリセット信号を生成するロジックです。そのようなロジックの例は、この連載の第 3 ページで示します。

ほとんどの合成ツールでは、レジスタの初期値の設定はとても簡単です。Verilog の「initial」を使います:

reg [15:0] counter;
initial counter = 1000;

合成向けの Verilog コードで「initial」を使えることは意外に思えるかもしれませんが、実際にはこの使い方は広くサポートされています。したがって、合成ツールがサポートしていれば(つまり、この「initial」の使い方がドキュメントに明記されていれば)、このキーワードは間違いなく好ましい方法です。そのうえ、代替方法はベンダー固有である傾向があり、FPGA ファミリに固有であることもあります。したがって、「initial」が必ずしも移植可能とは言えないとしても、おそらく最も移植性の高い選択肢でしょう。

初期値を設定する代替方法は、使用する FPGA によって異なります。この方法は通常、同期要素をプリミティブ(primitive)としてインスタンシエーション(instantiation)し、初期値をインスタンシエーションパラメータとして割り当てるというものです。たとえば、Xilinx のフリップフロップの場合:

FDCE myflipflop (
  .C(clk),
  .D(in),
  .Q(out),
  .CLR(1'b0),
  .CE(1'b1)
);

defparam myflipflop.INIT = 1;

「initial」を使うほうがずっといいですよね?

選択肢 3:非同期リセット

非同期リセットがなぜしばしば誤って使われるのかについてのページをまだ読んでいないなら、先に読むことをお勧めします。そもそもこの種のリセットを使うつもりがないのなら別ですが。

なぜか、多くの人が非同期リセットをあらゆる目的に適した正しい解決策だと考えています。たぶん、コード例によく出てくるからでしょう。あるいは、すべての同期要素に届くグローバルリセットを簡単に実現できるという錯覚のせいかもしれません。あるいは、古い ASIC の世界では、製造工程でのチップテストに非同期リセットが役立ったからかもしれません。チップ全体をリセットしてテストベクタの適用を始められるからです。

では現実の話をしましょう。非同期リセットを適切かつきれいに使う方法は、クロックを止めた状態で行うことです。「非同期」という言葉の本当の意味はそこにあります。実際のデザインでは、次の段階になります:

この手順の実装自体は難しくありませんが、各クロックの最初のエッジがグリッチなしできちんと形成されることを保証するのは難しいかもしれません。クロックバッファによくある問題として、クロックバッファの出力イネーブルの起動と、そこを通過する最初のクロックエッジとのあいだのタイミングに関する要件があります。このタイミング要件が守られないと、クロックバッファがグリッチ(FPGA のクロック要件に違反する短いパルス)を出力する可能性があります。これにより、このクロックに依存するすべての同期要素が予測不能な動作をする可能性があります。

残念ながら、FPGA ベンダーが提供するドキュメントには、このタイミング要件を確実に満たす方法が必ずしも説明されているわけではありません。そのため、リセット後の最初のクロックエッジが正しく動作することを保証できない場合があります。そして、最初のクロックエッジが保証されなければ、リセットは無意味です。

この方法を使う場合は、非同期リセットに関連するパス(path)にタイミング制約が課されないようにしてください。この場合、そのような適用は不要であり、場合によってはデフォルトで有効になっているかもしれません。

非同期リセットを確実に適用するもうひとつの方法が、よく提案されています。この方法はクロックゲーティングを伴わないため、クロックバッファに依存しません。考え方は、非同期リセット信号のアクティブ化は直接通すが、リセットの解除は同期式で行う、というフリップフロップを数個追加するものです。つまり、同期化された非同期リセット(synchronized asynchronous reset)です。

たとえば、元の非同期リセットが @external_resetn の場合、次のようにしてこの種のリセットを生成します:

   reg pre_rstn1, pre_rstn2;
   reg resetn;

   always @(posedge clk or negedge external_resetn)
     if (!external_resetn)
       begin
         resetn <= 0;
         pre_rstn2 <= 0;
         pre_rstn1 <= 0;
       end
     else
       begin
         resetn <= pre_rstn2;
         pre_rstn2 <= pre_rstn1;
         pre_rstn1 <= 1;
       end

@clk は、@resetn によってリセットされる同期要素が使うクロックであることに注意してください。

@external_resetn がアクティブ(ロー)のとき、3 つのレジスタはすべて非同期にアクティブ(ゼロ)になります。しかし、@external_resetn が解除されると、次のクロックエッジで @pre_rstn1 だけがインアクティブになり、その後続くクロックで @pre_rstn2 と @resetn へと伝播します。

追加の 2 つのレジスタの目的は、メタスタビリティ(metastability)を防ぎ、@resetn が安全な方法で解除されるようにすることです。これは、@external_resetn が @clk に対して悪いタイミングでインアクティブになる場合に必要です。その場合、@pre_rstn1 がメタスタブルな状態になる可能性があります(このページでメタスタビリティを説明しています)。

この同期化回路(synchronizer)の利点は、@external_resetn を非同期リセットとして使えることです。クロックがアクティブでなくても機能します。その一方で、同期要素が受け取るリセット信号は同期式にインアクティブになるため、タイミングを保証できます。

言うまでもありませんが、各クロックにはそれぞれ専用の同期化された非同期リセットが必要です。

上記のように @resetn を生成するだけでは不十分であることに注意してください。@clk のタイミング制約は、@resetn から同期要素に至るパスに適用しなければなりません。一部の FPGA ツールのデフォルト設定では、同期要素の非同期リセット入力で終わるパスのタイミングが無視されます。そのため、ツールの設定を変更する必要があるかもしれません。

したがって、@resetn を通常の非同期リセットとして使う場合、たとえば

  always @(posedge clk or negedge resetn)
    if (!resetn) // Are you sure this path is timed?
      the_register <= 0;
    else
      [ ... ]

その場合、上に示した同期化回路だけでは、リセットからの確実な回復を保証するのに十分ではありません。@resetn を起点とし、フリップフロップの非同期リセット入力で終わるパスが実際にタイミング検証の対象になっているかどうかを確認するのは、あなたの責任です。

また、この同期化回路は @external_resetn のグリッチには効果がないことも重要です。@external_resetn のアクティブパルス長が FPGA のフリップフロップの仕様より短いと、何が起きてもおかしくありません。したがって、@external_resetn は、十分に長いパルスを保証するロジックまたは外部回路で生成する必要があります。それが不可能な場合は、次の @sync_resetn の例のように、リセットを完全に同期化するしかありません:

  reg pre_rstn1, pre_rstn2;
   reg sync_resetn;

   always @(posedge clk)
     if (!external_resetn)
       begin
         sync_resetn <= 0;
         pre_rstn2 <= 0;
         pre_rstn1 <= 0;
       end
     else
       begin
         sync_resetn <= pre_rstn2;
         pre_rstn2 <= pre_rstn1;
         pre_rstn1 <= 1;
       end

しかし、この同期化回路は @clk がインアクティブの間は @external_resetn を無視します。これは、ロジックが @external_resetn を非同期リセットとして扱う必要がある場合、つまりクロックがアクティブでなくても機能すべき場合には問題です。

最初の同期化回路に戻りましょう。@resetn を単なる同期リセットとして使ったらどうなるでしょうか。たとえば次のようなコードです:

  always @(posedge clk) // @resetn not in sensitivity list!
    if (!resetn)
      the_register <= 0;
    else
      [ ... ]

これはだいたい問題ありません。@resetn の解除はタイミング制約によって確実にタイミング検証されるからです。ただし、@resetn のアクティブ化は非同期であり検証されないため、関連する同期要素はリセットが効き始める直前にランダムに動作する可能性があります。この目的には、上記の @sync_resetn のような完全に同期化されたリセットを使うほうが良いです。

このテーマの最後に、一般的なコメントを述べます。上の例ではローアクティブのリセットを選びました。主な理由は、リセット信号が抵抗を介して電源電圧に接続されたコンデンサで生成されていた時代からの伝統によるものです。コンデンサには当初電圧がかかっていないため、リセット入力は '0' でした。このコンデンサはすぐに十分な電荷を蓄えるため、リセット入力は '1' に変わりました。この古式ゆかしいパワーオンリセット方式が、今でも多くのリセットがローアクティブである理由です。

結論として、非同期リセットを確実に使うことは可能ですが、それを実現するのは多くの人が信じるほど簡単ではありません。確実な非同期リセットを実現する 2 つの方法を示しました。タイミングの問題を避けるために一時的にクロックを止める方法と、タイミングを保証するために同期化回路を使う方法です。FPGA ではいつものことですが、要はタイミングです。

現実の世界で非同期リセットに依存しているデザインのほとんどでは、これらの方法のどれも使われていません。その結果、FPGA デザインの信頼性は純粋な運任せになっています。

選択肢 4:同期リセット

同期リセットは、次のコードパターンで最もよく知られています:

  always @(posedge clk)
    if (reset)
      the_register <= 0;
    else
      [ ... ]

もっと良いと思うコードパターンは後で提案しますが、ここではこのパターンで話を進めます。ともあれ、ここではアクティブハイのリセットを選んだことに注意してください。同期リセットではアクティブハイがより一般的です。少なくとも私の印象ではそうです。

同期リセットは、非同期リセットよりほとんどすべての点で優れています。ただし、次の点を除きます:

FPGA は、アクティブなクロックがない状態で使われることはめったにありません(テストのためには従来からこれが必要だった ASIC とは異なります)。また、配線用の専用リソースの問題が本当にあるのかも明確ではありません。そこで、主要なテーマであるファンアウトに焦点を当てます。幸い、これは簡単に解決できます。

同じファンアウト問題は、先ほど提案した同期化回路を使う場合、非同期リセットにも同様に影響することを述べておく価値があります。したがって、「ファンアウトは同期リセットの欠点だ」と本当に言えるのは、クロックを止めた(ゲーティングした)状態で非同期リセットを使う人だけです。

ファンアウト問題を解決する方法として最初に思いつくのは、合成ツールの制約や属性を使ってファンアウトを制限することです。しかし、これはあまり好ましい方法ではありません。合成ツールは制限値に達するとフリップフロップを複製するだけだからです。その結果、複製されたフリップフロップの出力が、目的がまったく異なるモジュールへ接続されることがよくあります。そのため、これらの出力の行き先が FPGA 全体に散らばってしまう可能性があります。これにより、配線が長くなり、伝播遅延(propagation delay)が大きくなります。

シンプルで効率的な解決策は、ロジックのかなりの部分ごとにローカルリセットを作ることです。たとえば次のようなものです:

module medium_sized_module (
  input clk,
  input reset,
  input [15:0] in_data,
  output [15:0] out_data
);

  (* dont_touch = "true" *) reg local_reset;
  reg the_register;

  always @(posedge clk)
    local_reset <= reset;

  always @(posedge clk)
    if (local_reset)
      the_register <= 0;
    else
      [ ... ]

考え方は、@local_reset が @reset のローカルコピー(1 クロック遅延)であるということです。このモジュールとそれより下位に向けて、@reset の代わりに @local_reset を使うことで、ファンアウトを妥当な水準に抑えられます。このローカルリセットを使う回路は互いに密接に接続されていることが想定されるため、FPGA 上の特定の領域に配置される可能性が高いです。したがって、ローカルリセットがロジックファブリック上を長距離配線される必要はありません。

ロジックを最適化するために合成ツールがローカルリセット用レジスタを削除してしまわないようにすることが重要です。合成ツールは通常、異なるモジュールに属していても、同じ動作をするレジスタがあるとこれを削除します。上の例では Vivado の合成属性、すなわち「dont_touch」を示しました。各合成ツールには、それぞれ独自の方法があります(Quartus では、合成属性「dont_merge」で同じことが実現できます)。

合成ツールが実際にすべてのレジスタを保持したことを確認するには、これらのレジスタすべてに同じ名前(たとえば上記の local_reset)を付けて、インプリメント後のデザインでこの名前のレジスタを検索すると便利です。

もちろん、すべてのモジュールにローカルリセットを作る必要はありません。おおよその目安として、ファンアウト 50〜100 程度であればローカルリセットにとって妥当です。特に、FPGA 上の小さな物理領域内の論理要素に届く場合はなおさらです。

ファンアウト削減のテーマは、タイミングクロージャ(timing closure)の文脈でも議論しています。

同期リセットの詳細

同期リセットに関するよくある誤解は、合成ツールが同期リセットのコードパターンを認識すると、リセット信号をフリップフロップの同期リセット入力に接続する、というものです。そうなることもありますが、多くの場合はそうなりません。

これは非同期リセットとは異なります。非同期リセットは、フリップフロップの非同期リセット入力に接続する必要があります。そうしないと、クロックなしではリセットが機能しません。

合成ツールはコードパターンに特別な意味を与えず、Verilog コードから導かれる論理式を計算する傾向があります。次の例を考えてみましょう:

  always @(posedge clk)
    if (reset)
      the_register <= 0;
    else if (some_condition)
      the_register <= !the_register;
    else if (some_other_condition)
      the_register <= 0;

このコードのひとつの読み方は、always 文が同期リセットを要求する標準的なコードパターンで始まり、そのあとにレジスタの具体的な動作の定義が続く、というものです。したがって、@reset が該当フリップフロップの同期リセット入力に接続され、論理関数(LUT として実装される)の出力がフリップフロップのデータ入力に入ると期待するかもしれません。

実際には、合成ツールは通常、次のクロックでの @the_register の値をできるだけ簡潔に実装します。たとえば、フリップフロップのリセット入力が、式(reset || (some_other_condition && !some_condition))を実装した論理関数(つまり LUT)に接続されるかもしれません。

しかし、さらに興味深い可能性があります。フリップフロップのリセット入力がまったく使われないかもしれません。代わりにデータ入力だけが使われ、論理関数が入力のひとつとして @reset 信号を使うのです。つまり、@reset がハイなら論理関数の出力はゼロになります。この方法でも @reset は確かに @the_register をゼロにしますが、他の信号と特別に区別して扱われるわけではありません。

繰り返しになりますが、フリップフロップに同期リセット入力があるとしても、合成ツールは同期リセットのコードパターンを特別に扱わず、リセット信号も他の信号と区別して扱いません。フリップフロップのリセット入力は、Verilog コードが要求する動作を最適に実装するために使われます。つまり、リセット入力をリセット信号へ直接接続することもあれば、リセット信号を含む何らかの論理関数に接続することもあり、リセット入力を一切使わないこともあります。合成ツールは、性能目標をより良く達成できる方法を選ぶだけで、それ以外の意図はありません。

Xilinx の Vivado ユーザーは、DIRECT_RESET と EXTRACT_RESET という 2 つの合成属性を使うことで、この問題をより細かく制御できます。

非同期リセットのもうひとつの欠点を述べて、この話題を締めくくります。ほとんどの FPGA では、フリップフロップのリセット/セット入力は 1 つだけです。この入力は同期または非同期として動作できます。リセットが同期式の場合、合成ツールは要求された動作を実現するために、この入力を利用して LUT の数を減らすような工夫が見つかるかもしれません。しかし、非同期リセットがあると、そのような近道は不可能です。つまり、非同期リセットは合成ツールの手を縛り、より多くのロジックリソースを無駄遣いさせます。

レジスタの意図しないフリーズを避ける

リセットを実装するときによく使われるコードパターンには、次のコード例のような落とし穴があります:

   always @(posedge clk or negedge resetn)
     if (!resetn)
       begin
         reg1 <= 0;
         reg2 <= 0;
         // Ayeee! Forgot to reset reg3 !
       end
     else
       begin
         reg1 <= [ ... ];
         reg2 <= [ ... ];
         reg3 <= [ ... ];
       end

コメントが示唆しているように、@reg3 は @resetn がアクティブの間の begin-end ブロックに現れていません。その結果、上記の Verilog コードでは、@resetn がアクティブである限り @reg3 は値を変えてはならない、という要求になります。これは、@reg3 が次のように定義されているのと同じです:

   always @(posedge clk)
     if (resetn)
       reg3 <= [ ... ];

言い換えれば、@resetn は @reg3 にとってクロックイネーブルとして機能します。@resetn がハイのときだけクロックが有効になるということです。

同期リセットでもまったく同じことが起きます:

   always @(posedge clk)
     if (reset)
       begin
         reg1 <= 0;
         reg2 <= 0;
         // Ayeee! Forgot to reset reg3 !
       end
     else
       begin
         reg1 <= [ ... ];
         reg2 <= [ ... ];
         reg3 <= [ ... ];
       end

これは実際には理解しやすいです。単に 2 つの begin-end ブロックがあり、2 つ目のブロックは @reset がインアクティブのときだけ有効になるからです。したがって、この例での @reg3 の定義は明らかに次のようになります:

   always @(posedge clk)
     if (!reset)
       reg3 <= [ ... ];

したがって、当然の(そして必ずしも賢いとは言えない)結論は、リセット用の begin-end ブロックの中でレジスタを忘れないことです。実際、多くの FPGA 設計者は、必要かどうかに関係なくすべてのレジスタをリセットします。それが唯一の方法だと信じているからです。あるいは、各レジスタをそれぞれ独立した always 文で書くというコーディングスタイルを採用します。

しかし、一部のレジスタだけをリセットし、他はリセットしたくない場合はどうすればよいでしょうか?

非同期リセットの場合、レジスタを別々の always 文に入れるしか選択肢がありません。しかし、同期リセットなら簡単な方法があります:

   always @(posedge clk)
     begin
       reg1 <= [ ... ];
       reg2 <= [ ... ];
       reg3 <= [ ... ];

       if (reset)
         begin
           reg1 <= 0;
           reg2 <= 0;
           // I don't want to reset reg3, and that's fine!
         end
     end

「if (reset)」文を先頭に置き、「else」文の中に本来の処理を書く代わりに、「if (reset)」を最後に置きます。こうすると、リセット用の代入がそれより前のすべての代入を上書きします。

これは上のどの例とも等価ではないことに注意してください。@reg1 と @reg2 は @reset がアクティブのときにリセットされますが、@reg3 はリセットの影響をまったく受けません。

この代替的な同期リセットの適用方法に抵抗を感じるのであれば、それは理解できます。理由はいくつかあります。まず第一に、FPGA 設計では一般に、よく使われるコードパターンに従うのが良い習慣です。そうしないと、合成ツールがエキゾチックなバグを露呈するかもしれませんが、確立されたコードパターンであればその可能性ははるかに低いです(Golden Rule #4 を参照)。したがって、Verilog 標準がこの方法が機能することを明示的に要求しているとしても、この機能に依存するのは必ずしも良い考えではないと主張することはできます。

これは強い反論ですが、参考までに言うと、私はこのようなコードパターンを 10 年以上、さまざまな合成ツールで多用してきました。特に、自分のコードで同期リセットを定義するときは常にこの方法を使っています。これまでに問題が起きたことは一度もありません。

この方法を好まない別の理由として、通常のコードパターンではないため、合成ツールが同期リセットを意図しているというヒントを見逃すのではないか、という考えがあるかもしれません。しかし、上で述べたように、ほとんどの合成ツールはそもそもそのヒントを考慮しません。同期リセットを、ロジックの要求される動作の単なる定義のひとつとして扱います。したがって、この理由には根拠がありません。

安全だという私の言葉を信じてもらえるなら、頭痛の種をひとつ減らせるでしょう。

同じことは非同期リセットでもできるのでしょうか? たとえば、次のコードはどうなるでしょうか?

    always @(posedge clk or negedge resetn)
      begin
        reg1 <= [ ... ];
        reg2 <= [ ... ];

        if (!resetn)
          reg1 <= 0;
      end

これはもちろん、非同期リセットの一般的なコードパターンから外れています。Vivado の合成ツールを使った個人的なテストでは、ヒントを汲み取って @reg1 に非同期リセットを割り当てました。

しかし、@reg2 に要求されている動作は FPGA では実現できません。上に書いたとおり、@clk と @resetn の両方がクロックであり、@reg2 はそれぞれの立ち上がりエッジと立ち下がりエッジで新しい値をサンプリング(sampling)することになります。2 つのクロック入力を持つフリップフロップは、私が知る限りどの FPGA にも存在しないため、@reg2 の定義を合成することは不可能です。

Vivado の合成ツールは、「negedge resetn」の部分を無視することでこれに対応し、@clk だけをクロックとして使うフリップフロップを作成しました。合成結果にはこの奇妙さの痕跡はなく、合成ツールが警告を出したり、不満を述べたりすることもありませんでした。合成ツールが Verilog コードで定義されたとおりに動作しないロジックを作成したにもかかわらずです。

つまり、Vivado の合成ツールに限って言えば、同じコードパターンは非同期リセットでも実際に機能します。しかし、これに頼るべきではありません。Verilog コードは、ロジックに何をさせたいのかを正確に記述すべきです。そうでなければ、合成ツールが自分の好みで誤解釈しても、それは当然の権利なのです。

以上で、リセットに関する連載の第 2 回目は終了です。次のページでは、FPGA の電源投入後と外部リセット後の起動に関するさまざまな側面を取り上げます。

このページは英語から機械翻訳されたものです。不明な点があれば、原文を参照してください。
Copyright © 2021-2026. All rights reserved. (dcc38493)