Industriële fabricage
Industrieel internet der dingen | Industriële materialen | Onderhoud en reparatie van apparatuur | Industriële programmering |
home  MfgRobots >> Industriële fabricage >  >> Industrial programming >> VHDL

Hoe maak je een ringbuffer FIFO in VHDL

Circulaire buffers zijn populaire constructies voor het maken van wachtrijen in sequentiële programmeertalen, maar ze kunnen ook in hardware worden geïmplementeerd. In dit artikel zullen we een ringbuffer in VHDL maken om een ​​FIFO in blok-RAM te implementeren.

Er zijn veel ontwerpbeslissingen die u moet nemen bij het implementeren van een FIFO. Wat voor interface heb je nodig? Word je beperkt door middelen? Moet het bestand zijn tegen overlezen en overschrijven? Is latentie acceptabel? Dat zijn enkele van de vragen die bij mij opkomen als ik word gevraagd om een ​​FIFO te maken.

Er bestaan ​​veel gratis FIFO-implementaties online, evenals FIFO-generatoren zoals Xilinx LogiCORE. Maar toch geven veel ingenieurs er de voorkeur aan hun eigen FIFO's te implementeren. Want ook al voeren ze allemaal dezelfde basistaken uit voor de wachtrij en de wachtrij, ze kunnen enorm verschillen als ze rekening houden met de details.

Hoe een ringbuffer werkt

Een ringbuffer is een FIFO-implementatie die aaneengesloten geheugen gebruikt voor het opslaan van de gebufferde gegevens met een minimum aan gegevensverplaatsing. Nieuwe elementen blijven op dezelfde geheugenlocatie vanaf het moment van schrijven totdat ze worden gelezen en verwijderd uit de FIFO.

Er worden twee tellers gebruikt om de locatie en het aantal elementen in de FIFO bij te houden. Deze tellers verwijzen naar een offset vanaf het begin van de geheugenruimte waar de gegevens zijn opgeslagen. In VHDL is dit een index voor een matrixcel. Voor de rest van dit artikel zullen we verwijzen naar deze tellers zijn pointers .

Deze twee wijzers zijn de kop en staart aanwijzingen. De kop wijst altijd naar het geheugenslot dat de volgende geschreven gegevens zal bevatten, terwijl de staart verwijst naar het volgende element dat uit de FIFO zal worden gelezen. Er zijn andere varianten, maar dit is degene die we gaan gebruiken.

Lege staat

Als de kop en de staart naar hetzelfde element wijzen, betekent dit dat de FIFO leeg is. De afbeelding hierboven toont een voorbeeld FIFO met acht slots. Zowel de kop- als de staartaanwijzer wijzen naar element 0, wat aangeeft dat de FIFO leeg is. Dit is de beginstatus van de ringbuffer.

Merk op dat de FIFO nog steeds leeg zou zijn als beide aanwijzers zich op een andere index zouden bevinden, bijvoorbeeld 3. Voor elke schrijfactie gaat de hoofdaanwijzer één plaats vooruit. De staartaanwijzer wordt verhoogd elke keer dat de gebruiker van de FIFO een element leest.

Wanneer een van de aanwijzers zich op de hoogste index bevindt, zal de volgende schrijf- of leesbewerking ervoor zorgen dat de aanwijzer teruggaat naar de laagste index. Dit is het mooie van de ringbuffer, de gegevens bewegen niet, alleen de wijzers wel.

Hoofd leidt staart

De afbeelding hierboven toont dezelfde ringbuffer na vijf schrijfbewerkingen. De staartwijzer bevindt zich nog steeds op slot nummer 0, maar de kopwijzer is verplaatst naar slot nummer 5. De slots met gegevens zijn in de afbeelding lichtblauw gekleurd. De staartwijzer staat bij het oudste element, terwijl de kop naar het volgende vrije slot wijst.

Wanneer de kop een hogere index heeft dan de staart, kunnen we het aantal elementen in de ringbuffer berekenen door de staart van de kop af te trekken. In de afbeelding hierboven levert dat een telling van vijf elementen op.

Staart leidt hoofd

