Fabrication industrielle
Internet des objets industriel | Matériaux industriels | Entretien et réparation d'équipement | Programmation industrielle |
home  MfgRobots >> Fabrication industrielle >  >> Industrial programming >> VHDL

Comment créer un tampon circulaire FIFO en VHDL

Les tampons circulaires sont des constructions populaires pour créer des files d'attente dans les langages de programmation séquentiels, mais ils peuvent également être implémentés dans le matériel. Dans cet article, nous allons créer un tampon circulaire en VHDL pour implémenter un FIFO dans la RAM de bloc.

Vous devrez prendre de nombreuses décisions de conception lors de la mise en œuvre d'un FIFO. De quel type d'interface avez-vous besoin ? Êtes-vous limité par les ressources ? Doit-il être résistant à la lecture excessive et à l'écrasement ? La latence est-elle acceptable ? Ce sont quelques-unes des questions qui me viennent à l'esprit lorsqu'on me demande de créer un FIFO.

Il existe de nombreuses implémentations FIFO gratuites en ligne, ainsi que des générateurs FIFO comme Xilinx LogiCORE. Mais encore, de nombreux ingénieurs préfèrent implémenter leurs propres FIFO. Parce que même s'ils effectuent tous les mêmes tâches de base de mise en file d'attente et de retrait de la file d'attente, ils peuvent être très différents lorsqu'il s'agit de prendre en compte les détails.

Comment fonctionne un tampon circulaire

Un tampon en anneau est une implémentation FIFO qui utilise une mémoire contiguë pour stocker les données mises en mémoire tampon avec un minimum de mélange de données. Les nouveaux éléments restent au même emplacement mémoire depuis le moment de l'écriture jusqu'à ce qu'ils soient lus et supprimés de la FIFO.

Deux compteurs sont utilisés pour garder une trace de l'emplacement et du 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, ce sera un index vers une cellule de tableau. Pour la suite de cet article, nous désignerons ces compteurs par des pointeurs .

Ces deux pointeurs sont la tête et queue pointeurs. La tête pointe toujours vers l'emplacement mémoire qui contiendra les prochaines données écrites, tandis que la queue fait référence au prochain élément qui sera lu dans la FIFO. Il existe d'autres variantes, mais c'est celle-ci que nous allons utiliser.

État vide

Si la tête et la queue pointent vers le même élément, cela signifie que la FIFO est vide. L'image ci-dessus montre un exemple de FIFO avec huit emplacements. Les pointeurs de tête et de queue pointent tous les deux vers l'élément 0, indiquant que la FIFO est vide. Il s'agit de l'état initial du tampon circulaire.

Notez que le FIFO serait toujours vide si les deux pointeurs étaient à un autre index, par exemple, 3. Pour chaque écriture, le pointeur principal avance d'une place. Le pointeur de queue est incrémenté à chaque fois que l'utilisateur de la FIFO lit un élément.

Lorsque l'un des pointeurs se trouve à 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.

La tête mène à la queue

L'image ci-dessus montre le même tampon circulaire après cinq écritures. Le pointeur de queue est toujours à l'emplacement numéro 0, mais le pointeur de tête s'est déplacé vers l'emplacement numéro 5. Les emplacements contenant des données sont colorés en bleu clair dans l'illustration. Le pointeur de queue sera sur l'élément le plus ancien, tandis que la tête pointe vers le prochain emplacement libre.

Lorsque la tête a un indice supérieur à la queue, nous pouvons calculer le nombre d'éléments dans le tampon circulaire en soustrayant la queue de la tête. Dans l'image ci-dessus, cela donne un nombre de cinq éléments.

La queue mène la tête

Soustraire la tête de la queue ne fonctionne que si la tête mène la queue. Dans l'image ci-dessus, la tête est à l'indice 2 tandis que la queue est à l'indice 5. Ainsi, si nous effectuons ce calcul simple, nous obtenons 2 – 5 =-3, ce qui n'a aucun sens.

La solution consiste à décaler la tête avec le nombre total de slots dans la FIFO, 8 dans ce cas. Le calcul donne maintenant (2 + 8) - 5 =5, ce qui est la bonne réponse.

La queue poursuivra toujours la tête, c'est ainsi que fonctionne un tampon en anneau. La moitié du temps, la queue aura un indice plus élevé que la tête. Les données sont stockées entre les deux, comme indiqué par la couleur bleu clair dans l'image ci-dessus.

État complet

