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

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Funktionsweise eines Ringpuffers

Ringpuffer sind beliebte Konstrukte zur Implementierung von Warteschlangen (FIFOs) in sequenziellen Programmiersprachen, lassen sich aber auch in Hardware realisieren. In diesem Artikel erstellen wir einen Ringpuffer in VHDL, der einen FIFO in Block-RAM abbildet.

Bei der FIFO-Implementierung sind zahlreiche Entwurfsentscheidungen zu treffen: Welches Interface wird benötigt? Sind Ressourcen begrenzt? Muss der Puffer gegen Überlesen und Überschreiben geschützt sein? Ist Latenz akzeptabel? Diese Fragen stellen sich unweigerlich bei der Entwicklung eines FIFOs.

Es gibt viele freie FIFO-Implementierungen online sowie Generatoren wie Xilinx LogiCORE. Dennoch bevorzugen viele Ingenieure eine eigene Implementierung, da sich die Details – Timing, Ressourcenverbrauch, Robustheit – stark unterscheiden können, obwohl die Grundfunktionen (Enqueue/Dequeue) identisch sind.

Leerer Zustand

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Zeigen Head- und Tail-Zeiger auf denselben Speicherplatz, ist der FIFO leer. Die Abbildung zeigt ein Beispiel mit acht Slots: Beide Zeiger stehen auf Index 0 – der initiale Zustand.

Wichtig: Der FIFO wäre auch leer, wenn beide Zeiger auf einem anderen Index (z. B. 3) stehen. Bei jedem Schreibzugriff rückt der Head-Zeiger um eine Position vor; bei jedem Lesezugriff der Tail-Zeiger. Erreicht ein Zeiger den höchsten Index, springt er beim nächsten Zugriff zurück auf den niedrigsten Index – das Prinzip des Ringpuffers: Die Daten bleiben an Ort und Stelle, nur die Zeiger bewegen sich.

Head führt vor Tail

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Nach fünf Schreibzugriffen zeigt der Tail noch auf Slot 0, der Head auf Slot 5. Die belegten Slots sind hellblau markiert. Der Tail verweist auf das älteste Element, der Head auf den nächsten freien Slot.

Solange der Head-Index größer ist als der Tail-Index, ergibt die Differenz (Head − Tail) die Anzahl der gespeicherten Elemente – im Beispiel fünf.

Tail führt vor Head

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Führt der Tail (Index 5) vor dem Head (Index 2), liefert die einfache Subtraktion ein negatives Ergebnis (−3). Die Lösung: Den Head-Index um die Gesamtzahl der Slots (hier 8) erhöhen: (2 + 8) − 5 = 5 – korrekt.

Der Tail „jagt“ dem Head永远 hinterher; die Daten liegen immer zwischen den beiden Zeigern (hellblau).

Voller Zustand

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Ein voller Ringpuffer hat den Tail direkt hinter dem Head. Dadurch bleibt mindestens ein Slot ungenutzt (gelb markiert), um „voll“ von „leer“ unterscheiden zu können. Ein dediziertes Full/Empty-Flag würde alle Slots nutzbar machen, erfordert aber zusätzliche Register und LUTs. Wir wählen das „ein Slot frei lassen“-Verfahren, da es nur günstigen Block-RAM „verschwendet“.

Implementierung des Ringpuffer-FIFOs

Das Interface bestimmt die möglichen Implementierungen. Wir nutzen eine Variante des klassischen Read/Write-Enable-Interfaces mit Empty/Full/Valid-Signalen.

Back-Pressure durch Vorab-Signale

Reagiert die externe Logik erst bei assertiertem empty oder full, ist es oft zu spät, da sequenzielle Logik taktsynchron arbeitet.

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Lösung: empty_next und full_next signalisieren den Zustand einen Taktzyklus früher („almost empty/full“). Dies gibt der Außenlogik Zeit zu reagieren, auch bei kontinuierlichem Lesen/Schreiben.

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Entity-Deklaration

Das Entity besitzt zwei Generics: RAM_WIDTH (Bitbreite pro Speicherplatz) und RAM_DEPTH (Anzahl der Slots). Da ein Slot für die Voll-Erkennung reserviert ist, beträgt die nutzbare Kapazität RAM_DEPTH − 1. RAM_DEPTH sollte an die Block-RAM-Tiefe des Ziel-FPGAs angepasst werden, um ungenutzten Speicher zu vermeiden.

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;
    -- Fill level
    fill_count : out integer range RAM_DEPTH - 1 downto 0
  );
end ring_buffer;

rd_valid wird einen Taktzyklus nach rd_en asserted, sofern der FIFO nicht leer war. Die Flags: empty_next bei ≤1 Element, empty bei 0; full_next bei ≥ RAM_DEPTH−1 Elementen, full bei ≥ RAM_DEPTH−2. fill_count spiegelt die aktuelle Belegung wider und ist intern ohnehin nötig – nach außen geführt ist es „kostenlos“.

Deklarativer Bereich

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;

  -- Inkrement mit Wrap-Around
  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

