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.
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