Kampis Elektroecke

AXI-Stream Interface

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:

Signal Notwendig Beschreibung
ACLK Ja Globales Taktsignal für die Schaltung. Es wird die steigende Flanke ausgewertet.
ARESETn Ja Zum ACLK gehörendes Reset-Signal.
TVALID Ja Wird vom Master genutzt um gültige Daten zu markieren.
TREADY Ja Wird vom Slave genutzt um Bereitschaft zu signalisieren.
TDATA[8n-1:0] Ja Zu übertragende Daten.
TSTRB[8n-1:0] Nein Index um das dazugehörige Datenbyte als Daten- oder Positionsbyte zu markieren.
TKEEP[8n-1:0] Nein Index um das dazugehörige Datenbyte als Teil des Datenstroms weiterzuverarbeiten.
TLAST Nein Signalisiert das letzte Byte eines Paketes.
TID[8n-1:0] Nein Index um verschiedene Datenströme unterscheiden zu können.
TDEST[8d-1:0] Nein Informationen zum Routing für verschiedene Datenströme.
TUSER[8u-1:0] Nein Frei wählbarer Verwendungszweck.
Index Funktion
n Breite des Datenbus.
i Breite von TID. Es werden 8 Bit empfohlen.
d Breite von TDEST. Es werden 4 Bit empfohlen.
u Breite von TUSER. Es wird ein ganzzahlige Vielfache der Breite des Datenbus empfohlen.

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.

Reset
  • Zurücksetzen aller Signale
  • Zähler zurücksetzen
WaitForTriggerHigh
  • Warten bis Trigger gesetzt ist
WaitForTriggerLow
  • Warten bis Trigger gelöscht ist
WaitForReady
  • TDATA setzen
  • TVALID setzen
  • TLAST setzen, wenn die letzten Daten übertragen werden
WaitForSlave
  • Auf TREADY warten
  • Bei TREADY Signale TVALID und TLAST löschen und Zähler erhöhen bzw. zurücksetzen

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:

Reset
  • Internen FIFO löschen
  • Zähler zurücksetzen
Ready
  • TREADY setzen
WaitForValid
  • Auf TVALID warten
  • TDATA speichern und Zähler erhöhen, wenn TVALID gesetzt
  • Zähler zurücksetzen, wenn Maximum erreicht und TVALID gesetzt

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.

Ein Kommentar

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert