01signal.com

FPGA FIFO 入門

このページは、FIFO に関する全 5 ページのシリーズの最初のページです。

概要

FPGA FIFO は、単純なコンセプトに基づくメモリ要素です。アプリケーションロジックの一部が FIFO の片側にデータワードを書き込み、FIFO のもう一方の側では別のアプリケーションロジックがそれらのワードを同じ順序で読み出します(FIFO = First In First Out)。

このデータは FIFO 内部に格納されます。FIFO の深さ(depth)は、格納できるデータワードの数です。幅(width、すなわち 1 ワードあたりのビット数)と深さは、FIFO ごとにユーザーが設定するパラメータです。

FIFO は、おそらく FPGA 設計で最もよく使われる IP コア(IP core)でしょう。あるロジックがデータを生成し、別のロジックがそれを消費する場面では、まず FIFO を間に挟むことが即座に思い浮かびます(もちろん、FIFO が常に正しい解決策だというわけではありませんが…)。

コマンドラインインターフェース(特に UNIX / Linux)に慣れている人には、FIFO の使い方をコマンド間のパイプに例えるとわかりやすいでしょう。一方のプログラムの出力がもう一方のプログラムの入力になり、その間にある魔法の仕組みが残りを処理してくれます。

これほど広く使われているため、FPGA FIFO がどう動作すべきかについては、事実上の共通認識があります。どの FPGA 開発ソフトウェアでも、アプリケーションの設計で使う FIFO IP モジュールを生成する方法が用意されています。そのうえ、その FIFO モジュールには、他の FPGA FIFO と同じように動作する一連のポートが備わっている可能性が非常に高いです。

FPGA ベンダーが提供するソフトウェアを使えば、特定のニーズに合った FIFO を生成できます。GUI ツールで属性(幅、深さ、そのほか後述する属性)を設定するだけで、残りはツールが処理してくれます。あなたがやるべきことは、デザイン内でモジュールのインスタンシエーション(instantiation)を行うことだけです。FPGA の世界の他の多くの作業とは異なり、この作業は本当にそれだけなのです。

各 FPGA ベンダーがそれぞれ独自の FIFO IP を提供している以上、ドキュメントの細かい部分まで読むことが重要です。FPGA ベンダーによって FIFO を説明する用語は少しずつ異なり、モジュールのポート名も少しずつ違います。さらに、各ベンダーが提供する追加機能や設定オプションも少しずつ異なります。

とはいえ、デフォルト設定は、私が「ベースライン FIFO(baseline FIFO)」と呼ぶものに対応している可能性が高いです。そのうえ、必ず利用できる追加機能のセットもあります。

ただし、ロジックファブリック(logic fabric)内での FIFO の実装方法はベンダーによって異なります。そのため、FIFO の属性の意味を理解することは、FPGA のリソースを上手に使うために重要です。

要するに、FPGA FIFO を学んで理解するのは一度きりの努力で済みます。ある FPGA で使い方を覚えれば、別の FPGA でも簡単に同じように使えます。それ自体が、FIFO がこれほど普及している理由のひとつです。

ベースライン FIFO

FPGA FIFO には文書化された標準規格はありません。それでも、その動作については広い合意があります。

すべての FIFO には、ワードを書き込むためのインターフェースと読み出すためのインターフェースの 2 つがあります。ここでは、私が「ベースライン FIFO」と呼ぶもののインスタンシエーションを見てみましょう。これには重要なバリエーションがありますが、それについては後で説明します。

myfifo myfifo_ins
  (
   .rst(rst),       // Asynchronous reset input

   // Write interface ports
   .wr_clk(wr_clk), // Write clock input
   .wr_en(wr_en),   // Write Enable input
   .din(din),       // Write word input 
   .full(full),     // Full output

   // Read interface ports
   .rd_clk(rd_clk), // Read Clock input
   .rd_en(rd_en),   // Read Enable input
   .dout(dout),     // Read word output
   .empty(empty)    // Empty output
);

ポート名は Xilinx のツールで使われているものですが、他の FPGA ベンダーも似た名前を使っています。

FIFO モジュールのポートは 3 つのグループに分けられます。リセット信号(@rst)と、書き込みインターフェース、読み出しインターフェースです。それぞれのインターフェースには 4 つのポートがあります。