Un tampon annulaire complet aura la queue pointant vers l'index directement après la tête. Une conséquence de ce schéma est que nous ne pouvons jamais utiliser tous les slots pour stocker des données, il doit y avoir au moins un slot libre. L'image ci-dessus montre une situation où le tampon circulaire est plein. L'emplacement ouvert, mais inutilisable, est coloré en jaune.

Un signal vide/plein dédié pourrait également être utilisé pour indiquer que la mémoire tampon en anneau est pleine. Cela permettrait à tous les emplacements de mémoire de stocker des données, mais cela nécessite une logique supplémentaire sous la forme de registres et de tables de consultation (LUT). Par conséquent, nous allons utiliser le gardez-en un ouvert schéma pour notre implémentation du FIFO de tampon en anneau, car cela ne fait que gaspiller de la RAM de bloc moins chère.

L'implémentation FIFO du tampon en anneau

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 en anneau. Dans notre exemple, nous allons utiliser une variante de l'interface classique d'activation de lecture/écriture et vide/plein/valide.

Il y aura une écriture de données bus côté entrée qui transporte les données à pousser vers la FIFO. Il y aura également une autorisation d'écriture signal, qui, lorsqu'il est affirmé, entraînera le FIFO à échantillonner les données d'entrée.

Le côté sortie aura une lire les données et un lecture valide signal contrôlé par le FIFO. Il aura également une autorisation de lecture signal contrôlé par l'utilisateur en aval du FIFO.

Le vide et plein les signaux de contrôle font partie de l'interface FIFO classique, nous les utiliserons également. 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

Le problème d'attendre que le FIFO soit vide ou plein avant d'agir est que la logique d'interfaçage n'aura pas le temps de réagir. La logique séquentielle fonctionne cycle d'horloge à cycle d'horloge, les fronts montants de l'horloge séparent efficacement les événements de votre conception en pas de temps.

Une solution consiste à inclure presque vide et presque plein signaux qui précèdent les signaux d'origine d'un cycle d'horloge. Cela donne à la logique externe le temps de réagir, même lors d'une lecture ou d'une écriture continue.

Dans notre implémentation, les signaux précédents seront nommés empty_next et full_next , simplement parce que je préfère postfixer plutôt que préfixer les noms.

L'entité

L'image ci-dessous montre l'entité de notre tampon circulaire FIFO. En plus des signaux d'entrée et de sortie dans le port, il possède deux constantes génériques. Le RAM_WIDTH générique 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.

Le RAM_DEPTH générique définit le nombre d'emplacements qui seront réservés pour le tampon en anneau. Comme un emplacement est réservé pour indiquer que le tampon circulaire est plein, la capacité de la FIFO sera de RAM_DEPTH – 1. Le RAM_DEPTH La constante doit correspondre à la profondeur de la RAM sur le FPGA cible. La RAM inutilisée dans une primitive de bloc RAM sera gaspillée, elle ne peut pas être partagée avec une autre logique dans le FPGA.

entity ring_buffer is
  generic (
    RAM_WIDTH : natural;
    RAM_DEPTH : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- Write port
    wr_en : in std_logic;
    wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Read port
    rd_en : in std_logic;
    rd_valid : out std_logic;
    rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Flags
    empty : out std_logic;
    empty_next : out std_logic;
    full : out std_logic;
    full_next : out std_logic;

    -- The number of elements in the FIFO
    fill_count : out integer range RAM_DEPTH - 1 downto 0
  );
end ring_buffer;

En plus de l'horloge et de la réinitialisation, la déclaration de port inclura les ports classiques de lecture et d'écriture de données/activation. Ceux-ci sont utilisés par les modules en amont et en aval pour pousser de nouvelles données vers le FIFO et pour en extraire l'élément le plus ancien.

Le rd_valid signal est affirmé par le FIFO lorsque le rd_data port contient des données valides. Cet événement est retardé d'un cycle d'horloge après une impulsion sur le rd_en signal. Nous en dirons plus sur pourquoi il doit en être ainsi à la fin de cet article.

Vient ensuite les drapeaux vide/plein définis par le FIFO. Le empty_next le signal sera affirmé lorsqu'il reste 1 ou 0 éléments, tandis que empty n'est actif que lorsqu'il y a 0 éléments dans la FIFO. De même, le full_next signal indiquera qu'il y a de la place pour 1 ou 0 éléments supplémentaires, tandis que full n'affirme que lorsque le FIFO ne peut pas accueillir un autre élément de données.

Enfin, il y a un fill_count production. Il s'agit d'un nombre 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 allons l'utiliser en interne dans le module. Le casser à travers 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 à usage interne dans le module de tampon en anneau.

  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;

  -- Increment and wrap
  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;

