Vue d’ensemble
Cette page, la dernière d’une série consacrée aux FIFO, montre comment transformer un FIFO existant en un autre. D’abord, comment faire d’un FIFO « standard » un FIFO FWFT, puis l’inverse. S’ensuivent des méthodes plus avancées pour améliorer le timing du FIFO, c’est-à-dire pour le faire fonctionner à des fréquences plus élevées.
D’un point de vue pratique, il est assez inutile de lire cette page à moins d’avoir un problème pour satisfaire les contraintes temporelles (timing constraints), et que ce problème soit lié à un FIFO. Le contenu de cette page est difficile, et pas nécessaire pour l’usage courant des FIFO. Cela peut néanmoins valoir l’effort comme exercice, pour entraîner les muscles que vous utiliserez en concevant de la logique, en particulier la logique qui traite des données.
Bien que sans rapport direct, il existe une autre page qui montre comment créer un FIFO très profond à l’aide d’une mémoire externe (typiquement une mémoire DDR, mais tout ce qui est enveloppé d’une interface AXI peut convenir). Ce qui est intéressant avec cette astuce, c’est que même si cet énorme FIFO peut être aussi profond que la mémoire externe qu’il utilise, tout cela est transparent pour la logique applicative : il a la même interface qu’un FIFO de base.
Je devrais mentionner que j’ai écrit le code Verilog de cette page il y a de nombreuses années ; le style de codage diffère donc un peu de celui d’aujourd’hui.
Transformer un FIFO « standard » en FIFO FWFT
Pour rappeler rapidement ce que sont les FIFO FWFT, vus sur la page précédente : avec un FIFO « standard », un port @empty (vide) à l’état bas signifie que des données valides seront présentées sur la sortie du FIFO après que @rd_en sera passé à l’état haut sur un front montant d’horloge. Un FIFO FWFT présente les données sur sa sortie dès qu’elles sont disponibles ; un signal @empty (vide) à l’état bas signifie donc que les données sur la sortie sont valides.
La signification de @rd_en est également différente : pour un FIFO « standard », cela signifie « apporte-moi des données ». Sur un FIFO FWFT, c’est plutôt « je viens d’utiliser la donnée, apporte-moi la suivante si tu en as ».
Voici donc le module qui transforme un FIFO « standard » en FIFO FWFT. Sans grande surprise, il ne fait que manipuler @rd_en et @empty (vide). Les autres signaux sont simplement transmis tels quels.
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
Normalement, j’expliquerais le code ici, mais cela ne ferait que répéter les explications données pour le FIFO FWFT sur la page précédente.
Transformer un FIFO FWFT en FIFO « standard »
C’est vraiment simple. Puisqu’un @empty (vide) à l’état bas provenant d’un FIFO FWFT signifie que des données sont présentes sur le port de sortie, il suffit de créer un registre qui capture ces données lorsque @rd_en est à l’état haut.
Voici donc tout simplement :
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
Notez que seul @dout est modifié. @empty (vide) est transmis tel quel : s’il est à l’état haut, @dout_w n’est pas valide, donc @dout ne peut pas en capturer la valeur.
Astuces pour améliorer le timing
Bienvenue au clou du spectacle de ces quatre pages consacrées aux FIFO. C’est sans doute la partie la plus difficile à lire.
De temps en temps, lorsqu’on cherche à comprendre pourquoi une conception FPGA ne respecte pas les contraintes temporelles (c’est-à-dire n’atteint pas la fréquence d’horloge souhaitée), il s’avère que le chemin critique (critical path) commence et/ou se termine au niveau d’un FIFO. Commençons par les cas faciles à résoudre, et finissons par l’os le plus dur.
Lorsque @empty (vide) et/ou @full (plein) sont dans le chemin critique
Les signaux @empty (vide) et @full (plein) peuvent se retrouver dans le chemin critique, en particulier si @wr_en et @rd_en résultent de logique combinatoire (combinatorial logic) dépendant de ces signaux. Cela vient surtout du fait que ces signaux ne servent pas uniquement à demander une opération d’écriture ou de lecture au FIFO : ils servent aussi de signaux d’activation pour la logique applicative qui consomme ou produit les données. Si les données ne circulent pas, la logique se fige également.
Par conséquent, il y a souvent de nombreuses équations logiques qui dépendent de @wr_en et @rd_en, et ces fonctions logiques sont souvent assez compliquées. Le résultat est un fan-out (fan-out) élevé. Tout cela aboutit à un délai de propagation (propagation delay) problématique.
@empty (vide) et @full (plein) sont les sorties de bascules dans tout FIFO correctement écrit ; il n’y a donc pas grand-chose à améliorer de ce côté-là. Mais comme le logiciel du FPGA fournit souvent le FIFO sous forme de netlist synthétisée, il est impossible (ou du moins difficile) de dupliquer ces registres pour réduire leur fan-out. En outre, il peut y avoir une grande distance physique sur la logique du FPGA entre ces registres et la logique applicative qui utilise leurs valeurs de sortie. Sur les grands FPGA, cela peut être la contribution déterminante au délai des chemins.
La solution à ce problème a déjà été donnée sur cette page, à propos de @almost_empty (presque vide) et @almost_full (presque plein). En utilisant ces ports, les sorties @wr_en et @rd_en peuvent être des registres. Cela résout le problème de la fonction combinatoire, et permet également de contrôler le fan-out de ces signaux. En plus de cela, cela aide les outils à placer ces registres plus près de la logique qui consomme leurs valeurs, ce qui contribue à réduire le délai de propagation.
Lorsque @wr_en et/ou @din sont dans le chemin critique
Cette situation est sans conteste la plus facile à résoudre. Il suffit d’ajouter un étage de registres. Par exemple :
always @(posedge wr_clk)
begin
wr_en_reg <= wr_en;
din_reg <= din;
end
puis connectez @wr_en_reg et @din_reg au FIFO à la place. Pour éviter tout débordement (overflow) du FIFO, il faut utiliser @almost_full (presque plein) au lieu de @full (plein). Ou, plus généralement, il faut réduire le seuil de remplissage du FIFO d’une unité.
Lorsque @rd_en et/ou @dout sont dans le chemin critique
Là, ça se corse. Non seulement c’est un problème relativement difficile à résoudre, mais c’est aussi celui qui a le plus de chances de se produire. Il y a plusieurs raisons à cela :
- Le signal @rd_en est utilisé dans la logique combinatoire (combinatorial logic) interne du FIFO, notamment pour calculer l’adresse de lecture suivante. Le FIFO lui-même introduit donc une certaine part de délai.
- La logique applicative qui génère @rd_en est souvent une fonction combinatoire qui peut être compliquée. Cette fonction logique peut faire intervenir des registres d’état et divers indicateurs (flags) provenant de différentes parties de la logique du FPGA.
Quant à @dout :
- La sortie de données du FIFO est souvent connectée directement à une block RAM. Par rapport à une bascule du FPGA, ces RAM ont un délai de sortie par rapport à l’horloge (clock-to-output) nettement moins bon. Si le FIFO est implémenté avec plusieurs RAM, leurs sorties de données sont réunies dans un multiplexeur ; @dout est donc le résultat de cette logique combinatoire. Cela ajoute encore du délai.
- La logique applicative peut utiliser la valeur de @dout dans des fonctions combinatoires.
- La distance physique sur le FPGA entre les RAM du FIFO et la logique applicative peut ajouter du délai sur les grands FPGA.
L’objectif est donc d’éliminer le chemin combinatoire entre @rd_en et la logique du FIFO, et d’en faire de même pour @dout.
Détacher uniquement le chemin combinatoire de @dout
Je ne présente pas vraiment cela comme une solution, mais cette discussion peut vous aider à préparer le terrain pour comprendre l’étape suivante. Si cela ne fait que vous embrouiller, sautez cette section.
Supposons donc que nous voulions uniquement détacher le chemin combinatoire de @dout. Le module d’encapsulation (wrapper) qui convertit un FIFO FWFT en FIFO « standard » (comme montré plus haut) fait exactement cela : il ajoute un registre et, ce faisant, met fin au chemin combinatoire de @dout. Mais cela suppose d’avoir un FIFO FWFT comme point de départ.
Mais il y avait aussi un module d’encapsulation qui convertit un FIFO « standard » en FIFO FWFT. Alors, pourquoi ne pas convertir le FIFO dans les deux sens ? Ou écrire un module unique qui fait l’équivalent ? Quoi qu’il en soit, une solution de cette forme aggrave la situation pour @rd_en.
Cette solution mérite pourtant qu’on s’y attarde : la conversion en FIFO FWFT consistait simplement à suivre le moment où le @dout du FIFO encapsulé était valide, et à maintenir @fifo_rd_en à l’état haut lorsque @dout n’était pas valide (et/ou lorsque le @rd_en externe était à l’état haut).
La conversion inverse vers un FIFO « standard » se faisait en recopiant la valeur du @dout du FIFO encapsulé dans un registre lorsque @rd_en était à l’état haut.
Dans l’ensemble, le premier mécanisme maintenait le @dout du FIFO encapsulé valide lorsque c’était possible, et le second recopiait @dout dans un autre registre lorsque le @rd_en externe le demandait.
Mais cela ne résout pas le problème du chemin combinatoire de @rd_en : pour autoriser une lecture continue, il faut lire un mot du FIFO d’origine à chaque cycle où le @rd_en externe est à l’état haut. Sinon, le @dout du FIFO FWFT devient invalide, car il a été consommé mais pas mis à jour. Par conséquent, le @rd_en de ce FIFO interne doit être une fonction combinatoire du @rd_en externe. Pour changer cela, il faut ajouter un autre registre sur le chemin de @dout, comme indiqué ci-dessous.
Détacher les deux chemins combinatoires avec reg_fifo
Sans plus attendre, voici le module reg_fifo, qui détache les chemins combinatoires de @rd_en et @dout :
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
La première chose à noter est que @dout est un registre défini dans ce module et que @rd_en déclenche la mise à jour de ce registre. Il est important de ne pas les confondre avec les signaux similaires connectés au FIFO interne, à savoir @fifo_dout et @fifo_rd_en.
Passons maintenant au fonctionnement de ce module.
Comprendre le pipeline
Cette structure est essentiellement un tampon de glissement (skid buffer) : un pipeline (pipeline) à deux étages avec indicateurs de validité qui découple le @rd_en externe de la validation de lecture du FIFO interne, tout en permettant un flux de données continu.
Comme pour le convertisseur vers un FIFO FWFT, on trouve une instanciation (instantiation) d’un FIFO ordinaire, orig_fifo. La logique du module reg_fifo cherche à maintenir la valeur de @fifo_dout valide en lisant un mot d’orig_fifo lorsque @fifo_dout ne contient pas de valeur valide. Mais en plus, il y a un second registre, appelé @middle_dout. La logique cherche également à maintenir ce registre valide, en y plaçant la valeur de @fifo_dout lorsque c’est possible.
On peut donc considérer @fifo_dout, @middle_dout et @dout comme un pipeline qui fait avancer les données d’orig_fifo.
Deux registres indiquent quand ces étages du pipeline sont valides : @fifo_valid est à l’état haut lorsque @fifo_dout est valide, et @middle_valid est à l’état haut lorsque @middle_dout est valide.
Le but de ce pipeline est de pouvoir contourner son étage central : lorsque @rd_en est à l’état haut (et @empty (vide) à l’état bas), @dout reçoit sa nouvelle valeur de @middle_dout ou de @fifo_dout, mais il privilégie toujours @middle_dout. Autrement dit, si @middle_dout est valide, @dout utilise @middle_dout, sinon il utilise @fifo_dout. La manière dont cela permet de détacher le chemin combinatoire de @rd_en est expliquée plus loin.
Voyons d’abord les détails de l’implémentation. Il y a deux chemins distincts entre la sortie de données du FIFO et le registre de sortie de reg_fifo. Ils sont représentés séparément à gauche et à droite sur ce schéma :
Si aucun des deux étages du pipeline (@fifo_dout et @middle_dout) n’est valide, @empty (vide) est à l’état haut pour indiquer qu’il n’y a aucune source de données :
assign empty = !(fifo_valid || middle_valid);
La tentative de maintenir ces étages du pipeline valides se traduit par
assign fifo_rd_en = !fifo_empty && !(middle_valid && fifo_valid);
ce qui signifie que si l’un des deux étages du pipeline est invalide, on lit depuis orig_fifo, si possible. Si @fifo_dout est déjà valide, sa valeur est recopiée dans @middle_dout au moment même où @fifo_dout est mis à jour (nous y reviendrons plus bas).
Examinons maintenant les définitions de la paire @will_update_* :
assign will_update_middle = fifo_valid && (middle_valid == will_update_dout);
assign will_update_dout = rd_en && !empty;
Remarquez d’abord que @will_update_dout est égal à @rd_en, avec en plus une protection contre la lecture de reg_fifo lorsque @empty (vide) est à l’état haut.
Ensuite, nous avons @will_update_middle, qui commande la mise à jour de @middle_dout, comme suit :
always @(posedge rd_clk)
if (will_update_middle)
middle_dout <= fifo_dout;
En regardant la définition de @will_update_middle ci-dessus, deux conditions doivent être remplies pour mettre à jour @middle_dout : la première est que la valeur de @fifo_dout soit valide, ce qui est assez évident, et la seconde est l’expression (middle_valid == will_update_dout). Décomposons cette expression selon les quatre cas possibles, car c’est ce qui explique comment toute la mécanique fonctionne. Gardez à l’esprit que tout cela ne joue un rôle que lorsque @fifo_dout est valide :
- @middle_valid == 0 et @will_update_dout == 0. @middle_dout n’est pas valide et la valeur de @fifo_dout ne sera pas recopiée dans @dout. On met donc à jour @middle_dout à partir de @fifo_dout.
- @middle_valid == 0 et @will_update_dout == 1. @middle_dout n’est pas valide, mais @dout sera mis à jour ; il le sera donc évidemment à partir de @fifo_dout. Comme la valeur de @fifo_dout est consommée, elle ne peut pas être recopiée dans @middle_dout ; on ne fait donc rien.
- @middle_valid == 1 et @will_update_dout == 0. @middle_dout est valide et rien n’est recopié dans @dout. Les deux étages du pipeline sont valides et le resteront de toute façon. On ne fait donc rien.
- @middle_valid == 1 et @will_update_dout == 1. @middle_dout est valide et sera recopié dans @dout. On met donc à jour @middle_dout à partir de @fifo_dout afin qu’il reste valide.
Notez que @fifo_rd_en est à l’état bas lorsque @middle_valid et @fifo_valid sont tous deux à l’état haut. Par conséquent, aucune donnée n’est lue depuis orig_fifo dans les deux derniers cas de figure.
En particulier, lorsque les deux étages du pipeline sont valides et que @rd_en est à l’état haut, la valeur de @fifo_dout est recopiée dans @middle_dout. Et comme @fifo_rd_en est à l’état bas, @fifo_valid repassera à l’état bas au cycle d’horloge suivant. Ce n’est pas grave, car @middle_valid restera à l’état haut et pourra ainsi fournir des données au cycle suivant si nécessaire. Au cycle d’après, @fifo_valid repassera à l’état haut (s’il y a des données dans orig_fifo).
Alors pourquoi @fifo_rd_en n’est-il pas défini de manière à maintenir @fifo_dout valide dans cette situation précise ? Parce qu’alors @fifo_rd_en devrait être une fonction combinatoire de @rd_en, ce qui est précisément ce que ce pipeline à deux étages vise à éviter.
Fort de cela, examinons comment @dout est défini. Hormis le reset, la définition est :
always @(posedge rd_clk)
if (will_update_dout)
dout <= middle_valid ? middle_dout : fifo_dout;
En remplaçant @will_update_dout par sa définition, on obtient :
always @(posedge rd_clk)
if (rd_en && !empty)
dout <= middle_valid ? middle_dout : fifo_dout;
C’est semblable à la conversion d’un FIFO FWFT en FIFO « standard », à la différence qu’il y a deux sources possibles : si @middle_dout contient une valeur valide, on la prend. Sinon, on prend @fifo_dout. Si aucun des deux n’est valide, @empty (vide) est à l’état haut et il ne se passe de toute façon rien.
En quoi cela aide-t-il ? Pour ce qui est du timing de sortie, @dout est manifestement un registre. Quant à @rd_en, notez que @fifo_rd_en ne dépend que de @middle_valid et @fifo_valid, qui sont tous deux des registres. S’ajoute @fifo_empty (vide), qui est la sortie d’orig_fifo lui-même (ce chemin combinatoire est inévitable). Par conséquent, @fifo_rd_en ne dépend pas du @rd_en externe ; il n’y a donc pas de chemin combinatoire entre @rd_en et orig_fifo.
Suivi de la validité des registres des étages du pipeline
Pour compléter le tableau : les deux indicateurs *_valid nous disent si le registre correspondant contient des données valides. Pour @fifo_valid :
if (fifo_rd_en)
fifo_valid <= 1;
else if (will_update_middle || will_update_dout)
fifo_valid <= 0;
C’est comme la définition de @dout_valid dans la conversion d’un FIFO « standard » en FIFO FWFT vue plus haut : lorsque @fifo_rd_en est à l’état haut sur un front montant, @fifo_valid passe à l’état haut par conséquent. Ce qui est logique, car si l’on lit des données depuis orig_fifo, la sortie de ce FIFO est alors considérée comme valide. En revanche, si @fifo_rd_en était à l’état bas et que des données ont été recopiées dans @middle_dout ou @dout, on ne considère plus @fifo_dout comme valide : la sortie du FIFO vient d’être utilisée, et le FIFO ne l’a pas remplacée par de nouvelles données.
@middle_valid suit la même logique :
if (will_update_middle)
middle_valid <= 1;
else if (will_update_dout)
middle_valid <= 0;
Lorsque @will_update_middle est à l’état haut, des données sont recopiées dans @middle_dout ; @middle_valid passe donc lui aussi à l’état haut. Sinon, et si les données de @middle_dout sont recopiées dans @dout, @middle_valid repasse à l’état bas. @will_update_dout est une condition suffisante pour cela, car rappelez-vous que, plus haut, @dout privilégie la recopie depuis @middle_dout lorsque c’est possible.
Est-ce que cela fonctionne vraiment ?
Ce module est si compliqué qu’il mérite une preuve presque formelle de son fonctionnement. Une façon de répondre à cette question est de se demander combien des deux étages du pipeline, @fifo_dout et @middle_dout, sont valides. Cette valeur n’est pas définie dans le module reg_fifo, mais elle aurait pu l’être ainsi :
wire [1:0] valid_count;
assign valid_count = fifo_valid + middle_valid;
Cet @valid_count imaginaire peut évidemment prendre les valeurs 0, 1 ou 2. Il compte de la manière suivante :
- @valid_count augmente de un à chaque cycle d’horloge où @fifo_rd_en est à l’état haut et où (rd_en && !empty) est faux.
- @valid_count diminue de un à chaque cycle d’horloge où @fifo_rd_en est à l’état bas et où (rd_en && !empty) est vrai.
- Sinon, @valid_count ne change pas.
Jetez un œil aux équations logiques et convainquez-vous que ces trois affirmations sont correctes.
Voyons donc ce qui se passe lorsqu’il y a des données dans orig_fifo et que la logique applicative veut lire en continu :
La logique de reg_fifo cherche à faire monter @valid_count vers 2 en lisant depuis orig_fifo. Par ailleurs, @empty (vide) est à l’état bas lorsque @valid_count n’est pas nul ; @rd_en peut donc passer à l’état haut dès que @valid_count vaut 1. Ainsi, quand @valid_count vaut 1, @fifo_rd_en est à l’état haut parce que @valid_count n’est pas égal à 2.
Mais @valid_count n’atteindra pas la valeur 2, car @rd_en l’en empêche en restant à l’état haut. Les données circulent donc avec @fifo_rd_en et @rd_en maintenus à l’état haut, tandis que @valid_count reste à 1. Sauf au tout début, les données sont recopiées de @fifo_dout vers @dout.
Cet équilibre est rompu lorsqu’orig_fifo devient empty (vide) : dans ce cas, @valid_count retombe à zéro parce que @fifo_rd_en n’a plus le droit d’être à l’état haut. L’autre rupture d’équilibre se produit lorsque le FIFO n’est pas empty (vide) et que @rd_en repasse à l’état bas parce que la logique applicative ne souhaite plus lire : dans ce cas, @valid_count monte à 2, et y reste.
Mais plus tard, lorsque @rd_en repasse à l’état haut, @valid_count redescend à 1, et c’est seulement à ce moment-là que @fifo_rd_en repasse à l’état haut (sauf si orig_fifo est empty (vide)).
Encore une fois, @valid_count n’est qu’un signal théorique qui n’est pas implémenté dans le module. Espérons que cette explication aide à comprendre pourquoi les deux étages supplémentaires du pipeline garantissent un flux de données continu.
Remarques d’utilisation
Ce module peut être utilisé en remplacement direct du FIFO « standard » qu’il encapsule. D’un point de vue fonctionnel, rien ne change. orig_fifo verra toutefois un léger changement de comportement de @rd_en, @dout et @empty (vide), mais cela n’a pas d’importance tant qu’orig_fifo se comporte correctement comme un FIFO (ce qui est une hypothèse raisonnable). Les ports liés à l’écriture des données sont transmis tels quels ; il n’y a donc aucun changement de ce côté-là.
Comme le module ajoute quelques étages de pipeline, les compteurs de remplissage (fill counters) d’orig_fifo peuvent afficher des valeurs inférieures au nombre total de mots stockés dans reg_fifo (c’est-à-dire le nombre de mots stockés dans orig_fifo et dans les étages du pipeline, comptés ensemble). Par conséquent, si @almost_empty (presque vide) ou des ports similaires sont activés sur orig_fifo, ils peuvent donner une image pessimiste.
Un léger inconvénient de reg_fifo est que sa sortie @empty (vide) n’est pas un registre, mais une fonction combinatoire de deux registres. Ce n’est pas idéal pour le timing, mais l’impact est minime dans la plupart des cas. On peut corriger cela en définissant des registres combinatoires comme @next_fifo_valid et @next_middle_valid, dans le même esprit que @next_words_in_ram, comme indiqué sur cette page. Ce n’est pas implémenté ici, principalement parce que reg_fifo est déjà suffisamment compliqué en l’état.
Un FIFO FWFT au timing amélioré
Si vous avez besoin d’un FIFO FWFT au timing amélioré, c’est facile : utilisez l’exemple de basic_fwft_fifo montré plus haut, mais au lieu d’une instanciation d’un FIFO ordinaire, utilisez reg_fifo. Cette solution exige un registre supplémentaire, mais c’est un prix modeste à payer.
Ceci clôt cette série consacrée aux FIFO.
