Cette page est la quatrième d'une série de cinq pages consacrées aux FIFO. Elle propose une méthode pour faire face à la possibilité qu'un débordement se produise.
Introduction
Dans de nombreuses applications utilisant un FIFO, il est impossible de contrôler le flux des données qui arrivent. Par exemple, dans une application d'acquisition de données (data acquisition), la logique écrit directement les données capturées dans le FIFO. Si le FIFO est full (plein), ces données sont perdues. Dans ce type d'application, c'est la logique qui lit depuis le FIFO qui est chargée de consommer les données assez rapidement pour que le FIFO ne devienne jamais full (plein).
Mais si le FIFO devient full (plein), la contiguïté des données est rompue à cause de la perte de données : le FIFO ignore les tentatives d'écriture. Par conséquent, le contenu du FIFO est incorrect. Cette situation est appelée un débordement (overflow).
Un débordement est le résultat d'une sorte de dysfonctionnement et doit être évité. Cela dit, s'il se produit, il est important de détecter l'événement et d'en limiter les dégâts. Une stratégie pour cela est proposée ci-dessous.
Un FIFO modifié
L'effort principal doit toujours être d'empêcher qu'un débordement ne se produise. Mais si cela échoue, il ne reste plus qu'à garantir qu'aucune donnée erronée ne soit consommée depuis le FIFO. Autrement dit, que toutes les données lues depuis le FIFO soient contiguës et correctes.
La solution proposée est un FIFO modifié, qui se distingue de deux façons :
- Lorsqu'un débordement se produit (c'est-à-dire lorsque le FIFO devient full (plein)), plus aucune donnée ne peut y être écrite tant que le FIFO n'a pas été réinitialisé.
- Lorsque le FIFO devient empty (vide) après un débordement, il signale qu'il restera empty (vide) jusqu'à ce qu'un reset se produise. Nous appellerons cette situation un état EOF.
La première particularité garantit que les données lues depuis le FIFO modifié sont toujours contiguës et correctes : le FIFO refuse d'écrire des données dès que la contiguïté est rompue. La seconde particularité (l'EOF) transmet à la logique qui utilise le FIFO le message que quelque chose a mal tourné. Cela donne à cette logique l'occasion de relancer le flux de données.
Le nom EOF vient de l'anglais End of File (fin de fichier). Ce FIFO modifié est utile pour les applications d'acquisition de données (data acquisition) basées sur le cœur IP (IP core) Xillybus. Dans une telle application, le cœur IP (IP core) Xillybus achemine les données vers un ordinateur. Un simple programme informatique utilise l'API standard d'entrées-sorties sur fichiers pour lire les données du FIFO. Autrement dit, le programme ouvre un fichier et en lit les données de la manière habituelle. L'EOF du FIFO fait que ce fichier se comporte comme lorsque l'on atteint la fin d'un fichier ordinaire. C'est la réaction naturelle au fait qu'il n'y a plus de données à lire.
Une implémentation en Verilog
Voici un exemple de module Verilog qui implémente le FIFO modifié proposé ci-dessus :
module eof_fifo
(
input rst,
input wr_clk,
input rd_clk,
input [31:0] din,
input wr_en,
input rd_en,
output [31:0] dout,
output full,
output empty,
output eof
);
reg rst_sync;
reg rst_cross;
reg fifo_has_been_full;
reg fifo_has_been_nonfull;
reg has_been_full_cross;
reg has_been_full;
assign ok_to_write = !rst_sync && !full && !fifo_has_been_full;
assign eof = empty && has_been_full;
always @(posedge wr_clk)
begin
if (!full)
fifo_has_been_nonfull <= 1;
else if (rst_sync)
fifo_has_been_nonfull <= 0;
if (full && fifo_has_been_nonfull)
fifo_has_been_full <= 1;
else if (rst_sync)
fifo_has_been_full <= 0;
end
// Clock domain crossing logic: asynchronous -> wr_clk
always @(posedge wr_clk)
begin
rst_cross <= rst;
rst_sync <= rst_cross;
end
// Clock domain crossing logic: wr_clk -> rd_clk
always @(posedge rd_clk)
begin
has_been_full_cross <= fifo_has_been_full;
has_been_full <= has_been_full_cross;
end
fifo fifo_ins
(
.rst(rst),
.wr_clk(wr_clk),
.rd_clk(rd_clk),
.din(din),
.wr_en(wr_en && ok_to_write),
.rd_en(rd_en),
.dout(dout),
.full(full),
.empty(empty)
);
endmodule
On voit clairement que ce module est constitué d'une instanciation (instantiation) d'un FIFO « standard », plus d'un peu de logique supplémentaire. Notez que les ports de ce module sont presque exactement les mêmes que ceux d'un FIFO « standard ». Il n'y a qu'une seule différence : le FIFO modifié possède un port nommé @eof.
Notez aussi que @din et @dout ont une largeur de 32 bits. Si l'on souhaite un FIFO d'une largeur différente, la seule modification nécessaire est la déclaration de ces ports en haut du module.
Le FIFO modifié possède un port @full (plein), qui n'est pas forcément utile. La logique qui écrit dans le FIFO pourrait tout aussi bien ignorer ce port : si le FIFO devient full (plein), il n'y a de toute façon rien à faire. D'une manière ou d'une autre, le FIFO ignorera les tentatives d'écriture ultérieures. Dans la plupart des applications, savoir que le FIFO est devenu full (plein) n'aide pas beaucoup : le mieux est d'attendre que @eof passe à l'état haut, puis de relancer tout le mécanisme.
Empêcher les écritures après un débordement
Comme déjà mentionné, ce module est basé sur un FIFO « standard ». Tous les ports de ce FIFO sont directement connectés aux ports du module eof_fifo, sauf un : @wr_en. Ce port est connecté à « wr_en && ok_to_write » à la place. Il est donc évident que @ok_to_write sert à interrompre les opérations d'écriture après que le FIFO a été full (plein). La définition de ce fil est :
assign ok_to_write = !rst_sync && !full && !fifo_has_been_full;
Cette expression nous apprend qu'il y a trois situations qui empêchent les opérations d'écriture :
- Lorsque le FIFO est réinitialisé.
- Lorsque le FIFO est full (plein).
- Lorsque le FIFO a déjà été full (plein) par le passé.
always @(posedge wr_clk)
begin
if (!full)
fifo_has_been_nonfull <= 1;
else if (rst_sync)
fifo_has_been_nonfull <= 0;
if (full && fifo_has_been_nonfull)
fifo_has_been_full <= 1;
else if (rst_sync)
fifo_has_been_full <= 0;
end
@fifo_has_been_full est à l'état haut lorsque le FIFO a été full (plein). Ce registre passe à l'état haut lorsque @full (plein) et @fifo_has_been_nonfull sont tous deux à l'état haut.
La première partie n'a rien de surprenant : @full (plein) est connecté au port « full » du FIFO. Mais pourquoi @fifo_has_been_nonfull est-il nécessaire ? La raison est qu'un FIFO maintient souvent sa sortie « full » à l'état haut tant qu'il est maintenu en état de reset. Le but de cette particularité est d'indiquer à la logique de l'application que le FIFO n'est pas encore prêt à recevoir des données. Le rôle de @fifo_has_been_nonfull est d'empêcher que @fifo_has_been_full ne passe par erreur à l'état haut dans ce scénario.
@fifo_has_been_full repasse à l'état bas lorsque le FIFO est réinitialisé. Autrement dit, réinitialiser le FIFO est le seul moyen de reprendre un fonctionnement normal avec le FIFO modifié après qu'un débordement s'est produit. Notez que @rst est un reset asynchrone (asynchronous reset). Il est donc nécessaire d'ajouter de la logique pour créer un signal de reset qui appartienne au bon domaine d'horloge (clock domain). Ce signal est @rst_sync, qui est une copie de @rst.
Génération de l'EOF
Le port @eof est à l'état haut lorsque les deux conditions suivantes sont réunies :
- Toutes les données du FIFO ont été consommées : le FIFO est empty (vide).
- Plus aucune donnée ne sera écrite dans le FIFO : @fifo_has_been_full est à l'état haut parce que le FIFO a déjà été full (plein). Par conséquent, @ok_to_write reste à l'état bas jusqu'à ce que le FIFO soit réinitialisé.
assign eof = empty && has_been_full;
Notez que cette expression repose sur @has_been_full et non sur @fifo_has_been_nonfull : @eof et @empty (vide) appartiennent tous deux au domaine d'horloge (clock domain) de @rd_clk. En revanche, @fifo_has_been_nonfull appartient au domaine d'horloge de @wr_clk. @fifo_has_been_nonfull est donc recopié dans le domaine d'horloge de @rd_clk grâce à un changement de domaine d'horloge (clock domain crossing). Cette copie est @has_been_full.
@eof signifie donc : non seulement le FIFO est empty (vide), mais il ne sera pas rempli tant que vous n'aurez pas réinitialisé le FIFO.
Conclusion
Dans de nombreuses applications, il est impossible de garantir qu'un débordement ne se produira pas. Si cet événement n'est pas tolérable, il est possible d'en réduire les dégâts lorsqu'il survient : la stratégie consiste à laisser passer les données écrites jusqu'au débordement, puis à ne plus autoriser d'écritures supplémentaires. Le FIFO finira par devenir empty (vide). Lorsque cela arrive, le FIFO signale, par l'intermédiaire du port @eof, qu'aucune autre donnée n'arrivera.
Cette méthode garantit la contiguïté des données lues depuis le FIFO : aucune donnée n'a été perdue en cours de route parce que le FIFO était full (plein). Cela rend cette méthode utile pour les applications d'acquisition de données (data acquisition).
Cette page a montré comment implémenter un FIFO modifié basé sur cette stratégie. Ce FIFO modifié peut être utilisé en remplacement direct d'un FIFO « standard ». La seule différence importante est que la logique qui utilise le FIFO doit prêter attention au port @eof : lorsque ce port passe à l'état haut, la logique doit réinitialiser le FIFO et relancer le flux de données.
Ceci clôt la quatrième page d'cette série consacrée aux FIFO. La page suivante montre comment transformer un FIFO « standard » en FIFO FWFT et vice versa, ainsi que la manière d'améliorer le timing.