01signal.com

FIFO FPGA : différentes fonctionnalités et variantes

Portée

Cette page est la deuxième d'une série de cinq pages consacrées aux FIFO. Après avoir présenté les bases d'un FIFO dans la page précédente, il est temps d'aborder les variantes courantes et les fonctionnalités supplémentaires. Les FIFO sont souvent configurés comme une combinaison des options décrites ci-dessous.

FIFO à horloge unique

Même si le « FIFO de base » possède deux entrées prévues pour des horloges sans relation entre elles (unrelated clocks), il arrive souvent que les signaux des deux côtés soient synchrones avec la même horloge. On peut tout à fait connecter cette horloge commune à @wr_clk et @rd_clk. Mais comme c'est la même horloge sur les deux entrées d'horloge, il n'y a pas de changement de domaine d'horloge (clock domain crossing) à effectuer ; le FIFO contient donc de la logique inutile. Vous en saurez plus sur les domaines d'horloge (clock domains) ici.

Chaque fabricant de FPGA propose donc deux catégories de FIFO : le FIFO à double horloge (dual-clock FIFO) et le FIFO à horloge unique (single-clock FIFO). D'autres noms sont souvent utilisés : FIFO à horloges indépendantes (independent clock FIFO) par opposition à FIFO à horloge commune (common clock FIFO), ou encore FIFO asynchrone (asynchronous FIFO) par opposition à FIFO synchrone (synchronous FIFO). Le « FIFO de base » présenté sur la page précédente est un FIFO à double horloge.

Les FIFO à horloge unique ne contiennent aucune logique de synchronisation, puisque toute la logique est synchrone avec la même horloge. L'entrée de reset doit donc être synchrone avec la même horloge que les autres ports.

Outre le fait de ne pas gaspiller de logique FPGA, l'autre bonne raison d'utiliser des FIFO à horloge unique est la clarté. C'est une façon d'affirmer haut et fort qu'il n'est pas question de faire intervenir deux horloges.

En bref : si le FIFO ne sert pas de pont entre deux domaines d'horloge (clock domains), optez pour un FIFO à horloge unique.

FIFO FWFT

Comme cela a été souligné sur la page précédente, pour lire une donnée depuis un « FIFO de base », on fait passer @rd_en à l'état haut et on obtient la valeur sur la sortie @dout du FIFO au cycle d'horloge suivant. C'est un peu contre-intuitif : si la donnée est déjà dans le FIFO, pourquoi faudrait-il la demander ? Pourquoi le FIFO ne pourrait-il pas simplement la placer sur @dout et m'indiquer que je peux l'utiliser ?

