01signal.com

Introduction aux FIFO FPGA

Cette page est la première d'une série de cinq pages consacrées aux FIFO.

Vue d’ensemble

Un FIFO FPGA est un élément de mémoire au concept simple : une partie de la logique de l’application écrit des mots de données d’un côté du FIFO. De l’autre côté, une autre partie de la logique de l’application lit ces mots, dans le même ordre (FIFO = First In, First Out).

Ces données sont stockées à l’intérieur du FIFO. La profondeur du FIFO correspond au nombre de mots de données qu’il peut contenir. La largeur (c’est-à-dire le nombre de bits par mot) et la profondeur sont des paramètres que l’utilisateur configure pour chaque FIFO.

Les FIFO sont probablement le composant IP (IP core) le plus courant dans les conceptions FPGA. Chaque fois qu’une partie de la logique produit des données et qu’une autre partie les consomme, la solution immédiate qui vient à l’esprit est de placer un FIFO entre elles (ce qui ne veut pas dire qu’un FIFO est toujours la bonne solution, bien sûr…).

Pour ceux qui connaissent l’interface en ligne de commande (en particulier UNIX / Linux), l’utilisation des FIFO peut être comparée aux tubes (pipes) entre commandes : la sortie d’un programme devient l’entrée de l’autre, et une machinerie magique entre les deux s’occupe du reste.

En raison de leur usage omniprésent, il existe un accord de fait sur la façon dont un FIFO FPGA doit se comporter. Chaque logiciel de développement FPGA permet de générer un module IP de type FIFO destiné à la conception de l’application. Non seulement cela, mais ce module FIFO a très probablement un certain ensemble de ports qui se comportent comme ceux de n’importe quel autre FIFO FPGA.

Le logiciel fourni par le fabricant du FPGA permet de créer des FIFO adaptés à vos besoins précis. Il suffit de configurer ses attributs dans un outil graphique (largeur, profondeur et autres attributs dont je parlerai), et les outils s’occupent du reste. Il ne vous reste plus qu’à faire une instanciation (instantiation) du module dans votre conception. Contrairement à beaucoup d’autres tâches du monde FPGA, celle-ci est réellement aussi simple que cela.

Chaque fabricant de FPGA présentant son propre module IP FIFO, il est évidemment important de lire la documentation jusque dans les petits caractères. Les différents fabricants de FPGA utilisent une terminologie légèrement différente pour décrire leurs FIFO. Les noms des ports du module sont également légèrement différents. Chaque fabricant propose aussi un ensemble un peu différent de fonctionnalités supplémentaires et d’options de configuration.

Cela dit, il y a de fortes chances que les réglages par défaut correspondent à ce que j’appellerai un « FIFO de base ». Par-dessus cela, il existe certainement un ensemble de fonctions supplémentaires toujours disponibles.

Toutefois, la manière dont le FIFO est implémenté dans la logique du FPGA diffère d’un fabricant à l’autre. Il est donc important de comprendre la signification des attributs du FIFO pour bien utiliser les ressources du FPGA.

En fin de compte, apprendre à connaître et à comprendre les FIFO FPGA est un effort à ne fournir qu’une seule fois. Une fois que vous avez appris à les utiliser sur un FPGA, il est assez facile de faire de même sur un autre. Ce qui est en soi une raison de leur omniprésence.

Le FIFO de base

Il n’existe pas de norme écrite pour les FIFO FPGA, mais un large accord existe néanmoins sur leur comportement.

Tous les FIFO ont deux interfaces, une pour écrire des mots et une pour en lire. Examinons l’instanciation de ce que j’appellerai le « FIFO de base ». Il existe d’importantes variantes, que j’aborderai plus tard.

myfifo myfifo_ins
  (
   .rst(rst),       // Asynchronous reset input

   // Write interface ports
   .wr_clk(wr_clk), // Write clock input
   .wr_en(wr_en),   // Write Enable input
   .din(din),       // Write word input 
   .full(full),     // Full output

   // Read interface ports
   .rd_clk(rd_clk), // Read Clock input
   .rd_en(rd_en),   // Read Enable input
   .dout(dout),     // Read word output
   .empty(empty)    // Empty output
);

Les noms des ports sont ceux utilisés par les outils de Xilinx, mais les autres fabricants de FPGA utilisent des noms similaires.

Les ports du module FIFO se divisent en trois groupes : un signal de reset @rst, sur lequel je reviendrai plus tard. Et, comme on peut s’y attendre, il y a une interface d’écriture et une interface de lecture, chacune composée de quatre ports.

