Industrielle Fertigung
Industrielles Internet der Dinge | Industrielle Materialien | Gerätewartung und Reparatur | Industrielle Programmierung |
home  MfgRobots >> Industrielle Fertigung >  >> Industrial programming >> VHDL

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 EinstellungenAllgemeinGenerika/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:


VHDL

  1. Cloud und wie sie die IT-Welt verändert
  2. So nutzen Sie Ihre Daten optimal
  3. So initialisieren Sie RAM aus einer Datei mit TEXTIO
  4. Wie Sie sich auf KI mit IoT vorbereiten
  5. Wie das industrielle Internet das Asset Management verändert
  6. Best Practices für das Asset-Tracking:So holen Sie das Beste aus Ihren hart erarbeiteten Asset-Daten
  7. Wie bekommen wir ein besseres Bild vom IoT?
  8. So nutzen Sie das IoT in der Gastronomie optimal
  9. Wie Daten die Lieferkette der Zukunft ermöglichen
  10. Wie man Lieferkettendaten vertrauenswürdig macht