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

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

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

Er zijn veel ontwerpbeslissingen die je moet nemen bij het implementeren van een FIFO. Welke interface heb je nodig? Ben je beperkt door middelen? Moet het bestand zijn tegen over-lezen en overschrijven? Is latentie acceptabel? Dat zijn enkele van de vragen die rijzen wanneer je wordt gevraagd een FIFO te maken.

Er bestaan veel gratis FIFO-implementaties online, evenals FIFO-generators zoals Xilinx LogiCORE. Toch kiezen veel ingenieurs ervoor hun eigen FIFO's te schrijven. Omdat ze hoewel ze allemaal dezelfde basis taken van enqueue en dequeue uitvoeren, in detail grote verschillen kunnen vertonen.

Hoe een ringbuffer werkt

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

Twee tellers houden de locatie en het aantal elementen in de FIFO bij. Deze tellers verwijzen naar een offset vanaf het begin van het geheugengebied waar de data wordt opgeslagen. In VHDL is dit een index naar een array-cel. Voor de rest van dit artikel noemen we deze tellers pointers.

Deze twee pointers zijn de head (kop) en tail (staart). De head wijst altijd naar het geheugenslot dat de volgende geschreven data zal bevatten, terwijl de tail verwijst naar het volgende element dat uit de FIFO gelezen zal worden. Er zijn andere varianten, maar dit is de variant die we gaan gebruiken.

Lege toestand

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

Als de head en tail naar hetzelfde element wijzen, betekent dit dat de FIFO leeg is. De afbeelding hierboven toont een voorbeeld-FIFO met acht slots. Zowel de head- als de tail-pointer wijzen naar element 0, wat aangeeft dat de FIFO leeg is. Dit is de initiële toestand van de ringbuffer.

Let op dat de FIFO nog steeds leeg zou zijn als beide pointers naar een andere index zouden wijzen, bijvoorbeeld 3. Bij elke schrijfbeweegt de head-pointer één plaats vooruit. De tail-pointer wordt verhoogd telkens wanneer de gebruiker van de FIFO een element leest.

Wanneer een van de pointers op de hoogste index staat, zal de volgende schrijf- of leesactie de pointer terug naar de laagste index laten springen. Dit is de schoonheid van de ringbuffer: de data verplaatst zich niet, alleen de pointers doen dat.

Head voorbij tail

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

De afbeelding hierboven toont dezelfde ringbuffer na vijf schrijfacties. De tail-pointer staat nog steeds op slotnummer 0, maar de head-pointer is verplaatst naar slotnummer 5. De slots die data bevatten zijn in de illustratie lichtblauw gekleurd. De tail-pointer wijst naar het oudste element, terwijl de head naar het volgende vrije slot wijst.

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

Tail voorbij head

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

Het aftrekken van de head van de tail werkt alleen als de head de tail voorbij is. In de afbeelding hierboven staat de head op index 2 terwijl de tail op index 5 staat. Als we deze eenvoudige berekening uitvoeren, krijgen we 2 - 5 = -3, wat geen zin heeft.

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

De tail zal de head voor altijd achterna jagen, dat is hoe een ringbuffer werkt. De helft van de tijd zal de tail een hogere index hebben dan de head. De data wordt opgeslagen tussen de twee, zoals aangegeven door de lichtblauwe kleur in de afbeelding hierboven.

Volle toestand

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

Een volle ringbuffer heeft de tail wijzen naar de index direct na de head. Een gevolg van dit schema is dat we nooit alle slots kunnen gebruiken om data op te slaan; er moet minstens één vrij slot zijn. De afbeelding hierboven toont een situatie waarin de ringbuffer vol is. Het open, maar onbruikbare slot, is geel gekleurd.

Een speciaal leeg/vol-signaal zou ook gebruikt kunnen worden om aan te geven dat de ringbuffer vol is. Dit zou toestaan dat alle geheugenslots data opslaan, maar vereist extra logica in de vorm van registers en lookup-tabellen (LUTs). Daarom gaan we het één open houden-schema gebruiken voor onze implementatie van de ringbuffer FIFO, aangezien dit alleen goedkopere blok-RAM verspilt.

De ringbuffer FIFO-implementatie