ram_type modelliert den Speicher als Array von Vektoren. index_type begrenzt die Zeiger auf den gültigen Adressbereich. Interne Signale (_i) vermeiden inout-Ports. Die Prozedur incr handhabt das Wrap-Around der ganzzahligen Zeiger.

Concurrent Statements (Nebenläufige Zuweisungen)

  -- Intern nach außen durchreichen
  empty      <= empty_i;
  full       <= full_i;
  fill_count <= fill_count_i;

  -- Flag-Logik basierend auf fill_count_i
  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';

Diese Zuweisungen sind delta-zyklus-verzögert in der Simulation, aber synthetisierbar. Die Flags ändern sich nur bei Änderung von fill_count_i.

Head-Zeiger aktualisieren

  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;

Der Schutz full_i = '0' verhindert Überschreiben: Würde der Head bei vollem FIFO inkrementiert, zeigte er auf den Tail – der FIFO würde fälschlicherweise als leer erscheinen. So geht nur das überschriebene Datum verloren.

Tail-Zeiger aktualisieren

  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;

Analog zum Head wird empty_i geprüft. rd_valid pulsiert bei jedem gültigen Lesezugriff – ein Takt nach rd_en. Unbenutzt wird es in der Synthese optimiert.

Block-RAM-Inferenz

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

Synchroner Lese-/Schreibzugriff ohne Reset lässt das Synthesis-Tool Block-RAM inferieren. Kontinuierliches Schreiben „verriegelt“ den Wert beim Inkrementieren von head.

Füllstandszähler

  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;

Kombinatorischer Prozess, sensitiv auf head und tail. Da diese nur an Taktflanken wechseln, aktualisiert sich fill_count_i unmittelbar nach der Flanke.

Vollständiger VHDL-Code

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;
    wr_en      : in  std_logic;
    wr_data    : in  std_logic_vector(RAM_WIDTH - 1 downto 0);
    rd_en      : in  std_logic;
    rd_valid   : out std_logic;
    rd_data    : out std_logic_vector(RAM_WIDTH - 1 downto 0);
    empty      : out std_logic;
    empty_next : out std_logic;
    full       : out std_logic;
    full_next  : out std_logic;
    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;

  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
  empty      <= empty_i;
  full       <= full_i;
  fill_count <= fill_count_i;

  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';

  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;

  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;

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

  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;

Testbench

Die Testbench instanziiert den FIFO mit RAM_WIDTH = 16, RAM_DEPTH = 256. Nach Reset werden Werte 1–255 geschrieben bis full asserted, dann wieder gelesen. Das Wellenformbild (Analog-Darstellung von fill_count) zeigt den Füllstandverlauf.

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Am Ende der Simulation zeigen Head und Tail beide 255, fill_count = 0. Die Testbench dient nur der Demonstration; sie enthält keine Selbstprüfung.

Synthese in Vivado

Synthese für Xilinx Zynq (RAMB36E1, max. Tiefe 2048). RAM_DEPTH = 2048 nutzt genau einen Block-RAM. Ressourcenbericht bestätigt: 1 Block RAM, wenige LUTs/FFs.

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Verzicht auf das Valid-Signal?

Das rd_valid-Signal verzögert die Daten um einen Takt. Block-RAM ist vollsynchron: Lesen und Schreiben benötigen Taktflanken. Der Ausgabe-Register sitzt im Block-RAM-Primitive; dazwischen darf keine Logik liegen.

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Drei Takte von wr_data bis rd_data, plus ein weiterer für das nachgeschaltete Modul.

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Latenzreduktion?

Kombinatorisches Lesen (rd_data <= ram(tail)) spart einen Takt, verhindert aber Block-RAM-Inferenz → LUTRAM (verteilter RAM). Vivado zeigt: LUTs steigen von 37 auf 947. Block-RAM ist effizienter; für die meisten Anwendungen reicht der vorgestellte Ringpuffer.

Ein robuster VHDL-Ringpuffer-FIFO im Block-RAM entwerfen

Weiterführend: AXI Ready/Valid-Handshake für Ringpuffer-FIFO im Block-RAM. Nächster Artikel: Constrained Random Verification für bessere Testbenches.

VHDL

  1. String-Listen in VHDL erstellen: Best Practices & Beispiel
  2. Tcl-gesteuerter Testbench für ein VHDL Code-Lock-Modul erstellen
  3. VHDL-Testbench-Simulation sauber beenden
  4. PWM-Controller in VHDL implementieren: Design, Simulation & FPGA-Demo
  5. Zufallszahlen in VHDL generieren: Von Uniform zu OSVVM
  6. Selbstprüfenden VHDL-Testbench erstellen – Praxisleitfaden
  7. Dynamische verkettete Liste in VHDL mit Protected Types und Access-Pointern
  8. In-Prozess-Prozeduren für saubere VHDL-FSM-Entwürfe nutzen
  9. Impure Functions in VHDL nutzen: FSM-Lesbarkeit und Wartbarkeit steigern
  10. VHDL-Funktionen meistern: Praktischer Leitfaden für effizientes Design