Pour @din et @dout, ce sont deux ports vectoriels qui transportent les mots de données qui entrent dans le FIFO et qui en sortent. La largeur de ces mots est un choix que vous faites lors de la configuration du FIFO dans l’outil logiciel concerné. Vous définissez aussi la profondeur du FIFO, c’est-à-dire le nombre de mots qu’il peut contenir. Ces deux paramètres influencent la quantité de ressources mémoire du FPGA que le FIFO consomme.

Horloges

Il convient de noter que chacune de ces deux interfaces possède sa propre horloge : @wr_clk et @rd_clk. Les autres ports de chaque interface sont synchrones avec l’horloge correspondante.

Les FIFO sont souvent utilisés pour faire passer des données d’un domaine d’horloge (clock domain) à un autre, justement parce qu’ils ont deux horloges : si une partie de votre logique est synchrone avec clk_A, et une autre partie avec clk_B, comment les faire fonctionner ensemble ? La première idée de tout ingénieur FPGA est de placer un FIFO entre elles. Cela vient surtout du fait que le changement de domaine d’horloge (clock domain crossing) est un vrai casse-tête, et qu’utiliser un FIFO résout le problème facilement et en toute sécurité.

L’interface d’écriture

L’interface d’écriture est simple : @wr_clk, @wr_en et @din sont des entrées du FIFO, et @full (plein) est une sortie.

Quand @wr_en est à l’état haut sur le front montant de @wr_clk, le mot présent sur @din est écrit dans le FIFO. Le port @full (plein) est à l’état haut quand le FIFO est full (plein).

Par exemple, voici un chronogramme correspondant à l’écriture de cinq mots dans le FIFO :

Example waveform for writing words to a FIFO

Sur ce chronogramme, la logique de l’application écrit d’abord les mots D0 et D1. Le FIFO passe sa sortie @full (plein) à l’état haut pour indiquer qu’il est devenu full (plein) après l’écriture réussie de D1. La logique de l’application réagit en mettant @wr_en à l’état bas au même cycle d’horloge. Après quelques cycles d’horloge, le FIFO repasse @full (plein) à l’état bas pour signaler qu’on peut de nouveau écrire. Cela est très probablement dû à une activité de l’autre côté (c’est-à-dire que des données ont été lues depuis le FIFO).

La logique de l’application aurait pu commencer à écrire dès le cycle d’horloge où @full (plein) est passé à l’état bas ; dans cet exemple précis, elle commence un peu plus tard. Comme le montre le chronogramme, trois mots supplémentaires sont écrits.

Sur le chronogramme, lorsque @din est marqué avec la valeur « Dx », cela signifie que la valeur est ignorée et qu’il importe donc peu de savoir ce qui s’y trouve. Par exemple, dans la zone « Dx » entre D1 et D2, @din aurait pu rester sur D1, passer à D2 plus tôt que représenté, ou porter une valeur complètement différente. Le résultat aurait été le même.

Pour un exemple de code simple, supposons que je veuille remplir le FIFO avec des mots qui comptent de façon croissante, chaque fois que c’est possible :

assign wr_en = !full;

always @(posedge wr_clk)
  if (wr_en)
    din <= din + 1;

Cet exemple illustre la relation correcte entre @full (plein) et @wr_en : si @full (plein) est à l’état haut, @wr_en doit être à l’état bas au même cycle d’horloge. Et s’il ne l’est pas ? Et si nous ignorons le signal @full (plein) ? Il y a de fortes chances pour que le FIFO ignore @wr_en dans ce cas. Il se comportera donc probablement comme si son port @wr_en était connecté à @the_real_wr_en, défini comme suit :

assign the_real_wr_en = wr_en && !full;

Cependant, certains outils FPGA permettent de configurer le FIFO sans ce mécanisme de sécurité. Dans ce cas, pratiquement n’importe quoi peut se produire si l’on tente d’écrire une donnée dans un FIFO qui est full (plein).

D’une manière ou d’une autre, @full (plein) doit être respecté, sinon on aura l’impression que des données se sont échappées. Reprenons l’exemple ci-dessus : si @wr_en était resté à l’état haut en permanence, @din n’aurait pas cessé de compter, que les données aient été écrites ou non dans le FIFO. Ainsi, à la lecture des données de l’autre côté, la suite croissante aurait présenté des discontinuités.

Notez que @full (plein) ne peut passer de l’état bas à l’état haut que par un cycle d’écriture, c’est-à-dire immédiatement après un front montant d’horloge lorsque @wr_en est à l’état haut. Sauf lorsque le FIFO est réinitialisé, comme nous le verrons plus loin.

