AXI FIFO in Block RAM mit Ready/Valid-Handshake implementieren
Als ich das AXI-Interface erstmals beim Design eines Moduls kennenlernte, war ich von seinem schlanken Flow-Control-Ansatz überrascht. Im Gegensatz zu traditionellen Protokollen, die auf busy/valid, full/valid oder empty/valid Paaren basieren, nutzt AXI nur zwei Signale: ready und valid. Was als Frustration begann, wurde schnell zur Faszination.
AXI bettet Flow Control direkt in seinen Handshake ein, wodurch zusätzliche Statusleitungen entfallen. Zwar sind die Grundregeln einfach, doch bei der Abbildung auf FPGA-Hardware ist sorgfältige Aufmerksamkeit erforderlich. In diesem Tutorial gehe ich durch eine VHDL-Implementierung eines AXI FIFO, der auf Block RAM basiert.
AXI löst das „um-einen-Zyklus-verzögerte“ Problem
Überlesen und Überschreiben zu verhindern, ist eine häufige Herausforderung, wenn zwei taktsynchrone Module Datenaustauschen. Der Kern des Problems: Jedes Modul kann die Ausgabe des anderen erst einen Taktzyklus nach deren Auftreten beobachten.
Das obige Diagramm illustriert ein sequenzielles Modul, das in eine FIFO schreibt, die das klassische write enable/full-Signalisierungsschema nutzt. Der Writer assertiert wr_en, um Daten in die FIFO zu schieben, während die FIFO full anhebt, wenn kein Platz mehr ist. Das Problem: full wird genau an der steigenden Taktflanke assertiert, sodass der Writer im selben Zyklus nicht mehr darauf reagieren kann. Die Lösung ist ein Look-Ahead-Signal wie almost_empty, das der Upstream-Logik einen Vorsprung gibt.
Der Ready/Valid-Handshake
AXI-Stream-Interfaces lösen dieses Timing-Dilemma mit einem Zwei-Signal-Handshake: ready und valid. Der Empfänger treibt ready; ein High signalisiert Bereitschaft, ein neues Datum zu akzeptieren. Der Sender treibt valid; ein High bedeutet, die Daten auf dem Bus sind stabil und können gesampelt werden. Ein Transfer erfolgt nur, wenn beide Signale an derselben steigenden Flanke high sind.
Das Wellenformdiagramm demonstriert den Transfer eines einzelnen Datenelements. Daten werden an der steigenden Taktflanke gesampelt, genau wie in konventionellen synchronen Designs.
Implementierung
Es gibt mehrere Wege, einen AXI FIFO in VHDL zu bauen. Während ein Schieberegister funktionieren würde, ist ein Ringpuffer auf Block RAM die effizienteste und skalierbarste Wahl. Unten präsentiere ich ein modulares Design, das die Logik in dedizierte Prozesse trennt für Klarheit und Synthesefreundlichkeit.
Die Entity
Die Entity definiert zwei Generics – ram_width und ram_depth –, die Wortbreite und FIFO-Tiefe festlegen. Die Kapazität beträgt effektiv ram_depth - 1, da ein Slot reserviert bleibt, um volle von leerer FIFO zu unterscheiden.
entity axi_fifo is generic ( ram_width : natural; ram_depth : natural ); port ( clk : in std_logic; rst : in std_logic; -- AXI Eingangsinterface in_ready : out std_logic; in_valid : in std_logic; in_data : in std_logic_vector(ram_width - 1 downto 0); -- AXI Ausgangsinterface out_ready : in std_logic; out_valid : out std_logic; out_data : out std_logic_vector(ram_width - 1 downto 0) ); end axi_fifo;
Das Interface folgt AXI-Konventionen: Die Eingangsseite nutzt in_ready und in_valid, die Ausgangssseite spiegelt die Signale mit umgekehrter Richtung. Diese Symmetrie erlaubt das nahtlose Kaskadieren mehrerer FIFOs.
Signaldeklarationen
Zuerst definieren wir einen RAM-Typ, der den generischen Breite und Tiefe entspricht, und instanziieren das Array:
-- FIFO ist voll, wenn RAM ram_depth - 1 Elemente enthält type ram_type is array (0 to ram_depth - 1) of std_logic_vector(in_data'range); signal ram : ram_type;
Als Nächstes deklarieren wir einen Index-Subtyp, der jede Adresse im RAM darstellen kann, und vier Kernsignale: head, tail, count und eine verzögerte Kopie 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;
Wir halten auch interne Versionen der Ready- und Valid-Ausgänge, um das Lesen von out-Signalen innerhalb der Architektur zu vermeiden:
signal in_ready_i : std_logic; signal out_valid_i : std_logic;
Ein Hilfssignal trackt, wann simultanes Lesen und Schreiben im vorherigen Zyklus auftrat, was kritisch für das korrekte Setzen von out_valid bei fast leerer FIFO ist:
signal read_while_write_p1 : std_logic;
Subprogramme
Die next_index-Funktion bestimmt die nächste Adresse basierend darauf, ob ein Lese- oder Schreib-Handshake gerade abgeschlossen wurde. Sie wrappt am Array-Ende.
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;
Die index_proc-Prozedur kapselt die synchrone Logik, die entweder den head- oder tail-Zeiger aktualisiert. Das vermeidet Code-Duplikation und hält die Architektur aufgeräumt.
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;
Interne Signale auf Ausgänge kopieren
in_ready <= in_ready_i; out_valid <= out_valid_i;
Head und Tail aktualisieren
Wir instanziieren die Hilfsprozedur zweimal – einmal für den Write-Pointer (head) und einmal für den Read-Pointer (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);
Block RAM inferieren
Block RAM in den meisten FPGAs ist synchron. Um dem Synthesizer das Inferieren eines RAM-Primitivs zu ermöglichen, halten wir Lese- und Schreiboperationen in einem getakteten Prozess und vermeiden explizite Enable-Signale:
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;
Der Schreibvorgang erfolgt bedingungslos im Zyklus, wenn in_valid high ist, und die Lesedaten werden jeden Zyklus aktualisiert, sodass der Consumer ein valides Wort beobachten kann, wann immer die FIFO Daten enthält.
Elemente in der FIFO zählen
Das count-Signal spiegelt die aktuelle Belegung wider. Wenn head vor tail liegt, ist der Count einfach die Differenz; hat der Head gewrappt, addieren wir die Gesamttiefe:
PROC_COUNT : process(head, tail) begin if head < tail then count <= head - tail + ram_depth; else count <= head - tail; end if; end process;
Die verzögerte Kopie count_p1 wird benötigt, um zu detektieren, wann die FIFO im vorherigen Zyklus leer war, was out_valid beeinflusst:
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;
Ready-Ausgang aktualisieren
Der Writer soll informiert werden, wann die FIFO weitere Daten akzeptieren kann. Das Ready-Signal ist high, wann immer der Count kleiner als ram_depth - 1 ist:
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;
Simultanes Lesen und Schreiben detektieren
Wenn sowohl ein Lese- als auch ein Schreibvorgang im selben Zyklus auftreten, ändert sich die FIFO-Belegung nicht, aber der interne Zustand muss reflektieren, dass ein Wert erst nach zwei Zyklen verfügbar wird, wenn die FIFO fast leer ist. Wir erfassen dieses Ereignis in 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;
Valid-Ausgang aktualisieren
Das out_valid-Signal teilt nachgelagerter Logik mit, wann out_data ein nutzbares Wort hält. Der folgende kombinatorische Prozess setzt es standardmäßig high, löscht es aber in zwei Randfällen:
PROC_OUT_VALID : process(count, count_p1, read_while_write_p1) begin out_valid_i <= '1'; -- Wenn FIFO leer war oder im vorherigen Zyklus leer war if count = 0 or count_p1 = 0 then out_valid_i <= '0'; end if; -- Wenn simultanes Read/Write bei nur einem Item in der FIFO auftrat if count = 1 and read_while_write_p1 = '1' then out_valid_i <= '0'; end if; end process;
Die erste Bedingung garantiert, dass out_valid während der Propagierungsverzögerung nach dem ersten Schreiben low bleibt. Die zweite bewacht gegen eine Situation, in der die FIFO für einen Zyklus voll erscheint, aber tatsächlich keine Daten zum Ausgeben hält.
Synthese in Vivado
Um dieses Design in Xilinx Vivado zu synthetisieren, geben Sie konkrete Werte für die Generics an. Im Settings-Dialog (General → Generics/Parameters) wählen Sie eine Tiefe, die zu einem RAMB36E1-Primitiv für das Ziel-Zynq-Device passt. Nach Implementation ist die Ressourcennutzung moderat – ein Block RAM plus eine Handvoll LUTs und FFs.


AXI ist mehr als Ready/Valid
AXI (Advanced eXtensible Interface) ist Bestandteil von ARM's AMBA-Busarchitektur und bietet einen reichen Funktionsumfang jenseits des simplen Ready/Valid-Handshakes. Für tieferes Verständnis betrachten Sie folgende Ressourcen:
- Wikipedia: AXI
- ARM AXI Einführung
- Xilinx AXI Einführung
- AXI4-Spezifikation
VHDL
- Die Cloud-Revolution: Wie Cloud Computing IT, Sicherheit und Karrieren transformiert
- IoT-Daten freisetzen: Wie Business Rules Management Enterprise Value treibt
- FPGA Block RAM aus Textdateien mit VHDLs TEXTIO-Bibliothek initialisieren
- Fertigungsabläufe mit IoT auf KI vorbereiten
- Industrial Internet: Asset Management revolutionieren durch Konnektivität, Analytik und Nutzererfahrung
- Asset-Tracking Best Practices: Den Wert Ihrer Asset-Daten freisetzen
- IoT-Einblicke vertiefen: Data Visualization und Graph Databases nutzen
- IoT in der Gastronomie optimal nutzen: Effizienz steigern, Abfall reduzieren, Gäste begeistern
- Datengesteuerte Supply Chains: Treibstoff für die Logistik von morgen
- Vertrauenswürdige Supply-Chain-Daten sicherstellen: Eine bewährte Strategie