このページは、FPGA のリセットに関する連載全 3 回の第 1 回目です。非同期リセット(asynchronous reset)は、期待どおりには動作しないことを知らずに使っている人が非常に多いため、この最初のページでは、このテーマが一見したほど簡単ではない理由を説明します。
あなたのリセットは本当に機能していますか?
ステートマシン(state machine)をリセットする例として、次のコードを考えてみましょう。これは間違いです:
always @(posedge clk or negedge resetn)
if (!resetn)
state <= ST_START;
else
case (state)
ST_START:
begin
state <= ST_NEXT;
[ ... do some stuff maybe? ... ]
end
ST_NEXT:
begin
[ ... do something ... ]
end
endcase
では、このリセットのどこが悪いのか、疑問に思うかもしれません。これはまさに教科書に載っている書き方です。ローアクティブの非同期リセットで、@state を実装しているレジスタを初期状態に戻しています。いったい何が問題なのでしょうか?
議論を進めるため、まず @resetn 信号そのものは問題ないと仮定しましょう。つまり、プッシュボタンなどに直結されているわけではないとします。おそらく、リセット信号を生成するための専用チップが使われているか、FPGA 内部でリセットが生成されているのでしょう。いずれにせよ、リセットは安定してアクティブになり、十分な時間アクティブな状態を維持したあとに、インアクティブになると仮定します。それでもやはり間違いです。
「間違い」と言っても、FPGA がたまに、はっきりした理由もなく奇妙な動作をするという意味です。
では、どこに問題があるのでしょうか。リセットが十分長くアクティブであれば、@state は確実に初期状態になります。しかし、リセットがインアクティブ(上の例では '1' に戻ること)になったらどうなるでしょう? そのときこそ、対象のフリップフロップが @clk の立ち上がりエッジに反応しはじめるはずです。
しかし、フリップフロップがリセット信号から回復して、クロックの立ち上がりエッジでデータ入力をサンプリング(sampling)しはじめるまでには、少し時間がかかります。また、リセットは定義上非同期なので、@clk に対してどのタイミングで解除されてもかまいません。
リセット解除後、最初の @clk 立ち上がりエッジが早すぎるタイミングで来ると、フリップフロップはこの立ち上がりエッジを無視します。@clk に接続されているすべてのフリップフロップが同じように無視するのであれば、それで問題はありません。しかし、すべてのフリップフロップが完全に同じとは限りません。あるフリップフロップは他よりわずかに早くクロックエッジを受け取り、またあるフリップフロップは他より遅くリセット解除を受け取ります。
実を言うと、この説明は少し単純化しすぎています。問題をもっと正確に理解するには、タイミングの基礎を解説したページの Recovery と Removal の箇所を参照してください。
つまり、話をまとめるとこうです。運が悪いと、リセットがクロックの立ち上がりエッジに近いタイミングでインアクティブになることがあります。その結果、一部のフリップフロップは最初の立ち上がりエッジに応答し、別のフリップフロップはそれを無視します。実際には、判断に余計な時間がかかるフリップフロップもあるかもしれません。同じチップ上のフリップフロップ同士の差、あるいはクロックスキュー(clock skew)やリセットスキューのせいです。結局、一部のフリップフロップだけが他のフリップフロップより 1 クロックサイクル先に進んでしまうのです。
これがどれほど深刻なのかを理解するには、上の例を考えてみてください。合成ツール(synthesizer)がこれをステートマシンだと認識した場合、状態変数をワンホット符号化で実装する可能性が高いです。つまり、各状態に対して 1 ビットのレジスタを割り当てます。各レジスタは、ステートマシンが対応する状態にあるときにアクティブになります。ここでは、合成ツールが ST_START という状態に hot_state_0 というレジスタを割り当て、ST_NEXT という状態に hot_state_1 というレジスタを割り当てたとしましょう。リセットによって hot_state_0 はアクティブになり、hot_state_1 はインアクティブになることは明らかです。
ここで、このステートマシンは ST_START から ST_NEXT へ無条件に遷移することに注意してください。したがって、リセット解除後の最初のクロックで hot_state_0 はインアクティブになり、hot_state_1 はアクティブになります。
しかし、リセットが不運なタイミングでインアクティブになり、一部のフリップフロップは最初のクロックエッジを取り逃し、ほかは取り逃さないとしたらどうなるでしょうか。ひとつの可能性は、hot_state_0 が最初のクロックを取り逃す一方、hot_state_1 はそれに応答することです。結果として両方がアクティブになり、ワンホット符号化では不正な状態になります。逆の場合、両方のレジスタがインアクティブになるため、結果的にステートマシンのワンホット符号化用レジスタがすべてインアクティブになります。どちらにせよ、ステートマシンは正規の状態に回復できないかもしれません。
実際にはどのような症状になるのでしょうか。もちろんアプリケーション次第ですが、FPGA にもう一度リセットがかけられるまで、何かが正しく動作しない可能性が高いです。この問題はランダムに現れ、しかも頻度が高いとは限らないため、原因を突き止めるのは非常に難しいかもしれません。また、同じ FPGA デザインをコンパイルし直すたびに挙動が変わる可能性もあり、ボードが異なればさらに変わる可能性もあります。要するに、頭を悩ませるタイプのバグです。ここでの議論では問題の原因がテーマそのものなので、それほど深刻には聞こえないかもしれません。しかし、実際の回路でこの種の不安定動作が起きると、症状は何でもあり得ます。そして、しばしばFPGA に何かが取り憑いているように感じられるのです。
でも、いつもこうやってるし、ちゃんと動いてるよ!
そのとおりです。ほとんどのケースでは、リセット後の最初のクロックを一部のフリップフロップが取り逃しても、それほど問題になりません。
上のステートマシンの例が失敗し得る主な理由は、最初のクロックサイクルで初期状態を離れてしまうことです。実際のデザインのほとんどのステートマシンには、初期状態から離れるための規則があるので、最初の数クロックサイクルは必ず初期状態にとどまります。だから、この間違いをしても問題にならないのです。
では、別の例を挙げます。単純なカウンタです。
reg [15:0] counter;
always @(posedge clk or negedge resetn)
if (!resetn)
counter <= 0;
else
counter <= counter + 1;
この場合、@counter は 16 個のフリップフロップで構成されます。各フリップフロップは、データ入力に次のクロックサイクルにおけるカウンタ値を受け取り、非同期リセット入力には @resetn を受け取ります。
@resetn がアクティブのとき、@counter は値 0 になり、次のクロックサイクルにおけるカウンタ値は 1 になります。したがって、counter[0] を除くすべてのフリップフロップは、最初のクロックエッジを取り逃すかどうかにかかわらず 0 のままです。つまり、どちらの場合でもカウンタは正しくカウントを始めます。このようなコードを書くケースの大半では、カウンタが最初のクロックサイクルを取り逃しても、取り逃さなくても問題はありません。
しかし、次の例は話が違います。
reg [15:0] counter;
always @(posedge clk or negedge resetn)
if (!resetn)
counter <= 0;
else
counter <= counter - 1;
小さな違いですが、大きな意味があります。カウンタが 0 から始まりダウンカウントする場合、次のクロックサイクルでのカウンタ値は 0xffff になります。言い換えれば、リセット後の最初のクロックで、すべてのフリップフロップが値を変化させなければなりません。したがって、最初のクロックエッジに応答するフリップフロップとしないフリップフロップが混在すると、カウンタは事実上どんなランダムな値からでも始まり得ます。
しかし、ゼロにリセットしてからダウンカウントするカウンタを書く人はいませんよね。
では、もっと現実的な例を挙げます。クロックイネーブル(clock enable)信号です。これは、あたかもクロック周波数が半分に下がったかのようにロジックを動作させるための信号で、必要ならマルチサイクルパス(multi-cycle path)を設定することもできます。
reg en;
always @(posedge clk or negedge resetn)
if (!resetn)
en <= 0;
else
en <= !en;
always @(posedge clk)
if (en)
[ ... do something ... ]
ここでは何も問題が起きないように思えるかもしれません。クロックイネーブルの @en は単一レジスタなので、いつトグルしはじめても大した問題にはならないのでは……? 実際には、クロックイネーブル信号はファンアウト(fan-out)が大きくなりがちです。そのため、合成ツール(synthesizer)が、ファンアウトの上限を超えないようにこの信号を複製することがあります。
Vivado の合成ツールを使った私の個人的な実験では、@en を実装するために複製された各レジスタが、自分自身の出力信号に依存していました。言い換えれば、すべてのフリップフロップが次の出力を決める際に参照する単一の信号は存在せず、クロックの立ち上がりエッジで常に値を反転する独立したフリップフロップが多数あるだけでした。そのため、これらのフリップフロップが同じクロックサイクルでトグルを始めないと、以後、出力は永遠に食い違ったままになります。
このような事故が起きると、ロジックは完全に誤動作する可能性が高いです。それでも非同期リセットをどうしても使いたいなら、少なくともすべてのクロックイネーブルが単一のソースに依存するようにしてください。たとえば次のようにします。
reg pre_en; // Apply some don't-touch synthesis directive on this
reg en;
always @(posedge clk or negedge resetn)
if (!resetn)
pre_en <= 0;
else
pre_en <= !pre_en;
always @(posedge clk or negedge resetn)
if (!resetn)
en <= 0;
else
en <= pre_en;
always @(posedge clk)
if (en)
[ ... do something ... ]
ここでの工夫は、次の値を決めるレジスタを @pre_en にしておくことです。このフリップフロップはファンアウトが小さく、おそらく合成ツールが勝手に変更しないようにする属性も付いています。こうすることで、確実に単一のレジスタになります。@en を実装するすべてのフリップフロップは @pre_en に依存しているので、次のクロックでの @en の値はどのフリップフロップでも一致します。リセット後の最初のクロックは、一部のフリップフロップが取り逃しても問題ありません。最初のクロックサイクルでの @en の値はどうせ 0 だからです。
つまり結論としては、非同期リセットの使い方が間違っていても、たいていの場合は問題なく動きます。その主な理由は、リセットがクロックに対してどのタイミングでインアクティブになるのかという不確実さに対して、ロジックのほうがたいてい寛容だからです。とはいえ、非同期リセットをいい加減に使うと、ごくまれに誤動作を引き起こし、しかもその解決が極めて難しくなることがあります。
リセットとクロックの間にタイミング制約をかける
リセットの解除とクロックの立ち上がりエッジのあいだのタイミング関係が不確実になるのを避ける、一見して明らかな方法は、リセット信号にタイミング制約(timing constraint)をかけることです。しかし、そうするとリセット信号は同期式になってしまいます。
しかし、そのリセット信号はどのクロックと同期しているのでしょうか。ロジックデザイン全体に対して、ひとつのグローバルな非同期リセットを持つことは、しばしば便利です。このリセットは、ある特定のクロックに同期したロジックで生成されます。このリセットを、別のクロックに同期したロジックで使うと、クロックドメインクロッシング(clock domain crossing)になります。これはそれ自体がひとつのテーマですが、最も重要な点は、関連するパス(path)についてツールがタイミングを無視する可能性があることです。
したがって、非同期リセットにタイミング制約を使う場合の前提は、クロックごとに独立した非同期リセットが必要だということです。あるいは、どうしてもと言うなら、関連クロック(related clocks)のグループごとに独立した非同期リセットが必要です。そうでなければ、タイミング制約に意味はありません。それが奇妙に聞こえるとしたら、その非同期リセットはもはや非同期ではないからだと考えてください。
Verilog コードが非同期リセットのパターンになっているという事実は、何も変えません。フリップフロップの非同期リセット入力が実際に使われているかどうかも、そのリセット入力を非同期と見なすようにフリップフロップが設定されているかどうかも関係ありません。リセットがクロックに同期しており、かつタイミング制約が使われているなら、そのリセットは実質的に同期式です。このケースでは、Verilog コードも直接、同期式のパターンにすることを検討してもよいでしょう。
always @(posedge clk)
if (!resetn)
state <= ST_START;
[ ... ]
とはいえ、Intel のタイミングクロージャ(timing closure)に関する YouTube ビデオでは、クロックに同期した非同期リセットを使い、さらにタイミング制約も使うことを推奨しています。非同期リセットを選ぶのは、FPGA 内のグローバル配線用の専用リソースを利用するためです。私はこれはかなり奇妙だと思います。グローバル配線であっても、特に大規模な FPGA では遅延が大きくなり得るからです。ただし、これが理にかなうシナリオも確かにあります。
実は、たとえ実質的に同期式であっても非同期リセットを使い続ける理由がもうひとつあり、それについては次のページで説明します。それは、リセット信号のアクティブ化を非同期に伝播できるということです。これはシミュレーションにも、ASIC のテストにも役立ちます。
この「同期化された非同期リセット」の方法を使う場合、フリップフロップの非同期リセット入力に至るすべての信号パスにタイミング制約が適用されるようにすることが重要です。リセットが、受け側のフリップフロップと同じクロックに同期したフリップフロップで生成されているという事実だけでは、そのあいだのパスがタイミング検証の対象になることは保証されません。デフォルトでは、非同期入力で終わるパスを無視するタイミングツールもあるため、この適用を明示的に有効にしなければならない場合があります。タイミングレポートを確認して、これらのパスが実際にタイミング制約の対象になっていることを必ず検証してください。ここは、うっかり見落としがちなポイントです。
以上で、リセットに関する連載の第 1 回目は終了です。次のページでは、FPGA のリセットと初期化に関するさまざまな選択肢について説明します。