Il existe donc une variante courante qui fait exactement cela, appelée FIFO First Word Fall Through (FWFT, parfois aussi appelé read-ahead, show-ahead ou look-ahead). L'opposé d'un FIFO FWFT est souvent appelé « FIFO standard » (quelqu'un peut-il me montrer la norme ?).

L'idée est simple : lorsqu'un FIFO FWFT cesse d'être empty (vide) parce que des données y ont été écrites, il présente le premier mot sur @dout. La logique de l'application lit ensuite les mots en maintenant @rd_en à l'état haut. La différence ne porte donc que sur le premier mot.

Il est cependant plus facile de comprendre un FIFO FWFT en réalisant que la signification de deux de ses ports a changé : sur un FIFO FWFT, @rd_en signifie en réalité « je viens de consommer la donnée présente sur @dout, tu peux m'apporter la suivante », et @empty (vide) signifie en réalité « @dout n'est pas valide ».

Ce qui n'a pas changé, c'est que @rd_en ne doit pas être à l'état haut si @empty (vide) est à l'état haut. On ne peut pas prétendre avoir consommé une donnée invalide. La règle reste donc la même, mais pour une raison différente.

Le chronogramme suivant montre à quoi peut ressembler une lecture depuis un FIFO FWFT :

Example waveform for reading words from a FWFT FIFO

Notez que la première valeur valide sur @dout apparaît alors que @rd_en est à l'état bas, et que @empty (vide) passe à l'état bas au même moment que la valeur valide apparaît. Comme je viens de le mentionner, sur un FIFO FWFT, @empty (vide) signifie « @dout n'est pas valide », et le chronogramme le reflète.

Remarquez également que la première impulsion de @rd_en n'a pas lu une nouvelle valeur du FIFO ; elle a plutôt fait repasser @empty (vide) à l'état haut. La valeur de @dout est d'ailleurs devenue indéterminée au même moment. En réalité, @dout ne change généralement pas lorsque @empty (vide) repasse à l'état haut, mais on ne peut pas compter là-dessus.

Ensuite, le FIFO replace une valeur sur @dout et repasse @empty (vide) à l'état bas. La logique de l'application lit trois mots, puis repasse @rd_en à l'état bas. Au total, la logique de l'application a consommé quatre ou cinq mots du FIFO.

Notez que le chronogramme ne nous dit pas si la logique de l'application a également utilisé la valeur de D4. Elle a peut-être ignoré le cinquième mot, ce qui signifie qu'elle n'en a consommé que quatre. Ou alors elle a utilisé la valeur du cinquième mot. La seule chose que le chronogramme montre clairement est que la logique de l'application a maintenu @rd_en à l'état bas après quatre cycles d'horloge, empêchant ainsi le FIFO de continuer à mettre @dout à jour.

Autre chose à noter : on ne sait pas s'il reste d'autres données dans la mémoire du FIFO. Le fait que @empty (vide) soit à l'état bas à la fin de ce chronogramme signifie uniquement que @dout est valide.

Modifions maintenant l'exemple Verilog de la page précédente. Ce code calcule lui aussi la somme cumulée de tout ce qui sort du FIFO :

assign rd_en = !empty; // If @dout's value is valid, it's consumed.

always @(posedge rd_clk)
  if (!empty) // FIFO is FWFT, so !empty means @dout contains valid data
    sum <= sum + dout; // Don't try this at home: @sum is never reset.

Contrairement à l'exemple précédent, celui-ci utilise un FIFO FWFT : il n'est donc pas nécessaire d'avoir un registre qui mémorise la valeur de @rd_en au cycle d'horloge précédent. On peut consommer @dout lorsque @empty (vide) est à l'état bas. Cette règle simple fonctionne parce que @rd_en est à l'état haut quand @empty (vide) est à l'état bas ; chaque mot du FIFO est donc valide sur @dout pendant exactement un cycle d'horloge.

J'aimerais conclure le sujet des FIFO FWFT sur un point un peu à part. La différence entre un FIFO « standard » et un FIFO FWFT fait écho à une question fondamentale concernant le flux de données entre deux modules logiques : le côté récepteur doit-il demander les données ? Ou bien le côté émetteur présente-t-il les données dès que possible, le récepteur ne faisant que confirmer qu'il peut continuer ? Posez-vous toujours cette question lorsqu'un module transmet des données à un autre, et demandez-vous en particulier si les deux modules sont d'accord à ce sujet.

FIFO asymétriques

Il est généralement possible de définir le FIFO avec des largeurs différentes pour @din et @dout. Cela est utile, par exemple, si les données arrivent au FPGA sous forme de mots de 32 bits, mais que la logique de l'application les traite comme des octets, c'est-à-dire 8 bits par mot. Dans ce cas, configurez la largeur du côté écriture à 32 bits et celle du côté lecture à 8 bits. Les deux côtés se comportent comme d'habitude, si ce n'est qu'il faut quatre cycles de lecture pour consommer un mot qui a été inséré en un seul cycle d'écriture.

Lorsque le côté lecture est plus large que le côté écriture, le comportement est celui que l'on attend : les données écrites dans le FIFO ne sont disponibles côté lecture que lorsqu'elles ont rempli un mot complet de la taille du côté lecture.

Quant à l'ordre dans lequel les mots sont empaquetés, il semble que tous les FIFO utilisent Little Endian. Par exemple, un FIFO qui convertit des mots de 32 bits en mots de 8 bits procède ainsi : les bits du premier mot lu depuis le FIFO sont [7:0], puis viennent [15:8], [23:16] et [31:24].

Mais si vous voulez utiliser cette fonctionnalité, vérifiez toujours la documentation.

Dépendance à la logique combinatoire pour @empty et @full

Les ports @empty (vide) et @full (plein) partagent un même inconvénient : la logique de l'application doit y réagir dans le même cycle d'horloge. En d'autres termes, @rd_en doit être une fonction combinatoire (combinatorial logic) qui dépend de @empty (vide) pour garantir que ces deux signaux ne soient pas à l'état haut au même cycle d'horloge (c'est interdit, comme déjà mentionné). De même, @wr_en doit être une fonction combinatoire qui dépend de @full (plein).

L'utilisation de fonctions combinatoires peut devenir un obstacle à la satisfaction des contraintes temporelles (timing constraints). Cela peut poser problème lorsque la fréquence de l'horloge est élevée par rapport aux spécifications du FPGA, et lorsque la fonction logique est compliquée. La raison principale des difficultés est que @rd_en et @wr_en interviennent souvent dans la logique qui produit ou consomme les données. En particulier, la fonction logique qui calcule la validation d'horloge (clock enable) d'une grande partie de la logique peut dépendre de ces signaux. Par exemple, s'il y a un long pipeline qui traite des données provenant d'un FIFO, toute la logique du pipeline doit se figer lorsque le flux de données en provenance du FIFO s'interrompt momentanément.