Hoe je de interfacesignalen naar en van je FIFO definieert, gaat het aantal mogelijke implementaties van je ringbuffer beperken. In ons voorbeeld gebruiken we een variant van de klassieke read/write enable en empty/full/valid-interface.

Er zal een write data-bus aan de invoerkant zijn die de data transporteert die naar de FIFO gepusht moet worden. Er zal ook een write enable-signaal zijn, dat wanneer geactiveerd, de FIFO de invoerdata laat samplen.

De uitvoerkant zal een read data- en een read valid-signaal hebben die door de FIFO worden gecontroleerd. Er zal ook een read enable-signaal zijn dat door de downstream-gebruiker van de FIFO wordt gecontroleerd.

De empty- en full-controlsignalen maken deel uit van de klassieke FIFO-interface; we zullen ze ook gebruiken. Ze worden door de FIFO gecontroleerd en hun doel is om de toestand van de FIFO aan de lezer en schrijver te communiceren.

Terugkoppeling (back pressure)

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

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

Eén oplossing is om almost empty en almost full-signalen op te nemen die de originele signalen met één klokcyclus voorafgaan. Dit geeft de externe logica tijd om te reageren, zelfs bij continu lezen of schrijven.

In onze implementatie zullen de voorafgaande signalen empty_next en full_next heten, simpelweg omdat ik postfixes liever dan prefixes.

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

De entity

De afbeelding hieronder toont de entity van onze ringbuffer FIFO. Naast de in- en outputsignalen in de poort, heeft het twee generische constanten. De RAM_WIDTH-generiek definieert het aantal bits in de in- en uitvoerwoorden, het aantal bits dat elk geheugenslot zal bevatten.

De RAM_DEPTH-generiek definieert het aantal slots dat gereserveerd is voor de ringbuffer. Omdat één slot gereserveerd is om aan te geven dat de ringbuffer vol is, zal de capaciteit van de FIFO RAM_DEPTH - 1 zijn. De RAM_DEPTH-constante moet overeenkomen met de RAM-diepte op de doel-FPGA. Ongebruikte RAM binnen een blok-RAM-primitief wordt verspild, het kan niet gedeeld worden 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; -- Het aantal elementen in de FIFO fill_count : out integer range RAM_DEPTH - 1 downto 0 ); end ring_buffer; 

Naast de klok en reset, bevat de poortdeclaratie klassieke data/enable lees- en schrijfpoorten. Deze worden door de upstream- en downstream-modules gebruikt om nieuwe data naar de FIFO te pushen, en om het oudste element eruit te poppen.

Het rd_valid-signaal wordt door de FIFO geactiveerd wanneer de rd_data-poort geldige data bevat. Deze gebeurtenis is met één klokcyclus vertraagd na een puls op het rd_en-signaal. Later in dit artikel gaan we dieper in op waarom dit zo moet zijn.

Dan volgen de empty/full-vlaggen die door de FIFO worden ingesteld. Het empty_next-signaal wordt geactiveerd wanneer er 1 of 0 elementen over zijn, terwijl empty alleen actief is wanneer er 0 elementen in de FIFO zitten. Op dezelfde manier geeft het full_next-signaal aan dat er ruimte is voor 1 of 0 extra elementen, terwijl full alleen activeert wanneer de FIFO geen ander data-element meer kan verwerken.

Tot slot is er een fill_count-output. Dit is een integer die het aantal elementen weergeeft dat momenteel in de FIFO zit. Ik heb deze output opgenomen omdat we hem intern in de module gebruiken. Het beschikbaar stellen via de entity is vrijwel gratis, en de gebruiker kan dit signaal onverbonden laten bij het instantiëren van deze module.

De declaratieve regio

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

 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 en 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 declareren we een nieuw type om onze RAM te modelleren. Het ram_type-type is een array van vectoren, afgemeten door de generieke invoeren. Het nieuwe type wordt op de volgende regel gebruikt om het ram-signaal te declareren dat de data in de ringbuffer zal opslaan.

In het volgende blok declareren we index_type, een subtype van integer. De bereik wordt indirect bepaald door de RAM_DEPTH-generiek. Onder de subtype-declaratie gebruiken we het index-type om twee nieuwe signalen te declareren: de head- en tail-pointers.

