Kampis Elektroecke

AXI-Stream Interface für den Sender

In diesem Teil des I2S Tutorials für FPGAs möchte ich zeigen, wie der bereits erstellte I2S-Sender mit einem AXI-Stream Interface ausgestattet werden kann, wodurch es möglich ist den I2S-Sender über einen AXI-Stream FIFO mit dem Processing System zu verbinden.

Dazu wird ein neues Top-Design namens AXIS_I2S angelegt. Dieses Design soll über folgende I/O verfügen:

Signal Beschreibung
MCLK Master Clock
nReset Reset Eingang für das Audio Interface (Active Low)
ACLK Takt des AXI-Stream Interface
ARESETn Reset Eingang für das AXI-Stream Interface (Active Low)
TDATA_RXD Datenleitungen des AXI-Stream Interfaces
TREADY_RXD Ready-Signal des AXI-Stream Slaves
TVALID_RXD Valid-Signal des AXI-Stream Masters
SCLK Serial Clock der I2S-Schnittstelle
LRCLK LRCLK (WS) der I2S-Schnittstelle
SD Serial Data der I2S-Schnittstelle

Die dazu gehörige Entität sieht folgendermaßen aus:

entity AXIS_I2S is
    Generic (   RATIO   : INTEGER := 8;
                WIDTH   : INTEGER := 16
                );
    Port (  MCLK        : in STD_LOGIC;
            nReset      : in STD_LOGIC;
            LRCLK       : out STD_LOGIC;
            SCLK        : out STD_LOGIC;
            SD          : out STD_LOGIC;
            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
            );
end AXIS_I2S;

Über den Parameter RATIO wird das Verhältnis von SCLK zu MCKL und über den Parameter WIDTH die Breite eines Datenwortes pro Kanal definiert.


Hinweis:

Die hier vorgestellte Implementierung unterstützt lediglich 16 Bit Datenwörter pro Kanal (also 32 Bit für Stereo). Für höhere Busbreiten muss der nachfolgende Code angepasst werden.


In dem Design müssen folgende Komponenten implementiert werden:

  • Ein Teiler um aus MCLK den Eingangstakt für den I2S-Sender zu generieren
  • Ein AXI-Stream Slave Interface
  • Die Steuerung des I2S-Senders

Für den Teiler wird ein Prozess erstellt, der bei der steigenden Taktflanke von MCLK einen Zähler hochzählt und nach der halben Periode das Signal SCLK_Int umschaltet.

process
    variable Counter    : INTEGER := 0;
begin
    wait until rising_edge(MCLK);
    if(Counter < ((RATIO / 2) - 1)) then
        Counter := Counter + 1;
    else
        Counter := 0;
        SCLK_Int <= not SCLK_Int;
    end if;

    if(nReset = '0') then
        Counter := 0;
        SCLK_Int <= '0';
    end if;
end process;

Als nächstes wird das AXI-Stream Interface implementiert. Dazu wird ein Zustandsautomat genutzt:

process
begin
    wait until rising_edge(ACLK);
    case CurrentState is
        when State_Reset =>
            Tx_AXI <= (others => '0');
            CurrentState <= State_WaitForTransmitterReady;

        when State_WaitForTransmitterReady =>
            if(Ready_AXI = '1') then
                TREADY_RXD <= '1';
                CurrentState <= State_WaitForValid;
            else
                TREADY_RXD <= '0';
                CurrentState <= State_WaitForTransmitterReady;
            end if;
        when State_WaitForValid =>                        
            if(TVALID_RXD = '1') then
                TREADY_RXD <= '0';
                Tx_AXI <= TDATA_RXD;
                CurrentState <= State_WaitForTransmitterBusy;
            else
                TREADY_RXD <= '1';
                CurrentState <= State_WaitForValid;
            end if;
        when State_WaitForTransmitterBusy =>
            if(Ready_AXI = '0') then
                CurrentState <= State_WaitForTransmitterReady;
            else
                CurrentState <= State_WaitForTransmitterBusy;
            end if;
    end case;
    if(ARESETn = '0') then
            CurrentState <= State_Reset;
    end if;
end process;

Nach einem Reset wechselt der Automat vom Zustand State_Reset in den Zustand State_WaitForTransmitterReady, wo er auf das Ready-Signal des I2S-Senders wartet. Sobald der Sender bereit ist, wird das Signal TREADY_RXD des AXI-Stream Interfaces gesetzt, wodurch der Master darüber informiert wird, dass der Slave bereit ist Daten zu empfangen. Anschließend wechselt der Slave in den Zustand State_WaitForValid.

