01signal.com

registersの追加による FIFOs 上の timing の改善

概要

FIFOsに関するシリーズの最後のページであるこのページでは、既存の FIFO を別の FIFO に変更する方法を示します。まず、「standard FIFO」を「 FWFT FIFO」に変える方法と、その逆の方法です。これに続いて、 FIFOの timingを改善するためのより高度な方法がいくつかあります。つまり、より高い周波数で動作させることです。

実用的な観点からは、 timing constraintsを達成する際に FIFO に関連する問題がない限り、このページを読み通すことはまったく無意味です。このページの内容は難しく、FIFO の一般的な使用には必要ありません。それでも、 logic、特にデータを処理する logic を設計する際に使用する筋肉をトレーニングする目的で、練習として努力する価値があるかもしれません。

直接関係はありませんが、外部メモリを使用して非常に深い FIFO を作成する方法を示す別のページがあります (通常は DDR memoryですが、 AXI インターフェイスでラップされたものであれば何でも構いません)。このトリックの良い点は、この巨大な FIFO が使用する外部メモリと同じくらい深い場合でも、これらすべてが application logicに対して透過的であることです。 baseline FIFOと同じインターフェースです。

このページの Verilog コードは何年も前に書いたので、コーディング スタイルは現在とは少し異なります。

Standard FIFO ~ FWFT FIFO

前のページから FWFT FIFOs を簡単に要約すると、次のようになります。 「standard FIFO」では、低い @empty port は、 @rd_en が rising clock edgeで高い後、有効なデータが FIFOの output に表示されることを意味します。 FWFT FIFO は、 output のデータが利用可能になるとすぐに提示するため、 @empty 信号が低いということは、 output のデータが有効であることを意味します。

@rd_en の意味も異なります。 「standard FIFO」の場合、「データを持ってくる」という意味です。 FWFT FIFO では、「データを使用したところです。次のデータがある場合は、次のデータを持ってきてください」のようなものです。

これが「standard FIFO」を FWFT FIFOに変えた module です。当然のことながら、 @rd_en と @emptyを操作するだけです。残りの信号はそのまま通過します。

