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

So erstellen Sie einen Ringpuffer-FIFO in VHDL

Ringpuffer sind beliebte Konstrukte zum Erstellen von Warteschlangen in sequentiellen Programmiersprachen, sie können jedoch auch in Hardware implementiert werden. In diesem Artikel erstellen wir einen Ringpuffer in VHDL, um einen FIFO im Block-RAM zu implementieren.

Bei der Implementierung eines FIFO müssen Sie viele Entwurfsentscheidungen treffen. Welche Art von Schnittstelle benötigen Sie? Sind Sie durch Ressourcen begrenzt? Sollte es widerstandsfähig gegen Überlesen und Überschreiben sein? Ist die Latenz akzeptabel? Das sind einige der Fragen, die mir in den Sinn kommen, wenn ich gebeten werde, einen FIFO zu erstellen.

Es gibt online viele kostenlose FIFO-Implementierungen sowie FIFO-Generatoren wie Xilinx LogiCORE. Dennoch ziehen es viele Ingenieure vor, ihre eigenen FIFOs zu implementieren. Denn obwohl sie alle die gleichen grundlegenden Warteschlangen- und Dequeue-Aufgaben ausführen, können sie bei Berücksichtigung der Details sehr unterschiedlich sein.

Wie ein Ringpuffer funktioniert

Ein Ringpuffer ist eine FIFO-Implementierung, die zusammenhängenden Speicher zum Speichern der gepufferten Daten mit einem Minimum an Daten-Shuffling verwendet. Neue Elemente bleiben vom Zeitpunkt des Schreibens an am gleichen Speicherort, bis sie gelesen und aus dem FIFO entfernt werden.

Zwei Zähler werden verwendet, um den Ort und die Anzahl der Elemente im FIFO zu verfolgen. Diese Zähler beziehen sich auf einen Offset vom Beginn des Speicherplatzes, wo die Daten gespeichert sind. In VHDL ist dies ein Index für eine Array-Zelle. Für den Rest dieses Artikels beziehen wir uns darauf, dass diese Zähler Zeiger sind .

Diese beiden Zeiger sind der Kopf und Schwanz Zeiger. Der Kopf zeigt immer auf den Speicherplatz, der die nächsten geschriebenen Daten enthalten wird, während der Schwanz auf das nächste Element verweist, das aus dem FIFO gelesen wird. Es gibt noch andere Varianten, aber wir werden diese verwenden.

Leerer Zustand

Wenn Kopf und Ende auf dasselbe Element zeigen, bedeutet dies, dass der FIFO leer ist. Das obige Bild zeigt ein Beispiel-FIFO mit acht Slots. Sowohl der Kopf- als auch der Endzeiger zeigen auf Element 0, was anzeigt, dass der FIFO leer ist. Dies ist der Anfangszustand des Ringpuffers.

Beachten Sie, dass der FIFO immer noch leer wäre, wenn beide Zeiger auf einem anderen Index wären, beispielsweise 3. Für jeden Schreibvorgang bewegt sich der Kopfzeiger um eine Stelle vorwärts. Der Endzeiger wird jedes Mal inkrementiert, wenn der Benutzer des FIFO ein Element liest.

Wenn sich einer der Zeiger am höchsten Index befindet, bewirkt der nächste Schreib- oder Lesevorgang, dass sich der Zeiger zurück zum niedrigsten Index bewegt. Das ist das Schöne am Ringpuffer, die Daten bewegen sich nicht, nur die Zeiger.

Kopf führt Schwanz

Das obige Bild zeigt denselben Ringpuffer nach fünf Schreibvorgängen. Der Endzeiger befindet sich immer noch auf Steckplatznummer 0, aber der Kopfzeiger hat sich auf Steckplatznummer 5 bewegt. Die Steckplätze, die Daten enthalten, sind in der Abbildung hellblau gefärbt. Der Schwanzzeiger befindet sich auf dem ältesten Element, während der Kopf auf den nächsten freien Platz zeigt.