Daarna volgt een blok signaaldeclaraties die interne kopieën zijn van entity-signalen. Ze hebben dezelfde basnamen als de entity-signalen maar zijn gepostfixd met _i om aan te geven dat ze voor intern gebruik zijn. We gebruiken deze benadering omdat het als slechte stijl geldt om inout-modus op entity-signalen te gebruiken, hoewel dit hetzelfde effect zou hebben.

Tot slot declareren we een procedure genaamd incr die een index_type-signaal als parameter neemt. Dit subprogramma wordt gebruikt voor het incrementeren van de head- en tail-pointers, en om ze terug naar 0 te wrappen wanneer ze op de hoogste waarde staan. De head en tail zijn subtypes van integer, wat normaal geen wrapping-gedrag ondersteunt. We gebruiken de procedure om dit probleem te omzeilen.

Concurrente statements

Bovenaan de architectuur declareren we onze concurrente statements. Ik verzamel deze eens-regel-signaltoewijzingen vóór de normale processen omdat ze gemakkelijk over het hoofd worden gezien. Een concurrente statement is eigenlijk een vorm van proces, je kunt er meer over lezen hier:

Hoe je een Concurrent Statement in VHDL maakt

 -- Kopieer interne signalen naar output empty <= empty_i; full <= full_i; fill_count <= fill_count_i; -- Zet de vlaggen 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 concurrente toewijzingen kopiëren we de interne versies van de entity-signalen naar de output. Deze regels zorgen ervoor dat de entity-signalen de interne versies op exact hetzelfde moment volgen, maar met één delta-cyclus vertraging in simulatie.

Het tweede en laatste blok concurrente statements is waar we de outputvlagsen toewijzen, die de vol/lege toestand van de ringbuffer signaleren. We baseren de berekeningen op de RAM_DEPTH-generiek en op het fill_count-signaal. De RAM-diepte is een constante die niet verandert. Daarom zullen de vlaggen alleen veranderen als gevolg van een bijgewerkte vultelling.

Updaten van de head-pointer

De basisfunctionaliteit van de head-pointer is om te incrementeren wanneer het write enable-signaal van buitenaf geactiveerd wordt. We doen dit door het head-signaal door te geven aan 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'-statement om te beschermen tegen overschrijven. Deze logica kan weggelaten worden als je zeker weet dat de databron nooit zal proberen te schrijven naar de FIFO terwijl hij vol is. Zonder deze bescherming zal een overschrijving ervoor zorgen dat de ringbuffer weer leeg lijkt.

Als de head-pointer wordt geïncrementeerd terwijl de ringbuffer vol is, zal de head naar hetzelfde element wijzen als de tail. De module zal de ingesloten data dus "vergeten", en de FIFO-vulling lijkt leeg te zijn.

Door het full_i-signaal te evalueren vóór het incrementeren van de head-pointer, wordt alleen de overschreven waarde vergeten. Ik vind deze oplossing netter. Maar op welke manier ook, als overschrijvingen ooit gebeuren, is dat indicatief voor een malfunction buiten deze module.

Updaten van de tail-pointer

De tail-pointer wordt op een vergelijkbare manier geïncrementeerd als de head-pointer, maar het read_en-input wordt als trigger gebruikt. Net als bij de overschrijvingen beschermen we tegen over-lezen door and empty_i = '0' in de Booleaanse expressie op te nemen.

 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; 

Daarnaast pulseren we het rd_valid-signaal bij elke geldige leesactie. De readdata is altijd geldig op de klokcyclus na het activeren van rd_en, mits de FIFO niet leeg was. Met deze kennis is er eigenlijk geen noodzaak voor dit signaal, maar we voegen het toe voor gemak. Het rd_valid-signaal zal in synthetisering geoptimaliseerd worden als het onverbonden blijft bij het instantiëren van de module.

Blok-RAM infereren

Om de synthetiseringstool blok-RAM te laten infereren, moeten we de lees- en schrijfpoorten declareren in een synchroon proces zonder reset. We lezen en schrijven naar de RAM op elke klokcyclus, en laten de controlsignalen het gebruik van deze data regelen.

 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 de volgende schrijfactie plaatsvindt, maar het hoeft dat ook niet te weten. In plaats daarvan schrijven we continu. Wanneer het head-signaal wordt geïncrementeerd als gevolg van een schrijfactie, beginnen we met schrijven naar het volgende slot. Dit zal de geschreven waarde effectief vastleggen.

