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 une FIFO AXI dans un bloc de RAM à l'aide de la poignée de main prête/valide

J'étais un peu agacé par les particularités de l'interface AXI la première fois que j'ai dû créer une logique pour interfacer un module AXI. Au lieu des signaux de contrôle habituels occupé/valide, plein/valide ou vide/valide, l'interface AXI utilise deux signaux de contrôle nommés « prêt » et « valide ». Ma frustration s'est rapidement transformée en admiration.

L'interface AXI dispose d'un contrôle de flux intégré sans utiliser de signaux de contrôle supplémentaires. Les règles sont assez faciles à comprendre, mais il y a quelques pièges dont il faut tenir compte lors de la mise en œuvre de l'interface AXI sur un FPGA. Cet article vous montre comment créer une FIFO AXI en VHDL.

AXI résout le problème du retard d'un cycle

La prévention de la surlecture et de l'écrasement est un problème courant lors de la création d'interfaces de flux de données. Le problème est que lorsque deux modules logiques cadencés communiquent, chaque module ne pourra lire les sorties de son homologue qu'avec un retard de cycle d'horloge.

L'image ci-dessus montre le chronogramme d'un module séquentiel écrivant dans un FIFO qui utilise le write enable/full schéma de signalisation. Un module d'interfaçage écrit des données dans le FIFO en affirmant le wr_en signal. Le FIFO affirmera le full signaler lorsqu'il n'y a pas de place pour un autre élément de données, invitant la source de données à arrêter l'écriture.

Malheureusement, le module d'interfaçage n'a aucun moyen de s'arrêter dans le temps tant qu'il n'utilise que la logique cadencée. Le FIFO lève le full drapeau exactement au front montant de l'horloge. Simultanément, le module d'interfaçage tente d'écrire la donnée suivante. Il ne peut pas échantillonner et réagir au full signaler avant qu'il ne soit trop tard.

Une solution consiste à inclure un almost_empty supplémentaire signal, nous l'avons fait dans le didacticiel Comment créer un tampon FIFO en anneau en VHDL. Le signal supplémentaire précède le empty signal, laissant au module d'interfaçage le temps de réagir.

La poignée de main prêt/valide

Le protocole AXI implémente le contrôle de flux en utilisant seulement deux signaux de contrôle dans chaque direction, un appelé ready et l'autre valid . Le ready signal est contrôlé par le récepteur, un '1' logique valeur sur ce signal signifie que le récepteur est prêt à accepter un nouvel élément de données. Le valid signal, d'autre part, est contrôlé par l'expéditeur. L'expéditeur doit définir valid à '1' lorsque les données présentées sur le bus de données sont valides pour l'échantillonnage.

Voici la partie importante : le transfert de données ne se produit que lorsque ready et valid sont '1' au même cycle d'horloge. Le récepteur informe lorsqu'il est prêt à accepter des données, et l'expéditeur met simplement les données là-bas lorsqu'il a quelque chose à transmettre. Le transfert se produit lorsque les deux sont d'accord, lorsque l'expéditeur est prêt à envoyer et que le destinataire est prêt à recevoir.

La forme d'onde ci-dessus montre un exemple de transaction d'un élément de données. L'échantillonnage se produit sur le front montant de l'horloge, comme c'est généralement le cas avec la logique cadencée.

Mise en œuvre

Il existe de nombreuses façons d'implémenter une FIFO AXI en VHDL. Il pourrait s'agir d'un registre à décalage, mais nous utiliserons une structure de tampon en anneau car c'est le moyen le plus simple de créer un FIFO dans la RAM de bloc. Vous pouvez tout créer dans un processus géant à l'aide de variables et de signaux, ou vous pouvez diviser la fonctionnalité en plusieurs processus.

Cette implémentation utilise des processus séparés pour la plupart des signaux qui doivent être mis à jour. Seuls les processus devant être synchrones sont sensibles à l'horloge, les autres utilisent la logique combinatoire.

L'entité

La déclaration d'entité comprend un port générique qui est utilisé pour définir la largeur des mots d'entrée et de sortie, ainsi que le nombre d'emplacements pour lesquels réserver de l'espace dans la RAM. La capacité du FIFO est égale à la profondeur de la RAM moins un. Un emplacement est toujours laissé vide pour faire la distinction entre une FIFO pleine et une FIFO vide.

entity axi_fifo is
  generic (
    ram_width : natural;
    ram_depth : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- AXI input interface
    in_ready : out std_logic;
    in_valid : in std_logic;
    in_data : in std_logic_vector(ram_width - 1 downto 0);

    -- AXI output interface
    out_ready : in std_logic;
    out_valid : out std_logic;
    out_data : out std_logic_vector(ram_width - 1 downto 0)
  );