Tout d'abord, nous déclarons un nouveau type pour modéliser notre RAM. Le ram_type 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 ram signal qui contiendra les données dans la mémoire tampon en anneau.

Dans le bloc de code suivant, nous déclarons index_type , un sous-type d'entier. Sa portée sera indirectement régie par le RAM_DEPTH générique. Sous la déclaration de sous-type, nous utilisons le type index pour déclarer deux nouveaux signaux, les pointeurs de tête et de queue.

Vient ensuite un bloc de déclarations de signal qui sont des copies internes des signaux d'entité. Ils ont les mêmes noms de base que les signaux d'entité mais sont postfixés avec _i pour indiquer qu'ils sont à usage interne. Nous utilisons cette approche car il est considéré comme un mauvais style d'utiliser inout mode 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 index_type signal comme paramètre. Ce sous-programme sera utilisé pour incrémenter les pointeurs de tête et de queue et les ramener à 0 lorsqu'ils sont à la valeur la plus élevée. La tête et la queue sont des sous-types d'entier, qui ne prennent normalement pas en charge le comportement d'habillage. Nous allons utiliser la procédure pour contourner ce problème.

Énoncés simultanés

Au sommet de l'architecture, nous déclarons nos instructions concurrentes. Je préfère rassembler ces affectations de signaux en une seule ligne avant les processus normaux car elles sont facilement négligées. Une déclaration simultanée est en fait une forme de processus, vous pouvez en savoir plus sur les déclarations simultanées ici :

Comment créer une instruction simultanée en VHDL

  -- Copy internal signals to output
  empty <= empty_i;
  full <= full_i;
  fill_count <= fill_count_i;

  -- Set the flags
  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'affectations simultanées, nous copions les versions internes des signaux d'entité vers la sortie. Ces lignes garantiront que les signaux d'entité suivent les versions internes exactement au même moment, mais avec un retard de cycle delta dans la simulation.

Le deuxième et dernier bloc d'instructions simultanées est l'endroit où nous attribuons les drapeaux de sortie, signalant l'état plein/vide du tampon en anneau. Nous basons les calculs sur le RAM_DEPTH générique et sur le fill_count signal. La profondeur de la RAM est une constante qui ne changera pas. Par conséquent, les drapeaux ne changeront qu'à la suite d'un nombre de remplissages mis à jour.

Mettre à jour le pointeur principal

La fonction de base du pointeur de tête est de s'incrémenter chaque fois que le signal de validation d'écriture est appliqué depuis l'extérieur de ce module. Nous le faisons en passant le head signal au incr mentionné précédemment procédure.

  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 un and full_i = '0' supplémentaire déclaration de protection contre les écrasements. Cette logique peut être omise si vous êtes certain que la source de données n'essaiera jamais d'écrire dans la FIFO tant qu'elle est pleine. Sans cette protection, un écrasement videra à nouveau le tampon circulaire.

Si le pointeur de tête est incrémenté alors que le tampon circulaire est plein, la tête pointera vers le même élément que la queue. Ainsi, le module "oubliera" les données contenues, et le remplissage FIFO apparaîtra vide.

En évaluant le full_i signal avant d'incrémenter le pointeur de tête, il n'oubliera que la valeur écrasée. Je pense que cette solution est plus agréable. Mais dans tous les cas, si des écrasements se produisent, cela indique un dysfonctionnement en dehors de ce module.

Mettre à jour le pointeur de queue

Le pointeur de queue est incrémenté de la même manière que le pointeur de tête, mais le read_en l'entrée est utilisée comme déclencheur. Tout comme pour les écrasements, nous nous protégeons contre les lectures excessives 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 rd_valid signal à chaque lecture valide. Les données lues sont toujours valides sur le cycle d'horloge après rd_en a été affirmé, si le FIFO n'était pas vide. Avec cette connaissance, il n'y a pas vraiment besoin de ce signal, mais nous l'inclurons pour plus de commodité. Le rd_valid le signal sera optimisé dans la synthèse s'il n'est pas connecté lorsque le module est instancié.

Déduire la RAM de bloc

Pour que l'outil de synthèse infère le bloc RAM, nous devons déclarer les ports de lecture et d'écriture dans un processus synchrone sans réinitialisation. Nous allons lire et écrire dans la RAM à chaque cycle d'horloge, et laisser 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'a pas besoin de le savoir. Au lieu de cela, nous écrivons simplement en continu. Lorsque le head signal est incrémenté à la suite d'une écriture, nous commençons à écrire dans l'emplacement suivant. Cela verrouillera efficacement la valeur qui a été écrite.