Updaten van de vultelling

Het fill_count-signaal wordt gebruikt voor het genereren van de vol- en lege signalen, die op hun beurt gebruikt worden om overschrijven en over-lezen van de FIFO te voorkomen. De vulteller wordt bijgewerkt door een combinationeel proces dat gevoelig is voor de head- en tail-pointer, maar die signalen worden alleen bijgewerkt op de stijgende flank van de klok. Daarom zal de vultelling ook direct na de klokflank veranderen.

 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; 

De vultelling wordt berekend door de tail van de head af te trekken. Als de tail-index groter is dan de head, moeten we de waarde van de RAM_DEPTH-constante toevoegen om het juiste aantal elementen te krijgen dat momenteel in de ringbuffer zit.

De complete 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; -- Het aantal elementen in de 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 en 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 -- Kopieer interne signalen naar output empty <= empty_i; full <= full_i; fill_count <= fill_count_i; -- Zet de vlaggen 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 de head-pointer bij schrijven 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 de tail-pointer bij lezen en pulseer 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; -- Schrijf naar en lees van de RAM PROC_RAM : process(clk) begin if rising_edge(clk) then ram(head) <= wr_data; rd_data <= ram(tail); end if; end process; -- Update de vultelling 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 bovenstaande code toont de complete code voor de ringbuffer FIFO. Je kunt het formulier hieronder invullen om de ModelSim-projectbestanden en de testbench direct per e-mail te ontvangen.

De testbench

De FIFO wordt geïnstantieerd in een eenvoudige testbench om te demonstreren hoe het werkt. Je kunt de broncode voor de testbench samen met het ModelSim-project downloaden via het formulier hieronder.

De generieke invoeren zijn ingesteld op de volgende waarden:

  • RAM_WIDTH: 16
  • RAM_DEPTH: 256

De testbench reset de FIFO eerst. Wanneer de reset vrijgegeven wordt, schrijft de testbench opeenvolgende waarden (1-255) naar de FIFO totdat hij vol is. Ten slotte wordt de FIFO geleegd voordat de test compleet is.

We kunnen de golfform voor de volledige uitvoering van de testbench zien in de afbeelding hieronder. Het fill_count-signaal wordt als een analoge waarde weergegeven in de golfform om het vullingsniveau van de FIFO beter te illustreren.

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

De head, de tail en de vultelling zijn 0 aan het begin van de simulatie. Op het moment dat het full-signaal geactiveerd wordt, heeft de head de waarde 255, en zo heeft het fill_count-signaal. De vultelling gaat slechts tot 255, ondanks dat we een RAM-diepte van 256 hebben. Dat is omdat we de één open houden-methode gebruiken om vol en leeg te onderscheiden, zoals we eerder in dit artikel bespraken.

Op het draaipunt waar we stoppen met schrijven naar de FIFO en beginnen met lezen, bevriest de head-waarde terwijl de tail en vultelling beginnen te dalen. Uiteindelijk, aan het einde van de simulatie wanneer de FIFO leeg is, hebben zowel de head als de tail de waarde 255 terwijl de vultelling 0 is.

Deze testbench mag niet als adequate beschouwd worden voor iets anders dan demonstratiedoeleinden. Het heeft geen self-checking gedrag of logica om te verifiëren of de output van de FIFO correct is.

We zullen deze module in volgende week artikel gebruiken wanneer we ingaan op het onderwerp constrained random verification. Dit is een andere teststrategie dan de meer gebruikelijke gerichte tests. Kort gezegd, de testbench zal willekeurige interacties met de DUT (device under test) uitvoeren, en het gedrag van de DUT moet geverifieerd worden door een apart testbench-proces. Ten slotte, wanneer een aantal vooraf gedefinieerde gebeurtenissen hebben plaatsgevonden, is de test compleet.

Klik hier om het vervolgblogbericht te lezen:
Constrained random verification

Synthetiseren in Vivado

Ik heb de ringbuffer gesynthetiseerd in Xilinx Vivado omdat het de populairste FPGA-implementatietool is. Het zou echter op alle FPGA-architecturen moeten werken die dual-port blok-RAM hebben.

