Portée
Cette page, la troisième d'une série de cinq pages consacrées aux FIFO, présente une implémentation en Verilog d'un FIFO de base à horloge unique. Cela peut être utile pour écrire du code portable, mais l'objectif principal de cette page est de réexpliquer le fonctionnement d'un FIFO. Je vais donc montrer l'implémentation d'un FIFO « standard » ainsi que celle d'un FIFO FWFT. Il nous faut d'abord une RAM double port (dual port RAM) que les deux FIFO utiliseront.
La RAM double port
Voici le module Verilog qui implémente la RAM par inférence. Il est fort probable que n'importe quel synthétiseur (synthesizer) s'en sorte correctement, mais il est possible qu'il génère le type de RAM non souhaité (block RAM par opposition à RAM distribuée, distributed RAM). Il peut donc être nécessaire d'ajouter des directives de synthèse, ou éventuellement d'utiliser un module IP (IP core) fourni par le fabricant du FPGA pour une RAM double port, si cela convient mieux.
Voici donc le module :
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
Le FIFO « standard »
Et maintenant, passons au module qui implémente un FIFO « standard » (c'est-à-dire pas un FIFO FWFT) :
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
La seule remarque importante concerne le commentaire en haut, à propos des paramètres « depth » et « log2_depth » : depth doit être égal à 2log2_depth.
Le fonctionnement de ce module est assez simple : @fetch_data ressemble à @rd_en, mais il tient compte de @empty (vide). @fetch_data est donc une version sûre de @rd_en, dans le sens où, même si @rd_en et @empty (vide) sont à l'état haut en même temps (ce qui est interdit), rien de grave ne se produit, car @rd_en est ignoré dans ce cas.
@commit_data est la version sûre de @wr_en, de la même manière, en tenant compte de @full (plein).
La valeur suivante de @words_in_ram, @next_words_in_ram, est calculée comme une fonction combinatoire, à partir de @fetch_data et @commit_data (notons l'instruction « always @(*) »). @next_words_in_ram sert à produire les valeurs de plusieurs registres avec cet extrait de code :
always @(posedge clk)
begin
words_in_ram <= next_words_in_ram;
full <= (next_words_in_ram == depth);
empty <= (next_words_in_ram == 0);
[ ... ]
Remarquez que @full (plein) et @empty (vide) sont définis ici.
Ce qui rend ce FIFO si facile à implémenter, par rapport à un FIFO à double horloge, c'est la possibilité de définir @words_in_ram de cette façon et d'utiliser ce même registre des deux côtés du FIFO.
Dans la suite du code, on trouve la mise à jour de @rd_addr et @wr_addr, puis la clause pour @rst. Notez que @empty (vide) et @full (plein) passent tous deux à l'état haut suite au reset, mais @full (plein) reviendra à l'état bas lorsque le reset sera relâché.
Une remarque sur le style de codage : lorsque @rst est à l'état haut, les affectations dans le bloc begin-end de la condition if remplacent les affectations éventuellement faites plus haut ; @rst réinitialise donc bien tous les registres à leur valeur initiale. Ce n'est pas le style de codage le plus courant, mais il présente des avantages certains lorsque tous les registres ne sont pas réinitialisés. Ce cas précis ne démontre pas cet avantage, mais voyez cette page.
Enfin, on trouve une instanciation (instantiation) de la RAM double port.
Le FIFO FWFT
Comme cela est montré sur cette page, il est assez facile de transformer un FIFO « standard » en FIFO FWFT. Mais son implémentation directe permet d'aborder quelques points intéressants, alors la voici :
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
Remarquez d'abord que nous avons un nouveau registre, @has_more_words. Comparez sa définition avec celle du FIFO « standard » ci-dessus et convainquez-vous que @has_more_words est le complément logique de @empty (vide).
Ensuite, notez que la définition de @fetch_data a changé. Elle est désormais protégée par @has_more_words (sans surprise), ce qui donne :
assign fetch_data = (rd_en || empty) && has_more_words;
Rappelez-vous, plus haut, que sur un FIFO FWFT, @empty (vide) signifie « @dout n'est pas valide ». Cette affectation signifie donc qu'en plus de @rd_en, si la sortie n'est pas valide et qu'il y a des données dans la mémoire à lire, on effectue cette lecture. C'est ce qu'on appelle le « fall through » du premier mot.
Et enfin, la logique qui affecte @empty (vide) a été modifiée ainsi :
always @(posedge clk)
if (fetch_data)
empty <= 0;
else if (rd_en)
empty <= 1;
Cela dit simplement que si un mot est lu dans la mémoire, @empty (vide) passe à l'état bas au cycle d'horloge suivant, car il y a alors manifestement une nouvelle donnée valide. En revanche, si cela ne se produit pas et que @rd_en est tout de même activé, c'est que la logique de l'application vient de prendre le dernier mot disponible ; il faut donc faire passer @empty (vide) à l'état haut. Notez que si @rd_en est à l'état haut et @fetch_data à l'état bas, @has_more_words est forcément à l'état bas (voir l'affectation de @fetch_data ci-dessus). C'est pourquoi cette condition équivaut à lire le dernier mot du FIFO.
Ceci clôt la troisième page de cette série consacrée aux FIFO. La page suivante montre comment adapter un FIFO « standard » pour une application d'acquisition de données (data acquisition).