Wenn der Kopf einen höheren Index als der Schwanz hat, können wir die Anzahl der Elemente im Ringpuffer berechnen, indem wir den Schwanz vom Kopf subtrahieren. In der Abbildung oben ergibt das eine Anzahl von fünf Elementen.

Schwanz führt Kopf

Das Subtrahieren des Kopfes vom Schwanz funktioniert nur, wenn der Kopf dem Schwanz vorangeht. In der obigen Abbildung befindet sich der Kopf auf Index 2, während der Schwanz auf Index 5 liegt. Wenn wir also diese einfache Berechnung durchführen, erhalten wir 2 – 5 =-3, was keinen Sinn ergibt.

Die Lösung besteht darin, den Kopf mit der Gesamtzahl der Slots im FIFO, in diesem Fall 8, zu versetzen. Die Rechnung ergibt nun (2 + 8) – 5 =5, was die richtige Antwort ist.

Der Schwanz wird dem Kopf ewig nachjagen, so funktioniert ein Ringpuffer. Die Hälfte der Zeit hat der Schwanz einen höheren Index als der Kopf. Die Daten werden zwischen den beiden gespeichert, wie durch die hellblaue Farbe im Bild oben angezeigt wird.

Vollständiger Zustand

Bei einem vollen Ringpuffer zeigt der Schwanz direkt nach dem Kopf auf den Index. Eine Folge dieses Schemas ist, dass wir niemals alle Slots zum Speichern von Daten verwenden können, es muss mindestens ein freier Slot vorhanden sein. Das obige Bild zeigt eine Situation, in der der Ringpuffer voll ist. Der offene, aber unbenutzbare Steckplatz ist gelb gefärbt.

Ein dediziertes Leer/Voll-Signal könnte auch verwendet werden, um anzuzeigen, dass der Ringpuffer voll ist. Dies würde es allen Speichersteckplätzen ermöglichen, Daten zu speichern, erfordert jedoch zusätzliche Logik in Form von Registern und Nachschlagetabellen (LUTs). Daher verwenden wir die Option keep one open Schema für unsere Implementierung des Ringpuffer-FIFO, da dies nur billigeren Block-RAM verschwendet.

Die Ringpuffer-FIFO-Implementierung

Wie Sie die Schnittstellensignale zu und von Ihrem FIFO definieren, begrenzt die Anzahl möglicher Implementierungen Ihres Ringpuffers. In unserem Beispiel verwenden wir eine Variation der klassischen Read/Write Enable- und Empty/Full/Valid-Schnittstelle.

Es wird ein Daten schreiben geben Bus auf der Eingangsseite, der die Daten trägt, die zum FIFO geschoben werden sollen. Es wird auch eine Schreibfreigabe geben Signal, das, wenn es aktiviert wird, bewirkt, dass der FIFO die Eingangsdaten abtastet.

Die Ausgabeseite hat gelesene Daten und ein Lesen gültig vom FIFO gesteuertes Signal. Es wird auch eine Lesefreigabe haben Signal, das vom nachgeschalteten Benutzer des FIFO gesteuert wird.

Die leere und voll Steuersignale sind Teil der klassischen FIFO-Schnittstelle, wir werden sie auch verwenden. Sie werden vom FIFO gesteuert, und ihr Zweck ist es, den Zustand des FIFO an den Leser und Schreiber zu übermitteln.

Gegendruck

Das Problem beim Warten, bis der FIFO entweder leer oder voll ist, bevor Maßnahmen ergriffen werden, besteht darin, dass die Schnittstellenlogik keine Zeit zum Reagieren hat. Die sequentielle Logik arbeitet auf Taktzyklus-zu-Taktzyklus-Basis, die ansteigenden Flanken der Uhr trennen die Ereignisse in Ihrem Design effektiv in Zeitschritte.