module basic_fwft_fifo(rst,
                       rd_clk, rd_en, dout, empty,
                       wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg                   dout_valid;
   wire                  fifo_rd_en, fifo_empty;

   // orig_fifo is just a normal (non-FWFT) synchronous or asynchronous FIFO
   fifo orig_fifo
      (
       .rst(rst),
       .rd_clk(rd_clk),
       .rd_en(fifo_rd_en),
       .dout(dout),
       .empty(fifo_empty),
       .wr_clk(wr_clk),
       .wr_en(wr_en),
       .din(din),
       .full(full)
       );

   assign fifo_rd_en = !fifo_empty && (!dout_valid || rd_en);
   assign empty = !dout_valid;

   always @(posedge rd_clk or posedge rst)
      if (rst)
         dout_valid <= 0;
      else
         begin
            if (fifo_rd_en)
               dout_valid <= 1;
            else if (rd_en)
               dout_valid <= 0;
         end
endmodule

通常はここでコードを説明しますが、前のページの FWFT FIFO の説明を繰り返すだけです。

FWFT FIFO ~ standard FIFO

これは本当に簡単です。 FWFT FIFO からの低い @empty はデータが output portに存在することを意味するため、 @rd_en が高いときにこのデータをサンプリングする register を作成します。

だからそれはちょうどこれです:

module standard_fifo(rst,
                     rd_clk, rd_en, dout, empty,
                     wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg [(width-1):0]     dout;
   wire [(width-1):0]    dout_w;

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

   fwft_fifo wrapper
     (
      .wr_clk(wr_clk),
      .rd_clk(rd_clk),
      .rst(rst),
      .din(din),
      .wr_en(wr_en),
      .rd_en(rd_en && !empty),
      .dout(dout_w),
      .full(full),
      .empty(empty)
      );
endmodule

@dout のみが操作されることに注意してください。 @empty はそのまま通過します。 高い場合、 @dout_w は無効であるため、 @dout はそこから値をサンプリングできません。

timingを上達させるコツ

FIFOsのこれら 4 ページのグランド フィナーレへようこそ。これは間違いなく最も読みにくい部分です。

そのため、 FPGA design が timing constraints を達成しない (つまり、目的の clock frequencyに到達しない) 理由を理解しようとすると、 critical path が FIFOで開始および/または終了することがわかります。最初に解決しやすいケースを見て、難しい問題で終わりましょう。

@empty および/または @full が critical pathにある場合

@empty 信号と @full 信号は、特に @wr_en と @rd_en がこれらの combinatorial functions である場合、 critical pathに表示されることがあります。これは主に、これらの信号が FIFO からの書き込み操作または読み取り操作を要求するためだけでなく、データを消費または生成する application logic の enable signals としても機能するためです。 データが流れなかった場合、 logic もフリーズします。

そのため、 @wr_en や @rd_enに依存する logic equations が多く、 logic functions はかなり複雑なことがよくあります。その結果、高い fanoutが得られました。これはすべて、問題のある propagation delayに要約されます。

@empty と @full は、適切に作成された FIFOの flip-flops の outputs であるため、それで改善することはあまりありません。しかし、 FPGAのソフトウェアは FIFO を synthesized netlistとして配布することが多いため、 fanoutを削減するためにこれらの registers を複製することは不可能 (または少なくとも困難) です。また、 FPGAの logic fabric では、これらの registers とそれらの出力値を使用する application logic の間に大きな物理的距離が存在する可能性があります。大規模な FPGAsでは、これが pathsの delayに決定的な貢献をする可能性があります。

この問題の修正は、 @almost_empty および @almost_fullについて説明する際に、このページで既に提供されています。これらのポートを使用することで、 @wr_en と @rd_en の outputs を registersにすることができます。これにより、 combinatorial functionの問題が解決され、これらの信号の fanout を制御することもできます。その上、これはツールがこれらの registers をそれらの値を消費する logic の近くに配置するのにも役立つため、 propagation delayを減らすのに役立ちます。

@wr_en および/または @din が critical pathにある場合

この状況は間違いなく最も簡単に解決できます。 registersのレイヤーを追加するだけです。何かのようなもの

always @(posedge wr_clk)
  begin
    wr_en_reg <= wr_en;
    din_reg <= din_reg;
  end

@wr_en_reg と @din_reg を FIFO に接続します。 FIFO を overflowから回避するには、 @fullの代わりに @almost_full を使用する必要があります。または、より一般的に言えば、 FIFO を満たすためのしきい値を 1 減算する必要があります。

@rd_en および/または @dout が critical pathにある場合

今、私たちは真剣になっています。これは解決が比較的難しい問題であるだけでなく、最も発生する可能性が高い問題でもあります。これにはいくつかの理由があります。

@doutに関しては:

したがって、目標は、 @rd_en と FIFOの logicの間で combinatorial path を排除し、 @doutでも同じことを行うことです。

@doutの combinatorial pathのみ取り外し

これを解決策として提示するつもりはまったくありませんが、次のステップを把握するための準備としてこれを理解するのに役立つかもしれません.これだけで混乱する場合は、このセクションを飛ばしてください。

@doutの combinatorial pathだけを切り離したいとします。 FWFT FIFO を "standard FIFO" に変換する wrapper module (上記のように) は、まさにそれを行うことに注意してください。 registerを追加し、 @doutの combinatorial pathを終了します。しかし、それには出発点として FWFT FIFO が必要です。

しかし、「standard FIFO」を FWFT FIFOに変換する wrapper module がありました。では、 FIFO を前後に変換することはできますか?または、同等の機能を持つ単一の module を作成しますか?いずれにせよ、この形式の解決策は @rd_enの状況を悪化させます。

それでも、このソリューションは詳しく調べる価値があります。 FWFT FIFO への変換は、 wrapped FIFOの @dout が有効なときに追跡し、 @dout が無効なとき (および/または外部 @rd_en が高いとき) に @fifo_rd_en を高く保持することだけで構成されていました。

「standard FIFO」への変換は、 @rd_en が高いときに wrapped FIFOの @dout の値を register にコピーすることによって行われました。

全体として、最初のメカニズムは可能な限り wrapped FIFOの @dout を有効に保ち、2 番目のメカニズムは外部 @rd_en が要求したときに @dout を別の register にコピーしました。

しかし、これは @rd_enの combinatorial pathの問題を解決しません: 連続読み取りを可能にするために、外部 @rd_en が High である各 clock で元の FIFO からワードを読み取る必要があります。そうしないと、 FWFTの @dout は消費されたが更新されていないため、無効になります。したがって、この内部 FIFOの @rd_en は、外部 @rd_enの combinatorial function でなければなりません。これを変更したい場合は、次に示すように、別の register を @doutの pathに追加する必要があります。

combinatorial paths と reg_fifoの両方を取り外す

これ以上苦労することなく、これは @rd_en と @doutの combinatorial paths をデタッチする reg_fifo moduleです。

module reg_fifo(rst,
                rd_clk, rd_en, dout, empty,
                wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg                   fifo_valid, middle_valid;
   reg [(width-1):0]     dout, middle_dout;

   wire [(width-1):0]    fifo_dout;
   wire                  fifo_empty, fifo_rd_en;
   wire                  will_update_middle, will_update_dout;

   // orig_fifo is "standard" (non-FWFT) FIFO
   fifo orig_fifo
      (
       .rst(rst),
       .rd_clk(rd_clk),
       .rd_en(fifo_rd_en),
       .dout(fifo_dout),
       .empty(fifo_empty),
       .wr_clk(wr_clk),
       .wr_en(wr_en),
       .din(din),
       .full(full)
       );

   assign will_update_middle = fifo_valid && (middle_valid == will_update_dout);
   assign will_update_dout = rd_en && !empty;
   assign fifo_rd_en = !fifo_empty && !(middle_valid && fifo_valid);
   assign empty = !(fifo_valid || middle_valid);

   always @(posedge rd_clk)
      if (rst)
         begin
            fifo_valid <= 0;
            middle_valid <= 0;
            dout <= 0;
            middle_dout <= 0;
         end
      else
         begin
            if (will_update_middle)
               middle_dout <= fifo_dout;

            if (will_update_dout)
               dout <= middle_valid ? middle_dout : fifo_dout;

            if (fifo_rd_en)
               fifo_valid <= 1;
            else if (will_update_middle || will_update_dout)
               fifo_valid <= 0;

            if (will_update_middle)
               middle_valid <= 1;
            else if (will_update_dout)
               middle_valid <= 0;
         end
endmodule

最初に注意すべきことは、 @dout はこの moduleで定義された register であり、 @rd_en はこの registerの更新を引き起こすということです。これら 2 つを、 @fifo_dout および @fifo_rd_enである内部 FIFOに接続されている同様の信号と混同しないようにすることが重要です。

この module がどのように機能するかについて説明します。

pipelineを理解する

FWFT FIFOへのコンバーターと同じように、通常の FIFO、 orig_fifoの instantiation があります。 @fifo_dout に有効な値が含まれていない場合、 reg_fifo module 内の logic は、 orig_fifo からワードを読み取ることによって、 @fifo_doutの値を有効に維持しようとします。しかし、それに加えて、 @middle_doutと呼ばれる 2 番目の registerがあります。 logic は、可能であれば @fifo_doutの値を取得することにより、この register も有効に保とうとします。

したがって、 @fifo_dout、 @middle_dout 、および @dout を、 orig_fifo のデータを前方に移動する pipeline と見なすことができます。

これらの pipeline stages がいつ有効になるかを追跡する 2 つの registers があります。 @fifo_dout が有効な場合、 @fifo_valid はハイになり、 @middle_dout が有効な場合、 @middle_valid はハイになります。

この pipeline の目的は、中間段階をバイパスする機能です。 @rd_en が高い ( @empty が低い) 場合、 @dout は @middle_dout または @fifo_doutから新しい値を取得しますが、常に @middle_doutを優先します。つまり、 @middle_dout が有効な場合、 @dout は @middle_doutを使用し、そうでない場合は代わりに @fifo_dout を使用します。これがどのように @rd_enの combinatorial path をデタッチする鍵となるかについては、後で説明します。

それでは、まず、実装の詳細を見てみましょう。 FIFOの data output から fifo_regの output registerへの経路は 2 つあります。これらは、この図では左右に別々に示されています。

Data flow with extra registers

2 つの pipeline stages (@fifo_dout および @middle_dout) のいずれも有効でない場合、 @empty はハイになり、データを取得する場所がないことを示します。

assign empty = !(fifo_valid || middle_valid);

これらの pipeline stages を有効に保つ試みは、

assign fifo_rd_en = !fifo_empty && !(middle_valid && fifo_valid);

これは、2 つの pipeline stages のいずれかが無効な場合、可能であれば orig_fifoから読み取ることを示しています。 @fifo_dout がすでに有効な場合、その値は @fifo_dout が更新されると同時に @middle_dout にコピーされます (これについては以下で詳しく説明します)。

@will_update_* ペアの定義を見てみましょう。

assign will_update_middle = fifo_valid && (middle_valid == will_update_dout);
assign will_update_dout = rd_en && !empty;

最初に、 @will_update_dout が @rd_en に等しいことに注意してください。さらに、 @empty が High のときに reg_fifo から読み取ることに対する安全ガードが追加されています。

次に、 @middle_outの更新を制御する @will_update_middleがあります。

always @(posedge rd_clk)
  if (will_update_middle)
     middle_dout <= fifo_dout;

上記の @will_update_middleの定義を見ると、 @middle_doutを更新するには 2 つの条件があります。 1 つは、 @fifo_dout の値が有効であることです。これは明らかです。次に、 (middle_valid == will_update_dout)という式があります。機械全体がどのように機能するかを説明するため、この式を 4 つの可能なオプションに分解してみましょう。これらはすべて、 @fifo_dout が有効な場合にのみ役割を果たすことに注意してください。

@middle_valid と @fifo_valid が同時に High の場合、 @fifo_rd_en は Low になることに注意してください。その結果、最後の 2 つのケースのシナリオが発生した場合、 orig_fifo からデータはフェッチされません。

特に、2 つの pipeline stages が両方とも有効で、 @rd_en が高い場合、 @fifo_doutの値が @middle_doutにコピーされます。そして、 @fifo_rd_en が Low であるため、次の clock cycleで @fifo_valid が Low に変化します。 @middle_valid は High のままで、必要に応じて次の clock cycle でデータを提供できるため、これで問題ありません。その後の clock cycle では、 @fifo_valid が再び高くなります ( orig_fifoにデータがある場合)。

では、この特定の状況で @fifo_dout を有効に保つように @fifo_rd_en が定義されていないのはなぜでしょうか?その場合、 @fifo_rd_en は @rd_enの combinatorial function でなければならず、これはまさにこの dual-stage pipeline が回避するように設計されているためです。

これが手元にあるので、 @dout がどのように定義されているかを見てみましょう。 resetを除いて、定義は次のとおりです。

always @(posedge rd_clk)
  if (will_update_dout)
    dout <= middle_valid ? middle_dout : fifo_dout;

@will_update_dout をその定義に置き換えると、次のようになります。

always @(posedge rd_clk)
  if (rd_en && !empty)
    dout <= middle_valid ? middle_dout : fifo_dout;

これは、 FWFT FIFO から「standard FIFO」への変換に似ていますが、選択できるソースは 2 つだけです。 @middle_dout に有効な値が含まれている場合は、それが取得されます。そうでなければ、 @fifo_dout。どちらも有効でない場合、 @empty は高いので、とにかく何も起こりません。

なぜこれが役立つのですか? output timingに関して言えば、 @dout は明らかに registerです。 @rd_enに関しては、 @fifo_rd_en は registersである @middle_valid と @fifo_validのみに依存することに注意してください。さらに、 orig_fifo 自体の output である @fifo_empty(この combinatorial path は必然)。したがって、 @fifo_rd_en は外部 @rd_enに依存しないため、 @rd_en から orig_fifoまでの combinatorial path はありません。

pipeline stagesの registersの有効性の追跡

写真を完成させるために: 2 つの *_valid フラグは、関連する register に有効なデータが含まれているかどうかを示します。 @fifo_validについて:

if (fifo_rd_en)
   fifo_valid <= 1;
 else if (will_update_middle || will_update_dout)
   fifo_valid <= 0;

これは、上記の「standard FIFO」から FWFT FIFO への変換における @dout_valid の定義に似ています。 rising edgeで @fifo_rd_en がハイになると、その結果として @fifo_valid がハイになります。 orig_fifoからデータを読み取った場合、この FIFOの output は結果として有効と見なされるため、これは理にかなっています。しかし、 @fifo_rd_en が低く、データが @middle_dout または @doutのいずれかにコピーされた場合、 @fifo_dout はもはや有効であるとは見なされません。 FIFOの output は使用したばかりで、 FIFO は新しいデータに置き換えていません。

@middle_valid も同じロジックに従います。

if (will_update_middle)
   middle_valid <= 1;
 else if (will_update_dout)
   middle_valid <= 0;

@will_update_middle が High の場合、データは @middle_doutにコピーされるため、 @middle_valid も High になります。それ以外の場合、および @middle_dout のデータが @doutにコピーされる場合、 @middle_valid は Low に変化します。 @will_update_dout は、このための十分な条件です。なぜなら、 @dout は可能であれば @middle_doutからのコピーを好むことを上記で思い出してください。

それはまったく機能しますか?

この module は非常に複雑であるため、動作することをほぼ正式に証明する必要があります。したがって、この質問に答える 1 つの方法は、2 つの pipelines stages、 @fifo_dout 、および @middle_doutのうち、いくつが有効かを尋ねることです。この値は reg_fifo moduleでは定義されていませんが、次のように定義できた可能性があります。

wire [1:0] valid_count;
assign valid_count = fifo_valid + middle_valid;

この架空の @valid_count は、明らかに 0、 1 、または 2の値を取ることができます。次のようにカウントアップまたはカウントダウンします。

logic equationsを見て、これら 3 つのステートメントが正しいことを確信してください。

それでは、 orig_fifo にデータがあり、 application logic が連続して読み取りたい場合に何が起こるか見てみましょう。

reg_fifoの logic は、 orig_fifoから読み取ることにより、 @valid_count を 2 に向かって押し上げようとします。一方、 @valid_count がゼロでない場合、 @empty はローであるため、 @valid_count が 1 になるとすぐに @rd_en がハイになることが許可されます。したがって、 @valid_count が 1 の場合、 @valid_count が 2 ではないため、 @fifo_rd_en はハイになります。

しかし、 @valid_count は 2 の値に到達しません。これは、 @rd_en が高いままになることでそれを妨げているためです。したがって、データは @fifo_rd_en と @rd_en の両方が High に保持され、 @valid_count は 1 のままで流れます。最初を除いて、データは @fifo_dout から @doutにコピーされます。

この損益分岐点は、 orig_fifo が空になると壊れます。 この場合、 @fifo_rd_en を高くすることができないため、 @valid_count はゼロになります。もう 1 つのタイ ブレーカーは、 FIFO が空ではなく、 application logic がそれ以上読み取りたくないために @rd_en が低くなった場合です。 この場合、 @valid_count は 2 に上昇し、そこにとどまります。

しかし後で、 @rd_en が再びハイになると、 @valid_count は 1 に下がり、その時点でのみ @fifo_rd_en がハイに変わります ( orig_fifo が空でない場合)。

繰り返しますが、 @valid_count は単なる理論上の信号であり、 moduleには実装されていません。この説明は、2 つの余分な pipeline stages がデータの継続的なフローを保証する理由を理解するのに役立つことを願っています。

使用上の注意

この module は、それがラップする「standard FIFO」のドロップイン代替品として使用できます。機能的な観点からは、何も変わりません。ただし、 orig_fifo では、 @rd_en、 @dout 、および @emptyの動作にわずかな変化が見られますが、 orig_fifo が FIFO として正しく動作する限り問題ではありません (これは安全な仮定です)。データの書き込みに関係する ports はそのままパススルーされるため、これらに変更はありません。

module はいくつかの pipelines stagesを追加するため、 orig_fifoの fill counters は、 reg_fifo に保存されている単語の総数 (つまり、 orig_fifo と pipeline stages に保存されている単語の数を合わせて数えたもの) よりも低い値を示す場合があります。したがって、 @almost_empty または同様の ports が orig_fifoで有効になっている場合、悲観的な状況が発生する可能性があります。

reg_fifo のわずかな欠点は、その @empty output が registerではなく、2 つの registersの combinatorial function であることです。これは timingには最適ではありませんが、ほとんどのユース ケースで最小限の影響しかありません。これは、このページに示されているように、 @next_words_in_ramと同じ精神で @next_fifo_valid および @next_middle_valid のように combinatorial registers を定義することによって修正できます。主に reg_fifo はそのままでは十分に複雑であるため、ここでは実装されていません。

timingを改良した FWFT FIFO

このトピックを終了するには、以下に reg_fifoと同じことを行う module を示しますが、代わりに application logic に FWFT FIFO を提供します。 FWFT FIFOではなく、 standard FIFOに基づいていることに注意してください。だから、これと混同しないでください...

reg_fifo からこの module への移行は、 FWFT FIFOsに関して既に説明したこととほぼ同じです。

module fwft_reg_fifo(rst,
                     rd_clk, rd_en, dout, empty,
                     wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg                   middle_valid, dout_valid;
   reg [(width-1):0]     dout, middle_dout;

   wire [(width-1):0]    fifo_dout;
   wire                  fifo_empty, fifo_rd_en;
   wire                  will_update_middle, will_update_dout;

   // orig_fifo is "standard" (non-FWFT) FIFO
   fifo orig_fifo
      (
       .rst(rst),
       .rd_clk(rd_clk),
       .rd_en(fifo_rd_en),
       .dout(fifo_dout),
       .empty(fifo_empty),
       .wr_clk(wr_clk),
       .wr_en(wr_en),
       .din(din),
       .full(full)
       );

   assign will_update_middle = !fifo_empty && (middle_valid == will_update_dout);
   assign will_update_dout = (middle_valid || !fifo_empty) && (rd_en || !dout_valid);
   assign fifo_rd_en = !fifo_empty && !(middle_valid && dout_valid);
   assign empty = !dout_valid;

   always @(posedge rd_clk)
      if (rst)
         begin
            middle_valid <= 0;
            dout_valid <= 0;
            dout <= 0;
            middle_dout <= 0;
         end
      else
         begin
            if (will_update_middle)
               middle_dout <= fifo_dout;

            if (will_update_dout)
               dout <= middle_valid ? middle_dout : fifo_dout;

            if (will_update_middle)
               middle_valid <= 1;
            else if (will_update_dout)
               middle_valid <= 0;

            if (will_update_dout)
               dout_valid <= 1;
            else if (rd_en)
               dout_valid <= 0;
         end
endmodule

以上で FIFOsの連載は終了です。

このページは英語から自動翻訳されています。 不明な点は元のページを参照してください。
Copyright © 2021-2024. All rights reserved. (6f913017)