We moeten waarden toewijzen aan de generieke invoeren om de ringbuffer als zelfstandige module te kunnen implementeren. Dit wordt in Vivado gedaan via het menu Settings → General → Generics/Parameters, zoals in de afbeelding hieronder getoond.

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

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

De afbeelding hieronder toont de post-implementatie resourcegebruik, gerapporteerd door Vivado. Onze ringbuffer heeft inderdaad één blok-RAM en een handjevol LUTs en flip-flops verbruikt.

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

Afscheid nemen van het valid-signaal

Je vraagt je misschien af of de één klokcyclus vertraging tussen het rd_en- en het rd_valid-signaal eigenlijk noodzakelijk is. Immers, de data is al aanwezig op rd_data wanneer we rd_en activeren. Kunnen we deze waarde niet gewoon gebruiken en de ringbuffer het volgende element laten overslaan op de volgende klokcyclus terwijl we uit de FIFO lezen?

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

Streng genomen hebben we het valid-signaal niet nodig. Ik heb dit signaal alleen voor gemak opgenomen. Het cruciale deel is dat we moeten wachten tot de klokcyclus na het activeren van het rd_en-signaal, anders heeft de RAM geen tijd om te reageren.

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

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

De afbeelding hierboven toont een timingdiagram van hoe een waarde propageert van de wr_data-input in onze ringbuffer, door de RAM, en uiteindelijk op de rd_data-output verschijnt. Omdat elk signaal gesampled wordt op de stijgende klokflank, duurt het drie klokcycli vanaf het moment dat we de schrijfpoort besturen voordat het op de leespoort verschijnt. En er passeert een extra klokcyclus voordat de ontvangende module deze data kan benutten.

Latentie reduceren

Er zijn manieren om dit probleem te verlichten, maar dat gaat ten koste van extra resources in de FPGA. Laten we een experiment doen om één klokcyclus vertraging van de leespoort van onze ringbuffer te halen. In de onderstaande codefragment hebben we de rd_data-output gewijzigd van een synchroon proces naar een combinationeel proces dat gevoelig is voor de ram- en tail-signalen.

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

Helaas kan deze code niet op blok-RAM gemapt worden omdat er combinationele logica kan zitten tussen de RAM-uitgang en de eerste downstream-register op het rd_data-signaal.

De afbeelding hieronder toont het resourcegebruik gerapporteerd door Vivado. De blok-RAM is vervangen door LUTRAM; een vorm van gedistribueerde RAM geïmplementeerd in LUTs. Het LUT-gebruik is gestegen van 37 LUTs naar 947. Lookup-tabellen en flip-flops zijn duurder dan blok-RAM, dat is de hele reden waarom we blok-RAM hebben.

Ontwerpen van een robuuste VHDL-ringbuffer FIFO in blok-RAM

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

Update:
Hoe je een ringbuffer FIFO in blok-RAM maakt met de AXI ready/valid handshake

In de volgende blogpost zullen we een betere testbench voor de ringbuffer-module maken door gebruik te maken van constrained random verification.

Klik hier om het vervolgblogbericht te lezen:
Constrained random verification


VHDL

  1. Dynamische string-lijsten in VHDL: Best Practices & Voorbeeld
  2. Een Tcl-gedreven testbench maken voor een VHDL code-lock module
  3. Een VHDL-testbench netjes beëindigen: methoden en best practices
  4. Een PWM-controller implementeren in VHDL: Ontwerp, Simulatie en FPGA-demo
  5. Willekeurige getallen genereren in VHDL: Van Uniform naar OSVVM
  6. Een self-checking testbench in VHDL bouwen – Een praktische gids
  7. Een dynamische gelinkte lijst in VHDL implementeren met protected types en access pointers
  8. In-process procedures benutten voor schonere VHDL FSM-ontwerpen
  9. Impure functies in VHDL gebruiken: FSM-leesbaarheid en onderhoudbaarheid verbeteren
  10. VHDL-functies onder de knie krijgen: een praktische gids voor efficiënt ontwerp
  11. Een eindige-toestandsmachine in VHDL ontwerpen: een praktisch verkeerslichtvoorbeeld