Bon, pour être tout à fait précis, il existe un moyen d'éviter cette fonction combinatoire. Supposons par exemple que @wr_en soit déclaré comme registre et que @want_to_write soit un signal qui représente le besoin de la logique d'application d'écrire à un moment donné. On peut alors faire ceci :

always @(posedge wr_clk)
  wr_en <= want_to_write && !wr_en && !full;

Ceci garantit que @wr_en et @full (plein) ne sont jamais à l'état haut au même cycle d'horloge, car @full (plein) ne peut passer à l'état haut que lors du cycle qui suit un cycle où @wr_en était à l'état haut. La partie !wr_en de l'expression garantit que @wr_en n'est jamais à l'état haut pendant deux cycles d'horloge consécutifs. Ainsi, si @full (plein) passe à l'état haut, @wr_en sera à l'état bas dès le premier cycle grâce à !wr_en. @wr_en reste ensuite à l'état bas à cause de @full (plein) lui-même.

Mais avec cette solution, @wr_en doit rester à l'état bas pendant la moitié du temps. En conséquence, seulement 50 % du débit de données du FIFO est utilisé. Cela est généralement inacceptable.

La même solution est possible pour @rd_en, avec le même problème : elle n'utilise que la moitié du débit de données.

Cette discussion visait à introduire la section suivante : les ports « almost » (presque pleins / presque vides).

Presque plein, presque vide et ports similaires

On peut ajouter à un FIFO deux ports optionnels : un port @almost_full (presque plein) et/ou un port @almost_empty (presque vide).

@almost_empty (presque vide) est synchrone avec @rd_clk et ressemble à @empty (vide), à une petite différence près : @almost_empty (presque vide) est à l'état haut lorsque le FIFO est empty (vide), mais aussi lorsqu'il reste exactement un mot à lire dans le FIFO.

@almost_full (presque plein) est synchrone avec @wr_clk et est à l'état haut lorsque le FIFO est full (plein), mais aussi lorsqu'il est encore possible d'y écrire exactement un mot.

Les noms de ces deux ports de sortie dépendent du fabricant du FPGA et du logiciel qu'il fournit, mais il est toujours possible d'ajouter des ports ayant la même fonctionnalité. Il arrive simplement que certaines variantes de FIFO ne supportent pas ces ports.

En quoi ces ports aident-ils ? Eh bien, parce que ceci fonctionne parfaitement bien :

always @(posedge wr_clk)
  wr_en <= want_to_write && !almost_full;

Pas de logique combinatoire, et pas besoin de sauter la moitié des cycles d'écriture. Lorsque @almost_full (presque plein) est à l'état haut, @wr_en ne peut passer à l'état bas au même cycle d'horloge, mais seulement au cycle suivant. En conséquence, il peut encore y avoir une opération d'écriture après que @almost_full (presque plein) est passé à l'état haut. Mais cela ne pose pas de problème, car il y a de la place pour un mot.

Notez que si @want_to_write reste à l'état haut en continu pendant que le FIFO se remplit, la dernière opération d'écriture rend le FIFO complètement full (plein). Dans le cas contraire, il est possible que le FIFO se retrouve presque plein : si @wr_en est à l'état bas à cause de @want_to_write, et que le FIFO ne devient pas complètement full (plein) pour cette raison, il n'y aura pas de seconde chance. @almost_full (presque plein) ne repassera à l'état bas que lorsque l'autre côté lira des données du FIFO ; il y aura alors de la place pour deux mots ou plus dans le FIFO.

Cela a rarement de l'importance, mais pour la discussion, ceci garantit que le dernier mot est bien utilisé :

always @(posedge wr_clk)
  wr_en <= want_to_write && (!almost_full || (!full && !wr_en));

Cette expression de @wr_en s'appuie sur @almost_full (presque plein) la plupart du temps, sauf lorsqu'on peut encore écrire exactement un mot. C'est seulement dans ce cas que @wr_en dépend de @full (plein) et de @wr_en, comme dans l'expression précédente qui utilisait !wr_en.

Je doute cependant sérieusement que cette dernière expression de @wr_en soit utile.

L'histoire avec @almost_empty (presque vide) est similaire, et ceci est donc acceptable (mais ne copiez pas ceci dans votre code) :

always @(posedge rd_clk)
  rd_en <= want_to_read && !almost_empty;

Comme avec @almost_full (presque plein), il y a un problème avec le dernier mot : si @rd_en est à l'état bas à cause de @want_to_read, on perd cette occasion jusqu'à ce que le FIFO soit à nouveau rempli avec d'autres données. Contrairement au cas de @almost_full (presque plein), cela peut effectivement poser problème dans certains scénarios : si @almost_empty (presque vide) est à l'état haut alors que le FIFO n'est pas empty (vide), cela signifie qu'il y a dans le FIFO des données destinées à la lecture, mais qu'elles y restent coincées.

