Kampis Elektroecke

Design des I2S-Empfängers

Im nächsten Schritt möchte ich zeigen, wie ein I2S-Empfänger in einem FPGA umgesetzt werden kann. Das Ziel dieses Artikels soll es sein Daten, welche von einem nRF52 über I2S gesendet werden, zu empfangen und als 8-Bit Wert auf einer LED-Leiste anzuzeigen.

Das komplette Projekt teilt sich in drei Abschnitte auf, welche ich nach und nach erörtern werde:

  • Ein Top-Design für die Datenausgabe
  • Der I2S-Empfänger
  • Der Code für den nRF52 I2S-Master

Der Code für den nRF52 I2S-Master:

Der I2S-Master wird über einen nRF52832, welcher sich auf einem nRF52 DK befindet, realisiert. Als Firmware für den RF-SoC verwende ich eine angepasste Version des I2S-Beispiels aus dem nRF52-SDK.

#include "app_error.h"
#include "nrf_drv_i2s.h"

#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"

#define I2S_DATA_BLOCK_WORDS    16

static uint32_t Tx_Buffer[I2S_DATA_BLOCK_WORDS];

uint32_t PacketCounter = 0x00;
uint8_t PacketData = 0x00;

static nrf_drv_i2s_buffers_t const Buffers = 
{
    .p_tx_buffer = Tx_Buffer,
    .p_rx_buffer = NULL,
};

static void DataHandler(nrf_drv_i2s_buffers_t const* p_released, uint32_t status)
{
    ASSERT(p_released);

    if(!(status & NRFX_I2S_STATUS_NEXT_BUFFERS_NEEDED))
    {
        return;
    }

    if(!p_released->p_rx_buffer)
    {
        APP_ERROR_CHECK(nrf_drv_i2s_next_buffers_set(&Buffers));
    }
    else
    {
        APP_ERROR_CHECK(nrf_drv_i2s_next_buffers_set(p_released));
    }

    PacketCounter++;
}

int main(void)
{
    APP_ERROR_CHECK(NRF_LOG_INIT(NULL));
    NRF_LOG_DEFAULT_BACKENDS_INIT();
    NRF_LOG_INFO("I2S sender started...");

    nrf_drv_i2s_config_t I2S_Config = NRF_DRV_I2S_DEFAULT_CONFIG;
    I2S_Config.sdin_pin	    = NRFX_I2S_PIN_NOT_USED;
    I2S_Config.sdout_pin    = 27;
    I2S_Config.lrck_pin	    = 26;
    I2S_Config.sck_pin	    = 25;
    I2S_Config.mck_pin	    = 2;
    I2S_Config.mck_setup    = NRF_I2S_MCK_32MDIV16;
    I2S_Config.ratio	    = NRF_I2S_RATIO_96X;
    I2S_Config.channels	    = NRF_I2S_CHANNELS_STEREO;
    APP_ERROR_CHECK(nrf_drv_i2s_init(&I2S_Config, DataHandler));
    APP_ERROR_CHECK(nrf_drv_i2s_start(&Buffers, I2S_DATA_BLOCK_WORDS, 0));

    while(1)
    {
        for(uint16_t i = 0x00; i < I2S_DATA_BLOCK_WORDS; i++)
        {
            Tx_Buffer[i] = (0x05 << 0x10) | PacketData;
        }

        if(PacketCounter >= 64)
        {
            PacketData++;
            PacketCounter = 0x00;
        }

        NRF_LOG_FLUSH();
    }
}

Dieses Beispiel sendet permanent Daten über den I2S, wobei nach 64 Datenpaketen der Wert der Payload um eins erhöht wird.

Der I2S-Empfänger:

Der I2S-Empfänger hat die Aufgabe die ankommenden Daten in das FPGA einzutakten und an die übergeordnete Logik weiterzugeben.

Signal Beschreibung
MCLK Audio Master Clock
nReset Reset Eingang (Active Low)
Valid Signalisiert das Ende einer Übertragung
Left
Daten des linken Audio-Kanals
Right Daten des rechten Audio-Kanals
SCLK Serial Clock der I2S-Schnittstelle
LRCLK Links/Rechts Takt (WS) der I2S-Schnittstelle
SD Serial Data der I2S-Schnittstelle

Somit sieht die Entität des Empfängers folgendermaßen aus:

entity I2S_Receiver is
    Generic (   WIDTH   : INTEGER := 16
                );
    Port (  MCLK        : in STD_LOGIC;
            nReset      : in STD_LOGIC;
            Valid       : out STD_LOGIC;
            Left        : out STD_LOGIC_VECTOR((WIDTH - 1) downto 0);
            Right       : out STD_LOGIC_VECTOR((WIDTH - 1) downto 0);
            LRCLK       : in STD_LOGIC;
            SCLK        : in STD_LOGIC;
            SD          : in STD_LOGIC
            );
end I2S_Receiver;

Die Daten von der I2S-Schnittstelle müssen als erstes in die Takt-Domain des FPGAs einsynchronisiert werden. Dies erfolgt über einen entsprechenden Prozess und ein zweistufiges Schieberegister:

I2S_Sync_Proc : process
begin
    wait until falling_edge(MCLK);

    LRCLK_Sync <= LRCLK_Sync(0) & LRCLK;
    SCLK_Sync <= SCLK_Sync(0) & SCLK;
    SD_Sync <= SD_Sync(0) & SD;

    if(nReset = '0') then
        LRCLK_Sync <= (others => '0');
        SCLK_Sync <= (others => '0');
        SD_Sync <= (others => '0');
    end if;
end process;

Bei jeder fallenden Flanke am SCLK-Signal wird der Zustand des SD-Signals eingelesen und in einem weiteren Schieberegister gespeichert:

I2S_DataIn_Proc : process
begin
    wait until falling_edge(MCLK);

    if(SCLK_Sync = "10") then
        Data_Shift <= Data_Shift((WIDTH - 2) downto 0) & SD_Sync(1);
    end if;
end process;

Das Speichern der Daten erzeugt eine Verzögerung von einem Taktzyklus erzeugt und weil das LRCLK-Signal für das Ende einer Übertragung verwendet werden soll, muss das LRCLK-Signal entsprechend ebenfalls um einen Taktzyklus verzögert werden:

I2S_Delay_Proc : process
begin
    wait until falling_edge(MCLK);

    if(SCLK_Sync = "10") then
        LRCLK_Shift(0) <= LRCLK_Sync(1);
        LRCLK_Shift(1) <= LRCLK_Shift(0);
    end if;

    if(nReset = '0') then
        LRCLK_Shift <= (others => '0');
    end if;
end process;

Abschließend wird noch ein Prozess benötigt, welcher die Daten am Ende einer Übertragung, also bei einem Wechseln am LRCLK-Signal von High → Low oder Low → High, auf den entsprechenden Ausgang vom rechten oder linken Kanal legt. Zudem soll bei einer fallenden Flanke an SCLK und an LRCLK das Valid-Signal für einen Taktzyklus gesetzt werden um das Ende einer Übertragung zu signalisieren:

I2S_Copy_Proc : process
begin
    wait until falling_edge(MCLK);

    if(SCLK_Sync = "10") then
        if(LRCLK_Shift = "01") then
            Data_Left <= Data_Shift((WIDTH - 1) downto 0);
        elsif(LRCLK_Shift = "10") then
            Data_Right <= Data_Shift((WIDTH - 1) downto 0);
        end if;
    end if;

    if(nReset = '0') then
        Data_Right <= (others => '0');
        Data_Left <= (others => '0');
    end if;
end process;

Data_Valid <= '1' when ((SCLK_Sync = "10") and (LRCLK_Shift = "10")) else '0';

Left <= Data_Left;
Right <= Data_Right;
Valid <= Data_Valid;

Damit wäre der Empfänger fertig. Fehlt nur noch ein Top-Design um die Funktionsweise des Empfängers zu testen.

Das Top-Design:

Das Top-Design hat die Aufgabe die Daten aus dem I2S-Empfänger anzunehmen und die unteren 8 Bits des linken Audio-Kanals über eine LED-Leiste auszugeben.

entity Top is
    Generic (   RATIO   : INTEGER := 8;
                WIDTH   : INTEGER := 16
                );
    Port (  nReset      : in STD_LOGIC
            MCLK        : in STD_LOGIC;
            LRCLK       : in STD_LOGIC;
            SCLK        : in STD_LOGIC;
            SD          : in  STD_LOGIC;
            Data        : out STD_LOGIC_VECTOR(7 downto 0)
            );
end Top;

architecture Top_Arch of Top is

    signal Valid            : STD_LOGIC                                     := '0';

    signal Data_Left        : STD_LOGIC_VECTOR((WIDTH - 1) downto 0)        := (others => '0');
    signal Data_Right       : STD_LOGIC_VECTOR((WIDTH - 1) downto 0)        := (others => '0');

    component I2S_Receiver is
        Generic (   WIDTH   : INTEGER := 16
                    );
        Port (  MCLK        : in STD_LOGIC;
                nReset      : in STD_LOGIC;
                Valid       : out STD_LOGIC;
                Left        : out STD_LOGIC_VECTOR((WIDTH - 1) downto 0);
                Right       : out STD_LOGIC_VECTOR((WIDTH - 1) downto 0);
                LRCLK       : in STD_LOGIC;
                SCLK        : in STD_LOGIC;
                SD          : in STD_LOGIC
                );
    end component;

begin

    Receiver : I2S_Receiver generic map(    WIDTH => WIDTH
                                            )
                                  port map( MCLK => MCLK,
                                            nReset => nReset,
                                            Left => Data_Left,
                                            Right => Data_Right,
                                            Valid => Valid,
                                            LRCLK => LRCLK,
                                            SCLK => SCLK,
                                            SD => SD
                                            );

    process
    begin
        wait until rising_edge(MCLK);

        if(Valid = '1') then
            Data <= Data_Left(7 downto 0);
        end if;

        if(nReset = '0') then
            Data <= (others => '0');
        end if;
    end process;

end Top_Arch;

Um das Design zu testen muss der Code auf den nRF52 mit dem FPGA verbunden und der Code aufgespielt werden. Der nRF52 beginnt dann direkt damit die Daten an das FPGA zu senden, woraufhin das FPGA die Daten einliest und anzeigt.

Wenn alles geklappt hat ist der Sender einsatzbereit. Im nächsten Teil zeige ich, wie der Sender mit einem AXI-Stream Interface ausgestattet wird.

Zurück

Schreibe einen Kommentar

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