Conception d'un tampon circulaire VHDL robuste en mémoire bloc
Les tampons circulaires sont des structures populaires pour créer des files d'attente dans les langages de programmation séquentiels, mais ils peuvent également être implémentés en matériel. Dans cet article, nous allons créer un tampon circulaire en VHDL pour implémenter une FIFO en mémoire bloc (block RAM).
Fonctionnement d'un tampon circulaire
Un tampon circulaire est une implémentation de FIFO qui utilise une mémoire contiguë pour stocker les données mises en tampon avec un minimum de déplacement de données. Les nouveaux éléments restent au même emplacement mémoire depuis leur écriture jusqu'à leur lecture et suppression de la FIFO.
Deux compteurs permettent de suivre l'emplacement et le nombre d'éléments dans la FIFO. Ces compteurs font référence à un décalage par rapport au début de l'espace mémoire où les données sont stockées. En VHDL, il s'agit d'un index vers une cellule de tableau. Pour la suite de cet article, nous désignerons ces compteurs comme des pointeurs.
Ces deux pointeurs sont le head (tête) et le tail (queue). Le head pointe toujours vers l'emplacement mémoire qui contiendra la prochaine donnée écrite, tandis que le tail désigne l'élément suivant qui sera lu depuis la FIFO. D'autres variantes existent, mais c'est celle que nous allons utiliser.
État vide
Si le head et le tail pointent vers le même élément, cela signifie que la FIFO est vide. L'image ci-dessus montre une FIFO exemple avec huit emplacements. Les pointeurs head et tail pointent tous deux vers l'élément 0, indiquant que la FIFO est vide. C'est l'état initial du tampon circulaire.
Notez que la FIFO serait toujours vide si les deux pointeurs étaient à un autre index, par exemple 3. À chaque écriture, le pointeur head avance d'une position. Le pointeur tail est incrémenté chaque fois que l'utilisateur de la FIFO lit un élément.
Lorsque l'un des pointeurs atteint l'index le plus élevé, la prochaine écriture ou lecture fera revenir le pointeur à l'index le plus bas. C'est la beauté du tampon circulaire : les données ne bougent pas, seuls les pointeurs le font.
Head devance tail
L'image ci-dessus montre le même tampon circulaire après cinq écritures. Le pointeur tail est toujours à l'emplacement 0, mais le pointeur head a avancé à l'emplacement 5. Les emplacements contenant des données sont colorés en bleu clair dans l'illustration. Le pointeur tail se trouve sur l'élément le plus ancien, tandis que le head pointe vers le prochain emplacement libre.
Lorsque le head a un index supérieur au tail, on peut calculer le nombre d'éléments dans le tampon circulaire en soustrayant le tail du head. Sur l'image ci-dessus, cela donne un comptage de cinq éléments.
Tail devance head
Soustraire le head du tail ne fonctionne que si le head devance le tail. Sur l'image ci-dessus, le head est à l'index 2 tandis que le tail est à l'index 5. Ainsi, si nous effectuons ce calcul simple, nous obtenons 2 - 5 = -3, ce qui n'a pas de sens.
La solution consiste à décaler le head avec le nombre total d'emplacements dans la FIFO, 8 dans ce cas. Le calcul donne maintenant (2 + 8) - 5 = 5, ce qui est la réponse correcte.
Le tail poursuivra éternellement le head, c'est ainsi que fonctionne un tampon circulaire. La moitié du temps, le tail aura un index supérieur au head. Les données sont stockées entre les deux, comme indiqué par la couleur bleu clair sur l'image ci-dessus.
État plein
Un tampon circulaire plein aura le tail pointant vers l'index directement après le head. Une conséquence de ce schéma est qu'on ne peut jamais utiliser tous les emplacements pour stocker des données ; il doit y avoir au moins un emplacement libre. L'image ci-dessus montre une situation où le tampon circulaire est plein. L'emplacement libre mais inutilisable est coloré en jaune.
Un signal dédié empty/full pourrait également être utilisé pour indiquer que le tampon circulaire est plein. Cela permettrait à tous les emplacements mémoire de stocker des données, mais nécessiterait une logique additionnelle sous forme de registres et de tables de recherche (LUT). Par conséquent, nous allons utiliser le schéma garder un emplacement libre pour notre implémentation de la FIFO tampon circulaire, car cela ne gaspille que de la mémoire bloc moins coûteuse.
Implémentation de la FIFO tampon circulaire
La façon dont vous définissez les signaux d'interface vers et depuis votre FIFO va limiter le nombre d'implémentations possibles de votre tampon circulaire. Dans notre exemple, nous allons utiliser une variante de l'interface classique avec enable de lecture/écriture et signaux empty/full/valid.
Il y aura un bus données d'écriture du côté entrée qui transporte les données à pousser dans la FIFO. Il y aura aussi un signal enable d'écriture qui, quand il est activé, fera échantillonner les données d'entrée par la FIFO.
Le côté sortie aura un signal données de lecture et un signal valid de lecture contrôlés par la FIFO. Il aura aussi un signal enable de lecture contrôlé par l'utilisateur en aval de la FIFO.
Les signaux de contrôle empty et full font partie de l'interface FIFO classique, nous les utiliserons aussi. Ils sont contrôlés par la FIFO et leur but est de communiquer l'état de la FIFO au lecteur et à l'écrivain.
Contre-pression (back pressure)
Le problème avec l'attente que la FIFO soit soit vide soit pleine avant d'agir est que la logique d'interface n'aura pas le temps de réagir. La logique séquentielle fonctionne sur une base cycle d'horloge par cycle d'horloge, les fronts montants de l'horloge séparant effectivement les événements de votre conception en pas de temps.
Une solution consiste à inclure des signaux presque vide et presque plein qui précèdent les signaux originaux d'un cycle d'horloge. Cela donne à la logique externe le temps de réagir, même en lecture ou écriture continue.
Dans notre implémentation, les signaux précurseurs seront nommés empty_next et full_next, simplement parce que je préfère utiliser un suffixe plutôt qu'un préfixe pour les noms.
L'entité
L'image ci-dessous montre l'entité de notre FIFO tampon circulaire. En plus des signaux d'entrée et de sortie dans le port, elle a deux constantes génériques. La générique RAM_WIDTH définit le nombre de bits dans les mots d'entrée et de sortie, le nombre de bits que chaque emplacement mémoire contiendra.
La générique RAM_DEPTH définit le nombre d'emplacements qui seront réservés pour le tampon circulaire. Comme un emplacement est réservé pour indiquer que le tampon circulaire est plein, la capacité de la FIFO sera RAM_DEPTH - 1. La constante RAM_DEPTH doit être adaptée à la profondeur de la RAM sur le FPGA cible. La RAM inutilisée dans un bloc de RAM primitif sera gaspillée, elle ne peut pas être partagée avec d'autres logiques dans le FPGA.
entity ring_buffer is
generic (
RAM_WIDTH : natural;
RAM_DEPTH : natural
);
port (
clk : in std_logic;
rst : in std_logic;
-- Port écriture
wr_en : in std_logic;
wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);
-- Port lecture
rd_en : in std_logic;
rd_valid : out std_logic;
rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);
-- Drapeaux
empty : out std_logic;
empty_next : out std_logic;
full : out std_logic;
full_next : out std_logic;
-- Nombre d'éléments dans la FIFO
fill_count : out integer range RAM_DEPTH - 1 downto 0
);
end ring_buffer;
En plus de l'horloge et du reset, la déclaration de port inclut les ports de lecture et d'écriture classiques données/enable. Ceux-ci sont utilisés par les modules amont et aval pour pousser de nouvelles données dans la FIFO et pour dépiler l'élément le plus ancien.
Le signal rd_valid est activé par la FIFO quand le port rd_data contient des données valides. Cet événement est retardé d'un cycle d'horloge après une impulsion sur le signal rd_en. Nous en parlerons plus en détail à la fin de cet article.
Viennent ensuite les drapeaux empty/full définis par la FIFO. Le signal empty_next sera activé quand il reste 1 ou 0 éléments, tandis que empty n'est actif que quand il y a 0 éléments dans la FIFO. De même, le signal full_next indiquera qu'il reste de la place pour 1 ou 0 éléments supplémentaires, tandis que full ne s'active que quand la FIFO ne peut plus accueillir un autre élément de données.
Enfin, il y a une sortie fill_count. C'est un entier qui reflétera le nombre d'éléments actuellement stockés dans la FIFO. J'ai inclus ce signal de sortie simplement parce que nous l'utilisons en interne dans le module. Le faire ressortir par l'entité est essentiellement gratuit, et l'utilisateur peut choisir de laisser ce signal non connecté lors de l'instanciation de ce module.
La région déclarative
Dans la région déclarative du fichier VHDL, nous allons déclarer un type personnalisé, un sous-type, un certain nombre de signaux et une procédure pour usage interne dans le module tampon circulaire.
architecture rtl of ring_buffer is
type ram_type is array (0 to RAM_DEPTH - 1) of std_logic_vector(wr_data'range);
signal ram : ram_type;
subtype index_type is integer range ram_type'range;
signal head : index_type;
signal tail : index_type;
signal empty_i : std_logic;
signal full_i : std_logic;
signal fill_count_i : integer range RAM_DEPTH - 1 downto 0;
-- Procédure d'incrémentation avec retour à zéro
procedure incr(signal index : inout index_type) is
begin
if index = index_type'high then
index <= index_type'low;
else
index <= index + 1;
end if;
end procedure;
D'abord, nous déclarons un nouveau type pour modéliser notre RAM. Le type ram_type est un tableau de vecteurs, dimensionné par les entrées génériques. Le nouveau type est utilisé sur la ligne suivante pour déclarer le signal ram qui contiendra les données dans le tampon circulaire.
Dans le bloc de code suivant, nous déclarons index_type, un sous-type d'entier. Sa plage sera indirectement régie par la générique RAM_DEPTH. Sous la déclaration du sous-type, nous utilisons le type d'index pour déclarer deux nouveaux signaux, les pointeurs head et tail.
Suit ensuite un bloc de déclarations de signaux qui sont des copies internes des signaux d'entité. Ils ont les mêmes noms de base que les signaux d'entité mais sont suffixés par _i pour indiquer qu'ils sont pour usage interne. Nous utilisons cette approche car il est considéré comme de mauvais style d'utiliser le mode inout sur les signaux d'entité, bien que cela aurait le même effet.
Enfin, nous déclarons une procédure nommée incr qui prend un signal index_type comme paramètre. Ce sous-programme sera utilisé pour incrémenter les pointeurs head et tail, et les ramener à 0 quand ils sont à la valeur la plus haute. Les pointeurs head et tail sont des sous-types d'entier, qui ne supportent normalement pas le comportement de retour à zéro. Nous utiliserons la procédure pour contourner ce problème.
Assignations concurrentes
Au sommet de l'architecture, nous déclarons nos assignations concurrentes. Je préfère rassembler ces assignations de signaux en une ligne avant les processus normaux car elles sont facilement négligées. Une assignation concurrente est en fait une forme de processus.
begin
-- Copie des signaux internes vers les sorties
empty <= empty_i;
full <= full_i;
fill_count <= fill_count_i;
-- Définition des drapeaux
empty_i <= '1' when fill_count_i = 0 else '0';
empty_next <= '1' when fill_count_i <= 1 else '0';
full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0';
full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0';
Dans le premier bloc d'assignations concurrentes, nous copions les versions internes des signaux d'entité vers la sortie. Ces lignes assurent que les signaux d'entité suivent les versions internes exactement au même moment, mais avec un délai d'un cycle delta en simulation.
Le second et dernier bloc d'assignations concurrentes est où nous assignons les drapeaux de sortie, signalant l'état plein/vide du tampon circulaire. Nous basons les calculs sur la générique RAM_DEPTH et sur le signal fill_count. La profondeur de la RAM est une constante qui ne changera pas. Par conséquent, les drapeaux ne changeront qu'à la suite d'un fill count mis à jour.
Mise à jour du pointeur head
La fonction de base du pointeur head est de s'incrémenter chaque fois que le signal d'enable d'écriture est activé depuis l'extérieur de ce module. Nous faisons cela en passant le signal head à la procédure incr mentionnée précédemment.
PROC_HEAD : process(clk)
begin
if rising_edge(clk) then
if rst = '1' then
head <= 0;
else
if wr_en = '1' and full_i = '0' then
incr(head);
end if;
end if;
end if;
end process;
Notre code contient une instruction and full_i = '0' additionnelle pour protéger contre les réécritures. Cette logique peut être omise si vous êtes certain que la source de données n'essayera jamais d'écrire dans la FIFO pendant qu'elle est pleine. Sans cette protection, une réécriture fera redevenir le tampon circulaire vide.
Si le pointeur head est incrémenté alors que le tampon circulaire est plein, le head pointera vers le même élément que le tail. Ainsi, le module "oubliera" les données contenues, et le remplissage de la FIFO apparaîtra comme vide.
En évaluant le signal full_i avant d'incrémenter le pointeur head, il ne oubliera que la valeur réécrite. Je trouve cette solution plus propre. Mais dans les deux cas, si des réécritures se produisent, c'est indicatif d'un dysfonctionnement en dehors de ce module.
Mise à jour du pointeur tail
Le pointeur tail est incrémenté de manière similaire au pointeur head, mais l'entrée read_en est utilisée comme déclencheur. Tout comme pour les réécritures, nous protégeons contre les sur-lectures en incluant and empty_i = '0' dans l'expression booléenne.
PROC_TAIL : process(clk)
begin
if rising_edge(clk) then
if rst = '1' then
tail <= 0;
rd_valid <= '0';
else
rd_valid <= '0';
if rd_en = '1' and empty_i = '0' then
incr(tail);
rd_valid <= '1';
end if;
end if;
end if;
end process;
De plus, nous pulsons le signal rd_valid à chaque lecture valide. La donnée de lecture est toujours valide au cycle d'horloge suivant l'assertion de rd_en, si la FIFO n'était pas vide. Avec cette connaissance, il n'y a pas vraiment besoin de ce signal, mais nous l'incluons pour commodité. Le signal rd_valid sera optimisé lors de la synthèse s'il est laissé non connecté lors de l'instanciation du module.
Inférence de mémoire bloc
Pour que l'outil de synthèse infère de la mémoire bloc, nous devons déclarer les ports de lecture et d'écriture dans un processus synchrone sans reset. Nous lirons et écrirons dans la RAM à chaque cycle d'horloge, et laisserons les signaux de contrôle gérer l'utilisation de ces données.
PROC_RAM : process(clk)
begin
if rising_edge(clk) then
ram(head) <= wr_data;
rd_data <= ram(tail);
end if;
end process;
Ce processus ne sait pas quand la prochaine écriture aura lieu, mais il n'en a pas besoin. Au lieu de cela, nous écrivons simplement en continu. Quand le signal head est incrémenté suite à une écriture, nous commençons à écrire dans l'emplacement suivant. Cela verrouillera effectivement la valeur qui a été écrite.
Mise à jour du compteur de remplissage
Le signal fill_count est utilisé pour générer les signaux full et empty, qui à leur tour sont utilisés pour prévenir la réécriture et la sur-lecture de la FIFO. Le compteur de remplissage est mis à jour par un processus combinatoire sensible aux pointeurs head et tail, mais ces signaux ne sont mis à jour qu'au front montant de l'horloge. Par conséquent, le fill count changera également immédiatement après le front d'horloge.
PROC_COUNT : process(head, tail)
begin
if head < tail then
fill_count_i <= head - tail + RAM_DEPTH;
else
fill_count_i <= head - tail;
end if;
end process;
Le fill count est calculé simplement en soustrayant le tail du head. Si l'index du tail est supérieur au head, nous devons ajouter la valeur de la constante RAM_DEPTH pour obtenir le nombre correct d'éléments actuellement dans le tampon circulaire.
Code VHDL complet pour la FIFO tampon circulaire
library ieee;
use ieee.std_logic_1164.all;
entity ring_buffer is
generic (
RAM_WIDTH : natural;
RAM_DEPTH : natural
);
port (
clk : in std_logic;
rst : in std_logic;
-- Port écriture
wr_en : in std_logic;
wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);
-- Port lecture
rd_en : in std_logic;
rd_valid : out std_logic;
rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);
-- Drapeaux
empty : out std_logic;
empty_next : out std_logic;
full : out std_logic;
full_next : out std_logic;
-- Nombre d'éléments dans la FIFO
fill_count : out integer range RAM_DEPTH - 1 downto 0
);
end ring_buffer;
architecture rtl of ring_buffer is
type ram_type is array (0 to RAM_DEPTH - 1) of std_logic_vector(wr_data'range);
signal ram : ram_type;
subtype index_type is integer range ram_type'range;
signal head : index_type;
signal tail : index_type;
signal empty_i : std_logic;
signal full_i : std_logic;
signal fill_count_i : integer range RAM_DEPTH - 1 downto 0;
-- Procédure d'incrémentation avec retour à zéro
procedure incr(signal index : inout index_type) is
begin
if index = index_type'high then
index <= index_type'low;
else
index <= index + 1;
end if;
end procedure;
begin
-- Copie des signaux internes vers les sorties
empty <= empty_i;
full <= full_i;
fill_count <= fill_count_i;
-- Définition des drapeaux
empty_i <= '1' when fill_count_i = 0 else '0';
empty_next <= '1' when fill_count_i <= 1 else '0';
full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0';
full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0';
-- Mise à jour du pointeur head en écriture
PROC_HEAD : process(clk)
begin
if rising_edge(clk) then
if rst = '1' then
head <= 0;
else
if wr_en = '1' and full_i = '0' then
incr(head);
end if;
end if;
end if;
end process;
-- Mise à jour du pointeur tail en lecture et pulsation valid
PROC_TAIL : process(clk)
begin
if rising_edge(clk) then
if rst = '1' then
tail <= 0;
rd_valid <= '0';
else
rd_valid <= '0';
if rd_en = '1' and empty_i = '0' then
incr(tail);
rd_valid <= '1';
end if;
end if;
end if;
end process;
-- Écriture et lecture de la RAM
PROC_RAM : process(clk)
begin
if rising_edge(clk) then
ram(head) <= wr_data;
rd_data <= ram(tail);
end if;
end process;
-- Mise à jour du fill count
PROC_COUNT : process(head, tail)
begin
if head < tail then
fill_count_i <= head - tail + RAM_DEPTH;
else
fill_count_i <= head - tail;
end if;
end process;
end architecture;
Le code ci-dessus montre le code complet pour la FIFO tampon circulaire. Vous pouvez remplir le formulaire ci-dessous pour recevoir instantanément par email les fichiers de projet ModelSim ainsi que le testbench.
Le testbench
La FIFO est instanciée dans un testbench simple pour démontrer son fonctionnement. Vous pouvez télécharger le code source du testbench ainsi que le projet ModelSim en utilisant le formulaire ci-dessous.
Les entrées génériques ont été définies aux valeurs suivantes :
- RAM_WIDTH: 16
- RAM_DEPTH: 256
Le testbench réinitialise d'abord la FIFO. Quand le reset est relâché, le testbench écrit des valeurs séquentielles (1-255) dans la FIFO jusqu'à ce qu'elle soit pleine. Finalement, la FIFO est vidée avant la fin du test.
Nous pouvons voir la forme d'onde pour l'exécution complète du testbench dans l'image ci-dessous. Le signal fill_count est affiché comme une valeur analogique dans la forme d'onde pour mieux illustrer le niveau de remplissage de la FIFO.

Le head, le tail et le fill count sont à 0 au début de la simulation. Au point où le signal full est activé, le head a la valeur 255, et le signal fill_count aussi. Le fill count ne monte que jusqu'à 255 même si nous avons une profondeur RAM de 256. C'est parce que nous utilisons la méthode garder un emplacement libre pour distinguer entre plein et vide, comme nous l'avons discuté plus haut dans cet article.
Au point de bascule où nous arrêtons d'écrire dans la FIFO et commençons à lire, la valeur du head gèle tandis que le tail et le fill count commencent à diminuer. Finalement, à la fin de la simulation quand la FIFO est vide, le head et le tail ont tous deux la valeur 255 tandis que le fill count est à 0.
Ce testbench ne doit pas être considéré comme adéquat pour autre chose que des fins de démonstration. Il n'a aucun comportement d'auto-vérification ou logique pour vérifier que la sortie de la FIFO est correcte.
Nous utiliserons ce module dans l'article de la semaine prochaine quand nous plongerons dans le sujet de la vérification aléatoire contrainte. C'est une stratégie de test différente des tests dirigés plus couramment utilisés. En bref, le testbench effectuera des interactions aléatoires avec le DUT (device under test), et le comportement du DUT devra être vérifié par un processus de testbench séparé. Finalement, quand un certain nombre d'événements prédéfinis se seront produits, le test sera complet.
Cliquez ici pour lire l'article de suivi :
Vérification aléatoire contrainte
Synthèse dans Vivado
J'ai synthétisé le tampon circulaire dans Xilinx Vivado car c'est l'outil d'implémentation FPGA le plus populaire. Cependant, cela devrait fonctionner sur toutes les architectures FPGA qui ont de la mémoire bloc double port.
Nous devons assigner des valeurs aux entrées génériques pour pouvoir implémenter le tampon circulaire comme un module autonome. Cela se fait dans Vivado en utilisant le menu Settings → General → Generics/Parameters, comme montré dans l'image ci-dessous.

La valeur pour RAM_WIDTH est fixée à 16, ce qui est le même que dans la simulation. Mais j'ai mis RAM_DEPTH à 2048 car c'est la profondeur maximale du primitif RAMB36E1 dans l'architecture Xilinx Zynq que j'ai choisie. Nous aurions pu sélectionner une valeur plus basse, mais cela aurait quand même utilisé le même nombre de mémoires bloc. Une valeur plus élevée aurait entraîné l'utilisation de plus d'une mémoire bloc.
L'image ci-dessous montre l'utilisation des ressources post-implémentation, telle que rapportée par Vivado. Notre tampon circulaire a bien consommé une mémoire bloc et une poignée de LUTs et bascules.

Se passer du signal valid
Vous vous demandez peut-être si le délai d'un cycle d'horloge entre le signal rd_en et le signal rd_valid est réellement nécessaire. Après tout, la donnée est déjà présente sur rd_data quand nous activons le signal rd_en. Ne pouvons-nous pas simplement utiliser cette valeur et laisser le tampon circulaire passer à l'élément suivant au cycle d'horloge suivant pendant que nous lisons depuis la FIFO ?
Strictement parlant, nous n'avons pas besoin du signal valid. J'ai inclus ce signal juste pour commodité. La partie cruciale est qu'il faut attendre le cycle d'horloge après avoir activé le signal rd_en, sinon la RAM n'aura pas le temps de réagir.
La mémoire bloc dans les FPGA sont des composants entièrement synchrones, ils ont besoin d'un front d'horloge à la fois pour lire et écrire des données. L'horloge de lecture et d'écriture n'a pas à provenir de la même source d'horloge, mais il doit y avoir des fronts d'horloge. De plus, il ne peut y avoir aucune logique entre la sortie de la RAM et le prochain registre (bascules). C'est parce que le registre utilisé pour horloger la sortie de la RAM est à l'intérieur du primitif de mémoire bloc.
L'image ci-dessus montre un diagramme de chronogramme de la façon dont une valeur se propage depuis l'entrée wr_data dans notre tampon circulaire, à travers la RAM, et apparaît finalement sur la sortie rd_data. Comme chaque signal est échantillonné sur le front montant de l'horloge, il faut trois cycles d'horloge depuis le moment où nous commençons à piloter le port d'écriture avant qu'il n'apparaisse sur le port de lecture. Et un cycle d'horloge additionnel passe avant que le module récepteur ne puisse utiliser cette donnée.
Réduction de la latence
Il existe des moyens d'atténuer ce problème, mais cela a un coût en ressources supplémentaires utilisées dans le FPGA. Essayons une expérience pour réduire d'un cycle d'horloge le délai du port de lecture de notre tampon circulaire. Dans l'extrait de code ci-dessous, nous avons changé la sortie rd_data d'un processus synchrone à un processus combinatoire sensible aux signaux ram et tail.
PROC_READ : process(ram, tail)
begin
rd_data <= ram(tail);
end process;
Malheureusement, ce code ne peut pas être mappé sur de la mémoire bloc car il peut y avoir de la logique combinatoire entre la sortie de la RAM et le premier registre en aval sur le signal rd_data.
L'image ci-dessous montre l'utilisation des ressources telle que rapportée par Vivado. La mémoire bloc a été remplacée par de la LUTRAM ; une forme de RAM distribuée implémentée dans les LUTs. L'utilisation des LUT a explosé de 37 LUTs à 947. Les tables de recherche et les bascules sont plus chères que la mémoire bloc, c'est toute la raison pour laquelle nous avons de la mémoire bloc en premier lieu.

Il existe de nombreuses façons d'implémenter une FIFO tampon circulaire en mémoire bloc. Vous pourriez être capable d'économiser le cycle d'horloge supplémentaire en utilisant un autre design, mais cela coûtera sous forme de logique de support supplémentaire. Pour la plupart des applications, le tampon circulaire présenté dans cet article sera suffisant.
Mise à jour :
Comment créer une FIFO tampon circulaire en mémoire bloc en utilisant le handshake AXI ready/valid
Dans le prochain article de blog, nous créerons un meilleur testbench pour le module tampon circulaire en utilisant la vérification aléatoire contrainte.
Cliquez ici pour lire l'article de suivi :
Vérification aléatoire contrainte
VHDL
- Créer des listes de chaînes en VHDL : Bonnes pratiques et exemple
- Créer un banc de test piloté par Tcl pour un module verrou code VHDL
- Terminer proprement une simulation de banc de test VHDL
- Implémenter un contrôleur PWM en VHDL : Conception, simulation et démo FPGA
- Générer des nombres aléatoires en VHDL : De l'uniforme à OSVVM
- Comment construire un testbench auto-vérifiant en VHDL - Guide pratique
- Implémentation d'une liste chainée dynamique en VHDL avec types protégés et pointeurs d'accès
- Exploiter les procédures in-process pour un design FSM VHDL plus propre
- Maîtriser les fonctions VHDL : Guide pratique pour un design efficace