Voici donc la façon sûre de procéder :

always @(posedge rd_clk)
  rd_en <= want_to_read && (!almost_empty || (!empty && !rd_en));

Compteurs de remplissage

La logique d'application effectue souvent des opérations par blocs. Par exemple, une logique qui lit depuis le FIFO des paquets de données d'une longueur constante et les transmet sur un support physique. Comme les données sont stockées dans un FIFO, la logique d'application doit savoir qu'il y a suffisamment de données pour former un paquet complet avant de commencer à lire.

De même, la logique d'application produit souvent une quantité fixe de données à stocker dans un FIFO, par exemple lors d'une lecture en rafale (burst) de données depuis une mémoire externe. L'opération ne doit pas démarrer s'il n'y a pas assez de place dans le FIFO pour terminer la rafale.

À ces fins, les FIFO proposent généralement des compteurs de remplissage (fill counters), un port programmable empty (vide) et un port programmable full (plein). Les compteurs de remplissage, parfois appelés compteurs de données (data counters), se présentent sous des formes et des configurations très variées selon le fabricant du FPGA ; lisez donc attentivement la documentation du FIFO. Trois points principaux méritent votre attention :

En outre, il existe des ports programmable empty (vide) et programmable full (plein), versions étendues de @almost_empty (presque vide) et @almost_full (presque plein). L'idée est que, comme l'utilisation des compteurs de remplissage revient presque certainement à écrire quelque chose comme

assign dont_start_reading = (rd_data_count < 64);

pourquoi ne pas proposer ce signal directement et l'appeler prog_empty ? Encore une fois, lisez attentivement la documentation du FIFO.

Encore une fois, lorsqu'il est important de lire le dernier mot du FIFO, demandez-vous si votre logique le fera réellement. Cette question est semblable à celle de la discussion précédente sur @almost_empty (presque vide).

Il va presque sans dire qu'il faudra demander ces ports supplémentaires lors de la configuration du FIFO, si vous les voulez.

Interface AXI

Ce sujet n'est pas directement lié, mais il vaut la peine d'être mentionné pour éviter toute confusion, car ce terme revient souvent dans les discussions sur les FIFO.

AXI est un ensemble d'interfaces défini dans la norme AMBA, introduite par ARM. Comme on peut s'y attendre, les FIFO à interface AXI sont généralement destinés à fonctionner comme périphérique d'un processeur (CPU).

L'interface du « FIFO de base » est souvent appelée interface « native », par opposition à une interface AXI.

Il existe deux grands types d'interfaces AXI : l'AXI « classique » (typiquement AXI3, AXI4 ou AXI Lite), qui est un bus avec adresse et données. Le second type, AXI-S (AXI stream), est destiné à des flux de données, éventuellement découpés en paquets.

Lorsqu'un FIFO est configuré en AXI3, AXI4 ou AXI Lite, une logique supplémentaire lui est ajoutée afin qu'il puisse être connecté à un processeur en tant que périphérique adressable via cette interface. Je ne m'étendrai pas davantage, car c'est un sujet complètement différent.

Mais comme l'interface de type flux ressemble un peu au comportement d'un FIFO, il est possible de convertir les signaux de poignée de main (handshake) de l'interface AXI-S en une interface « native ». Notez que l'AXI-S comporte souvent d'autres signaux dont il faut également s'occuper.

Ainsi, étant donné les signaux AXI-S d'écriture dans le FIFO, @axi_w_valid, @axi_w_ready et @axi_w_data, on peut les connecter aux ports d'un FIFO « standard » avec :

assign axi_w_ready = !full;
assign wr_en = axi_w_valid && axi_w_ready;
assign din = axi_w_data;

De même, les signaux AXI-S de lecture depuis le FIFO, @axi_r_valid, @axi_r_ready et @axi_r_data, peuvent être connectés aux ports d'un FIFO FWFT avec :

assign axi_r_valid = !empty; // Non-empty means valid with FWFT FIFOs
assign rd_en = axi_r_valid && axi_r_ready;
assign axi_r_data = dout;

Encore une fois, pour que cela fonctionne, le FIFO doit être une variante FWFT.

Ceci clôt la deuxième page de cette série consacrée aux FIFO. La page suivante montre comment implémenter un FIFO à horloge unique en Verilog.

Cette page a été traduite de l’anglais par une machine. En cas de doute, veuillez vous reporter au texte original
Copyright © 2021-2026. All rights reserved. (dcc38493)