In diesem Zustand wartet der Slave darauf das der Master das Signal TVALID_RXD setzt um gültige Daten zu markieren. Sobald das Signal gesetzt wird, werden die Daten in einen internen FIFO geschrieben. Der Automat wechselt dann in den Zustand State_WaitForTransmitterBusy.

Jetzt wartet der Zustandsautomat darauf, dass der I2S-Sender mit der Übertragung der Daten beginnt und das Ready-Signal löscht. Sobald dies geschehen ist, wechselt der Automat zurück in den Zustand State_WaitForTransmitterReady und wartet wieder so lange bis der I2S-Sender bereit ist.

Damit wäre das AXI-Stream Interface in der Theorie fertig. Leider wird es zum Schluss noch etwas kniffelig, da der aktuelle Schaltungsentwurf zwei verschiedene Taktdomainen verwendet:

  • Die Domain von ACLK, also dem Taktsignal des AXI-Stream Interfaces
  • Die Domain von MCLK, also dem Taktsignal des Audio Interfaces

In der Regel können diese beiden Taktsignale nicht aus einer Taktquelle generiert werden (z. B. über einen Taktteiler), da das AXI-Interface auf typischerweise 100 MHz läuft und das Audio Interface Taktraten benötigt, die sich sauber auf die Abtastfrequenz runterteilen lassen, wie z. B. 12,288 MHz. Dies hat zur Folge, dass bei der Implementierung Timing Fehler auf Grund eines zu hohen Worst Negative Slack (WNS) und Total Negative Slack (TNS) auftreten:

Zudem ist das Risiko von fehlerhaften Daten durch eine auftretende Metastabilität der Flipflops bei unterschiedlichen Taktdomainen sehr hoch. Metastabilität tritt u. a. dann auf, wenn ein Flipflop schaltet und sich die Daten genau in dem Moment ändern.

Daher müssen die Signale, die von den einzelnen Taktdomainen genutzt werden, über entsprechende Schaltungen in die jeweils andere Taktdomain überführt werden. Xilinx beschreibt in dem Dokument UG953 entsprechende Makros, die für diesen Zweck genutzt werden können.

Makro Funktion
xpm_cdc_gray Dieser Funktionsblock nutzt Gray-Code um einen Datenbus von einer Clockdomain (src) in eine andere Clockdomain (dest) zu überführen.
xpm_cdc_single Überführt ein einzelnes Signal von einer Clockdomain (src) in eine andere Clockdomain (dest).

Für den vorliegenden VHDL-Code können die Beispiele der Makros direkt übernommen werden:

xpm_cdc_Data : xpm_cdc_gray generic map ( DEST_SYNC_FF => 4,
                                          SIM_ASSERT_CHK => 0,
                                          SIM_LOSSLESS_GRAY_CHK => 0,
                                          WIDTH => (2 * WIDTH)
                                          )
                              port map (  src_clk => ACLK,
                                          src_in_bin => Tx_AXI,
                                          dest_clk => MCLK,
                                          dest_out_bin => Tx_Transmitter
                                          );

xpm_cdc_Ready : xpm_cdc_single generic map ( DEST_SYNC_FF => 4,
                                             SRC_INPUT_REG => 1
                                             )
                                 port map (  src_clk => MCLK,
                                             src_in => Ready_Transmitter,
                                             dest_clk => ACLK,
                                             dest_out => Ready_AXI
                                             );

Zum Schluss muss noch der I2S-Sender eingefügt und die erzeugten Signale weitergegeben werden.

Transmitter : I2S_Transmitter generic map ( WIDTH => WIDTH
                                            )
                                  port map( Clock => SCLK_Int,
                                            nReset => nReset,
                                            Ready => Ready_Transmitter,
                                            Tx => Tx_Transmitter,
                                            LRCLK => LRCLK,
                                            SCLK => SCLK,
                                            SD => SD
                                            );

Damit ist das AXI-Stream Interface für den I2S-Sender ist damit fertig und einsatzbereit. Der komplette Code sollte nun wie folgt aussehen:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
library xpm;
use xpm.vcomponents.all;

entity AXIS_I2S is
    Generic (   RATIO   : INTEGER := 8;
                WIDTH   : INTEGER := 16
                );
    Port (  MCLK        : in STD_LOGIC;
            nReset      : in STD_LOGIC;
            LRCLK       : out STD_LOGIC;
            SCLK        : out STD_LOGIC;
            SD          : out STD_LOGIC;
            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
            );