L’interface de lecture

L’interface de lecture est assez similaire, mais pas exactement la même. @rd_clk et @rd_en sont des entrées du FIFO ; @dout et @empty (vide) sont des sorties.

Quand @rd_en est à l’état haut sur le front montant de @rd_clk, un nouveau mot est lu depuis la mémoire du FIFO, et @dout est mis à jour avec sa valeur après ce front montant, c’est-à-dire au cycle d’horloge suivant. Le port @empty (vide) est à l’état haut quand le FIFO est empty (vide).

Dans ce chronogramme d’exemple, cinq mots sont lus depuis le FIFO :

Example waveform for reading words from a FIFO

Sur ce chronogramme, la logique de l’application commence par lire trois mots. En réponse au passage de @empty (vide) à l’état haut (en même temps que D2 apparaît sur @dout), la logique de l’application met @rd_en à l’état bas dans le même cycle d’horloge. Comme précédemment, il aurait été acceptable de repasser @rd_en à l’état haut dès le cycle où @empty (vide) est revenu à l’état bas (en raison de données écrites dans le FIFO de l’autre côté). Au lieu de cela, elle attend quelques cycles, puis lit deux mots supplémentaires.

Et maintenant, une petite remarque : si vous comparez ce chronogramme au précédent, vous remarquerez peut-être que cinq mots ont été écrits et que cinq mots ont été lus. Pourquoi @empty (vide) n’est-il pas passé à l’état haut en même temps que D4 apparaissait ? Parce que je voulais montrer qu’il est acceptable d’arrêter la lecture même si le FIFO n’est pas empty (vide). Pour les besoins de cet exemple imaginaire, d’autres mots ont été écrits dans le FIFO ; il ne s’est donc pas vidé après la lecture de D4.

Notez que @dout conserve sa valeur quand @rd_en est à l’état bas. La logique de l’application peut compter là-dessus : @dout contient toujours la valeur du dernier mot lu depuis le FIFO (sauf après un reset).

Plus important encore, notez que la nouvelle valeur de @dout apparaît après le front montant où @rd_en est à l’état haut. Le FIFO se comporte donc comme le code Verilog suivant :

always @(posedge rd_clk)
  if (rd_en && !empty)
    dout <= next_word_to_show;

Ce code Verilog factice montre aussi que la plupart des FIFO ignorent @rd_en lorsque @empty (vide) est à l’état haut au même cycle d’horloge. Comme pour l’interface d’écriture, @rd_en ne doit pas être à l’état haut si @empty (vide) est à l’état haut pendant ce cycle. Encore une fois, certains FIFO peuvent être configurés sans ce mécanisme de protection ; ne transgressez donc pas cette règle.

@empty (vide) ne peut passer de l’état bas à l’état haut qu’après un cycle de lecture, c’est-à-dire après que @rd_en a été à l’état haut sur le front montant de l’horloge. La seule exception est le reset du FIFO.

À titre d’exemple, voici un extrait de code Verilog simplifié (sans reset) qui lit des mots depuis le FIFO et calcule la somme cumulée.

assign rd_en = want_to_read_now && !empty;

always @(posedge rd_clk)
  begin
    rd_en_d <= rd_en;

    if (rd_en_d)
      sum <= sum + dout; // Don't try this at home: @sum is never reset.
  end

Pour les besoins de la démonstration, j’ai ajouté un signal @want_to_read_now, qui indique que la logique souhaite lire. @rd_en n’est malgré tout à l’état haut que si le FIFO n’est pas empty (vide).

Faites attention à @rd_en_d, qui contient la valeur de @rd_en avec un retard d’un cycle d’horloge. Par conséquent, @rd_en_d est à l’état haut au moment même où une valeur nouvelle et valide se trouve sur @dout. C’est pourquoi @rd_en_d est utilisé comme condition pour consommer la valeur de @dout. Le délai entre @rd_en et @dout complique un peu les choses, comme le montre cet exemple.

Synchronisation et latence

Comme j’ai dessiné les chronogrammes d’exemple ci-dessus séparément pour l’écriture et la lecture, ils omettent un point important : il faut quelques cycles d’horloge entre l’écriture du premier mot dans un FIFO empty (vide) et le passage de @empty (vide) à l’état bas. De même, il faut quelques cycles d’horloge entre la lecture du premier mot d’un FIFO full (plein) et le passage de @full (plein) à l’état bas.

Cela vient du fait que l’information concernant l’écriture dans le FIFO doit se propager à travers les deux domaines d’horloge avant d’atteindre l’autre côté du FIFO. La logique nécessaire au franchissement des domaines d’horloge introduit un délai de quelques cycles d’horloge. La sortie @empty (vide) réagit donc un peu plus tard. Même histoire pour la sortie @full (plein).

Alors, de combien de cycles d’horloge s’agit-il ? Cela dépend de beaucoup de choses, notamment de la relation temporelle entre les fronts des deux horloges à l’instant considéré. Bref, c’est difficile à dire.

Parmi les éléments qui influent sur ce délai figure le nombre d’étages de synchronisation, souvent un paramètre que l’on peut régler pour le FIFO. Deux étages est un choix courant, mais on peut en sélectionner davantage. Cela peut contribuer à la fiabilité du FIFO, au prix d’une utilisation un peu plus importante de ressources logiques. Cela augmente aussi la latence des sorties @empty (vide) et @full (plein), comme nous venons de le voir.

Si vous tenez vraiment à dorloter votre FIFO, passez les étages de synchronisation à trois, pour vous sentir absolument super en sécurité.

L’entrée de reset

Tous les FIFO FPGA ont un signal de reset. Comme le FIFO utilise deux horloges, on n’attend pas de ce signal de reset qu’il soit synchrone avec l’une ou l’autre ; il est donc asynchrone. La logique interne du FIFO se charge de synchroniser le reset en interne pour chacun des deux domaines d’horloge.

Que fait donc le reset ? Pour commencer, il vide le FIFO et met @empty (vide) à l’état haut. S’il y avait des données dans le FIFO, elles sont perdues.

Quant à la sortie @full (plein), il est courant (et recommandé) que les FIFO maintiennent cette sortie à l’état haut après un reset, jusqu’à ce que le FIFO soit prêt à recevoir des données (c’est-à-dire à effectuer des cycles d’écriture). Ce comportement peut toutefois être optionnel ; il est donc recommandé de vérifier ce point dans la documentation du FIFO. Après tout, après un reset, le FIFO n’est pas réellement full (plein). De plus, faire passer @full (plein) à l’état haut à cause d’un reset enfreint la règle mentionnée plus haut : @full (plein) ne doit normalement passer à l’état haut qu’à la suite d’une écriture.

Il est important de savoir qu’il s’écoule quelques cycles d’horloge entre le moment où le signal de reset devient actif et celui où les sorties @empty (vide) et @full (plein) passent à l’état haut. Cela est dû à la logique de synchronisation du FIFO. Les choses sont donc un peu floues pendant les quelques cycles d’horloge qui entourent l’activation du reset. Assurez-vous que la logique de l’application ne tente pas d’écrire ou de lire dans le FIFO pendant ces quelques cycles autour du reset.

Même si le signal de reset est asynchrone, il doit provenir de la sortie d’un registre (bascule) du FPGA. Le reset ne doit pas être la sortie d’une logique combinatoire (combinatorial logic), car le FIFO pourrait subir des resets involontaires à cause de glitches (parasites).

En fait, beaucoup d’ingénieurs FPGA supposent à tort que brancher pratiquement n’importe quoi sur l’entrée de reset fonctionnera. Le fabricant du FPGA peut pourtant avoir des exigences inattendues concernant le signal de reset. Par exemple, ceci est tiré du guide produit de son FIFO (PG057) de Xilinx :

Si le reset asynchrone dure un cycle de l’horloge la plus lente et que son activation se produit très près du front montant de cette horloge, la détection du reset peut ne pas se faire correctement et provoquer un comportement inattendu. Pour éviter de telles situations, il est toujours recommandé de maintenir le reset asynchrone actif pendant au moins 3 [ ... ] cycles de l’horloge la plus lente...

(Chapitre 3, « Resets »)

Xilinx recommande donc que le reset soit actif pendant au moins trois cycles d’horloge. Je ne suis pas sûr que beaucoup de gens connaissent cette recommandation. Quoi qu’il en soit, lisez le guide d’utilisation du FIFO de votre fabricant pour savoir comment générer correctement ce signal de reset.

Comment le FIFO est implémenté

Même si les outils logiciels du fabricant s’occupent de tout pour que le FIFO fonctionne correctement, il est bon de savoir quelles ressources du FPGA sont utilisées, en particulier pour éviter une pénurie de certains types de ressources.

Chaque FPGA a ses options, mais je vais mentionner brièvement quelques options courantes :

Ceci clôt la première page de cette série consacrée aux FIFO. La page suivante traite des variantes courantes des FIFO et de leurs fonctionnalités supplémentaires.

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)