Altifigence Academy

57 / 63 · 概念

単一クロック FIFO のポインタと境界条件の方針

深さ 4 の FIFO のポインタと occupancy を実装し、empty と full で同時 push/pop を受け付ける条件を明確にする。

データを移動せずに順序を守る

FIFOFIFO 先に入れたデータを先に取り出すキューです。満杯・空・同時読出しと書込みの許可規則も動作の取り決めに含まれます。 は先に入れたデータを先に取り出す。単一クロックでは、読み出しと書き込みを同じ立ち上がりエッジで決定する。この講義では DEPTH=4、データ幅 8 ビットに固定する。rptr は次に取り出す位置、wptr は次に入れる位置を指す。それぞれ 2 ビットなので、3 の次は自然に 0 へ戻る。occupancy は格納数 0~4 を表す必要があるため、3 ビットを要する。

rptr=wptr は empty でも full でも起こり得る。そのため、アドレスの一致だけで empty を決めると、満杯のデータを失ってしまう。ここでは個数 CC を別に管理し、empty=(C=0)\mathrm{empty}=(C=0)、full=(C=4)\mathrm{full}=(C=4) で区別する。深さ D=4D=4 のとき、アドレス幅 WaddrW_{\mathrm{addr}} と個数幅 WcountW_{\mathrm{count}} は次のとおりである。

Waddr=⌈log⁡2D⌉=2Wcount=⌈log⁡2(D+1)⌉=3\begin{aligned} W_{\mathrm{addr}}&=\lceil\log_2 D\rceil=2\\ W_{\mathrm{count}}&=\lceil\log_2(D+1)\rceil=3 \end{aligned}

要求と受け付けを区別する境界条件

push と pop は要求である。p と q をそれぞれ実際の push・pop の受け付けとすると、reset でないエッジの直前状態から次のように計算する。

q=pop∧(C>0)p=push∧((C<4)∨q)C+=C+p−q\begin{aligned} q&=\mathrm{pop}\land(C>0)\\ p&=\mathrm{push}\land((C<4)\lor q)\\ C^{+}&=C+p-q \end{aligned}

empty で両方の要求が来ても、pop できる既存データがないため push だけを受け付ける。新しいデータがそのエッジで直ちに出力されるわけではない。full で両方の要求が来ると、以前の先頭を取り出して場所を空けられるため両方を受け付ける。full は「現在 4 個格納されている」という状態であり、この方針では無条件の push 禁止信号ではない。

エッジ直前の状態push のみpop のみpush と pop
C=01 個格納拒否push のみ受け付け、C=1
0<C<4C+1C−1両方を受け付け、C を保持
C=4拒否C=3両方を受け付け、C=4

クロックを進めて境界条件を確認する

最初は push と pop を同時に要求してみよう。空なので push だけが受け付けられるはずである。次は pop を無効にして 4 スロットを満たし、新しい入力値とともに両要求を再び有効にする。占有数は 4 のまま、出力には以前の先頭、記憶領域には新しい末尾が残るはずである。

クロックを進める FIFO

4 要素の FIFO です。初期状態は空で、読み書きポインタは 0 です。空でなければ pop を受け付けます。空きがあるか、同じエッジで pop を受け付ける場合に push を受け付けます。

図は各立ち上がりエッジ立上りエッジ クロックが 0 から 1 に変わる瞬間です。CLK=1 の区間全体を指すレベルとは区別します。 詳しく見る直後の状態を示す教育用モデルである。pop が受け付けられると output valid が 1 になる。次のエッジで pop が受け付けられなければ、出力レジスタレジスタ 複数ビットの状態を記憶する回路です。このコースの同期式レジスタは、クロックエッジで定められた入力を記憶します。 詳しく見るの値は保持されるが valid は 0 になる。空のスロットに残る古いビットは表示しない。「最初から」は実習全体を初期化する機能であり、RTL のメモリリセットリセット 状態を仕様で定めた初期値に戻す制御です。同期式か非同期式か、他の制御より優先されるかを合わせて定めます。 詳しく見る動作を表してはいない。

レジスタ付き出力まで含めた RTL