Mettre à jour le nombre de remplissages

Le fill_count Le signal est utilisé pour générer les signaux plein et vide, qui à leur tour sont utilisés pour empêcher l'écrasement et la surlecture du FIFO. Le compteur de remplissage est mis à jour par un processus combinatoire qui est sensible au pointeur de tête et de queue, mais ces signaux ne sont mis à jour qu'au front montant de l'horloge. Par conséquent, le nombre de remplissages 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 nombre de remplissage est calculé simplement en soustrayant la queue de la tête. Si l'indice de queue est supérieur à la tête, il faut ajouter la valeur du RAM_DEPTH constante pour obtenir le nombre correct d'éléments qui sont actuellement dans le tampon circulaire.

Le code VHDL complet pour le tampon circulaire FIFO

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;

    -- Write port
    wr_en : in std_logic;
    wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Read port
    rd_en : in std_logic;
    rd_valid : out std_logic;
    rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Flags
    empty : out std_logic;
    empty_next : out std_logic;
    full : out std_logic;
    full_next : out std_logic;

    -- The number of elements in the 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;

  -- Increment and wrap
  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

  -- Copy internal signals to output
  empty <= empty_i;
  full <= full_i;
  fill_count <= fill_count_i;

  -- Set the flags
  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';

  -- Update the head pointer in write
  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;

  -- Update the tail pointer on read and pulse 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;

  -- Write to and read from the RAM
  PROC_RAM : process(clk)
  begin
    if rising_edge(clk) then
      ram(head) <= wr_data;
      rd_data <= ram(tail);
    end if;
  end process;

  -- Update the 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 le tampon circulaire FIFO. Vous pouvez remplir le formulaire ci-dessous pour recevoir instantanément les fichiers du projet ModelSim ainsi que le testbench.

Le banc d'essai

Le FIFO est instancié dans un banc de test simple pour démontrer son fonctionnement. Vous pouvez télécharger le code source du testbench avec le projet ModelSim en utilisant le formulaire ci-dessous.

Les entrées génériques ont été définies sur les valeurs suivantes :

Le banc de test réinitialise d'abord le FIFO. Lorsque la réinitialisation est relâchée, le testbench écrit des valeurs séquentielles (1-255) dans la FIFO jusqu'à ce qu'elle soit pleine. Enfin, le FIFO est vidé avant la fin du test.

Nous pouvons voir la forme d'onde pour l'exécution complète du banc d'essai dans l'image ci-dessous. Le fill_count signal est affiché sous forme de valeur analogique dans la forme d'onde pour mieux illustrer le niveau de remplissage du FIFO.

La tête, la queue et le nombre de remplissage sont 0 au début de la simulation. Au point où le full signal est affirmé, la tête a la valeur 255, ainsi que le fill_count signal. Le nombre de remplissage ne monte qu'à 255 même si nous avons une profondeur de RAM de 256. C'est parce que nous utilisons le garder un ouvert méthode pour faire la distinction entre plein et vide, comme nous l'avons vu précédemment dans cet article.

Au tournant où nous arrêtons d'écrire dans le FIFO et commençons à lire à partir de celui-ci, la valeur de tête se fige tandis que la queue et le nombre de remplissage commencent à diminuer. Enfin, à la fin de la simulation lorsque le FIFO est vide, la tête et la queue ont la valeur 255 alors que le nombre de remplissage est de 0.

Ce banc d'essai ne doit pas être considéré comme adéquat pour autre chose que des fins de démonstration. Il n'a aucun comportement ou logique d'auto-vérification pour vérifier que la sortie du FIFO est correcte du tout.

Nous utiliserons ce module dans l'article de la semaine prochaine lorsque nous aborderons le sujet de la vérification aléatoire contrainte . Il s'agit d'une stratégie de test différente des tests dirigés les plus couramment utilisés. En bref, le banc de test effectuera des interactions aléatoires avec le DUT (appareil sous test), et le comportement du DUT doit être vérifié par un processus de banc de test séparé. Enfin, lorsqu'un certain nombre d'événements prédéfinis se sont produits, le test est terminé.

Cliquez ici pour lire l'article de blog suivant :
Vérification aléatoire contrainte

Synthétiser dans Vivado

J'ai synthétisé le tampon en anneau 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 une mémoire RAM à bloc double port.

Nous devons attribuer certaines valeurs aux entrées génériques pour pouvoir implémenter le tampon en anneau en tant que module autonome. Cela se fait dans Vivado en utilisant les ParamètresGénéralGénériques/Paramètres menu, comme indiqué dans l'image ci-dessous.

