Implémenter une FIFO AXI en RAM bloc avec handshake Ready/Valid
Quand j'ai rencontré l'interface AXI pour la première fois en concevant un module, j'ai été surpris par son approche de contrôle de flux simplifiée. Contrairement aux protocoles traditionnels qui reposent sur des paires busy/valid, full/valid, ou empty/valid, AXI utilise seulement deux signaux : ready et valid. Ce qui a commencé par de la frustration est rapidement devenu de la fascination.
AXI intègre le contrôle de flux directement dans son handshake, éliminant le besoin de lignes de statut supplémentaires. Bien que les règles de base soient simples, une attention particulière est requise lors du mappage du protocole sur du matériel FPGA. Dans ce tutoriel, je parcours une implémentation VHDL d'une FIFO AXI qui s'exécute sur de la RAM bloc.
AXI résout le problème du retard d'un cycle
Empêcher la sur-lecture et l'écrasement est un défi courant quand deux modules synchronisés sur horloge échangent des flux de données. Le cœur du problème est que chaque module ne peut observer la sortie de l'autre qu'un cycle d'horloge après qu'elle se produise.
Le diagramme ci-dessus illustre un module séquentiel écrivant dans une FIFO qui utilise le schéma de signalisation classique write enable/full. L'écrivain affirme wr_en pour pousser des données dans la FIFO, tandis que la FIFO lève full quand plus d'espace n'est disponible. Le problème est que full est affirmé exactement sur le front montant de l'horloge, donc l'écrivain ne peut plus y réagir dans le même cycle. La solution est de fournir un signal d'anticipation comme almost_empty, donnant à la logique amont une avance.
Le handshake ready/valid
Les interfaces AXI stream résolvent ce dilemme de timing avec un handshake à deux signaux : ready et valid. Le récepteur pilote ready ; une valeur haute indique qu'il est prêt à accepter un nouveau datum. L'expéditeur pilote valid ; une valeur haute signifie que les données sur le bus sont stables et peuvent être échantillonnées. Un transfert se produit seulement quand les deux signaux sont hauts sur le même front montant.
La forme d'onde démontre le transfert d'un seul élément de données. Les données sont échantillonnées sur le front montant d'horloge, comme dans les conceptions synchrones conventionnelles.
Implémentation
Il y a plusieurs façons de construire une FIFO AXI en VHDL. Bien qu'un registre à décalage fonctionnerait, un tampon circulaire soutenu par de la RAM bloc est le choix le plus efficace et scalable. Ci-dessous, je présente une conception modulaire qui sépare la logique en processus dédiés pour la clarté et l'amabilité du synthétiseur.
L'entité
L'entité définit deux paramètres génériques – ram_width et ram_depth – qui définissent la largeur du mot et la profondeur de la FIFO. La capacité est effectivement ram_depth - 1, car un slot est réservé pour différencier une FIFO pleine d'une vide.
entity axi_fifo is generic ( ram_width : natural; ram_depth : natural ); port ( clk : in std_logic; rst : in std_logic; -- Interface d'entrée AXI in_ready : out std_logic; in_valid : in std_logic; in_data : in std_logic_vector(ram_width - 1 downto 0); -- Interface de sortie AXI out_ready : in std_logic; out_valid : out std_logic; out_data : out std_logic_vector(ram_width - 1 downto 0) ); end axi_fifo;
L'interface suit les conventions AXI : le côté entrée utilise in_ready et in_valid, tandis que le côté sortie miroire les signaux avec directions inversées. Cette symétrie permet de chaîner plusieurs FIFOs sans couture.
Déclarations de signaux
D'abord, nous définissons un type RAM qui correspond à la largeur et profondeur génériques, puis nous instancions le tableau :
-- La FIFO est pleine quand la RAM contient ram_depth - 1 éléments type ram_type is array (0 to ram_depth - 1) of std_logic_vector(in_data'range); signal ram : ram_type;
Ensuite, nous déclarons un sous-type d'index qui peut représenter n'importe quelle adresse dans la RAM, et quatre signaux principaux : head, tail, count, et une copie retardée count_p1 :
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;
Nous gardons aussi des versions internes des sorties ready et valid pour éviter de lire les signaux out à l'intérieur de l'architecture :
signal in_ready_i : std_logic; signal out_valid_i : std_logic;
Un signal auxiliaire suit quand une lecture et une écriture simultanées se produisent dans le cycle précédent, ce qui est critique pour définir correctement out_valid quand la FIFO est presque vide :
signal read_while_write_p1 : std_logic;
Sous-programmes
La fonction next_index détermine l'adresse suivante basée sur si un handshake de lecture ou écriture vient de se terminer. Elle reboucle quand la fin du tableau est atteinte.
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;
La procédure index_proc encapsule la logique synchrone qui met à jour soit le pointeur head soit tail. Cela évite la duplication de code et garde l'architecture propre.
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;
Copier les signaux internes vers les sorties
in_ready <= in_ready_i; out_valid <= out_valid_i;
Mettre à jour head et tail
Nous instancions la procédure auxiliaire deux fois – une fois pour le pointeur d'écriture (head) et une fois pour le pointeur de lecture (tail) :
PROC_HEAD : index_proc(clk, rst, head, in_ready_i, in_valid); PROC_TAIL : index_proc(clk, rst, tail, out_ready, out_valid_i);
Inférer la RAM bloc
La RAM bloc dans la plupart des FPGA est synchrone. Pour laisser le synthétiseur inférer une primitive RAM, nous gardons les opérations de lecture et écriture dans un processus horlogé et évitons les signaux d'activation explicites :
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;
L'écriture se produit inconditionnellement sur le cycle où in_valid est haut, et les données de lecture sont mises à jour chaque cycle pour que le consommateur puisse observer un mot valide quand la FIFO contient des données.
Compter le nombre d'éléments dans la FIFO
Le signal count reflète l'occupation courante. Quand head est en avant de tail, le compte est simplement la différence ; si head a rebouclé, nous ajoutons la profondeur totale :
PROC_COUNT : process(head, tail) begin if head < tail then count <= head - tail + ram_depth; else count <= head - tail; end if; end process;
La copie retardée count_p1 est nécessaire pour détecter quand la FIFO était vide au cycle précédent, ce qui influence out_valid :
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 la sortie ready
L'écrivain doit être informé quand la FIFO peut accepter plus de données. Le signal ready est haut chaque fois que le compte est inférieur à ram_depth - 1 :
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 lecture et écriture simultanées
Quand une lecture et une écriture se produisent dans le même cycle, la FIFO ne change pas son occupation mais l'état interne doit refléter qu'une valeur ne deviendra disponible qu'après deux cycles quand la FIFO est presque vide. Nous capturons donc cet événement dans read_while_write_p1 :
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 la sortie valid
Le signal out_valid dit à la logique aval quand out_data tient un mot utilisable. Le processus combinatoire ci-dessous le met haut par défaut mais l'efface dans deux cas limites :
PROC_OUT_VALID : process(count, count_p1, read_while_write_p1) begin out_valid_i <= '1'; -- Si la FIFO est vide ou était vide au cycle précédent if count = 0 or count_p1 = 0 then out_valid_i <= '0'; end if; -- Si une lecture/écriture simultanée s'est produite quand la FIFO n'avait qu'un élément if count = 1 and read_while_write_p1 = '1' then out_valid_i <= '0'; end if; end process;
La première condition garantit que out_valid reste bas pendant le délai de propagation après la première écriture. La seconde protège contre une situation où la FIFO semble rester pleine pour un cycle mais ne contient en fait aucune donnée à sortir.
Synthèse dans Vivado
Pour synthétiser cette conception dans Xilinx Vivado, fournissez des valeurs concrètes pour les génériques. Dans la boîte de dialogue Settings (General → Generics/Parameters), choisissez une profondeur qui correspond à une primitive RAMB36E1 pour le dispositif Zynq cible. Après implémentation, l'utilisation des ressources est modeste – une RAM bloc plus une poignée de LUTs et FFs.


AXI est plus que ready/valid
AXI (Advanced eXtensible Interface) est un composant de l'architecture de bus AMBA d'ARM et offre un ensemble de fonctionnalités riche au-delà du simple handshake ready/valid. Pour une compréhension plus approfondie, considérez les ressources suivantes :
- Wikipédia : AXI
- Introduction AXI d'ARM
- Introduction AXI de Xilinx
- Spécification AXI4
VHDL
- La révolution du Cloud : Comment le Cloud Computing transforme l'IT, la sécurité et les carrières
- Débloquer les données IoT : comment le Business Rules Management pilote la valeur d'entreprise
- Initialiser la RAM bloc FPGA depuis des fichiers texte avec la bibliothèque TEXTIO de VHDL
- Préparer vos opérations manufacturing pour l'IA avec l'IoT
- L'Internet industriel : révolutionner la gestion d'actifs par la connectivité, l'analytique et l'expérience utilisateur
- Meilleures pratiques de suivi d'actifs : débloquer la valeur de vos données d'actifs
- Améliorer la compréhension de l'Internet des objets : Exploiter la visualisation de données et bases de données graphes
- Optimiser l'IoT pour les restaurants : gagner en efficacité, réduire le gaspillage et ravir les clients
- Supply chains data-driven : propulser la logistique de demain