end AXIS_I2S;

architecture AXIS_I2S_Arch of AXIS_I2S is
    type AXIS_State_t is (State_Reset, State_WaitForTransmitterReady, State_WaitForValid, State_WaitForTransmitterBusy);
    signal CurrentState : AXIS_State_t                                              := State_Reset;
    signal Tx_AXI               : STD_LOGIC_VECTOR(((2 * WIDTH) - 1) downto 0)      := (others => '0');
    signal Ready_AXI            : STD_LOGIC;
    signal Tx_Transmitter       : STD_LOGIC_VECTOR(((2 * WIDTH) - 1) downto 0)      := (others => '0');
    signal Ready_Transmitter    : STD_LOGIC;
    signal SCLK_Int             : STD_LOGIC                                         := '0';
    component I2S_Transmitter is
        Generic (   WIDTH   : INTEGER := 16
                    );
        Port (  Clock   : in STD_LOGIC;
                nReset  : in STD_LOGIC;
                Ready   : out STD_LOGIC;
                Tx      : in STD_LOGIC_VECTOR(((2 * WIDTH) - 1) downto 0);
                LRCLK   : out STD_LOGIC;
                SCLK    : out STD_LOGIC;
                SD      : out STD_LOGIC
                );
    end component;

begin

    Transmitter : I2S_Transmitter generic map ( WIDTH => WIDTH
                                                )
                                  port map(     Clock => SCLK_Int,
                                                nReset => nReset,
                                                Ready => Ready_Transmitter,
                                                Tx => Tx_Transmitter,
                                                LRCLK => LRCLK,
                                                SCLK => SCLK,
                                                SD => SD
                                                );
   xpm_cdc_Data : xpm_cdc_gray generic map (    DEST_SYNC_FF => 4,
                                                SIM_ASSERT_CHK => 0,
                                                SIM_LOSSLESS_GRAY_CHK => 0,
                                                WIDTH => (2 * WIDTH)
                                                )
                                    port map (  src_clk => ACLK,
                                                src_in_bin => Tx_AXI,
                                                dest_clk => MCLK,
                                                dest_out_bin => Tx_Transmitter
                                                );
   xpm_cdc_Ready : xpm_cdc_single generic map ( DEST_SYNC_FF => 4,
                                                SRC_INPUT_REG => 1
                                                )
                                    port map (  src_clk => MCLK,
                                                src_in => Ready_Transmitter,
                                                dest_clk => ACLK,
                                                dest_out => Ready_AXI
                                                );
    process
        variable Counter    : INTEGER := 0;
    begin
        wait until rising_edge(MCLK);
        if(Counter < ((RATIO / 2) - 1)) then
            Counter := Counter + 1;
        else
            Counter := 0;
            SCLK_Int <= not SCLK_Int;
        end if;

        if(nReset = '0') then
            Counter := 0;
            SCLK_Int <= '0';
        end if;
    end process;

    process
    begin
        wait until rising_edge(ACLK);
        case CurrentState is
            when State_Reset =>
                Tx_AXI <= (others => '0');
                CurrentState <= State_WaitForTransmitterReady;
            when State_WaitForTransmitterReady =>
                if(Ready_AXI = '1') then
                    TREADY_RXD <= '1';
                    CurrentState <= State_WaitForValid;
                else
                    TREADY_RXD <= '0';
                    CurrentState <= State_WaitForTransmitterReady;
                end if;
            when State_WaitForValid =>                        
                if(TVALID_RXD = '1') then
                    TREADY_RXD <= '0';
                    Tx_AXI <= TDATA_RXD;
                    CurrentState <= State_WaitForTransmitterBusy;
                else
                    TREADY_RXD <= '1';
                    CurrentState <= State_WaitForValid;
                end if;
            when State_WaitForTransmitterBusy =>
                if(Ready_AXI = '0') then
                    CurrentState <= State_WaitForTransmitterReady;
                else
                    CurrentState <= State_WaitForTransmitterBusy;
                end if;
        end case;
        if(ARESETn = '0') then
            CurrentState <= State_Reset;
        end if;
    end process;
end AXIS_I2S_Arch;

Der erstellte AXI-Stream I2S-Sender kann jetzt noch bei Bedarf als IP-Core für Vivado erstellt werden, wobei ich die notwendigen Schritte hier auslassen werde.. Alternativ kann der fertige IP-Core auch aus dem GitLab-Repository des Projektes heruntergeladen werden.

Zurück

Last Updated on

Schreibe einen Kommentar

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