La valeur pour le RAM_WIDTH est fixé à 16, ce qui est le même que dans la simulation. Mais j'ai mis le RAM_DEPTH à 2048 car c'est la profondeur max de la primitive RAMB36E1 dans l'architecture Xilinx Zynq que j'ai choisie. Nous aurions pu sélectionner une valeur inférieure, mais cela aurait quand même utilisé le même nombre de blocs RAM. Une valeur plus élevée aurait entraîné l'utilisation de plus d'un bloc de RAM.

L'image ci-dessous montre l'utilisation des ressources après la mise en œuvre, telle que rapportée par Vivado. Notre tampon circulaire a en effet consommé un bloc de RAM et une poignée de LUT et de bascules.

Abandonner le signal valide

Vous vous demandez peut-être si le délai d'un cycle d'horloge entre le rd_en et le rd_valid signal est en fait nécessaire. Après tout, les données sont déjà présentes sur rd_data quand on affirme le rd_en signal. Ne pouvons-nous pas simplement utiliser cette valeur et laisser le tampon circulaire passer à l'élément suivant lors du prochain cycle d'horloge pendant que nous lisons à partir du FIFO ?

À proprement parler, nous n'avons pas besoin du valid signal. J'ai inclus ce signal juste pour plus de commodité. La partie cruciale est que nous devons attendre le cycle d'horloge après avoir affirmé le rd_en signal, sinon la RAM n'aura pas le temps de réagir.

Les blocs RAM dans les FPGA sont des composants entièrement synchrones, ils ont besoin d'un front d'horloge pour lire et écrire des données. L'horloge de lecture et d'écriture ne doit pas nécessairement 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 registre suivant (bascules). C'est parce que le registre qui est utilisé pour synchroniser la sortie RAM est à l'intérieur de la primitive de bloc RAM.

L'image ci-dessus montre au chronogramme comment une valeur se propage à partir du wr_data entrée dans notre tampon circulaire, via la RAM, et apparaît enfin sur le rd_data production. Étant donné que chaque signal est échantillonné sur le front d'horloge montant, il faut trois cycles d'horloge à partir du 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 supplémentaire s'écoule avant que le module récepteur puisse utiliser ces données.

Réduire la latence

Il existe des moyens d'atténuer ce problème, mais cela se fait au prix de ressources supplémentaires utilisées dans le FPGA. Essayons une expérience pour couper un délai de cycle d'horloge du port de lecture de notre tampon en anneau. Dans l'extrait de code ci-dessous, nous avons changé le rd_data sortie d'un processus synchrone vers un processus combinatoire sensible au ram et tail signal.

  PROC_READ : process(ram, tail)
   begin
     rd_data <= ram(tail);
   end process;

Malheureusement, ce code ne peut pas être mappé pour bloquer la RAM car il peut y avoir une logique combinatoire entre la sortie de la RAM et le premier registre en aval sur le rd_data signal.

L'image ci-dessous montre l'utilisation des ressources telle que rapportée par Vivado. Le bloc RAM a été remplacé par LUTRAM; une forme de RAM distribuée implémentée dans les LUT. L'utilisation des LUT est passée de 37 LUT à 947. Les tables de consultation et les bascules sont plus chères que la RAM en bloc, c'est la raison pour laquelle nous avons la RAM en bloc en premier lieu.

Il existe de nombreuses façons d'implémenter un FIFO de tampon en anneau dans la RAM de bloc. Vous pourrez peut-être économiser le cycle d'horloge supplémentaire en utilisant une autre conception, 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 un FIFO de tampon en anneau dans la RAM de bloc à l'aide de la poignée de main prête/valide AXI

Dans le prochain article de blog, nous créerons un meilleur banc d'essai pour le module de mémoire tampon en anneau en utilisant la vérification aléatoire contrainte .

Cliquez ici pour lire l'article de blog suivant :
Vérification aléatoire contrainte


VHDL

  1. Comment créer une liste de chaînes en VHDL
  2. Comment créer un banc d'essai piloté par Tcl pour un module de verrouillage de code VHDL
  3. Comment arrêter la simulation dans un testbench VHDL
  4. Comment créer un contrôleur PWM en VHDL
  5. Comment générer des nombres aléatoires en VHDL
  6. Comment créer un banc d'essai d'auto-vérification
  7. Comment créer une liste chaînée en VHDL
  8. Comment utiliser une procédure dans un processus en VHDL
  9. Comment utiliser une fonction en VHDL