@din と @dout は、FIFO に入るデータワードと FIFO から出るデータワードを運ぶ 2 つのベクタポートです。ワードの幅は、関連するソフトウェアツールを使って FIFO を設定するときに決めます。FIFO の深さ(depth)、つまり格納できるワード数も設定します。この 2 つのパラメータは、FIFO が消費する FPGA のメモリリソースの量に影響します。

クロック

注目すべき点は、2 つのインターフェースにはそれぞれ専用のクロック(@wr_clk と @rd_clk)があることです。各インターフェースの他のポートは、それぞれのクロックに同期しています。

FIFO は、あるクロックドメイン(clock domain)から別のクロックドメインへデータを移すためによく使われます。FIFO には 2 つのクロックがあるからです。設計中の一部のロジックが clk_A に同期し、別の部分が clk_B に同期している場合、どうやってそれらを連携させればよいでしょうか。FPGA エンジニアなら誰でも、まず FIFO を間に置くことを考えるはずです。これは主に、クロックドメインをまたぐ処理(clock domain crossing)が頭痛の種であり、FIFO を使えば簡単かつ安全に解決できるためです。

書き込みインターフェース

書き込みインターフェースは単純です。@wr_clk、@wr_en、@din は FIFO への入力で、@full は出力です。

@wr_clk の立ち上がりエッジで @wr_en が High のとき、@din のデータが FIFO にプッシュされます。FIFO が full(満杯)のとき、@full ポートは High になります。

たとえば、FIFO に 5 ワードを書き込む波形は次のようになります。

Example waveform for writing words to a FIFO

この波形では、アプリケーションロジックはまず D0 と D1 を書き込みます。D1 の書き込みが成功して FIFO が full(満杯)になると、FIFO は @full 出力を High にして知らせます。アプリケーションロジックはこれに応答して、同じクロックサイクルの間に @wr_en を Low に下げます。数クロックサイクル後、FIFO は @full を Low にし、再び書き込んでもよいことを示します。これはおそらく、もう一方の側での動作(FIFO からのデータ読み出し)があったためです。

アプリケーションロジックは、@full が Low に変わったクロックサイクルで書き込みを始めることもできましたが、この例では少し遅れて書き込みを始めています。波形に示されているとおり、追加の 3 ワードが書き込まれます。

波形の中で @din が「Dx」とマークされている場所は、その値が無視されることを意味します。したがって、そこにどんな値があっても構いません。たとえば、D1 と D2 の間の「Dx」の区間では、@din は D1 のままでも、図より早く D2 に変わっていても、まったく別の値でも構いません。結果は同じだったはずです。

簡単なコーディング例として、可能なときはいつでも、カウントアップするワードで FIFO を満たしたいとしましょう。

assign wr_en = !full;

always @(posedge wr_clk)
  if (wr_en)
    din <= din + 1;

これは、@full と @wr_en の正しい関係を示しています。@full が High なら、同じクロックサイクルでは @wr_en も Low でなければなりません。では、そうでなかったらどうなるでしょうか。@full 信号を無視したらどうなるでしょう。おそらく FIFO は wr_en を無視します。つまり、FIFO は、wr_en ポートが次のように定義される @the_real_wr_en に接続されているかのように振る舞うでしょう。

assign the_real_wr_en = wr_en && !full;

ただし、FPGA ツールによっては、この安全機構なしで FIFO を構成できるものもあります。その場合、full(満杯)の FIFO にデータを書き込もうとすると、ほぼ何が起こってもおかしくありません。

いずれにせよ、@full は尊重しなければなりません。そうしないと、データが漏れてしまったかのように見えるでしょう。先ほどの例を考えてみてください。もし @wr_en が常に High だったら、@din はデータが FIFO に書き込まれたかどうかに関係なくカウントアップし続けます。そのため、もう一方の側でデータを読み出すと、カウントアップが不連続になっていたはずです。

なお、@full が Low から High に変化するのは、書き込みサイクルの結果、すなわち @wr_en が High の状態でのクロック立ち上がりエッジの直後だけです。ただし、後述する FIFO のリセット時は例外です。

