Bei dem AXI-Stream Interface handelt es sich um eine Schnittstelle um z. B. Peripheriekomponenten mit einem Prozessor zu verbinden. Die Schnittstelle dient ausschließlich dem Transport von Nutzdaten (die sogenannte Payload) und Konfigurationsdaten müssen über andere Schnittstellen, wie z. B. AXI oder AXI-Lite versendet werden. Im Gegensatz zu einer AXI oder AXI-Lite Schnittstelle verwendet AXI-Stream keine Adressleitungen um das Ziel zu adressieren. Üblicherweise wird ein Stream-FIFO oder ein DMA-Controller verwendet um das AXI-Stream Interface mit Daten zu versorgen.
Das AXI-Stream Interface nutzt die folgenden Signale, wobei nicht alle genutzt werden müssen:
Jede AXI-Stream Komponente verwendet ein einziges Taktsignal namens ACLK für das Interface. Sämtliche Signale werden bei einer steigenden Taktflanke eingelesen und die Ausgangssignale dürfen sich erst nach einer steigenden Taktflanke verändern. Zudem verwendet jede Komponente einen Reset für das AXI-Stream Interface. Dieser Reset wird ARESETn genannt, ist Active Low und kann asynchron behandelt werden.
Ein minimaler Transfer über das AXI-Stream Interface sieht folgendermaßen aus:
Ein Transfer findet statt, wenn der Master über TVALID gültige Daten markiert und der Slave über TREADY seine Bereitschaft zum Empfangen von Daten signalisiert hat. Die Reihenfolge kann dabei variieren.
Als nächstes wollen wir uns mal anschauen, wie ein grundlegender AXI-Stream Master bzw. ein AXI-Stream Slave aufgebaut werden können und wie man sie nutzen kann.
AXI-Stream Master:
Der Master hat die Aufgabe die Kommunikation zwischen einem (oder mehreren Slaves zu steuern). Dazu werden die Handshake-Signale TREADY und TVALID verwendet, wobei das Signal TVALID vom Master und das Signal TREADY vom Slave gesteuert wird. Für die Steuerung des Signals TVALID gelten für den Master die folgenden Regeln:
- Ein Master darf nicht auf TREADY warten um TVALID zu setzen.
- Wenn der Master TVALID gesetzt hat, muss das Signal solange gesetzt bleiben bis der Handshake abgeschlossen ist.
- TVALID muss während eines Resets gelöscht werden.
- Bei einem asynchronen Reset reicht es aus, wenn TVALID bei der ersten steigenden Taktflanke nach dem Reset gesetzt wird.
Mit diesen Grundlagen soll nun der AXI-Stream Master entworfen werden. Der Master hat die Aufgabe bei einem Trigger-Signal Daten zu generieren und diese an einen Slave zu senden, wobei die Generierung der Daten über einen parametrierbaren Zähler erfolgen soll. Für die Umsetzung soll ein Zustandsautomat genutzt werden.
Daraus ergibt sich die folgende Beschreibung des Zustandsautomaten:
entity Top is Generic ( LENGTH : INTEGER := 100 ); Port ( ACLK : in STD_LOGIC; ARESETn : in STD_LOGIC; Trigger : in STD_LOGIC; TDATA_TXD : out STD_LOGIC_VECTOR(31 downto 0); TREADY_TXD : in STD_LOGIC; TVALID_TXD : out STD_LOGIC; TLAST_TXD : out STD_LOGIC ); end Top; architecture Top_Arch of Top is type State_t is (Reset, WaitForTriggerHigh, WaitForTriggerLow, WaitForReady, WaitForSlave); signal NextState : State_t := Reset; signal Counter : INTEGER := 0; begin process begin wait until rising_edge(ACLK); if(ARESETn = '0') then NextState <= Reset; else case NextState is when Reset => Counter <= 0; TDATA_TXD <= (others => '0'); TVALID_TXD <= '0'; TLAST_TXD <= '0'; NextState <= WaitForTriggerHigh; when WaitForTriggerHigh => if(Trigger = '1') then NextState <= WaitForTriggerLow; else NextState <= WaitForTriggerHigh; end if; when WaitForTriggerLow => if(Trigger = '0') then NextState <= WaitForReady; else NextState <= WaitForTriggerLow; end if; when WaitForReady => TDATA_TXD <= STD_LOGIC_VECTOR(to_unsigned(Counter, 32)); TVALID_TXD <= '1'; if(Counter < (LENGTH - 1)) then TLAST_TXD <= '0'; else TLAST_TXD <= '1'; end if; NextState <= WaitForSlave; when WaitForSlave => if(TREADY_TXD = '1') then TVALID_TXD <= '0'; TLAST_TXD <= '0'; if(Counter < (LENGTH - 1)) then Counter <= Counter + 1; NextState <= WaitForReady; else Counter <= 0; NextState <= WaitForTriggerHigh; end if; else NextState <= WaitForSlave; end if; end case; end if; end process; end Top_Arch;
Der fertige AXI-Stream Master kann mit Hilfe des AXI-Stream Verification Blocks, welcher über ein entsprechendes Blockdesign in das Projekt eingefügt wird, getestet werden. Dieser IP Block stellt ein Interface zur Verfügung, welches genutzt werden kann, um das AXI-Stream Interface des IP Blocks über eine entsprechende Testbench zu steuern.
Achtung:
Der AXI-Stream Verification Block kann nur für eine Simulation und nur zusammen mit einer SystemVerilog Testbench genutzt werden!
Anschließend wird eine Testbench erstellt und der AXI-Stream Master, sowie das Blockdesign mit dem Stream Verification Block eingebunden:
import axi4stream_vip_pkg::*; import StreamReader_axi4stream_vip_0_0_pkg::*; module Testbench(); bit SimulationClock = 0; bit nSimulationReset = 0; bit TREADY; bit TVALID; bit TLAST; bit[31:0] TDATA; bit Trigger; StreamReader Reader( .ACLK(SimulationClock), .ARESETN(nSimulationReset), .TDATA(TDATA), .TLAST(TLAST), .TREADY(TREADY), .TVALID(TVALID) ); Top DUT( .ACLK(SimulationClock), .ARESETn(nSimulationReset), .TDATA_TXD(TDATA), .TREADY_TXD(TREADY), .TVALID_TXD(TVALID), .TLAST_TXD(TLAST), .Trigger(Trigger) ); endmodule
Weiterhin wird noch ein Taktsignal und ein Objekt für die Nutzung des Stream Verification Blocks benötigt.
StreamReader_axi4stream_vip_0_0_slv_t ReadAgent; always #8 SimulationClock = ~SimulationClock;
Zum Start der Simulation wird über das Objekt des Stream Verification Blocks ein neuer Agent erzeugt, der die Kommunikation zwischen Testbench und Stream Verification Block übernimmt.
initial begin ReadAgent = new("Read agent", Reader.StreamReader.inst.IF); ReadAgent.vif_proxy.set_dummy_drive_type(XIL_AXI4STREAM_VIF_DRIVE_NONE); #500ns; nSimulationReset <= 1'b1; ReadAgent.start_slave(); end
Achtung:
Bei der Verwendung des AXI-Stream Verification Blocks muss sehr genau auf die Namensgebung im Blockdesign geachtet werden, da der vergebene Namen auch in die Testbench übertragen werden muss, damit die Simulation nicht fehlschlägt!
Das Reset-Signal muss wegen dem Stream Verification Block mindestens 16 Taktzyklen gesetzt bleiben, da es ansonsten zu einem Abbruch der Simulation kommt.
Ein paralleler Prozess setzt dann den Trigger für 500 ns und ließt die empfangenen Daten vom Stream Verification Block aus:
fork begin Trigger <= 1'b1; #500ns; Trigger <= 1'b0; #1000ns; end begin SlaveReceive(); end join_any task SlaveReceive(); axi4stream_ready_gen Ready; Ready= ReadAgent.driver.create_ready("Ready"); Ready.set_ready_policy(XIL_AXI4STREAM_READY_GEN_OSC); Ready.set_low_time(2); Ready.set_high_time(6); ReadAgent.driver.send_tready(Ready); endtask
Über einen SlaveMonitor
werden die Daten dann in ein Array geschrieben, sodass sie im Waveviewer betrachtet werden können:
axi4stream_monitor_transaction SlaveMonitor_Transaction; axi4stream_monitor_transaction SlaveMonitor_Transaction_Queue[$]; xil_axi4stream_uint SlaveMonitor_Transaction_QueueSize = 0; initial begin forever begin ReadAgent.monitor.item_collected_port.get(SlaveMonitor_Transaction); SlaveMonitor_Transaction_Queue.push_back(SlaveMonitor_Transaction); SlaveMonitor_Transaction_QueueSize++; end end
AXI-Stream Slave:
Bei einem AXI-Stream Slave-Interface handelt es sich um das Gegenstück zum Master-Interface. Der Slave hat dem entsprechend die Aufgabe die vom Master bereitgestellten Daten zu empfangen. Die Bereitschaft zum Empfangen von Daten signalisiert der Slave mit dem TREADY-Signal. Für das TREADY-Signal muss der Slave die folgenden Regeln einhalten:
- TREADY kann darf vor, nach oder zeitgleich mit TVALID gesetzt werden.
- Ein Slave darf auf ein TVALID-Signal vom Master warten, bevor er TREADY setzt.
- Der Slave darf TREADY wieder löschen, falls TVALID noch nicht gesetzt worden ist.
In diesem Beispiel soll ein AXI-Stream Slave implementiert werden, der einen einfachen Speicher darstellen und sämtliche empfangenen Daten in einem internen FIFO speichern. Über einen Eingangsport kann zudem bestimmt werden, welche Daten aus dem internen FIFO über einen Ausgangsport ausgegeben werden sollen. Auch in diesem Beispiel soll ein Zustandsautomat genutzt werden um die Logik zu implementieren:
Zusätzlich muss noch die Logik für die Datenausgabe eingebaut werden. Da es sich bei dieser Logik um eine rein kombinatorische Lösung handelt, ist diese losgelöst von dem Zustandsautomaten.
Es ergibt sich die folgende Beschreibung für das Design:
entity Top is Generic ( FIFO_SIZE : INTEGER := 32 ); Port ( ACLK : in STD_LOGIC; ARESETn : in STD_LOGIC; TDATA_RXD : in STD_LOGIC_VECTOR(31 downto 0); TREADY_RXD : out STD_LOGIC; TVALID_RXD : in STD_LOGIC; TLAST_RXD : in STD_LOGIC; Index : in STD_LOGIC_VECTOR(4 downto 0); DataOut : out STD_LOGIC_VECTOR(31 downto 0) ); end Top; architecture Top_Arch of Top is type State_t is (Reset, Ready, WaitForValid); type FIFO_t is array(0 to (FIFO_SIZE - 1)) of STD_LOGIC_VECTOR(31 downto 0); signal NextState : State_t := Reset; signal FIFO : FIFO_t := (others => (others => '0')); signal FIFO_Counter : INTEGER := 0; begin process begin wait until rising_edge(ACLK); if(ARESETn = '0') then NextState <= Reset; else case NextState is when Reset => FIFO <= (others => (others => '0')); FIFO_Counter <= 0; NextState <= Ready; when Ready => TREADY_RXD <= '1'; NextState <= WaitForValid; when WaitForValid => if(TVALID_RXD = '1') then TREADY_RXD <= '0'; FIFO(FIFO_Counter) <= TDATA_RXD; if((FIFO_Counter = (FIFO_SIZE - 1)) or (TLAST_RXD = '1')) then FIFO_Counter <= 0; else FIFO_Counter <= FIFO_Counter + 1; end if; NextState <= Ready; else NextState <= WaitForValid; end if; end case; end if; end process; DataOut <= FIFO(to_integer(UNSIGNED(Index))); end Top_Arch;
Zum Testen des Designs wird wieder auf den AXI-Stream Verification Block zurückgegriffen, nur das er in diesem Fall als AXI-Stream Master genutzt wird.
In die Testbench wird nun der AXI-Stream Slave, sowie das Blockdesign mit dem Stream Verification Block eingebunden:
import axi4stream_vip_pkg::*; import StreamWriter_axi4stream_vip_0_0_pkg::*; module Testbench(); bit SimulationClock = 0; bit nSimulationReset = 0; bit[31:0] SimulationData; bit TREADY; bit TVALID; bit TLAST; bit[31:0] TDATA; bit[4:0] Index; bit[31:0] DataOut; StreamWriter Writer( .ACLK(SimulationClock), .ARESETN(nSimulationReset), .TDATA(TDATA), .TLAST(TLAST), .TREADY(TREADY), .TVALID(TVALID) ); Top DUT( .ACLK(SimulationClock), .ARESETN(nSimulationReset), .TDATA_RXD(TDATA), .TREADY_RXD(TREADY), .TVALID_RXD(TVALID), .TLAST_RXD(TLAST), .DataOut(DataOut), .Index(Index) ); endmodule
Auch hier wird wieder Taktsignal und ein Objekt für die Nutzung des Stream Verification Blocks benötigt.
StreamWriter_axi4stream_vip_0_0_mst_t WriteAgent; always #8 SimulationClock = ~SimulationClock;
Anschließend wird wieder ein neuer Agent erzeugt:
initial begin WriteAgent = new("Master agent", Writer.StreamWriter.inst.IF); WriteAgent.vif_proxy.set_dummy_drive_type(XIL_AXI4STREAM_VIF_DRIVE_NONE); #500ns; nSimulationReset <= 1'b1; WriteAgent.start_master(); end
Über die Funktion SendData
werden anschließend Testdaten erzeugt und über das AXI-Stream Master-Interface an den Slave gesendet. Die Funktion SendData
erwartet dazu einen Startwert und die Länge des Datenpaketes in 32-Bit Wörtern als Argumente:
task SendData(xil_axi4stream_uint Start, xil_axi4stream_uint MessageLength); for(xil_axi4stream_uint CurrentMessage = 0; CurrentMessage < MessageLength; CurrentMessage++) begin axi4stream_transaction Write; for(xil_axi4stream_uint CurrentByte = 0; CurrentByte < 4; CurrentByte++) begin SimulationData[CurrentByte * 8+:8] = Start + (CurrentByte * (CurrentMessage + 1)); end Write = WriteAgent.driver.create_transaction("Master write transaction"); Write.set_data_beat(SimulationData); Write.set_last(0); if(CurrentMessage == (MessageLength - 1)) begin Write.set_last(1); end WriteAgent.driver.send(Write); end endtask fork begin SendData(0, 4); end join_any Index = 16'h2; #10000 $finish;
Die Funktion SendData
setzt beim letzten Datenwort zudem noch TLAST, sodass der Slave das Ende eines Datenpakets erkennen kann (wird in diesem Beispiel allerdings nicht ausgewertet).
Das komplette Projekt steht in meinem GitHub-Repository zum Download bereit.
Hi,
danke für die ausführliche Ausführung und Erklärung. Super!
Ich hätte jedoch noch eine Anmerkung: Leider konnte ich mit der Erklärung des TSTRB Signals nicht viel anfangen. Auf der folgenden Seite ist dies sehr gut erklärt worden.
https://community.arm.com/support-forums/f/embedded-forum/2848/axi-protocol—strobe-signal-value?ReplySortBy=CreatedDate&ReplySortOrder=Ascending
Viele Grüße