Eine Lösung besteht darin, fast leer einzufügen und fast voll Signale, die den ursprünglichen Signalen um einen Taktzyklus vorausgehen. Dies gibt der externen Logik Zeit zum Reagieren, selbst wenn kontinuierlich gelesen oder geschrieben wird.

In unserer Implementierung werden die vorhergehenden Signale empty_next genannt und full_next , einfach weil ich es vorziehe, Namen mit Postfixes zu versehen, anstatt ihnen voranzustellen.

Die Entität

Das Bild unten zeigt die Entität unseres Ringpuffer-FIFO. Zusätzlich zu den Eingangs- und Ausgangssignalen im Port hat er zwei generische Konstanten. Die RAM_WIDTH Generic definiert die Anzahl der Bits in den Eingangs- und Ausgangswörtern, die Anzahl der Bits, die jeder Speicherplatz enthalten wird.

Die RAM_DEPTH Generic definiert die Anzahl der Slots, die für den Ringpuffer reserviert werden. Da ein Schlitz reserviert ist, um anzuzeigen, dass der Ringpuffer voll ist, beträgt die Kapazität des FIFO RAM_DEPTH – 1. Die RAM_DEPTH Konstante sollte an die RAM-Tiefe auf dem Ziel-FPGA angepasst werden. Ungenutztes RAM innerhalb eines Block-RAM-Grundelements wird verschwendet, es kann nicht mit anderer Logik im FPGA geteilt werden.

entity ring_buffer is
  generic (
    RAM_WIDTH : natural;
    RAM_DEPTH : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- Write port
    wr_en : in std_logic;
    wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Read port
    rd_en : in std_logic;
    rd_valid : out std_logic;
    rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Flags
    empty : out std_logic;
    empty_next : out std_logic;
    full : out std_logic;
    full_next : out std_logic;

    -- The number of elements in the FIFO
    fill_count : out integer range RAM_DEPTH - 1 downto 0
  );
end ring_buffer;

Zusätzlich zur Uhr und zum Zurücksetzen enthält die Portdeklaration klassische Daten/Lese- und Schreibports aktivieren. Diese werden von den Upstream- und Downstream-Modulen verwendet, um neue Daten in den FIFO zu schieben und um das älteste Element daraus zu entnehmen.

Die rd_valid Signal wird vom FIFO aktiviert, wenn rd_data Port enthält gültige Daten. Dieses Ereignis wird um einen Taktzyklus nach einem Impuls auf rd_en verzögert Signal. Wir werden am Ende dieses Artikels mehr darüber sprechen, warum das so sein muss.

Dann kommen die vom FIFO gesetzten Leer/Voll-Flags. Die empty_next Das Signal wird aktiviert, wenn 1 oder 0 Elemente übrig sind, während empty ist nur aktiv, wenn 0 Elemente im FIFO vorhanden sind. Ebenso die full_next Signal zeigt an, dass Platz für 1 oder 0 weitere Elemente vorhanden ist, während full wird nur bestätigt, wenn der FIFO kein weiteres Datenelement aufnehmen kann.

Schließlich gibt es noch einen fill_count Ausgang. Dies ist eine ganze Zahl, die die Anzahl der aktuell im FIFO gespeicherten Elemente widerspiegelt. Ich habe dieses Ausgangssignal einfach eingefügt, weil wir es intern im Modul verwenden werden. Das Ausbrechen durch die Entität ist im Wesentlichen kostenlos, und der Benutzer kann dieses Signal beim Instanziieren dieses Moduls unverbunden lassen.

Die deklarative Region