SystemVerilog
module fifo4 (
  input  logic       clk, rst, push, pop,
  input  logic [7:0] din,
  output logic [7:0] dout,
  output logic       dout_valid, full, empty,
  output logic [2:0] occupancy
);
  timeunit 1ns;
  timeprecision 1ps;
  logic [7:0] mem [0:3];
  logic [1:0] rptr, wptr;
  logic take_push, take_pop;

  assign empty = (occupancy == 3'd0);
  assign full  = (occupancy == 3'd4);
  assign take_pop  = !rst && pop && !empty;
  assign take_push = !rst && push && (!full || take_pop);

  always_ff @(posedge clk) begin
    if (rst) begin
      rptr <= 2'd0;
      wptr <= 2'd0;
      occupancy <= 3'd0;
      dout_valid <= 1'b0;
    end else begin
      dout_valid <= take_pop;
      if (take_push) begin
        mem[wptr] <= din;
        wptr <= wptr + 2'd1;
      end
      if (take_pop) begin
        dout <= mem[rptr];
        rptr <= rptr + 2'd1;
      end
      case ({take_push, take_pop})
        2'b10: occupancy <= occupancy + 3'd1;
        2'b01: occupancy <= occupancy - 3'd1;
        default: occupancy <= occupancy;
      endcase
    end
  end
endmodule

occupancy の増加と減少を、独立した二つの if で代入していない。同時受け付けのとき、同じレジスタへノンブロッキングノンブロッキング代入 順序 RTL で <= と書く代入です。右辺を評価して更新を予約するので、同じエッジのレジスタが更新前の状態に基づいて計算できます。 詳しく見る代入を二度行うと、最後の代入だけが残るためである。二つのポインタはそれぞれの受け付けで進むが、個数は四つの組み合わせをまとめて一度だけ決める。受け付け条件によって、0 からの減少と 4 からの増加も防ぐ。

full で同じアドレスを読み書きする場合

A、B、C、D を順に入れると、rptr=wptr=0、occupancy=4 になる。ここで pop=push=1、din=E とすると、エッジ後は dout=A、dout_valid=1、論理的な内容は [B,C,D,E] になる。両ポインタは 1 となり、個数は 4 を保つ。mem[0] は E に変わるが、dout へのノンブロッキング代入の右辺は以前の A を読む。この read-first の競合動作は、full 時の入れ替え方針に不可欠である。

その次に push=0、pop=1 とすると B が出て、個数は 3 になる。一方、empty で din=Z と両要求を与えると、個数だけが 1 になり、dout_valid=0 のままである。Z を出力するには次のエッジで pop を要求する必要がある。empty 時の即時転送と full 時の入れ替えは、別々の設計上の選択である。

rst はアクティブ High の同期 reset であり、ポインタ、個数、dout_valid を初期化する。メモリと dout の値は消さない。reset 後に古いビットが残っていても、empty が pop を阻止し、新しい push が書いた位置だけを後で読むため、正常動作に配列全体の reset は不要である。dout_valid=0 の値は消費しない。valid=1 の出力は受け付けた pop の応答であり、出力側に別の ready はないので、消費側はその応答を受け取る必要がある。

DEPTH が 2 の累乗でない場合、単純な 2 ビットのオーバーフローは再利用できない。例えば深さ 3 では、アドレス 2 の次に明示的に 0 へ戻す。この例の合成結果が RAM を使うなら、同一アドレスで以前のデータを返す動作に対応するかも確認する。このコードは単一クロック用であり、異なるクロックを使う FIFO のポインタ同期へそのまま適用するものではない。

自分で考えてみましょう

reset 直後から、各エッジで次の要求を与える。(1) push A と pop を同時、(2) push B のみ、(3) push C のみ、(4) push D のみ、(5) push E と pop を同時、(6) pop のみ。各段階後の queue の内容、occupancy、dout_valid、有効な出力値を記せ。5 番目の段階で read-first が必要な理由も説明せよ。

解説を見る

(1) [A]、1、valid=0。(2) [A,B]、2、valid=0。(3) [A,B,C]、3、valid=0。(4) [A,B,C,D]、4、valid=0。(5) [B,C,D,E]、4、valid=1、dout=A。(6) [C,D,E]、3、valid=1、dout=B。段階 1 には既存の項目がないので pop を拒否する。段階 5 では rptr=wptr=0 で同じスロットに読み書きが重なるため、以前の先頭 A を読む必要がある。write-first で E を返すと FIFO の順序と内容が壊れる。valid=0 の出力値は採点対象ではない。

選択はこのブラウザに適用されます。フッターからいつでも変更できます。