読み出しインターフェース

読み出しインターフェースはかなり似ていますが、まったく同じではありません。@rd_clk と @rd_en は FIFO への入力で、@dout と @empty は出力です。

@rd_clk の立ち上がりエッジで @rd_en が High のとき、FIFO のメモリから新しいワードが読み出され、@dout はその立ち上がりエッジの後、つまり次のクロックサイクルでその値に更新されます。FIFO が empty(空)のとき、@empty ポートは High になります。

次の波形は、FIFO から 5 ワードを読み出す例です。

Example waveform for reading words from a FIFO

この波形では、アプリケーションロジックはまず 3 ワードを読み出します。@empty が High に変わったこと(同時に @dout に D2 が現れています)に応答して、アプリケーションロジックは同じクロックサイクルで @rd_en を Low にします。先ほどと同じように、@empty が Low に変わった(もう一方の側からのデータ書き込みによる)同じクロックサイクルで @rd_en を再び High に戻しても構いませんでした。しかしこの例では、数クロックサイクル待ってから、さらに 2 ワードを読み出しています。

ところで、細かいことをひとつ。上の波形とこの波形を比べると、5 ワードが書き込まれ、5 ワードが読み出されていることに気づくかもしれません。では、D4 が現れたとき、なぜ @empty は High にならなかったのでしょうか。それは、FIFO が empty(空)でなくても読み出しを止めて構わないことを示したかったからです。この架空の例では、FIFO にはさらに別のワードも書き込まれていたため、D4 を読み出した後も FIFO は empty(空)にならなかったのです。

@rd_en が Low の間、@dout は値を保持することに注意してください。アプリケーションロジックはこれを利用できます。@dout には、FIFO から最後に読み出されたワードの値が常に含まれています(リセット後を除く)。

さらに重要なのは、@dout の新しい値は、@rd_en が High のときの立ち上がりエッジのに現れることです。したがって、FIFO は次の Verilog コードのように動作します。

always @(posedge rd_clk)
  if (rd_en && !empty)
    dout <= next_word_to_show;

この疑似 Verilog コードは、同じクロックサイクルで @empty が High の場合、ほとんどの FIFO が @rd_en を無視するという事実も示しています。書き込みインターフェースと同様に、そのクロックサイクルで @empty が High なら、@rd_en を High にしてはいけません。繰り返しになりますが、FIFO によってはこの保護機構を持たないように構成できるものもあるため、この規則を破らないでください。

@empty が Low から High に変化するのは、読み出しサイクルの後、つまりクロックの立ち上がりエッジで @rd_en が High になった後だけです。唯一の例外は、FIFO がリセットされたときです。

例として、FIFO からワードを読み出して累積和を計算する、簡略化した Verilog コード(リセットなし)を示します。

assign rd_en = want_to_read_now && !empty;

always @(posedge rd_clk)
  begin
    rd_en_d <= rd_en;

    if (rd_en_d)
      sum <= sum + dout; // Don't try this at home: @sum is never reset.
  end

説明のため、ロジックが読み出したいことを示す @want_to_read_now 信号を追加しました。それでも @rd_en が High になるのは、FIFO が empty(空)でない場合だけです。

@rd_en_d に注目してください。@rd_en_d には、@rd_en の値が 1 クロックサイクル遅れて入ります。その結果、@rd_en_d が High になるタイミングは、@dout に新しい有効な値が出るタイミングと一致します。@dout の値を取り込む条件として @rd_en_d を使うのはこのためです。@rd_en から @dout までの遅延が少し厄介な点であり、この例でもそれが示されています。

同期とレイテンシ

上の例の波形は書き込みと読み出しを別々に描いたため、重要な点が抜け落ちています。FIFO が empty(空)の状態で最初のワードを書き込んでから @empty ポートが Low に変わるまでには、数クロックサイクルかかります。同様に、full(満杯)の FIFO から最初のワードを読み出してから @full ポートが Low に変わるまでにも、数クロックサイクルかかります。

これは、FIFO への書き込みに関する情報が FIFO の反対側へ届くまでに、2 つのクロックドメイン間を伝搬する必要があるためです。クロックドメインをまたぐために必要なロジックが、数クロックサイクルの遅延を生み出します。したがって、@empty ポートの応答は少し遅れます。@full ポートについても同じです。

では、この遅延は何クロックサイクルになるのでしょうか。それは多くの要因に依存します。その中には、その瞬間における 2 つのクロックのエッジ間の時間関係も含まれます。要するに、正確には言いにくいのです。

この遅延に影響するもののひとつが、同期ステージ(synchronization stages)の数です。これは FIFO で設定できるパラメータであることがよくあります。通常は 2 段が選ばれますが、それ以上にすることもできます。同期ステージを増やすと FIFO の信頼性を高められますが、その代わりにロジックリソースを少し多く消費します。また、前述のとおり、@empty ポートと @full ポートのレイテンシも増加します。

もし本当に FIFO を甘やかしたいのなら、同期ステージを 3 段にして、絶対に安全な気分を味わうのもよいでしょう。

リセット入力

すべての FPGA FIFO にはリセット信号があります。FIFO は 2 つのクロックを使うため、このリセット信号はどちらかのクロックに同期しているとは想定されていません。つまり、非同期リセット(asynchronous reset)です。FIFO の内部ロジックが、2 つのクロックドメインのそれぞれに対して、リセットを内部で同期するしくみになっています。

リセットは何をするのでしょうか。まず、FIFO を空にし、@empty を High に設定します。FIFO 内にデータがあれば、そのデータは失われます。

@full 出力については、リセット後、FIFO がデータを受け取れる状態(書き込みサイクルが可能な状態)になるまで @full を High に維持するのが一般的で、推奨もされています。ただし、この動作はオプションの場合があるため、FIFO のドキュメントで確認するのがよいでしょう。そもそも、リセット直後の FIFO は実際には full(満杯)ではありません。また、リセットによって @full が High に変わるのは、前述の規則(@full は書き込みによってのみ High に変化する)に反します。

リセット信号がアクティブになってから @empty ポートと @full ポートが High に変わるまでに、数クロックサイクルかかることにも注意が必要です。これは FIFO の同期ロジックによるものです。そのため、リセットがアクティブになる前後の数クロックサイクルは、動作があいまいになります。リセットの前後数クロックサイクルの間に、アプリケーションロジックが FIFO への書き込みや読み出しを試みないようにしてください。

リセット信号が非同期(asynchronous)であっても、FPGA のレジスタ(フリップフロップ)の出力でなければなりません。リセットを組み合わせ論理(combinatorial logic)の出力にしてはいけません。グリッチによって、FIFO が意図しないリセットを受ける可能性があるからです。

実際には、多くの FPGA エンジニアが、リセットポートにほぼ何をつないでも動作すると思い込んでいます。しかし、FPGA ベンダーはリセット信号について意外な仕様を要求している場合があります。たとえば、Xilinx の FIFO 製品ガイド(PG057)から次の一節を引用します。

非同期リセット(asynchronous reset)の幅が、最も遅いクロックの 1 クロックサイクル分しかなく、そのアサートが最も遅いクロックの立ち上がりエッジに非常に近い場合、リセットの検出が正しく行われず、予期しない動作を引き起こす可能性があります。このような事態を避けるため、非同期リセットは常に、最も遅いクロックの少なくとも 3 [ ... ] クロックサイクルにわたってアサートすることが推奨されます。

(第 3 章「Resets」)

つまり Xilinx は、リセットを少なくとも 3 クロックサイクルの間アクティブにすることを推奨しています。この推奨をどのくらいの人が知っているでしょうか。ぜひ、お使いのベンダーの FIFO のユーザーガイドを読んで、リセット信号を正しく生成する方法を確認してください。

FIFO の実装方法

ベンダーのソフトウェアツールが、FIFO を正しく動作させるためにすべてを処理してくれるとはいえ、FPGA のどのリソースが使われるのかを把握しておくのは良い考えです。特に、特定の種類のリソースが不足しないようにするうえで重要です。

FPGA ごとに選択肢は異なりますが、よくあるものをいくつか簡単に紹介します。

これで、FIFO に関するこのシリーズの最初のページは終わりです。次のページでは、FIFO の一般的なバリエーションと追加機能について説明します。

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