Im deklarativen Bereich der VHDL-Datei deklarieren wir einen benutzerdefinierten Typ, einen Untertyp, eine Reihe von Signalen und eine Prozedur zur internen Verwendung im Ringpuffermodul.

  type ram_type is array (0 to RAM_DEPTH - 1) of
    std_logic_vector(wr_data'range);
  signal ram : ram_type;

  subtype index_type is integer range ram_type'range;
  signal head : index_type;
  signal tail : index_type;

  signal empty_i : std_logic;
  signal full_i : std_logic;
  signal fill_count_i : integer range RAM_DEPTH - 1 downto 0;

  -- Increment and wrap
  procedure incr(signal index : inout index_type) is
  begin
    if index = index_type'high then
      index <= index_type'low;
    else
      index <= index + 1;
    end if;
  end procedure;

Zuerst deklarieren wir einen neuen Typ, um unseren RAM zu modellieren. Die ram_type Typ ist ein Array von Vektoren, dessen Größe durch die generischen Eingaben bestimmt wird. Der neue Typ wird in der nächsten Zeile verwendet, um ram zu deklarieren Signal, das die Daten im Ringpuffer hält.

Im nächsten Codeblock deklarieren wir index_type , ein Untertyp von Integer. Seine Reichweite wird indirekt durch RAM_DEPTH geregelt generisch. Unterhalb der Subtyp-Deklaration verwenden wir den index-Typ, um zwei neue Signale zu deklarieren, die Head- und Tail-Zeiger.

Dann folgt ein Block von Signaldeklarationen, die interne Kopien von Entitätssignalen sind. Sie haben die gleichen Basisnamen wie die Entity-Signale, sind aber mit _i nachgestellt um anzuzeigen, dass sie für den internen Gebrauch bestimmt sind. Wir verwenden diesen Ansatz, weil es als schlechter Stil angesehen wird, inout zu verwenden Modus auf Entity-Signale, obwohl dies den gleichen Effekt hätte.

Schließlich deklarieren wir eine Prozedur namens incr was einen index_type braucht Signal als Parameter. Dieses Unterprogramm wird verwendet, um die Kopf- und Endzeiger zu inkrementieren und sie auf 0 zurückzusetzen, wenn sie den höchsten Wert haben. Head und Tail sind Subtypen von Integer, die normalerweise kein Wrapping-Verhalten unterstützen. Wir werden das Verfahren verwenden, um dieses Problem zu umgehen.

Übereinstimmende Aussagen

An der Spitze der Architektur erklären wir unsere übereinstimmenden Aussagen. Ich ziehe es vor, diese einzeiligen Signalzuweisungen vor den normalen Prozessen zu sammeln, weil sie leicht übersehen werden. Eine gleichzeitige Anweisung ist eigentlich eine Form von Prozess, Sie können hier mehr über gleichzeitige Anweisungen lesen:

So erstellen Sie eine gleichzeitige Anweisung in VHDL

  -- Copy internal signals to output
  empty <= empty_i;
  full <= full_i;
  fill_count <= fill_count_i;

  -- Set the flags
  empty_i <= '1' when fill_count_i = 0 else '0';
  empty_next <= '1' when fill_count_i <= 1 else '0';
  full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0';
  full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0';

Im ersten Block gleichzeitiger Zuweisungen kopieren wir die internen Versionen der Entity-Signale zum Ausgang. Diese Leitungen stellen sicher, dass die Entity-Signale den internen Versionen genau zur gleichen Zeit folgen, jedoch mit einer Verzögerung von einem Delta-Zyklus in der Simulation.

Im zweiten und letzten Block gleichzeitiger Anweisungen weisen wir die Ausgangsflags zu, die den vollen/leeren Zustand des Ringpuffers signalisieren. Wir basieren die Berechnungen auf dem RAM_DEPTH generisch und auf dem fill_count Signal. Die RAM-Tiefe ist eine Konstante, die sich nicht ändert. Daher ändern sich die Flags nur als Ergebnis einer aktualisierten Füllzahl.

Aktualisierung des Kopfzeigers

Die Grundfunktion des Kopfzeigers besteht darin, immer dann zu inkrementieren, wenn das Schreibfreigabesignal von außerhalb dieses Moduls aktiviert wird. Wir tun dies, indem wir den head übergeben Signal an den zuvor erwähnten incr Verfahren.

  PROC_HEAD : process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        head <= 0;
      else

        if wr_en = '1' and full_i = '0' then
          incr(head);
        end if;

      end if;
    end if;
  end process;

Unser Code enthält einen zusätzlichen and full_i = '0' Anweisung zum Schutz vor Überschreibungen. Diese Logik kann weggelassen werden, wenn Sie sicher sind, dass die Datenquelle niemals versuchen wird, in das FIFO zu schreiben, während es voll ist. Ohne diesen Schutz führt ein Überschreiben dazu, dass der Ringpuffer wieder leer wird.

Wenn der Kopfzeiger inkrementiert wird, während der Ringpuffer voll ist, zeigt der Kopf auf dasselbe Element wie der Schwanz. Somit „vergisst“ das Modul die enthaltenen Daten und die FIFO-Füllung scheint leer zu sein.

Durch Auswertung des full_i Signal vor dem Inkrementieren des Kopfzeigers, vergisst es nur den überschriebenen Wert. Ich finde diese Lösung schöner. Aber so oder so, wenn es jemals zu Überschreibungen kommt, deutet dies auf eine Fehlfunktion außerhalb dieses Moduls hin.

Aktualisierung des Endzeigers

Der Endzeiger wird auf ähnliche Weise wie der Kopfzeiger inkrementiert, aber read_en Eingang wird als Trigger verwendet. Genau wie beim Überschreiben schützen wir uns vor Überlesen, indem wir and empty_i = '0' einfügen im booleschen Ausdruck.

  PROC_TAIL : process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        tail <= 0;
        rd_valid <= '0';
      else
        rd_valid <= '0';

        if rd_en = '1' and empty_i = '0' then
          incr(tail);
          rd_valid <= '1';
        end if;

      end if;
    end if;
  end process;

Zusätzlich pulsieren wir den rd_valid Signal bei jedem gültigen Lesevorgang. Die gelesenen Daten sind immer im Taktzyklus nach rd_en gültig wurde geltend gemacht, wenn das FIFO nicht leer war. Mit diesem Wissen besteht für dieses Signal eigentlich keine Notwendigkeit, aber wir werden es der Einfachheit halber einbeziehen. Die rd_valid Signal wird in der Synthese wegoptimiert, wenn es bei der Instanziierung des Moduls nicht verbunden bleibt.

Block-RAM ableiten

Damit das Synthesetool auf Block-RAM schließen kann, müssen wir die Lese- und Schreibports in einem synchronen Prozess ohne Reset deklarieren. Wir lesen und schreiben bei jedem Taktzyklus in den RAM und überlassen die Verwendung dieser Daten den Steuersignalen.

  PROC_RAM : process(clk)
  begin
    if rising_edge(clk) then
      ram(head) <= wr_data;
      rd_data <= ram(tail);
    end if;
  end process;

Dieser Prozess weiß nicht, wann der nächste Schreibvorgang stattfinden wird, muss es aber auch nicht wissen. Stattdessen schreiben wir einfach kontinuierlich. Wenn der head Signal als Ergebnis eines Schreibvorgangs inkrementiert wird, beginnen wir mit dem Schreiben in den nächsten Slot. Dadurch wird der geschriebene Wert effektiv gesperrt.

Aktualisierung des Füllzählers

Der fill_count Das Signal wird zum Erzeugen der Voll- und Leersignale verwendet, die wiederum zum Verhindern des Überschreibens und Überlesens des FIFO verwendet werden. Der Füllzähler wird durch einen kombinatorischen Prozess aktualisiert, der für den Kopf- und Endzeiger empfindlich ist, aber diese Signale werden nur an der ansteigenden Flanke des Takts aktualisiert. Daher ändert sich auch die Füllzahl unmittelbar nach der Taktflanke.

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

Die Füllzahl wird einfach durch Subtrahieren des Schwanzes vom Kopf berechnet. Wenn der Endindex größer als der Kopf ist, müssen wir den Wert von RAM_DEPTH hinzufügen konstant, um die korrekte Anzahl von Elementen zu erhalten, die sich derzeit im Ringpuffer befinden.

Der vollständige VHDL-Code für den Ringpuffer-FIFO

library ieee;
use ieee.std_logic_1164.all;

entity ring_buffer is
  generic (
    RAM_WIDTH : natural;
    RAM_DEPTH : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- Write port
    wr_en : in std_logic;
    wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Read port
    rd_en : in std_logic;
    rd_valid : out std_logic;
    rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Flags
    empty : out std_logic;
    empty_next : out std_logic;
    full : out std_logic;
    full_next : out std_logic;

    -- The number of elements in the FIFO
    fill_count : out integer range RAM_DEPTH - 1 downto 0
  );
end ring_buffer;

architecture rtl of ring_buffer is

  type ram_type is array (0 to RAM_DEPTH - 1) of
    std_logic_vector(wr_data'range);
  signal ram : ram_type;

  subtype index_type is integer range ram_type'range;
  signal head : index_type;
  signal tail : index_type;

  signal empty_i : std_logic;
  signal full_i : std_logic;
  signal fill_count_i : integer range RAM_DEPTH - 1 downto 0;

  -- Increment and wrap
  procedure incr(signal index : inout index_type) is
  begin
    if index = index_type'high then
      index <= index_type'low;
    else
      index <= index + 1;
    end if;
  end procedure;

begin

  -- Copy internal signals to output
  empty <= empty_i;
  full <= full_i;
  fill_count <= fill_count_i;

  -- Set the flags
  empty_i <= '1' when fill_count_i = 0 else '0';
  empty_next <= '1' when fill_count_i <= 1 else '0';
  full_i <= '1' when fill_count_i >= RAM_DEPTH - 1 else '0';
  full_next <= '1' when fill_count_i >= RAM_DEPTH - 2 else '0';

  -- Update the head pointer in write
  PROC_HEAD : process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        head <= 0;
      else

        if wr_en = '1' and full_i = '0' then
          incr(head);
        end if;

      end if;
    end if;
  end process;

  -- Update the tail pointer on read and pulse valid
  PROC_TAIL : process(clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        tail <= 0;
        rd_valid <= '0';
      else
        rd_valid <= '0';

        if rd_en = '1' and empty_i = '0' then
          incr(tail);
          rd_valid <= '1';
        end if;

      end if;
    end if;
  end process;

  -- Write to and read from the RAM
  PROC_RAM : process(clk)
  begin
    if rising_edge(clk) then
      ram(head) <= wr_data;
      rd_data <= ram(tail);
    end if;
  end process;

  -- Update the fill count
  PROC_COUNT : process(head, tail)
  begin
    if head < tail then
      fill_count_i <= head - tail + RAM_DEPTH;
    else
      fill_count_i <= head - tail;
    end if;
  end process;

end architecture;

Der obige Code zeigt den vollständigen Code für den Ringpuffer-FIFO. Sie können das folgende Formular ausfüllen, um die ModelSim-Projektdateien sowie die Testbench sofort per Post zu erhalten.

Die Prüfbank

Das FIFO wird in einer einfachen Testbench instanziiert, um zu demonstrieren, wie es funktioniert. Sie können den Quellcode für die Testbench zusammen mit dem ModelSim-Projekt herunterladen, indem Sie das folgende Formular verwenden.

Die generischen Eingaben wurden auf folgende Werte gesetzt:

Die Testbench setzt zuerst den FIFO zurück. Wenn der Reset freigegeben wird, schreibt die Testbench sequentielle Werte (1-255) in den FIFO, bis er voll ist. Schließlich wird der FIFO geleert, bevor der Test abgeschlossen ist.

Wir können die Wellenform für den vollständigen Lauf der Testbench im Bild unten sehen. Der fill_count Signal als analoger Wert in der Wellenform dargestellt, um den Füllstand des FIFOs besser zu veranschaulichen.

Der Head-, der Tail- und der Fill-Count sind zu Beginn der Simulation 0. An der Stelle, wo der full Signal gesetzt wird, hat der Kopf den Wert 255, ebenso der fill_count Signal. Die Füllzahl geht nur bis 255, obwohl wir eine RAM-Tiefe von 256 haben. Das liegt daran, dass wir das keep one open verwenden Methode, um zwischen voll und leer zu unterscheiden, wie wir zuvor in diesem Artikel besprochen haben.

An dem Wendepunkt, an dem wir aufhören, in den FIFO zu schreiben und daraus zu lesen, friert der Head-Wert ein, während der Tail- und Fill-Zähler zu sinken beginnen. Schließlich haben am Ende der Simulation, wenn der FIFO leer ist, sowohl der Kopf als auch der Schwanz den Wert 255, während der Füllzähler 0 ist.

Diese Testbench sollte nur für Demonstrationszwecke als angemessen angesehen werden. Es hat kein selbstüberprüfendes Verhalten oder keine Logik, um zu überprüfen, ob die Ausgabe des FIFO überhaupt korrekt ist.

Wir werden dieses Modul im Artikel der nächsten Woche verwenden, wenn wir uns mit dem Thema eingeschränkte zufällige Verifizierung befassen . Dies ist eine andere Teststrategie als die häufiger verwendeten gerichteten Tests. Kurz gesagt, die Testbench führt zufällige Interaktionen mit dem DUT (Device Under Test) durch, und das Verhalten des DUT muss durch einen separaten Testbench-Prozess verifiziert werden. Schließlich ist der Test abgeschlossen, wenn eine Reihe von vordefinierten Ereignissen aufgetreten sind.

Klicken Sie hier, um den folgenden Blogpost zu lesen:
Eingeschränkte zufällige Überprüfung

Synthetisieren in Vivado

Ich habe den Ringpuffer in Xilinx Vivado synthetisiert, weil es das beliebteste FPGA-Implementierungstool ist. Es sollte jedoch auf allen FPGA-Architekturen mit Dual-Port-Block-RAM funktionieren.

Wir müssen den generischen Eingängen einige Werte zuweisen, um den Ringpuffer als eigenständiges Modul implementieren zu können. Dies geschieht in Vivado über die EinstellungenAllgemeinGenerika/Parameter Menü, wie im Bild unten gezeigt.

Der Wert für RAM_WIDTH auf 16 gesetzt, was der gleiche ist wie in der Simulation. Aber ich habe den RAM_DEPTH eingestellt bis 2048, da dies die maximale Tiefe des RAMB36E1-Primitives in der Xilinx Zynq-Architektur ist, die ich gewählt habe. Wir hätten einen niedrigeren Wert wählen können, aber es hätte immer noch die gleiche Anzahl von Block-RAMs verwendet. Ein höherer Wert hätte dazu geführt, dass mehr als ein Block-RAM verwendet wurde.

Das folgende Bild zeigt die Ressourcennutzung nach der Implementierung, wie von Vivado gemeldet. Unser Ringpuffer hat tatsächlich einen Block RAM und eine Handvoll LUTs und Flip-Flops verbraucht.

Ditching the valid signal

Sie fragen sich vielleicht, ob die Verzögerung von einem Taktzyklus zwischen rd_en und die rd_valid Signal ist eigentlich notwendig. Schließlich sind die Daten bereits auf rd_data vorhanden wenn wir den rd_en bestätigen Signal. Können wir diesen Wert nicht einfach verwenden und den Ringpuffer beim Lesen aus dem FIFO beim nächsten Taktzyklus zum nächsten Element springen lassen?

Genau genommen brauchen wir den valid nicht Signal. Ich habe dieses Signal nur der Einfachheit halber eingefügt. Der entscheidende Teil ist, dass wir bis zum Taktzyklus warten müssen, nachdem wir rd_en bestätigt haben Signal, sonst hat das RAM keine Zeit zu reagieren.

Block-RAM in FPGAs sind vollsynchrone Komponenten, sie benötigen eine Taktflanke sowohl zum Lesen als auch zum Schreiben von Daten. Lese- und Schreibtakt müssen nicht von derselben Taktquelle kommen, aber Taktflanken müssen vorhanden sein. Außerdem darf zwischen dem RAM-Ausgang und dem nächsten Register (Flip-Flops) keine Logik vorhanden sein. Dies liegt daran, dass sich das Register, das zum Takten des RAM-Ausgangs verwendet wird, innerhalb des Block-RAM-Grundelements befindet.

Das obige Bild zeigt ein Zeitdiagramm, wie sich ein Wert von wr_data ausbreitet Eingabe in unseren Ringpuffer, durch das RAM und erscheint schließlich auf dem rd_data Ausgang. Da jedes Signal an der ansteigenden Taktflanke abgetastet wird, dauert es drei Taktzyklen, bis wir beginnen, den Schreibport anzusteuern, bevor es am Leseport erscheint. Und ein zusätzlicher Taktzyklus vergeht, bevor das empfangende Modul diese Daten verwenden kann.

Reduktion der Latenz

Es gibt Möglichkeiten, dieses Problem zu entschärfen, aber es geht zu Lasten zusätzlicher Ressourcen, die im FPGA verwendet werden. Versuchen wir ein Experiment, um die Verzögerung eines Taktzyklus vom Leseport unseres Ringpuffers zu verringern. Im Code-Snippet unten haben wir den rd_data geändert Ausgabe von einem synchronen Prozess an einen kombinatorischen Prozess, der für ram empfindlich ist und tail Signal.

  PROC_READ : process(ram, tail)
   begin
     rd_data <= ram(tail);
   end process;

Leider kann dieser Code nicht dem Block-RAM zugeordnet werden, da es möglicherweise eine kombinatorische Logik zwischen dem RAM-Ausgang und dem ersten Downstream-Register auf dem rd_data gibt Signal.

Das folgende Bild zeigt die von Vivado gemeldete Ressourcennutzung. Das Block-RAM wurde durch LUTRAM ersetzt; eine Form von verteiltem RAM, die in LUTs implementiert ist. Die LUT-Nutzung ist von 37 LUTs auf 947 sprunghaft angestiegen. Nachschlagetabellen und Flip-Flops sind teurer als Block-RAM, das ist der ganze Grund, warum wir überhaupt Block-RAM haben.

Es gibt viele Möglichkeiten, einen Ringpuffer-FIFO im Block-RAM zu implementieren. Sie können den zusätzlichen Taktzyklus möglicherweise sparen, indem Sie ein anderes Design verwenden, aber es kostet in Form von zusätzlicher unterstützender Logik. Für die meisten Anwendungen ist der in diesem Artikel vorgestellte Ringpuffer ausreichend.

Aktualisierung:
Erstellen eines Ringpuffer-FIFOs im Block-RAM mit dem AXI-Ready/Valid-Handshake

Im nächsten Blogbeitrag werden wir eine bessere Testbench für das Ringpuffermodul erstellen, indem wir eingeschränkte zufällige Verifizierung verwenden .

Klicken Sie hier, um den folgenden Blogpost zu lesen:
Eingeschränkte zufällige Überprüfung


VHDL

  1. So erstellen Sie eine Liste von Zeichenfolgen in VHDL
  2. So erstellen Sie eine Tcl-gesteuerte Testbench für ein VHDL-Code-Sperrmodul
  3. So stoppen Sie die Simulation in einer VHDL-Testbench
  4. So erstellen Sie einen PWM-Controller in VHDL
  5. So generieren Sie Zufallszahlen in VHDL
  6. So erstellen Sie eine selbstüberprüfende Testbench
  7. So erstellen Sie eine verknüpfte Liste in VHDL
  8. So verwenden Sie eine Prozedur in einem Prozess in VHDL
  9. So verwenden Sie eine unreine Funktion in VHDL
  10. So verwenden Sie eine Funktion in VHDL