対象範囲
このページは、FIFO に関する全 5 ページのシリーズの 2 ページ目です。前のページで FIFO の基本を紹介しましたので、次はよく使われるバリエーションと追加機能について説明します。FIFO は、以下で説明するオプションの組み合わせとして構成されることがよくあります。
シングルクロック FIFO
「ベースライン FIFO」には無関係なクロックを入力するポートが 2 つありますが、実際には両側の信号が同じクロックに同期している場合がよくあります。この同じクロックを @wr_clk と @rd_clk の両方に接続しても問題ありません。しかし、両方のクロック入力が同じクロックであれば、クロックドメイン(clock domain)をまたぐ必要がないため、FIFO 内に不要なロジックが含まれることになります。クロックドメインの詳細についてはこちらを参照してください。
したがって、どの FPGA ベンダーも FIFO の 2 つのカテゴリを提供しています。デュアルクロック FIFO とシングルクロック FIFO です。他の呼び方もよく使われます。Independent Clock FIFO と Common Clock FIFO、あるいは Asynchronous FIFO と Synchronous FIFO などです。前のページで紹介した「ベースライン FIFO」はデュアルクロック FIFO です。
シングルクロック FIFO には同期ロジックが一切ありません。すべてのロジックが同じクロックに同期しているからです。したがって、リセット入力も他のポートと同じクロックに同期していなければなりません。
FPGA のロジックを無駄にしないこと以外に、シングルクロック FIFO を使うもうひとつの良い理由は、明確さです。2 つのクロックを関与させる意図がないことを、はっきりと宣言する方法でもあります。
要するに、FIFO が 2 つのクロックドメイン間を接続しないのであれば、シングルクロック FIFO を使いましょう。
FWFT FIFO
前のページで強調したとおり、「ベースライン FIFO」からデータを読み出す手順は、@rd_en を High にして、次のクロックサイクルで @dout の値を取得するというものでした。これは直感に反します。データがすでに FIFO 内にあるのなら、なぜわざわざ要求しなければならないのでしょうか。FIFO がデータを @dout に載せて、「これを使ってよいですよ」と教えてくれればよいのではないでしょうか。
そこで、まさにその動作をする一般的なバリエーションがあります。それは First Word Fall Through FIFO(FWFT。読み出し先行、show-ahead、look-ahead と呼ばれることもあります)です。FWFT FIFO の反対は、しばしば「スタンダード FIFO(Standard FIFO)」と呼ばれます(その「標準」がどこにあるのか、誰か教えてほしいものですが)。
アイデアは単純です。FWFT FIFO は、empty(空)でなくなると(データが書き込まれると)、最初のワードを @dout に提示します。アプリケーションロジックは @rd_en を High に保持することでワードを読み出します。したがって、違いは最初のワードに関してだけです。
ただし、FWFT FIFO を理解しやすくするには、その 2 つのポートの意味が変わっていることに気づくとよいでしょう。FWFT FIFO の @rd_en は、実際には「@dout のデータを消費したので、次のデータを出してよい」という意味になります。また @empty は、実際には「@dout は有効ではない」という意味になります。
変わっていないのは、@empty が High のとき、@rd_en を High にしてはいけないという規則です。無効なデータを消費したと言うことはできません。つまり、規則自体は変わらず、その理由が変わっただけです。
次の波形は、FWFT FIFO からの読み出しがどのように見えるかを示しています。
@rd_en が Low の間に @dout に最初の有効な値が現れ、同時に @empty が Low に変わることに注目してください。先ほど述べたとおり、FWFT FIFO では @empty は「@dout が有効ではない」ことを意味し、波形にもそれが反映されています。
また、最初の @rd_en のパルスは FIFO から新しい値を読み出したのではなく、@empty を再び High に変えただけであることにも注意してください。それに伴い、@dout の値も同時に不明になります。実際には、@empty が High に変わっても @dout は通常変化しませんが、それに依存してはいけません。
この後、FIFO は再び @dout に値を載せ、@empty を Low に変えます。アプリケーションロジックは 3 ワードを読み出し、その後 @rd_en を Low にします。全体として、アプリケーションロジックは FIFO から 4 つまたは 5 つのワードを消費したことになります。
この波形からは、アプリケーションロジックが D4 の値も使ったのかどうかはわかりません。5 ワード目を無視したのかもしれません。その場合、消費したのは 4 ワードだけです。あるいは 5 ワード目の値を使ったのかもしれません。波形から明確なのは、アプリケーションロジックが 4 クロックサイクル後に @rd_en を Low に保持したため、FIFO が @dout を更新し続けることを許さなかった、ということだけです。
もうひとつ注意すべき点は、FIFO のメモリ内にさらにデータがあるかどうかがわからないことです。この波形の最後で @empty が Low であることは、@dout が有効であることだけを意味しています。
それでは、前のページの Verilog の例を修正してみましょう。このコードも、FIFO から出てくるすべての値の累積和を計算します。
assign rd_en = !empty; // If @dout's value is valid, it's consumed.
always @(posedge rd_clk)
if (!empty) // FIFO is FWFT, so !empty means @dout contains valid data
sum <= sum + dout; // Don't try this at home: @sum is never reset.
前の例と異なり、ここでは FWFT FIFO を使っているため、前のクロックサイクルの @rd_en の値を保持するレジスタは不要です。その代わり、@empty が Low のときに @dout を消費できます。@empty が Low の間は @rd_en が High になるため、FIFO からの各ワードはちょうど 1 クロックサイクルの間だけ @dout 上で有効になります。この単純な規則でうまくいくのです。
FWFT の話題は、やや関連性のない点で締めくくりたいと思います。「スタンダード」FIFO と FWFT FIFO の違いは、任意の 2 つのロジックモジュール間のデータフローに関する根本的な問題を反映しています。受け取り側はデータを要求する必要があるのか、それとも送り出し側はできるだけ早くデータを提示し、受け取り側は続行してよいことを確認するだけなのか。あるモジュールが別のモジュールへデータを渡すときは、常にこの問いを自分に投げかけ、特に両方のモジュールがこの点について一致しているかを確認してください。
非対称 FIFO
@din と @dout に異なる幅を設定できる FIFO が一般的です。これは、たとえばデータが 32 ビットワードで FPGA に到着するが、アプリケーションロジックがそれをバイト(すなわち 1 ワード 8 ビット)として処理する場合に役立ちます。この場合、書き込み側の幅を 32 ビット、読み出し側の幅を 8 ビットに設定します。両側は通常どおり動作しますが、1 回の書き込みサイクルで挿入されたワードを消費するには 4 回の読み出しサイクルが必要になります。
読み出し側の幅が書き込み側より広い場合も、期待どおりに動作します。FIFO に書き込まれたデータは、書き込まれたデータが読み出し側のサイズのワードを満たすまで、読み出し側では利用できません。
ワードがどのような順序で詰め込まれるかについては、すべての FIFO がリトルエンディアンを使っているようです。たとえば、FIFO が 32 ビットワードを 8 ビットワードに詰め替える場合、FIFO から最初に読み出されるワードのビット範囲は [7:0] で、次に [15:8]、[23:16]、[31:24] の順になります。
ただし、この機能を使いたい場合は、必ずドキュメントを確認してください。
@empty と @full への組み合わせ論理の依存
@empty ポートと @full ポートには共通の欠点があります。アプリケーションロジックが同じクロックサイクル内で応答しなければならないことです。言い換えれば、@rd_en と @empty が同じクロックサイクルで High にならないようにするには(これは前述のとおり禁止されています)、@rd_en は @empty に依存する組み合わせ論理(combinatorial logic)の関数でなければなりません。同様に、@wr_en は @full に依存する組み合わせ論理の関数でなければなりません。
組み合わせ論理を使うことは、タイミング制約(timing constraints)を達成するうえでの障害になり得ます。これは、クロック周波数が高い場合(FPGA の仕様と比較して)や、論理関数が複雑な場合に問題となります。問題の主な原因は、@rd_en と @wr_en の両方が、データを生成または消費するロジックで使われることが多いことです。特に、多数のロジックに対するクロックイネーブル(clock enable)を計算する論理関数が、これらの信号に依存することがあります。たとえば、FIFO から来るデータを処理する長いパイプライン(pipeline)がある場合、FIFO からのデータフローが一時的に止まると、パイプライン内のすべてのロジックを停止させなければなりません。
正確に言うと、その組み合わせ論理を回避する方法もあります。たとえば、@wr_en をレジスタとして宣言し、@want_to_write は、特定の時点で書き込みたいというアプリケーションロジックの要求を表す信号だとします。次のようにできます。
always @(posedge wr_clk)
wr_en <= want_to_write && !wr_en && !full;
これにより、@wr_en と @full が同じクロックサイクルで High になることは決してありません。@full が High に変化できるのは、@wr_en が High であった次のクロックサイクルだけだからです。式中の !wr_en の部分は、@wr_en が 2 クロックサイクル連続して High にならないことを保証します。したがって、@full が High になると、最初のクロックサイクルでは !wr_en のために @wr_en は Low になります。その後 @wr_en は @full 自体によって Low のままになります。
しかし、この解決策では @wr_en は半分の時間 Low でなければなりません。その結果、FIFO のデータレートの 50% しか使えません。これは通常、許容できません。
@rd_en についても同じ解決策が可能ですが、この解決策にはデータレートを半分しか使えないという同じ問題があります。
ここまでの議論は、次のセクションにつなげるためのものでした。「almost」ポートの話です。
Almost full、almost empty、および類似のポート
FIFO には @almost_full ポートや @almost_empty ポートという 2 つのオプションポートを追加できます。
@almost_empty は @rd_clk に同期し、@empty と似ていますが、少しだけ違います。@almost_empty は、FIFO が empty(空)のとき、さらに FIFO から読み出すワードがちょうど 1 つだけあるときにも High になります。
同様に、@almost_full は @wr_clk に同期し、FIFO が full(満杯)のとき、さらに FIFO にちょうど 1 ワードだけ書き込める状態のときにも High になります。
これらの 2 つの出力ポートの名前は FPGA ベンダーとその提供ソフトウェアによって異なりますが、同じ機能のポートを追加できることは常にあります。ただ、FIFO の種類によっては、これらのポートに対応していないものもあります。
これらのポートがどのように役立つのでしょうか。次のコードが完全に正しく動作するからです。
always @(posedge wr_clk)
wr_en <= want_to_write && !almost_full;
組み合わせ論理は不要で、書き込みサイクルの半分をスキップする必要もありません。@almost_full が High のとき、@wr_en は同じクロックサイクルでは Low になれず、次のクロックサイクルで Low になります。その結果、@almost_full が High に変わった後に 1 回の書き込みが発生し得ます。しかし、それで問題ありません。1 ワード分の空きがあるからです。
FIFO を満たしている間、@want_to_write が継続的に High に保持されている場合、最後の書き込みで FIFO は完全に full(満杯)になります。そうでない場合、FIFO はほぼ full(満杯)の状態で終わる可能性があります。@wr_en が @want_to_write のせいで Low になり、そのせいで FIFO が完全に full(満杯)にならなかった場合、もう一度書き込む機会はありません。@almost_full が Low に変わるのは、もう一方の側が FIFO からデータを読み出したときだけなので、FIFO には 2 ワード以上の空きができます。
それはほとんど問題になりませんが、議論のために言うと、次のコードは最後のワードも確実に使うことを保証します。
always @(posedge wr_clk)
wr_en <= want_to_write && (!almost_full || (!full && !wr_en));
この @wr_en の式は、ほとんどの場合 @almost_full に依存しますが、ちょうど 1 ワード書き込める状態のときだけは別です。その場合のみ、@wr_en は @full と @wr_en に依存します。これは、前述の !wr_en を使った式と似ています。
ただし、この最後の @wr_en の式が実際に役立つかどうかは、私は大いに疑問に思います。
@almost_empty の話も同様です。つまり、次のコードで問題ありません(ただし、このコードをそのまま自分のコードにコピーしないでください)。
always @(posedge rd_clk)
rd_en <= want_to_read && !almost_empty;
@almost_full と同様に、最後のワードに関する問題があります。@rd_en が @want_to_read のせいで Low になると、FIFO がさらにデータで満たされるまで、その機会を失います。@almost_full の場合と違って、これはシナリオによっては確実に問題になり得ます。@almost_empty が High でも FIFO が empty(空)でない場合、FIFO 内には読み出し対象のデータがあるのに、そのデータが FIFO 内に留まってしまうからです。
したがって、安全な方法は次のとおりです。
always @(posedge rd_clk)
rd_en <= want_to_read && (!almost_empty || (!empty && !rd_en));
フィルカウンタ
アプリケーションロジックは、データをチャンク単位で処理することがよくあります。たとえば、FIFO から一定長のデータパケットを読み出し、そのパケットを何らかの物理メディア経由で送信するロジックなどです。データは FIFO に蓄えられているため、アプリケーションロジックは、読み出しを開始する前にパケットを満たすのに十分なデータがあるかどうかを知る必要があります。
同様に、アプリケーションロジックが FIFO に格納する一定量のデータを生成することもよくあります。たとえば、外部メモリからのバーストデータの読み出しなどです。バーストを完了させるのに十分な空きが FIFO にない限り、その操作を開始すべきではありません。
これらの目的のために、FIFO は通常、フィルカウンタ(fill counters)、プログラマブル empty ポート、プログラマブル full ポートをサポートしています。フィルカウンタ(データカウンタと呼ばれることもあります)は、FPGA ベンダーによって形や種類がさまざまで、FIFO のドキュメントを注意深く読む必要があります。注意すべき主な点は 3 つあります。
- カウンタはどのクロックに同期しているか?言うまでもありませんが、カウンタを使うロジックと同じクロックでなければなりません。
- カウンタは何を教えてくれるのか?「読み出しカウンタ」は通常、FIFO 内に格納されているワード数を示します。では「書き込みカウンタ」はどうでしょうか?格納されているワード数なのか、それとも FIFO が full(満杯)になるまでに書き込めるワード数なのか?
- カウンタは何を保証するのか?フィルカウンタは、通常、その本来の目的に関して悲観的です。たとえば、「読み出しカウンタ」は一時的に FIFO 内の実際のワード数より少ない値を出力することが許されているのが一般的です。これは、ワードが FIFO に書き込まれているときに発生します。これらのカウンタは書き込み操作に対しては遅れて値を増やし、読み出し操作に対しては早めに値を減らすためです。これは @rd_en を制御するロジックでの使用には理にかなっています。しかし、この種のカウンタを @wr_en を制御するロジックが使うと、オーバーフロー(overflow)を引き起こす可能性があります。その目的のためには「書き込みカウンタ」があります。それでも、ドキュメントの細部まで読むようにしてください。
さらに、プログラマブル empty とプログラマブル full もあります。これらは @almost_empty と @almost_full の拡張版です。フィルカウンタの使い方がほぼ確実に次のようになるのであれば、
assign dont_start_reading = (rd_data_count < 64);
その信号を直接用意して prog_empty と呼べばよいではないか、という考え方です。繰り返しになりますが、FIFO のドキュメントを注意深く読んでください。
繰り返しになりますが、FIFO 内の最後のワードを読み出すことが重要な場合は、自分のロジックが本当にそうするかどうかを必ず自問してください。これは、前述の @almost_empty に関する議論と似た問いです。
言うまでもありませんが、これらの追加ポートが必要な場合は、FIFO を構成するときに要求しなければなりません。
AXI インターフェース
この話題は直接関係ありませんが、FIFO の文脈ではこの用語がよく出てくるため、混乱を避けるために言及しておく価値があります。
AXI は、ARM が導入した AMBA 標準で定義されているインターフェースのセットです。予想どおり、AXI インターフェースを備えた FIFO は、通常 CPU のペリフェラルとして動作することを意図しています。
「ベースライン FIFO」のインターフェースは、AXI インターフェースに対して「ネイティブ」インターフェースと呼ばれることがよくあります。
AXI インターフェースには主に 2 つのタイプがあります。「通常の」AXI(通常は AXI3、AXI4、または AXI Lite)は、アドレスとデータを持つバスです。もうひとつのタイプは AXI-S(ストリーム AXI)で、データのストリーム(パケットに分割されることもあります)を対象としています。
FIFO が AXI3 / AXI4 または AXI Lite として構成される場合、このインターフェースを介して CPU のアドレス付きペリフェラルとして接続できるように、追加のロジックが FIFO に組み込まれます。これについては、まったく別の話題になるため、これ以上詳しく説明しません。
しかし、ストリーミングインターフェースは FIFO の動作とやや似ているため、AXI-S インターフェースのハンドシェイク信号を「ネイティブ」なものに変換することが可能です。AXI-S には、他にも処理が必要な信号が含まれることが多いことに注意してください。
FIFO への書き込み用の AXI-S 信号として @axi_w_valid、@axi_w_ready、@axi_w_data がある場合、それらを「スタンダード」FIFO のポートに次のように接続できます。
assign axi_w_ready = !full;
assign wr_en = axi_w_valid && axi_w_ready;
assign din = axi_w_data;
同様に、FIFO からの読み出し用の AXI-S 信号 @axi_r_valid、@axi_r_ready、@axi_r_data は、FWFT FIFO のポートに次のように接続できます。
assign axi_r_valid = !empty; // Non-empty means valid with FWFT FIFOs
assign rd_en = axi_r_valid && axi_r_ready;
assign axi_r_data = dout;
繰り返しになりますが、これを機能させるには FIFO が FWFT タイプでなければならないことに注意してください。
これで、FIFO に関するこのシリーズの 2 ページ目は終わりです。次のページでは、シングルクロック FIFO を Verilog で実装する方法を示します。