Het hoofd van de staart aftrekken werkt alleen als het hoofd de staart leidt. In de bovenstaande afbeelding staat de kop op index 2 terwijl de staart op index 5 staat. Als we deze eenvoudige berekening dus uitvoeren, krijgen we 2 – 5 =-3, wat geen zin heeft.

De oplossing is om de kop te compenseren met het totale aantal slots in de FIFO, 8 in dit geval. De berekening levert nu (2 + 8) – 5 =5 op, wat het juiste antwoord is.

De staart zal altijd de kop achterna zitten, zo werkt een ringbuffer. De helft van de tijd zal de staart een hogere index hebben dan de kop. De gegevens worden tussen de twee opgeslagen, zoals aangegeven door de lichtblauwe kleur in de afbeelding hierboven.

Volledige staat

Bij een buffer met volledige ring wijst de staart naar de index direct na de kop. Een gevolg van dit schema is dat we nooit alle slots kunnen gebruiken om gegevens op te slaan, er moet minstens één vrij slot zijn. De afbeelding hierboven toont een situatie waarin de ringbuffer vol is. De open, maar onbruikbare gleuf is geel gekleurd.

Een speciaal leeg/vol-signaal kan ook worden gebruikt om aan te geven dat de ringbuffer vol is. Hierdoor zouden alle geheugenslots gegevens kunnen opslaan, maar het vereist extra logica in de vorm van registers en opzoektabellen (LUT's). Daarom gaan we de keep one open . gebruiken schema voor onze implementatie van de ringbuffer FIFO, omdat dit alleen maar goedkoper blok-RAM verspilt.

De ringbuffer FIFO-implementatie

Hoe u de interfacesignalen van en naar uw FIFO definieert, zal het aantal mogelijke implementaties van uw ringbuffer beperken. In ons voorbeeld gaan we een variant gebruiken van de klassieke lees/schrijf-enabled en lege/volledige/geldige interface.

Er zal een schrijfgegevens . zijn bus aan de invoerzijde die de gegevens draagt ​​die naar de FIFO moeten worden gepusht. Er zal ook een schrijfmogelijkheid . zijn signaal, dat, wanneer bevestigd, ervoor zorgt dat de FIFO de invoergegevens bemonstert.

De uitvoerzijde heeft een gegevens lezen en een lees geldig signaal bestuurd door de FIFO. Het heeft ook een leesfunctie signaal bestuurd door de downstreamgebruiker van de FIFO.

De lege en vol besturingssignalen maken deel uit van de klassieke FIFO-interface, we zullen ze ook gebruiken. Ze worden bestuurd door de FIFO en hun doel is om de lezer en schrijver de status van de FIFO te communiceren.

Terugdruk

Het probleem met wachten tot de FIFO leeg of vol is voordat actie wordt ondernomen, is dat de interfacelogica geen tijd heeft om te reageren. Sequentiële logica werkt op basis van klokcyclus tot klokcyclus, de stijgende flanken van de klok scheiden de gebeurtenissen in uw ontwerp effectief in tijdstappen.

Een oplossing is om bijna leeg . op te nemen en bijna vol signalen die één klokcyclus voorafgaan aan de oorspronkelijke signalen. Dit geeft de externe logica de tijd om te reageren, zelfs bij continu lezen of schrijven.

In onze implementatie krijgen de voorgaande signalen de naam empty_next en full_next , simpelweg omdat ik de voorkeur geef aan postfix in plaats van prefixnamen.

De entiteit

De afbeelding hieronder toont de entiteit van onze ringbuffer FIFO. Naast de ingangs- en uitgangssignalen in de poort heeft deze twee generieke constanten. De RAM_WIDTH generiek definieert het aantal bits in de invoer- en uitvoerwoorden, het aantal bits dat elk geheugenslot zal bevatten.

De RAM_DEPTH generiek definieert het aantal slots dat wordt gereserveerd voor de ringbuffer. Omdat er één slot is gereserveerd om aan te geven dat de ringbuffer vol is, wordt de capaciteit van de FIFO RAM_DEPTH – 1. De RAM_DEPTH constante moet worden afgestemd op de RAM-diepte op de doel-FPGA. Ongebruikte RAM binnen een blok RAM-primitief zal worden verspild, het kan niet worden gedeeld met andere logica in de FPGA.

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;

Naast de klok en reset, zal de poortdeclaratie klassieke data bevatten/lees- en schrijfpoorten inschakelen. Deze worden door de upstream- en downstream-modules gebruikt om nieuwe gegevens naar de FIFO te pushen en om het oudste element eruit te halen.

De rd_valid signaal wordt bevestigd door de FIFO wanneer de rd_data poort bevat geldige gegevens. Deze gebeurtenis wordt met één klokcyclus vertraagd na een puls op de rd_en signaal. We zullen aan het einde van dit artikel meer vertellen over waarom het zo moet zijn.

Dan komen de lege/volle vlaggen die door de FIFO zijn ingesteld. De empty_next signaal wordt bevestigd als er nog 1 of 0 elementen over zijn, terwijl empty is alleen actief als er 0 elementen in de FIFO zijn. Evenzo is de full_next signaal geeft aan dat er ruimte is voor 1 of 0 extra elementen, terwijl full beweert alleen wanneer de FIFO geen ander gegevenselement kan accommoderen.

Ten slotte is er een fill_count uitvoer. Dit is een geheel getal dat het aantal elementen weerspiegelt dat momenteel in de FIFO is opgeslagen. Ik heb dit uitgangssignaal gewoon opgenomen omdat we het intern in de module gaan gebruiken. Het doorbreken ervan via de entiteit is in wezen gratis, en de gebruiker kan ervoor kiezen om dit signaal niet aangesloten te laten bij het instantiëren van deze module.

De declaratieve regio

In het declaratieve gebied van het VHDL-bestand zullen we een aangepast type, een subtype, een aantal signalen en een procedure voor intern gebruik in de ringbuffermodule declareren.

  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;

Eerst verklaren we een nieuw type om ons RAM-geheugen te modelleren. De ram_type type is een array van vectoren, gedimensioneerd door de generieke invoer. Het nieuwe type wordt op de volgende regel gebruikt om de ram . te declareren signaal dat de gegevens in de ringbuffer houdt.

In het volgende codeblok declareren we index_type , een subtype van geheel getal. Het bereik wordt indirect bepaald door de RAM_DEPTH algemeen. Onder de subtypedeclaratie gebruiken we het indextype om twee nieuwe signalen te declareren, de kop- en staartaanwijzers.

Dan volgt een blok signaalverklaringen die interne kopieën zijn van entiteitssignalen. Ze hebben dezelfde basisnamen als de entiteitssignalen, maar worden gefixeerd met _i om aan te geven dat ze voor intern gebruik zijn. We gebruiken deze aanpak omdat het als een slechte stijl wordt beschouwd om inout te gebruiken modus op entiteitssignalen, hoewel dit hetzelfde effect zou hebben.

Ten slotte declareren we een procedure met de naam incr waarvoor een index_type . nodig is signaal als parameter. Dit subprogramma zal worden gebruikt om de kop- en staartaanwijzers te verhogen en ze terug te brengen naar 0 wanneer ze de hoogste waarde hebben. De kop en staart zijn subtypen van integer, die normaal gesproken geen wrap-gedrag ondersteunen. We zullen de procedure gebruiken om dit probleem te omzeilen.

Gelijktijdige uitspraken

Bovenaan de architectuur declareren we onze gelijktijdige verklaringen. Ik verzamel deze one-liner-signaaltoewijzingen liever vóór de normale processen, omdat ze gemakkelijk over het hoofd worden gezien. Een gelijktijdige verklaring is eigenlijk een vorm van proces, u kunt hier meer lezen over gelijktijdige verklaringen:

Een gelijktijdige verklaring maken 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';

In het eerste blok van gelijktijdige toewijzingen kopiëren we de interne versies van de entiteitssignalen naar de uitvoer. Deze lijnen zorgen ervoor dat de entiteitssignalen de interne versies op exact hetzelfde moment volgen, maar met één deltacyclusvertraging in de simulatie.

Het tweede en laatste blok van gelijktijdige instructies is waar we de uitvoervlaggen toewijzen, wat de volledige/lege status van de ringbuffer aangeeft. We baseren de berekeningen op de RAM_DEPTH generiek en op de fill_count signaal. De RAM-diepte is een constante die niet zal veranderen. Daarom zullen de vlaggen alleen veranderen als gevolg van een bijgewerkte vultelling.

De hoofdaanwijzer bijwerken

De basisfunctie van de hoofdaanwijzer is om te verhogen wanneer het schrijfinschakelsignaal van buiten deze module wordt afgegeven. We doen dit door de head . door te geven signaal naar de eerder genoemde incr procedure.

  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;

Onze code bevat een extra and full_i = '0' instructie om te beschermen tegen overschrijven. Deze logica kan worden weggelaten als u zeker weet dat de gegevensbron nooit zal proberen naar de FIFO te schrijven terwijl deze vol is. Zonder deze beveiliging zal een overschrijving ervoor zorgen dat de ringbuffer weer leeg raakt.

Als de kopaanwijzer wordt verhoogd terwijl de ringbuffer vol is, wijst de kop naar hetzelfde element als de staart. De module zal dus de ingesloten gegevens "vergeten", en de FIFO-vulling lijkt leeg te zijn.

Door de full_i . te evalueren signaal voordat de hoofdaanwijzer wordt verhoogd, wordt alleen de overschreven waarde vergeten. Deze oplossing vind ik mooier. Maar hoe dan ook, als er ooit overschrijvingen plaatsvinden, wijst dit op een storing buiten deze module.

De staartaanwijzer bijwerken

De staartaanwijzer wordt op dezelfde manier verhoogd als de hoofdaanwijzer, maar de read_en ingang wordt gebruikt als de trigger. Net als bij het overschrijven beschermen we tegen overlezen door and empty_i = '0' . op te nemen in de Booleaanse uitdrukking.

  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;

Bovendien pulseren we de rd_valid signaal bij elke geldige uitlezing. De gelezen gegevens zijn altijd geldig op de klokcyclus na rd_en werd beweerd, als de FIFO niet leeg was. Met deze kennis is dit signaal niet echt nodig, maar we zullen het voor het gemak opnemen. De rd_valid signaal wordt weggeoptimaliseerd in de synthese als het niet is aangesloten wanneer de module wordt geïnstantieerd.

Blok RAM afleiden

Om ervoor te zorgen dat de synthesetool blok-RAM afleidt, moeten we de lees- en schrijfpoorten in een synchroon proces declareren zonder reset. We lezen en schrijven naar het RAM bij elke klokcyclus en laten de besturingssignalen het gebruik van deze gegevens afhandelen.

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

Dit proces weet niet wanneer het volgende schrijven zal plaatsvinden, maar het hoeft het ook niet te weten. In plaats daarvan schrijven we gewoon continu. Wanneer de head signaal wordt verhoogd als gevolg van een schrijfactie, beginnen we met schrijven naar de volgende sleuf. Dit zal effectief de waarde vergrendelen die is geschreven.

De opvultelling bijwerken

De fill_count signaal wordt gebruikt voor het genereren van de volle en lege signalen, die op hun beurt worden gebruikt om overschrijven en overlezen van de FIFO te voorkomen. De vulteller wordt bijgewerkt door een combinatieproces dat gevoelig is voor de kop- en staartaanwijzer, maar die signalen worden alleen bijgewerkt aan de stijgende flank van de klok. Daarom verandert het aantal vullingen ook onmiddellijk na de klokflank.

  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;

Het aantal vullingen wordt eenvoudig berekend door de staart van de kop af te trekken. Als de staartindex groter is dan de kop, moeten we de waarde van de RAM_DEPTH . optellen constant om het juiste aantal elementen te krijgen dat zich momenteel in de ringbuffer bevindt.

De volledige VHDL-code voor de ringbuffer 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;

De code hierboven toont de volledige code voor de ringbuffer FIFO. U kunt het onderstaande formulier invullen om de ModelSim-projectbestanden en de testbench direct naar u gemaild te krijgen.

De testbank

De FIFO wordt geïnstantieerd in een eenvoudige testbank om te demonstreren hoe het werkt. U kunt de broncode voor de testbench samen met het ModelSim-project downloaden door het onderstaande formulier te gebruiken.

De generieke ingangen zijn ingesteld op de volgende waarden:

  • RAM_WIDTH:16
  • RAM_DEPTH:256

De testbench reset eerst de FIFO. Wanneer de reset wordt losgelaten, schrijft de testbench sequentiële waarden (1-255) naar de FIFO totdat deze vol is. Ten slotte wordt de FIFO geleegd voordat de test is voltooid.

We kunnen de golfvorm voor de volledige run van de testbank zien in de onderstaande afbeelding. De fill_count signaal wordt weergegeven als een analoge waarde in de golfvorm om het vulniveau van de FIFO beter te illustreren.

De kop, de staart en het aantal vullingen zijn 0 aan het begin van de simulatie. Op het punt waar de full signaal wordt bevestigd, heeft de kop de waarde 255, en dat geldt ook voor de fill_count signaal. Het aantal vullingen gaat maar tot 255, ook al hebben we een RAM-diepte van 256. Dat komt omdat we de keep one open gebruiken methode om onderscheid te maken tussen vol en leeg, zoals we eerder in dit artikel hebben besproken.

Op het keerpunt waar we stoppen met schrijven naar de FIFO en beginnen met lezen, bevriest de kopwaarde terwijl de staart en het aantal vullingen beginnen af ​​te nemen. Ten slotte, aan het einde van de simulatie, wanneer de FIFO leeg is, hebben zowel de kop als de staart de waarde 255 terwijl het aantal vullingen 0 is.

Deze testbank mag niet als geschikt worden beschouwd voor iets anders dan demonstratiedoeleinden. Het heeft geen zelfcontrolerend gedrag of logica om te verifiëren dat de uitvoer van de FIFO helemaal correct is.

We zullen deze module gebruiken in het artikel van volgende week wanneer we ingaan op het onderwerp beperkte willekeurige verificatie . Dit is een andere teststrategie dan de meer algemeen gebruikte gerichte tests. Kortom, de testbank zal willekeurige interacties uitvoeren met de TU Delft (device under test), en het gedrag van de TU Delft moet worden geverifieerd door een afzonderlijk testbankproces. Ten slotte, wanneer een aantal vooraf gedefinieerde gebeurtenissen hebben plaatsgevonden, is de test voltooid.

Klik hier om de vervolgblogpost te lezen:
Beperkte willekeurige verificatie

Synthetiseren in Vivado

Ik heb de ringbuffer in Xilinx Vivado gesynthetiseerd omdat het de meest populaire FPGA-implementatietool is. Het zou echter moeten werken op alle FPGA-architecturen met dual-port block RAM.

We moeten enkele waarden toekennen aan de generieke ingangen om de ringbuffer als stand-alone module te kunnen implementeren. Dit doe je in Vivado met behulp van de InstellingenAlgemeenAlgemeen/Parameters menu, zoals weergegeven in de onderstaande afbeelding.

De waarde voor de RAM_WIDTH is ingesteld op 16, wat hetzelfde is als in de simulatie. Maar ik heb de RAM_DEPTH . ingesteld tot 2048 omdat dit de maximale diepte is van de RAMB36E1-primitief in de Xilinx Zynq-architectuur die ik heb gekozen. We hadden een lagere waarde kunnen selecteren, maar het zou nog steeds hetzelfde aantal blok-RAM's hebben gebruikt. Een hogere waarde zou hebben geresulteerd in het gebruik van meer dan één blok RAM.

De onderstaande afbeelding toont het resourcegebruik na de implementatie, zoals gerapporteerd door Vivado. Onze ringbuffer heeft inderdaad één blok RAM en een handvol LUT's en flip-flops verbruikt.

Het geldige signaal weggooien

U vraagt ​​zich misschien af ​​of de vertraging van één klokcyclus tussen de rd_en en de rd_valid signaal echt nodig is. De gegevens zijn immers al aanwezig op rd_data wanneer we de rd_en . bevestigen signaal. Kunnen we deze waarde niet gewoon gebruiken en de ringbuffer naar het volgende element laten springen in de volgende klokcyclus zoals we lezen van de FIFO?

Strikt genomen hebben we de valid . niet nodig signaal. Ik heb dit signaal alleen voor het gemak toegevoegd. Het cruciale deel is dat we moeten wachten tot de klokcyclus nadat we de rd_en . hebben bevestigd signaal, anders heeft dat RAM geen tijd om te reageren.

Blok-RAM in FPGA's zijn volledig synchrone componenten, ze hebben een klokflank nodig om zowel gegevens te lezen als te schrijven. De lees- en schrijfklok hoeft niet van dezelfde klokbron te komen, maar er moeten klokranden zijn. Verder mag er geen logica zijn tussen de RAM-uitgang en het volgende register (flip-flops). Dit komt omdat het register dat wordt gebruikt om de RAM-uitvoer te klokken zich binnen het blok RAM-primitief bevindt.

De afbeelding hierboven laat een timingdiagram zien van hoe een waarde zich voortplant vanuit de wr_data invoer in onze ringbuffer, via het RAM, en verschijnt uiteindelijk op de rd_data uitvoer. Omdat elk signaal wordt gesampled op de stijgende klokflank, duurt het drie klokcycli vanaf het moment dat we de schrijfpoort beginnen aan te sturen voordat het op de leespoort verschijnt. En er gaat een extra klokcyclus voorbij voordat de ontvangende module deze gegevens kan gebruiken.

De latentie verminderen

Er zijn manieren om dit probleem te verminderen, maar dit gaat ten koste van extra middelen die in de FPGA worden gebruikt. Laten we een experiment proberen om één klokcyclusvertraging van de leespoort van onze ringbuffer te verminderen. In het onderstaande codefragment hebben we de rd_data . gewijzigd uitvoer van een synchroon proces naar een combinatieproces dat gevoelig is voor de ram en tail signaal.

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

Helaas kan deze code niet worden toegewezen om RAM te blokkeren, omdat er combinatielogica kan zijn tussen de RAM-uitvoer en het eerste stroomafwaartse register op de rd_data signaal.

De onderstaande afbeelding toont het resourcegebruik zoals gerapporteerd door Vivado. Het blok RAM is vervangen door LUTRAM; een vorm van gedistribueerd RAM geïmplementeerd in LUT's. Het LUT-gebruik is omhooggeschoten van 37 LUT's naar 947. Opzoektabellen en flip-flops zijn duurder dan blok-RAM, dat is de hele reden waarom we in de eerste plaats blok-RAM hebben.

Er zijn veel manieren om een ​​ringbuffer-FIFO in blok-RAM te implementeren. U kunt de extra klokcyclus misschien besparen door een ander ontwerp te gebruiken, maar dit kost in de vorm van extra ondersteunende logica. Voor de meeste toepassingen is de ringbuffer die in dit artikel wordt gepresenteerd voldoende.

Bijwerken:
Hoe maak je een ringbuffer FIFO in blok RAM met behulp van de AXI ready/valid handshake

In de volgende blogpost zullen we een betere testbench maken voor de ringbuffermodule door gebruik te maken van beperkte willekeurige verificatie .

Klik hier om de vervolgblogpost te lezen:
Beperkte willekeurige verificatie


VHDL

  1. Een lijst met strings maken in VHDL
  2. Hoe maak je een Tcl-gestuurde testbench voor een VHDL-codeslotmodule?
  3. Simulatie stoppen in een VHDL-testbench
  4. Een PWM-controller maken in VHDL
  5. Hoe willekeurige getallen te genereren in VHDL
  6. Hoe maak je een zelfcontrolerende testbank aan
  7. Een gekoppelde lijst maken in VHDL
  8. Een procedure gebruiken in een proces in VHDL
  9. Een onzuivere functie gebruiken in VHDL
  10. Een functie gebruiken in VHDL
  11. Een eindige-toestandsmachine maken in VHDL