Add an AXI-Stream Interface

In this part of the I2S tutorial for FPGAs, I would like to show how the can expand the I2S transmitter with an AXI stream interface.

For this purpose, a new top design called AXIS_I2S is created. This design should have the following interface:

This block design results in the following entity:

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;

The ratio of SCLK to MCKL is defined via the parameter RATIO and the width of a data word per channel via the parameter WIDTH.

This implementation only supports 16-bit data words per channel (i.e. 32 bit for stereo). The following code must be adapted for larger bus widths.

The following components must be implemented in the design:

  • A clock prescaler to create the input clock for the I2S transmitter

  • An AXI-Stream slave interface

  • The control logic for the I2S transmitter

A process is created for the divider, which counts up a counter on the rising clock edge of MCLK and switches the signal SCLK_Int after half the period.

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;

The next step is to implement the AXI-Stream interface. A state machine is used for this:

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;

After a reset, the machine changes from the State_Reset state to the State_WaitForTransmitterReady state, where it waits for the ready signal from the I2S transmitter. As soon as the transmitter is ready, the TREADY_RXD signal of the AXI-Stream interface is set, whereby the master is informed that the slave is ready to receive data. The slave then changes to the State_WaitForValid state.

In this state, the slave waits for the master to set the TVALID_RXD signal to mark valid data. As soon as the signal is set, the data is written to an internal FIFO. The machine then changes to the State_WaitForTransmitterBusy state.

Now the state machine waits for the I2S transmitter to start transmitting the data and to delete the ready signal. As soon as this is done, the machine switches back to the State_WaitForTransmitterReady state and waits again until the I2S transmitter is ready.

With that, the AXI-Stream Interface would be finished in theory. Unfortunately, it gets a bit tricky at the end, since the current circuit design uses two different clock domains:

  • The clock domain for ACLK

  • The clock domain for MCLK

In general, these two clock signals cannot be generated from a clock source (e.g. via a clock divider), since the AXI interface typically runs at 100 MHz and the audio interface requires clock rates that can be neatly divided down to the sampling frequency, such as e.g. 12.288 MHz. As a result, timing errors occur during implementation due to excessive worst negative slack (WNS) and total negative slack (TNS):

Also, the risk of incorrect data due to the metastability of the flip-flops occurring with different clock domains is very high. Metastability occurs a. then when a flip-flop switches and the data changes at that very moment.

Therefore, the signals that are used by the individual clock domains must be transferred to the other clock domain in each case via corresponding circuits. Xilinx describes corresponding macros in document UG953 that can be used for this purpose.

The examples of the macros can be used directly for the VHDL code:

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
                                             );

Finally, the I2S transmitter must be inserted and the generated signals passed on.

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
                                            );

The AXI-Stream Interface for the I2S transmitter is now ready and ready for use. The complete code should now look like this:

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;

The created AXI-Stream I2S transmitter can now be created as an IP core for Vivado if required, whereby I will omit the necessary steps here. Alternatively, the finished IP core can also be downloaded from the project's GitLab repository.

Last updated