end axi_fifo; 

Les deux premiers signaux de la déclaration de port sont les entrées d'horloge et de réinitialisation. Cette implémentation utilise une réinitialisation synchrone et est sensible au front montant de l'horloge.

Il existe une interface d'entrée de style AXI utilisant les signaux de contrôle prêts/valides et un signal de données d'entrée de largeur générique. Vient enfin l'interface de sortie AXI avec des signaux similaires à ceux de l'entrée, mais avec des directions inversées. Les signaux appartenant à l'interface d'entrée et de sortie sont préfixés par in_ ou out_ .

La sortie d'un FIFO AXI peut être connectée directement à l'entrée d'un autre, les interfaces s'emboîtent parfaitement. Cependant, une meilleure solution que de les empiler serait d'augmenter le ram_depth générique si vous voulez un FIFO plus grand.

Déclarations de signal

Les deux premières instructions de la région déclarative du fichier VHDL déclarent le type de RAM et son signal. La RAM est dimensionnée dynamiquement à partir des entrées génériques.

-- The FIFO is full when the RAM contains ram_depth - 1 elements
type ram_type is array (0 to ram_depth - 1)
  of std_logic_vector(in_data'range);
signal ram : ram_type;

Le deuxième bloc de code déclare un nouveau sous-type entier et quatre signaux à partir de celui-ci. Le index_type est dimensionné pour représenter exactement la profondeur de la RAM. Le head Le signal indique toujours l'emplacement RAM qui sera utilisé lors de la prochaine opération d'écriture. Le tail le signal pointe vers l'emplacement auquel on accédera lors de la prochaine opération de lecture. La valeur du count signal est toujours égal au nombre d'éléments actuellement stockés dans la FIFO, et count_p1 est une copie du même signal retardée d'un cycle d'horloge.

-- Newest element at head, oldest element at tail
subtype index_type is natural range ram_type'range;
signal head : index_type;
signal tail : index_type;
signal count : index_type;
signal count_p1 : index_type;

Viennent ensuite deux signaux nommés in_ready_i et out_valid_i . Ce ne sont que des copies des sorties d'entité in_ready et out_valid . Le _i suffixe signifie simplement interne , cela fait partie de mon style de codage.

-- Internal versions of entity signals with mode "out"
signal in_ready_i : std_logic;
signal out_valid_i : std_logic;

Enfin, nous déclarons un signal qui sera utilisé pour indiquer une lecture et une écriture simultanées. J'expliquerai son but plus loin dans cet article.

-- True the clock cycle after a simultaneous read and write
signal read_while_write_p1 : std_logic;

Sous-programmes

Après les signaux, nous déclarons une fonction pour incrémenter notre index_type personnalisé . Le next_index la fonction regarde le read et valid paramètres pour déterminer s'il y a une transaction de lecture ou de lecture/écriture en cours. Si tel est le cas, l'index sera incrémenté ou enveloppé. Sinon, la valeur d'index inchangée est renvoyée.

function next_index(
  index : index_type;
  ready : std_logic;
  valid : std_logic) return index_type is
begin
  if ready = '1' and valid = '1' then
    if index = index_type'high then
      return index_type'low;
    else
      return index + 1;
    end if;
  end if;

  return index;
end function;

Pour nous éviter des saisies répétitives, nous créons la logique de mise à jour du head et tail signaux dans une procédure, au lieu de deux processus identiques. Le update_index la procédure prend les signaux d'horloge et de réinitialisation, un signal de index_type , un ready signal, et un valid signal comme entrées.

procedure index_proc(
  signal clk : in std_logic;
  signal rst : in std_logic;
  signal index : inout index_type;
  signal ready : in std_logic;
  signal valid : in std_logic) is
begin
    if rising_edge(clk) then
      if rst = '1' then
        index <= index_type'low;
      else
        index <= next_index(index, ready, valid);
      end if;
    end if;
end procedure;

Ce processus entièrement synchrone utilise le next_index fonction pour mettre à jour le index Signal lorsque le module n'est plus réinitialisé. Lors de la réinitialisation, le index signal sera défini sur la valeur la plus basse qu'il peut représenter, qui est toujours 0 en raison de la façon dont index_type et ram_type est déclaré. Nous aurions pu utiliser 0 comme valeur de réinitialisation, mais j'essaie autant que possible d'éviter le codage en dur.

Copier les signaux internes vers la sortie

Ces deux instructions simultanées copient les versions internes des signaux de sortie vers les sorties réelles. Nous devons opérer sur des copies internes car VHDL ne nous permet pas de lire les signaux d'entité avec le mode out à l'intérieur du module. Une alternative aurait été de déclarer in_ready et out_valid avec mode inout , mais la plupart des normes de codage des entreprises restreignent l'utilisation de inout signaux d'entité.

in_ready <= in_ready_i;
out_valid <= out_valid_i;

Mettez à jour l'en-tête et la queue

Nous avons déjà discuté du index_proc procédure qui permet de mettre à jour le head et tail signaux. En mappant les signaux appropriés aux paramètres de ce sous-programme, nous obtenons l'équivalent de deux processus identiques, l'un pour contrôler l'entrée FIFO et l'autre pour la sortie.

-- Update head index on write
PROC_HEAD : index_proc(clk, rst, head, in_ready_i, in_valid);

-- Update tail index on read
PROC_TAIL : index_proc(clk, rst, tail, out_ready, out_valid_i);

Étant donné que le head et le tail sont mis à la même valeur par la logique de réinitialisation, le FIFO sera initialement vide. C'est ainsi que fonctionne ce tampon en anneau, lorsque les deux pointent vers le même index, cela signifie que le FIFO est vide.

Déduire la RAM du bloc

Dans la plupart des architectures FPGA, les primitives de bloc RAM sont des composants entièrement synchrones. Cela signifie que si nous voulons que l'outil de synthèse déduit la RAM de bloc de notre code VHDL, nous devons placer les ports de lecture et d'écriture à l'intérieur d'un processus cadencé. De plus, il ne peut y avoir aucune valeur de réinitialisation associée au bloc RAM.

PROC_RAM : process(clk)
begin
  if rising_edge(clk) then
    ram(head) <= in_data;
    out_data <= ram(next_index(tail, out_ready, out_valid_i));
  end if;
end process;

Il n'y a pas d'autorisation de lecture ou autorisation d'écriture ici, ce serait trop lent pour AXI. Au lieu de cela, nous écrivons en permanence dans l'emplacement de RAM pointé par le head indice. Ensuite, lorsque nous déterminons qu'une transaction d'écriture s'est produite, nous avançons simplement le head pour verrouiller la valeur écrite.

De même, out_data est mis à jour à chaque cycle d'horloge. Le tail le pointeur se déplace simplement vers l'emplacement suivant lorsqu'une lecture se produit. Notez que le next_index La fonction est utilisée pour calculer l'adresse du port de lecture. Nous devons faire cela pour nous assurer que la RAM réagit assez rapidement après une lecture et commence à sortir la valeur suivante.

Compter le nombre d'éléments dans le FIFO

Compter le nombre d'éléments dans la RAM consiste simplement à soustraire le head du tail . Si le head s'est terminé, nous devons le compenser par le nombre total d'emplacements dans la RAM. Nous avons accès à ces informations via le ram_depth constante de l'entrée générique.

PROC_COUNT : process(head, tail)
begin
  if head < tail then
    count <= head - tail + ram_depth;
  else
    count <= head - tail;
  end if;
end process;

Nous devons également garder une trace de la valeur précédente du count signal. Le processus ci-dessous en crée une version retardée d'un cycle d'horloge. Le _p1 postfix est une convention de nommage pour l'indiquer.

PROC_COUNT_P1 : process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      count_p1 <= 0;
    else
      count_p1 <= count;
    end if;
  end if;
end process;

Mettre à jour le prêt sortie

Le in_ready le signal doit être '1' lorsque ce module est prêt à accepter une autre donnée. Cela devrait être le cas tant que le FIFO n'est pas plein, et c'est exactement ce que dit la logique de ce processus.

PROC_IN_READY : process(count)
begin
  if count < ram_depth - 1 then
    in_ready_i <= '1';
  else
    in_ready_i <= '0';
  end if;
end process;

Détecter la lecture et l'écriture simultanées

En raison d'un cas particulier que j'expliquerai dans la section suivante, nous devons être en mesure d'identifier les opérations de lecture et d'écriture simultanées. Chaque fois qu'il y a des transactions de lecture et d'écriture valides au cours du même cycle d'horloge, ce processus définira le read_while_write_p1 signaler à '1' sur le cycle d'horloge suivant.

PROC_READ_WHILE_WRITE_P1: process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      read_while_write_p1 <= '0';

    else
      read_while_write_p1 <= '0';
      if in_ready_i = '1' and in_valid = '1' and
        out_ready = '1' and out_valid_i = '1' then
        read_while_write_p1 <= '1';
      end if;
    end if;
  end if;
end process;

Mettre à jour le valide sortie

Le out_valid signal indique aux modules en aval que les données présentées sur out_data est valide et peut être échantillonné à tout moment. Le out_data le signal provient directement de la sortie RAM. Implémenter le out_valid le signal est un peu délicat en raison du délai de cycle d'horloge supplémentaire entre l'entrée et la sortie de la RAM du bloc.

La logique est implémentée dans un processus combinatoire afin qu'elle puisse réagir sans délai au changement du signal d'entrée. La première ligne du processus est une valeur par défaut qui définit le out_valid signaler au '1' . Ce sera la valeur dominante si aucune des deux instructions If suivantes n'est déclenchée.

PROC_OUT_VALID : process(count, count_p1, read_while_write_p1)
begin
  out_valid_i <= '1';

  -- If the RAM is empty or was empty in the prev cycle
  if count = 0 or count_p1 = 0 then
    out_valid_i <= '0';
  end if;

  -- If simultaneous read and write when almost empty
  if count = 1 and read_while_write_p1 = '1' then
    out_valid_i <= '0';
  end if;

end process;

La première instruction If vérifie si le FIFO est vide ou était vide dans le cycle d'horloge précédent. Évidemment, le FIFO est vide lorsqu'il contient 0 éléments, mais nous devons également examiner le niveau de remplissage du FIFO dans le cycle d'horloge précédent.

Considérez la forme d'onde ci-dessous. Initialement, le FIFO est vide, comme indiqué par le count le signal étant 0 . Ensuite, une écriture se produit au troisième cycle d'horloge. L'emplacement de RAM 0 est mis à jour au cycle d'horloge suivant, mais il faut un cycle supplémentaire avant que les données n'apparaissent sur le out_data production. Le but du or count_p1 = 0 est de s'assurer que out_valid reste '0' (entouré en rouge) pendant que la valeur se propage dans la RAM.

La dernière instruction If protège contre un autre cas particulier. Nous venons de parler de la façon de gérer le cas particulier de l'écriture sur vide en vérifiant les niveaux de remplissage FIFO actuels et précédents. Mais que se passe-t-il si et nous effectuons une lecture et une écriture simultanées lorsque count est déjà 1 ?

La forme d'onde ci-dessous montre une telle situation. Initialement, il y a une donnée D0 présente dans la FIFO. Il est là depuis un certain temps, donc count et count_p1 sont 0 . Ensuite, une lecture et une écriture simultanées surviennent dans le troisième cycle d'horloge. Un élément quitte le FIFO et un nouveau y entre, rendant les compteurs inchangés.

Au moment de la lecture et de l'écriture, il n'y a pas de valeur suivante dans la RAM prête à être sortie, comme il y en aurait eu si le niveau de remplissage était supérieur à un. Nous devons attendre deux cycles d'horloge avant que la valeur d'entrée n'apparaisse sur la sortie. Sans aucune information supplémentaire, il serait impossible de détecter ce cas d'angle, et la valeur de out_valid au cycle d'horloge suivant (marqué en rouge continu) serait réglé par erreur sur '1' .

C'est pourquoi nous avons besoin du read_while_write_p1 signal. Il détecte qu'il y a eu une lecture et une écriture simultanées, et nous pouvons en tenir compte en définissant out_valid à '0' dans ce cycle d'horloge.

Synthétiser dans Vivado

Pour implémenter la conception en tant que module autonome dans Xilinx Vivado, nous devons d'abord donner des valeurs aux entrées génériques. Ceci peut être réalisé dans Vivado en utilisant les ParamètresGénéralGénériques/Paramètres menu, comme indiqué dans l'image ci-dessous.

Les valeurs génériques ont été choisies pour correspondre à la primitive RAMB36E1 dans l'architecture Xilinx Zynq qui est le périphérique cible. L'utilisation des ressources après la mise en œuvre est illustrée dans l'image ci-dessous. Le FIFO AXI utilise un bloc de RAM et un petit nombre de LUT et de bascules.

AXI est plus que prêt/valide

AXI signifie Advanced eXtensible Interface, il fait partie de la norme AMBA (Advanced Microcontroller Bus Architecture) d'ARM. La norme AXI est bien plus que la poignée de main lecture/valide. Si vous voulez en savoir plus sur AXI, je vous recommande ces ressources pour une lecture plus approfondie :


VHDL

  1. Le cloud et comment il change le monde informatique
  2. Comment tirer le meilleur parti de vos données
  3. Comment initialiser la RAM à partir d'un fichier à l'aide de TEXTIO
  4. Comment vous préparer pour l'IA à l'aide de l'IoT
  5. Comment l'Internet industriel change la gestion des actifs
  6. Meilleures pratiques en matière de suivi des actifs :comment tirer le meilleur parti de vos données d'actifs durement gagnées
  7. Comment obtenir une meilleure image de l'IoT ?
  8. Comment tirer le meilleur parti de l'IoT dans la restauration
  9. Comment les données permettent la chaîne d'approvisionnement du futur