Hoe maak je een AXI FIFO in blok RAM met behulp van de ready/valid handshake?
Ik ergerde me een beetje aan de eigenaardigheden van de AXI-interface toen ik voor het eerst logica moest maken om een AXI-module te koppelen. In plaats van de reguliere bezet/geldig, vol/geldig of leeg/geldig stuursignalen, gebruikt de AXI-interface twee stuursignalen genaamd "gereed" en "geldig". Mijn frustratie veranderde al snel in ontzag.
De AXI interface heeft een ingebouwde flow control zonder gebruik te maken van extra stuursignalen. De regels zijn eenvoudig genoeg om te begrijpen, maar er zijn een paar valkuilen waarmee u rekening moet houden bij het implementeren van de AXI-interface op een FPGA. Dit artikel laat zien hoe je een AXI FIFO maakt in VHDL.
AXI lost het probleem met één cyclusvertraging op
Het voorkomen van overlezen en overschrijven is een veelvoorkomend probleem bij het maken van datastroominterfaces. Het probleem is dat wanneer twee geklokte logische modules communiceren, elke module de uitgangen van zijn tegenhanger slechts met één klokcyclusvertraging kan lezen.
De afbeelding hierboven toont het timingdiagram van een sequentiële module die schrijft naar een FIFO die de write enable/full gebruikt signaleringsschema. Een interfacemodule schrijft gegevens naar de FIFO door de wr_en
. te bevestigen signaal. De FIFO bevestigt de full
signaleren wanneer er geen ruimte is voor een ander gegevenselement, waardoor de gegevensbron wordt gevraagd te stoppen met schrijven.
Helaas kan de interfacemodule niet op tijd stoppen zolang deze alleen geklokte logica gebruikt. De FIFO verhoogt de full
vlag precies op de stijgende flank van de klok. Tegelijkertijd probeert de interfacemodule het volgende data-element te schrijven. Het kan niet samplen en reageren op de full
signaal voordat het te laat is.
Een oplossing is het toevoegen van een extra almost_empty
signaal, hebben we dit gedaan in de tutorial Hoe maak je een ringbuffer FIFO in VHDL-zelfstudie. Het extra signaal gaat vooraf aan de empty
signaal, waardoor de interfacemodule tijd heeft om te reageren.
De klaar/geldige handdruk
Het AXI-protocol implementeert stroomregeling met slechts twee stuursignalen in elke richting, één genaamd ready
en de andere valid
. De ready
signaal wordt aangestuurd door de ontvanger, een logische '1'
waarde op dit signaal betekent dat de ontvanger klaar is om een nieuw gegevensitem te accepteren. De valid
signaal, aan de andere kant, wordt gecontroleerd door de afzender. De afzender stelt valid
. in tot '1'
wanneer de op de databus gepresenteerde gegevens geldig zijn voor bemonstering.
Hier komt het belangrijkste deel: gegevensoverdracht vindt alleen plaats wanneer zowel ready
en valid
zijn '1'
op dezelfde klokcyclus. De ontvanger informeert wanneer het klaar is om gegevens te accepteren, en de afzender plaatst de gegevens eenvoudigweg daar wanneer het iets te verzenden heeft. Overdracht vindt plaats wanneer beide akkoord gaan, wanneer de afzender klaar is om te verzenden en de ontvanger klaar is om te ontvangen.
De bovenstaande golfvorm toont een voorbeeldtransactie van één gegevensitem. Sampling vindt plaats op de stijgende klokflank, zoals meestal het geval is bij geklokte logica.
Implementatie
Er zijn veel manieren om een AXI FIFO in VHDL te implementeren. Het kan een schuifregister zijn, maar we zullen een ringbufferstructuur gebruiken omdat dit de meest eenvoudige manier is om een FIFO in blok-RAM te maken. Je kunt het allemaal creëren in één gigantisch proces met behulp van variabelen en signalen, of je kunt de functionaliteit opsplitsen in meerdere processen.
Deze implementatie gebruikt aparte processen voor de meeste signalen die geüpdatet moeten worden. Alleen de processen die synchroon moeten zijn, zijn gevoelig voor de klok, de andere gebruiken combinatorische logica.
De entiteit
De entiteitsverklaring bevat een generieke poort die wordt gebruikt voor het instellen van de breedte van de invoer- en uitvoerwoorden, evenals het aantal slots waarvoor ruimte in het RAM moet worden gereserveerd. De capaciteit van de FIFO is gelijk aan de RAM-diepte min één. Er wordt altijd één slot leeg gehouden om onderscheid te maken tussen een volle en een lege FIFO.
entity axi_fifo is generic ( ram_width : natural; ram_depth : natural ); port ( clk : in std_logic; rst : in std_logic; -- AXI input interface in_ready : out std_logic; in_valid : in std_logic; in_data : in std_logic_vector(ram_width - 1 downto 0); -- AXI output interface out_ready : in std_logic; out_valid : out std_logic; out_data : out std_logic_vector(ram_width - 1 downto 0) ); end axi_fifo;
De eerste twee signalen in de poortdeclaratie zijn de klok- en reset-ingangen. Deze implementatie maakt gebruik van synchrone reset en is gevoelig voor de stijgende flank van de klok.
Er is een AXI-stijl ingangsinterface die gebruik maakt van de gereed/geldige stuursignalen en een ingangsdatasignaal van generieke breedte. Eindelijk komt de AXI-uitgangsinterface met soortgelijke signalen als de ingang, alleen met omgekeerde richtingen. Signalen die bij de invoer- en uitvoerinterface horen, worden voorafgegaan door in_
of out_
.
De uitgang van de ene AXI FIFO kon direct worden aangesloten op de ingang van een andere, de interfaces sluiten perfect op elkaar aan. Hoewel, een betere oplossing dan ze te stapelen zou zijn om de ram_depth
. te verhogen generiek als u een grotere FIFO wilt.
Signaalverklaringen
De eerste twee verklaringen in het declaratieve gebied van het VHDL-bestand verklaren het RAM-type en het signaal ervan. Het RAM wordt dynamisch gedimensioneerd op basis van de generieke ingangen.
-- The FIFO is full when the RAM contains ram_depth - 1 elements type ram_type is array (0 to ram_depth - 1) of std_logic_vector(in_data'range); signal ram : ram_type;
Het tweede codeblok declareert een nieuw geheeltallig subtype en vier signalen daaruit. De index_type
is bemeten om precies de diepte van het RAM-geheugen weer te geven. De head
signaal geeft altijd de RAM-sleuf aan die bij de volgende schrijfbewerking zal worden gebruikt. De tail
signaal wijst naar de sleuf die bij de volgende leesbewerking zal worden gebruikt. De waarde van de count
signaal is altijd gelijk aan het aantal elementen dat momenteel in de FIFO is opgeslagen, en count_p1
is een kopie van hetzelfde signaal vertraagd met één klokcyclus.
-- Newest element at head, oldest element at tail subtype index_type is natural range ram_type'range; signal head : index_type; signal tail : index_type; signal count : index_type; signal count_p1 : index_type;
Dan komen er twee signalen genaamd in_ready_i
en out_valid_i
. Dit zijn slechts kopieën van de entiteitsuitgangen in_ready
en out_valid
. De _i
postfix betekent gewoon intern , het maakt deel uit van mijn codeerstijl.
-- Internal versions of entity signals with mode "out" signal in_ready_i : std_logic; signal out_valid_i : std_logic;
Ten slotte declareren we een signaal dat zal worden gebruikt om gelijktijdig lezen en schrijven aan te geven. Ik zal het doel later in dit artikel uitleggen.
-- True the clock cycle after a simultaneous read and write signal read_while_write_p1 : std_logic;
Subprogramma's
Na de signalen declareren we een functie voor het verhogen van onze aangepaste index_type
. De next_index
functie kijkt naar de read
en valid
parameters om te bepalen of er een lopende lees- of lees-/schrijftransactie is. Als dat het geval is, wordt de index verhoogd of ingepakt. Zo niet, dan wordt de ongewijzigde indexwaarde geretourneerd.
function next_index( index : index_type; ready : std_logic; valid : std_logic) return index_type is begin if ready = '1' and valid = '1' then if index = index_type'high then return index_type'low; else return index + 1; end if; end if; return index; end function;
Om ons te behoeden voor herhaaldelijk typen, creëren we de logica voor het bijwerken van de head
en tail
signalen in een procedure, in plaats van als twee identieke processen. De update_index
procedure neemt de klok en reset signalen, een signaal van index_type
, een ready
signaal, en een valid
signaal als ingangen.
procedure index_proc( signal clk : in std_logic; signal rst : in std_logic; signal index : inout index_type; signal ready : in std_logic; signal valid : in std_logic) is begin if rising_edge(clk) then if rst = '1' then index <= index_type'low; else index <= next_index(index, ready, valid); end if; end if; end procedure;
Dit volledig synchrone proces gebruikt de next_index
functie om de index
. bij te werken signaal wanneer de module niet meer kan worden gereset. In reset, de index
signaal wordt ingesteld op de laagste waarde die het kan vertegenwoordigen, wat altijd 0 is vanwege de manier waarop index_type
en ram_type
wordt verklaard. We hadden 0 als resetwaarde kunnen gebruiken, maar ik probeer zoveel mogelijk harde codering te vermijden.
Kopieer interne signalen naar de uitgang
Deze twee gelijktijdige instructies kopiëren de interne versies van de uitgangssignalen naar de daadwerkelijke uitgangen. We moeten werken met interne kopieën omdat VHDL ons niet toestaat entiteitssignalen te lezen met modus out
binnenkant van de module. Een alternatief zou zijn geweest om in_ready
. te declareren en out_valid
met modus inout
, maar de meeste coderingsnormen van bedrijven beperken het gebruik van inout
entiteitssignalen.
in_ready <= in_ready_i; out_valid <= out_valid_i;
Kop en staart bijwerken
We hebben het al gehad over de index_proc
procedure die wordt gebruikt om de head
. bij te werken en tail
signalen. Door de juiste signalen toe te wijzen aan de parameters van dit subprogramma, krijgen we het equivalent van twee identieke processen, één voor het aansturen van de FIFO-invoer en één voor de uitvoer.
-- Update head index on write PROC_HEAD : index_proc(clk, rst, head, in_ready_i, in_valid); -- Update tail index on read PROC_TAIL : index_proc(clk, rst, tail, out_ready, out_valid_i);
Aangezien zowel de head
en de tail
zijn ingesteld op dezelfde waarde door de resetlogica, zal de FIFO aanvankelijk leeg zijn. Dat is hoe deze ringbuffer werkt, wanneer beide naar dezelfde index wijzen, betekent dit dat de FIFO leeg is.
Blok RAM afleiden
In de meeste FPGA-architecturen zijn de blok-RAM-primitieven volledig synchrone componenten. Dit betekent dat als we willen dat de synthesetool blok-RAM afleidt uit onze VHDL-code, we de lees- en schrijfpoorten in een geklokt proces moeten plaatsen. Er kunnen ook geen reset-waarden zijn gekoppeld aan blok-RAM.
PROC_RAM : process(clk) begin if rising_edge(clk) then ram(head) <= in_data; out_data <= ram(next_index(tail, out_ready, out_valid_i)); end if; end process;
Er is geen lezen inschakelen of schrijf inschakelen hier zou dat te traag zijn voor AXI. In plaats daarvan schrijven we continu naar het RAM-slot waarnaar wordt verwezen door de head
inhoudsopgave. Wanneer we vervolgens vaststellen dat er een schrijftransactie heeft plaatsgevonden, gaan we eenvoudigweg de head
. vooruit om de geschreven waarde vast te leggen.
Evenzo, out_data
wordt bij elke klokcyclus bijgewerkt. De tail
aanwijzer gaat gewoon naar de volgende sleuf wanneer er wordt gelezen. Merk op dat de next_index
functie wordt gebruikt om het adres voor de leespoort te berekenen. We moeten dit doen om ervoor te zorgen dat het RAM snel genoeg reageert na het lezen en begint met het uitvoeren van de volgende waarde.
Tel het aantal elementen in de FIFO
Het aantal elementen in het RAM tellen is gewoon een kwestie van de head
van de tail
. Als de head
is ingepakt, moeten we dit compenseren met het totale aantal slots in het RAM. We hebben toegang tot deze informatie via de ram_depth
constante van de generieke invoer.
PROC_COUNT : process(head, tail) begin if head < tail then count <= head - tail + ram_depth; else count <= head - tail; end if; end process;
We moeten ook de vorige waarde van de count
. bijhouden signaal. Het onderstaande proces maakt er een versie van die met één klokcyclus is vertraagd. De _p1
postfix is een naamgevingsconventie om dit aan te geven.
PROC_COUNT_P1 : process(clk) begin if rising_edge(clk) then if rst = '1' then count_p1 <= 0; else count_p1 <= count; end if; end if; end process;
Update de klaar uitvoer
De in_ready
signaal zal '1'
. zijn wanneer deze module klaar is om een ander gegevensitem te accepteren. Dit zou het geval moeten zijn zolang de FIFO niet vol is, en dat is precies wat de logica van dit proces zegt.
PROC_IN_READY : process(count) begin if count < ram_depth - 1 then in_ready_i <= '1'; else in_ready_i <= '0'; end if; end process;
Detecteer gelijktijdig lezen en schrijven
Vanwege een hoekgeval dat ik in de volgende sectie zal uitleggen, moeten we gelijktijdige lees- en schrijfbewerkingen kunnen identificeren. Elke keer dat er geldige lees- en schrijftransacties zijn tijdens dezelfde klokcyclus, stelt dit proces de read_while_write_p1
in signaal naar '1'
op de volgende klokcyclus.
PROC_READ_WHILE_WRITE_P1: process(clk) begin if rising_edge(clk) then if rst = '1' then read_while_write_p1 <= '0'; else read_while_write_p1 <= '0'; if in_ready_i = '1' and in_valid = '1' and out_ready = '1' and out_valid_i = '1' then read_while_write_p1 <= '1'; end if; end if; end if; end process;
Update de geldige uitvoer
De out_valid
signaal geeft aan downstream-modules aan dat de gegevens gepresenteerd op out_data
is geldig en kan op elk moment worden bemonsterd. De out_data
signaal komt rechtstreeks van de RAM-uitgang. Implementatie van de out_valid
signaal is een beetje lastig vanwege de extra klokcyclusvertraging tussen blok RAM-invoer en -uitvoer.
De logica wordt geïmplementeerd in een combinatieproces, zodat het zonder vertraging kan reageren op het veranderende ingangssignaal. De eerste regel van het proces is een standaardwaarde die de out_valid
. instelt signaal naar '1'
. Dit is de geldende waarde als geen van de twee volgende If-statements wordt geactiveerd.
PROC_OUT_VALID : process(count, count_p1, read_while_write_p1) begin out_valid_i <= '1'; -- If the RAM is empty or was empty in the prev cycle if count = 0 or count_p1 = 0 then out_valid_i <= '0'; end if; -- If simultaneous read and write when almost empty if count = 1 and read_while_write_p1 = '1' then out_valid_i <= '0'; end if; end process;
Het eerste If-statement controleert of de FIFO leeg is of leeg was in de vorige klokcyclus. Uiteraard is de FIFO leeg als er 0 elementen in zitten, maar we moeten ook kijken naar het vulniveau van de FIFO in de vorige klokcyclus.
Beschouw de golfvorm hieronder. Aanvankelijk is de FIFO leeg, zoals aangegeven door de count
signaal is 0
. Dan vindt een schrijven plaats op de derde klokcyclus. RAM-slot 0 wordt bijgewerkt in de volgende klokcyclus, maar het duurt een extra cyclus voordat de gegevens op de out_data
verschijnen uitvoer. Het doel van de or count_p1 = 0
verklaring is om ervoor te zorgen dat out_valid
blijft '0'
(rood omcirkeld) terwijl de waarde zich door het RAM voortplant.
De laatste If-verklaring beschermt tegen een ander hoekgeval. We hebben zojuist besproken hoe u omgaat met het speciale geval van 'write-on-empty' door de huidige en eerdere FIFO-vulniveaus te controleren. Maar wat gebeurt er als en we een gelijktijdige lees- en schrijfbewerking uitvoeren wanneer count
is al 1
?
De onderstaande golfvorm toont een dergelijke situatie. Aanvankelijk is er één data-item D0 aanwezig in de FIFO. Het is er al een tijdje, dus beide count
en count_p1
zijn 0
. Dan komt er in de derde klokcyclus een gelijktijdig lezen en schrijven. Eén item verlaat de FIFO en een nieuwe komt erin, waardoor de tellers ongewijzigd blijven.
Op het moment van lezen en schrijven is er geen volgende waarde in het RAM die klaar is om te worden uitgevoerd, zoals dat het geval zou zijn als het vulniveau hoger was dan één. We moeten twee klokcycli wachten voordat de invoerwaarde op de uitvoer verschijnt. Zonder aanvullende informatie zou het onmogelijk zijn om dit hoekgeval en de waarde van out_valid
. te detecteren bij de volgende klokcyclus (gemarkeerd als ononderbroken rood) zou ten onrechte worden ingesteld op '1'
.
Daarom hebben we de read_while_write_p1
. nodig signaal. Het detecteert dat er gelijktijdig wordt gelezen en geschreven, en we kunnen hier rekening mee houden door out_valid
in te stellen tot '0'
in die klokcyclus.
Synthetiseren in Vivado
Om het ontwerp als een stand-alone module in Xilinx Vivado te implementeren, moeten we eerst waarden geven aan de generieke inputs. Dit kan in Vivado worden bereikt door de Instellingen . te gebruiken → Algemeen → Algemeen/Parameters menu, zoals weergegeven in de onderstaande afbeelding.
De generieke waarden zijn gekozen om overeen te komen met de RAMB36E1-primitief in de Xilinx Zynq-architectuur die het doelapparaat is. Het gebruik van resources na de implementatie wordt weergegeven in de onderstaande afbeelding. De AXI FIFO gebruikt één blok RAM en een klein aantal LUT's en flip-flops.
AXI is meer dan klaar/geldig
AXI staat voor Advanced eXtensible Interface en maakt deel uit van ARM's Advanced Microcontroller Bus Architecture (AMBA) -standaard. De AXI-standaard is veel meer dan de read/valid handshake. Als je meer wilt weten over AXI, raad ik deze bronnen aan om verder te lezen:
- Wikipedia:AXI
- ARM AXI introductie
- Introductie Xilinx AXI
- AXI4-specificatie
VHDL
- Cloud en hoe het de IT-wereld verandert
- Hoe u het meeste uit uw gegevens haalt
- Hoe RAM vanuit een bestand te initialiseren met TEXTIO
- Hoe u zich voorbereidt op AI met behulp van IoT
- Hoe het industriële internet activabeheer verandert
- Praktische tips voor het bijhouden van activa:hoe u uw zuurverdiende activagegevens optimaal kunt benutten
- Hoe krijgen we een beter beeld van het IoT?
- Hoe u het meeste uit IoT haalt in de restaurantbranche
- Hoe data de supply chain van de toekomst mogelijk maakt
- Hoe u supply chain-gegevens betrouwbaar maakt
- Hoe AI het probleem van 'vuile' gegevens aanpakt