So erstellen Sie ein AXI-FIFO im Block-RAM mit dem Ready/Valid-Handshake
Ich war ein wenig genervt von den Besonderheiten der AXI-Schnittstelle, als ich das erste Mal Logik erstellen musste, um ein AXI-Modul anzubinden. Anstelle der regulären Steuersignale Beschäftigt/Gültig, Voll/Gültig oder Leer/Gültig verwendet die AXI-Schnittstelle zwei Steuersignale namens „Bereit“ und „Gültig“. Meine Frustration verwandelte sich bald in Ehrfurcht.
Die AXI-Schnittstelle verfügt über eine integrierte Flusskontrolle ohne Verwendung zusätzlicher Steuersignale. Die Regeln sind einfach zu verstehen, aber es gibt ein paar Fallstricke, die man berücksichtigen muss, wenn man die AXI-Schnittstelle auf einem FPGA implementiert. Dieser Artikel zeigt Ihnen, wie Sie ein AXI-FIFO in VHDL erstellen.
AXI löst das Problem der Verzögerung um einen Zyklus
Das Verhindern von Überlesen und Überschreiben ist ein häufiges Problem beim Erstellen von Datenstromschnittstellen. Das Problem besteht darin, dass bei der Kommunikation zweier getakteter Logikmodule jedes Modul die Ausgänge seines Gegenstücks nur mit einer Verzögerung von einem Taktzyklus lesen kann.
Das obige Bild zeigt das Zeitdiagramm eines sequentiellen Moduls, das in einen FIFO schreibt, der die Option Write enable/full verwendet Signalisierungsschema. Ein Schnittstellenmodul schreibt Daten in den FIFO, indem es den wr_en
aktiviert Signal. Der FIFO bestätigt den full
signalisiert, wenn kein Platz für ein weiteres Datenelement vorhanden ist, und veranlasst die Datenquelle, das Schreiben zu beenden.
Leider hat das Schnittstellenmodul keine Möglichkeit, rechtzeitig anzuhalten, solange es nur getaktete Logik verwendet. Der FIFO erhöht die full
Flag genau an der steigenden Flanke der Uhr. Gleichzeitig versucht das Schnittstellenmodul, das nächste Datenelement zu schreiben. Es kann die full
nicht abtasten und darauf reagieren signalisieren, bevor es zu spät ist.
Eine Lösung besteht darin, ein zusätzliches almost_empty
hinzuzufügen signalisieren, haben wir dies im Tutorial So erstellen Sie einen Ringpuffer-FIFO in VHDL gemacht. Das Zusatzsignal geht vor empty
Signal, das dem Schnittstellenmodul Zeit zum Reagieren gibt.
Der Bereit/Gültig-Handshake
Das AXI-Protokoll implementiert die Flusskontrolle mit nur zwei Steuersignalen in jede Richtung, eines namens ready
und die andere valid
. Die ready
Signal wird vom Empfänger gesteuert, ein logischer '1'
Wert auf diesem Signal bedeutet, dass der Empfänger bereit ist, ein neues Datenelement zu akzeptieren. Die valid
Signal hingegen wird vom Sender kontrolliert. Der Absender muss valid
einstellen bis '1'
wenn die auf dem Datenbus präsentierten Daten für die Abtastung gültig sind.
Hier kommt der wichtige Teil: Datenübertragung findet nur statt, wenn beide ready
und valid
sind '1'
im selben Taktzyklus. Der Empfänger informiert, wenn er bereit ist, Daten zu akzeptieren, und der Sender stellt die Daten einfach bereit, wenn er etwas zu übertragen hat. Die Übertragung findet statt, wenn beide einverstanden sind, wenn der Sender sendebereit und der Empfänger empfangsbereit ist.
Die obige Wellenform zeigt eine beispielhafte Transaktion eines Datenelements. Die Abtastung erfolgt an der steigenden Taktflanke, wie es normalerweise bei getakteter Logik der Fall ist.
Implementierung
Es gibt viele Möglichkeiten, ein AXI-FIFO in VHDL zu implementieren. Es könnte ein Schieberegister sein, aber wir werden eine Ringpufferstruktur verwenden, da dies der einfachste Weg ist, einen FIFO im Block-RAM zu erstellen. Sie können alles in einem riesigen Prozess mit Variablen und Signalen erstellen oder die Funktionalität in mehrere Prozesse aufteilen.
Diese Implementierung verwendet getrennte Prozesse für die meisten Signale, die aktualisiert werden müssen. Nur die Prozesse, die synchron sein müssen, sind taktempfindlich, die anderen verwenden kombinatorische Logik.
Die Entität
Die Entity-Deklaration enthält einen generischen Port, der zum Einstellen der Breite der Eingangs- und Ausgangswörter sowie der Anzahl der Slots verwendet wird, für die Platz im RAM reserviert werden soll. Die Kapazität des FIFO ist gleich der RAM-Tiefe minus eins. Ein Slot wird immer leer gehalten, um zwischen einem vollen und einem leeren FIFO zu unterscheiden.
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;
Die ersten beiden Signale in der Portdeklaration sind die Clock- und Reset-Eingänge. Diese Implementierung verwendet ein synchrones Zurücksetzen und reagiert empfindlich auf die steigende Flanke der Uhr.
Es gibt eine Eingangsschnittstelle im AXI-Stil, die die Bereit/Gültig-Steuersignale und ein Eingangsdatensignal mit generischer Breite verwendet. Schließlich kommt die AXI-Ausgangsschnittstelle mit ähnlichen Signalen wie der Eingang, nur mit umgekehrten Richtungen. Signalen, die zur Ein- und Ausgangsschnittstelle gehören, wird in_
vorangestellt oder out_
.
Der Ausgang eines AXI-FIFOs könnte direkt mit dem Eingang eines anderen verbunden werden, die Schnittstellen passen perfekt zusammen. Eine bessere Lösung als das Stapeln wäre jedoch, die ram_depth
zu erhöhen generisch, wenn Sie einen größeren FIFO wünschen.
Signaldeklarationen
Die ersten beiden Anweisungen im deklarativen Bereich der VHDL-Datei deklarieren den RAM-Typ und sein Signal. Die Größe des RAM wird dynamisch von den generischen Eingaben bestimmt.
-- 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;
Der zweite Codeblock deklariert einen neuen Integer-Untertyp und vier Signale davon. Die index_type
ist so bemessen, dass er genau die Tiefe des RAM darstellt. Die head
Signal zeigt immer den RAM-Steckplatz an, der bei der nächsten Schreiboperation verwendet wird. Die tail
Signal zeigt auf den Schlitz, auf den bei der nächsten Leseoperation zugegriffen wird. Der Wert von count
Signal ist immer gleich der Anzahl der aktuell im FIFO gespeicherten Elemente und count_p1
ist eine um einen Taktzyklus verzögerte Kopie desselben Signals.
-- 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;
Dann kommen zwei Signale namens in_ready_i
und out_valid_i
. Dies sind lediglich Kopien der Entitätsausgaben in_ready
und out_valid
. Der _i
postfix bedeutet einfach intern , es ist Teil meines Programmierstils.
-- Internal versions of entity signals with mode "out" signal in_ready_i : std_logic; signal out_valid_i : std_logic;
Schließlich deklarieren wir ein Signal, das verwendet wird, um ein gleichzeitiges Lesen und Schreiben anzuzeigen. Ich werde seinen Zweck später in diesem Artikel erläutern.
-- True the clock cycle after a simultaneous read and write signal read_while_write_p1 : std_logic;
Unterprogramme
Nach den Signalen deklarieren wir eine Funktion zum Inkrementieren unseres benutzerdefinierten index_type
. Der next_index
Funktion betrachtet den read
und valid
Parameter, um festzustellen, ob eine laufende Lese- oder Lese-/Schreibtransaktion stattfindet. Wenn dies der Fall ist, wird der Index inkrementiert oder umgebrochen. Wenn nicht, wird der unveränderte Indexwert zurückgegeben.
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;
Um uns das wiederholte Eintippen zu ersparen, erstellen wir die Logik zum Aktualisieren des head
und tail
Signale in einer Prozedur statt als zwei identische Prozesse. Der update_index
Die Prozedur nimmt die Uhr- und Reset-Signale, ein Signal von index_type
, ein ready
Signal und ein valid
Signal als Eingänge.
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;
Dieser vollständig synchrone Prozess verwendet den next_index
Funktion zum Aktualisieren des index
Signal, wenn das Modul nicht zurückgesetzt ist. Beim Zurücksetzen wird der index
Signal wird auf den niedrigsten Wert gesetzt, den es darstellen kann, was immer 0 ist, weil index_type
und ram_type
ist deklariert. Wir hätten 0 als Reset-Wert verwenden können, aber ich versuche so viel wie möglich, Hard-Coding zu vermeiden.
Interne Signale zum Ausgang kopieren
Diese beiden gleichzeitigen Anweisungen kopieren die internen Versionen der Ausgangssignale in die tatsächlichen Ausgänge. Wir müssen mit internen Kopien arbeiten, da VHDL es uns nicht erlaubt, Entitätssignale mit dem Modus out
zu lesen innerhalb des Moduls. Eine Alternative wäre gewesen, in_ready
zu deklarieren und out_valid
mit Modus inout
, aber die meisten Unternehmenscodierungsstandards schränken die Verwendung von inout
ein Entitätssignale.
in_ready <= in_ready_i; out_valid <= out_valid_i;
Kopf und Schwanz aktualisieren
Wir haben bereits den index_proc
besprochen Prozedur, die verwendet wird, um den head
zu aktualisieren und tail
Signale. Indem wir die entsprechenden Signale den Parametern dieses Unterprogramms zuordnen, erhalten wir das Äquivalent von zwei identischen Prozessen, einen zur Steuerung des FIFO-Eingangs und einen für den Ausgang.
-- 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);
Da sowohl der head
und der tail
durch die Rücksetzlogik auf den gleichen Wert gesetzt werden, ist der FIFO anfänglich leer. So funktioniert dieser Ringpuffer, wenn beide auf denselben Index zeigen, bedeutet dies, dass der FIFO leer ist.
Block-RAM ableiten
In den meisten FPGA-Architekturen sind die Block-RAM-Grundelemente vollständig synchrone Komponenten. Das bedeutet, dass wir, wenn wir wollen, dass das Synthesetool Block-RAM aus unserem VHDL-Code ableitet, die Lese- und Schreibports in einen getakteten Prozess einfügen müssen. Außerdem können dem Block-RAM keine Reset-Werte zugeordnet werden.
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;
Es gibt keine Lesefreigabe oder Schreibfreigabe hier wäre das für AXI zu langsam. Stattdessen schreiben wir kontinuierlich in den RAM-Steckplatz, auf den head
zeigt Index. Wenn wir dann feststellen, dass eine Schreibtransaktion stattgefunden hat, setzen wir einfach head
fort um den geschriebenen Wert zu sperren.
Ebenso out_data
wird bei jedem Taktzyklus aktualisiert. Der tail
Der Zeiger bewegt sich einfach zum nächsten Slot, wenn ein Lesevorgang stattfindet. Beachten Sie, dass der next_index
Funktion wird verwendet, um die Adresse für den Leseport zu berechnen. Wir müssen dies tun, damit das RAM nach einem Lesevorgang schnell genug reagiert und mit der Ausgabe des nächsten Werts beginnt.
Zähle die Anzahl der Elemente im FIFO
Das Zählen der Anzahl der Elemente im RAM ist einfach eine Frage der Subtraktion von head
aus dem tail
. Wenn der head
gewickelt hat, müssen wir es durch die Gesamtzahl der Steckplätze im RAM ausgleichen. Wir haben Zugriff auf diese Informationen über den ram_depth
Konstante aus der generischen Eingabe.
PROC_COUNT : process(head, tail) begin if head < tail then count <= head - tail + ram_depth; else count <= head - tail; end if; end process;
Wir müssen auch den vorherigen Wert von count
verfolgen Signal. Der folgende Prozess erstellt eine Version davon, die um einen Taktzyklus verzögert ist. Der _p1
postfix ist eine Namenskonvention, um dies anzuzeigen.
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;
Aktualisieren Sie die Fertig Ausgabe
Der in_ready
Signal soll '1'
sein wenn dieses Modul bereit ist, ein weiteres Datenelement zu akzeptieren. Dies sollte der Fall sein, solange das FIFO nicht voll ist, und genau das sagt die Logik dieses Prozesses.
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;
Gleichzeitiges Lesen und Schreiben erkennen
Aufgrund eines Sonderfalls, den ich im nächsten Abschnitt erläutern werde, müssen wir in der Lage sein, gleichzeitige Lese- und Schreibvorgänge zu identifizieren. Jedes Mal, wenn es während desselben Taktzyklus gültige Lese- und Schreibtransaktionen gibt, setzt dieser Prozess den read_while_write_p1
Signal an '1'
im folgenden Taktzyklus.
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;
Aktualisieren Sie die gültige Ausgabe
Der out_valid
Signal zeigt nachgeschalteten Modulen an, dass die Daten auf out_data
präsentiert werden ist gültig und kann jederzeit bemustert werden. Die out_data
Signal kommt direkt vom RAM-Ausgang. Implementierung des out_valid
Signal ist wegen der zusätzlichen Taktzyklusverzögerung zwischen Eingang und Ausgang des Block-RAM ein wenig knifflig.
Die Logik ist in einem kombinatorischen Verfahren implementiert, so dass sie ohne Verzögerung auf das sich ändernde Eingangssignal reagieren kann. Die erste Zeile des Prozesses ist ein Standardwert, der den out_valid
setzt Signal an '1'
. Dies ist der vorherrschende Wert, wenn keine der beiden nachfolgenden If-Anweisungen ausgelöst wird.
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;
Die erste If-Anweisung prüft, ob der FIFO leer ist oder im vorherigen Taktzyklus leer war. Offensichtlich ist der FIFO leer, wenn 0 Elemente darin sind, aber wir müssen auch den Füllstand des FIFO im vorherigen Taktzyklus untersuchen.
Betrachten Sie die Wellenform unten. Anfänglich ist der FIFO leer, wie durch count
angezeigt Signal ist 0
. Dann tritt beim dritten Taktzyklus ein Schreibvorgang auf. RAM-Steckplatz 0 wird im nächsten Taktzyklus aktualisiert, aber es dauert einen zusätzlichen Zyklus, bevor die Daten auf dem out_data
erscheinen Ausgang. Der Zweck des or count_p1 = 0
Anweisung ist sicherzustellen, dass out_valid
bleibt '0'
(rot eingekreist), während sich der Wert durch den RAM ausbreitet.
Die letzte If-Anweisung schützt vor einem weiteren Sonderfall. Wir haben gerade darüber gesprochen, wie man den Sonderfall des Schreibens auf Leer handhabt, indem man die aktuellen und vorherigen FIFO-Füllstände prüft. Aber was passiert, wenn und wir gleichzeitig lesen und schreiben, wenn count
ist bereits 1
?
Die folgende Wellenform zeigt eine solche Situation. Anfänglich ist ein Datenelement D0 im FIFO vorhanden. Es ist schon eine Weile da, also beides count
und count_p1
sind 0
. Dann folgt im dritten Taktzyklus ein gleichzeitiges Lesen und Schreiben. Ein Element verlässt den FIFO und ein neuer kommt hinein, wodurch die Zähler unverändert bleiben.
Im Moment des Lesens und Schreibens steht kein nächster Wert im RAM zur Ausgabe bereit, wie es bei einem Füllstand größer eins der Fall gewesen wäre. Wir müssen zwei Taktzyklen warten, bis der Eingangswert am Ausgang erscheint. Ohne zusätzliche Informationen wäre es unmöglich, diesen Eckfall und den Wert von out_valid
zu erkennen beim folgenden Taktzyklus (durchgehend rot markiert) würde fälschlicherweise auf '1'
gesetzt werden .
Deshalb brauchen wir den read_while_write_p1
Signal. Es erkennt, dass gleichzeitig gelesen und geschrieben wurde, und wir können dies berücksichtigen, indem wir out_valid
setzen bis '0'
in diesem Taktzyklus.
Synthetisieren in Vivado
Um das Design als eigenständiges Modul in Xilinx Vivado zu implementieren, müssen wir zunächst den generischen Eingaben Werte zuweisen. Dies erreichen Sie in Vivado über die Einstellungen → Allgemein → Generika/Parameter Menü, wie im Bild unten gezeigt.
Die generischen Werte wurden so gewählt, dass sie mit dem RAMB36E1-Grundelement in der Xilinx-Zynq-Architektur, dem Zielgerät, übereinstimmen. Die Ressourcennutzung nach der Implementierung ist im Bild unten dargestellt. Das AXI FIFO verwendet einen Block-RAM und eine kleine Anzahl von LUTs und Flip-Flops.
AXI ist mehr als bereit/gültig
AXI steht für Advanced eXtensible Interface und ist Teil des Advanced Microcontroller Bus Architecture (AMBA)-Standards von ARM. Der AXI-Standard ist viel mehr als der Read/Valid-Handshake. Wenn Sie mehr über AXI erfahren möchten, empfehle ich diese Ressourcen zum Weiterlesen:
- Wikipedia:AXI
- ARM AXI-Einführung
- Xilinx AXI-Einführung
- AXI4-Spezifikation
VHDL
- Cloud und wie sie die IT-Welt verändert
- So nutzen Sie Ihre Daten optimal
- So initialisieren Sie RAM aus einer Datei mit TEXTIO
- Wie Sie sich auf KI mit IoT vorbereiten
- Wie das industrielle Internet das Asset Management verändert
- Best Practices für das Asset-Tracking:So holen Sie das Beste aus Ihren hart erarbeiteten Asset-Daten
- Wie bekommen wir ein besseres Bild vom IoT?
- So nutzen Sie das IoT in der Gastronomie optimal
- Wie Daten die Lieferkette der Zukunft ermöglichen
- Wie man Lieferkettendaten vertrauenswürdig macht