対象範囲
このページは、FIFO に関する全 5 ページのシリーズの 3 ページ目です。このページでは、ベースラインのシングルクロック FIFO を Verilog で実装する方法を示します。これは移植性のあるコードを書くのに役立ちますが、このページの主な目的は FIFO の動作をもう一度確認することです。そこで、「スタンダード FIFO」と FWFT FIFO の両方の実装を示します。ただし、その前に、両方の FIFO が使うデュアルポート RAM が必要です。
デュアルポート RAM
この Verilog モジュールは、推論(inference)によって RAM を実装します。どのシンセサイザ(synthesizer)でも正しく処理してくれる可能性は高いですが、意図しない種類の RAM(ブロック RAM か分散 RAM か)が生成される可能性もあります。そのため、シンセサイザのディレクティブを追加する必要があるかもしれません。あるいは、FPGA ベンダーが供給するデュアルポート RAM 用の IP コア(IP core)を使うのが最適な場合もあるでしょう。
というわけで、モジュールは次のとおりです。
module dualport_ram #(parameter depth = 64,
log2_depth = 6,
width = 8
)
(
input clk,
input [(log2_depth-1):0] wr_addr,
input [(log2_depth-1):0] rd_addr,
output reg [(width-1):0] rd_data,
input [(width-1):0] wr_data,
input rd_en,
input wr_en
);
reg [(width-1):0] inferred_ram[0:(depth-1)];
always @(posedge clk)
begin
if (wr_en)
inferred_ram[wr_addr] <= wr_data;
if (rd_en)
rd_data <= inferred_ram[rd_addr];
end
endmodule
「スタンダード FIFO」
それでは、「スタンダード FIFO」(つまり FWFT FIFO ではない FIFO)を実装するモジュールを見てみましょう。
module fifo
#(parameter depth = 64, // Must equal 2^log2_depth exactly
log2_depth = 6,
width = 8
)
(
input clk,
input rst,
input wr_en,
input [(width-1):0] din,
input rd_en,
output [(width-1):0] dout,
output reg full,
output reg empty
);
reg [log2_depth:0] next_words_in_ram; // Combinatorial
reg [log2_depth:0] words_in_ram;
reg [(log2_depth-1):0] rd_addr;
reg [(log2_depth-1):0] wr_addr;
wire fetch_data, commit_data;
assign fetch_data = rd_en && !empty;
assign commit_data = wr_en && !full;
always @(*)
if (commit_data && !fetch_data)
next_words_in_ram <= words_in_ram + 1;
else if (!commit_data && fetch_data)
next_words_in_ram <= words_in_ram - 1;
else
next_words_in_ram <= words_in_ram;
always @(posedge clk)
begin
words_in_ram <= next_words_in_ram;
full <= (next_words_in_ram == depth);
empty <= (next_words_in_ram == 0);
if (fetch_data)
rd_addr <= rd_addr + 1;
if (commit_data)
wr_addr <= wr_addr + 1;
if (rst)
begin
empty <= 1;
full <= 1;
words_in_ram <= 0;
rd_addr <= 0;
wr_addr <= 0;
end
end
dualport_ram
#(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
(.clk(clk), .wr_addr(wr_addr),
.rd_addr(rd_addr),
.wr_en(commit_data),
.rd_en(fetch_data),
.wr_data(din),
.rd_data(dout)
);
endmodule
唯一重要な使用上の注意は、先頭のコメントにある depth と log2_depth のパラメータに関するものです。depth は 2log2_depth と正確に一致しなければなりません。
このモジュールの動作は非常に単純です。@fetch_data は @rd_en に似ていますが、@empty(空)を考慮に入れています。したがって、@fetch_data は @rd_en の安全なバージョンです。たとえ @rd_en と @empty(空)が同時に High になっても(これは不正ですが)、その場合は @rd_en が無視されるため、悪いことは何も起こりません。
@commit_data も同様に、@full(満杯)を考慮に入れることで、@wr_en の安全なバージョンになっています。
@words_in_ram の次の値である @next_words_in_ram は、@fetch_data と @commit_data に基づいて組み合わせ論理(combinatorial logic)の関数として計算されます(always @(*) 文に注目してください)。@next_words_in_ram は、次のコード断片でいくつかのレジスタの値を生成するために使われます。
always @(posedge clk)
begin
words_in_ram <= next_words_in_ram;
full <= (next_words_in_ram == depth);
empty <= (next_words_in_ram == 0);
[ ... ]
ここで @full(満杯)と @empty(空)が定義されている点に注意してください。
デュアルクロック FIFO と比べて、この FIFO の実装がこれほど簡単なのは、@words_in_ram をこのように定義し、この同じレジスタを FIFO の両側で使えるからです。
コードの続きでは、@rd_addr と @wr_addr の更新があり、その後に @rst の節があります。リセットによって @empty(空)と @full(満杯)の両方が High に変化しますが、@full(満杯)はリセットが解除されると Low に戻ることに注意してください。
コーディングスタイルについてひとつ注意があります。@rst が High のとき、if 文の begin-end 節内の代入は、その前に行われた代入を上書きするため、@rst は確かにすべてのレジスタを初期値にリセットします。これは最も一般的なコーディングスタイルではありませんが、すべてのレジスタをリセットしない場合には明確な利点があります。この具体的な例ではその利点は示されていませんが、このページを参照してください。
最後に、デュアルポート RAM のインスタンシエーション(instantiation)があります。
FWFT FIFO
「スタンダード FIFO」を FWFT FIFO に変換するのは、このページで示したとおり、非常に簡単です。しかし、FWFT FIFO を直接実装すると、いくつかの興味深い点を議論できます。そこで、その実装を示します。
module fwft_fifo
#(parameter depth = 64, // Must equal 2^log2_depth exactly
log2_depth = 6,
width = 8
)
(
input clk,
input rst,
input wr_en,
input [(width-1):0] din,
input rd_en,
output [(width-1):0] dout,
output reg full,
output reg empty
);
reg [log2_depth:0] next_words_in_ram; // Combinatorial
reg [log2_depth:0] words_in_ram;
reg [(log2_depth-1):0] rd_addr;
reg [(log2_depth-1):0] wr_addr;
reg has_more_words;
wire fetch_data, commit_data;
assign fetch_data = (rd_en || empty) && has_more_words;
assign commit_data = wr_en && !full;
always @(*)
if (commit_data && !fetch_data)
next_words_in_ram <= words_in_ram + 1;
else if (!commit_data && fetch_data)
next_words_in_ram <= words_in_ram - 1;
else
next_words_in_ram <= words_in_ram;
always @(posedge clk)
begin
words_in_ram <= next_words_in_ram;
full <= (next_words_in_ram == depth);
has_more_words <= (next_words_in_ram != 0);
if (fetch_data)
rd_addr <= rd_addr + 1;
if (commit_data)
wr_addr <= wr_addr + 1;
if (fetch_data)
empty <= 0;
else if (rd_en)
empty <= 1;
if (rst)
begin
empty <= 1;
full <= 1;
words_in_ram <= 0;
has_more_words <= 0;
rd_addr <= 0;
wr_addr <= 0;
end
end
dualport_ram
#(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
(.clk(clk), .wr_addr(wr_addr),
.rd_addr(rd_addr),
.wr_en(commit_data),
.rd_en(fetch_data),
.wr_data(din),
.rd_data(dout)
);
endmodule
まず、@has_more_words という新しいレジスタがあることに注目してください。その定義を上記の「スタンダード FIFO」と比較して、@has_more_words が @empty(空)の否定(logical NOT)になっていることを確認してください。
次に、@fetch_data の定義が変わっていることに注意してください。今度は @has_more_words によって保護されています(当然のことですが)。その定義は次のとおりです。
assign fetch_data = (rd_en || empty) && has_more_words;
以前に説明したとおり、FWFT FIFO では @empty(空)は「@dout は有効ではない」という意味です。したがって、この代入は、@rd_en に加えて、出力が有効ではなく、かつメモリアレイに読み出すデータがある場合には、読み出しを実行することを意味します。これが最初のワードのフォールスルー(fall-through)です。
そして最後に、@empty(空)を代入するロジックが次のように変わっています。
always @(posedge clk)
if (fetch_data)
empty <= 0;
else if (rd_en)
empty <= 1;
これはつまり、メモリアレイからワードが読み出されると、次のクロックサイクルで @empty(空)が Low に変わるということです。新しい有効なデータが存在するのは明らかだからです。しかし、その読み出しが起こらず、それでも @rd_en がイネーブルされている場合、アプリケーションロジックは利用可能な最後のワードを受け取ったばかりなので、@empty(空)を High にします。@rd_en が High で @fetch_data が Low の場合、@has_more_words は確実に Low であることに注意してください(上の @fetch_data の代入を参照)。この条件が FIFO の最後のワードを読み出すことと等価なのは、このためです。
これで、FIFO に関するこのシリーズの 3 ページ目は終わりです。次のページでは、データ取得(data acquisition)アプリケーションで使うために「スタンダード FIFO」を適応させる方法を示します。