Analizator widma z FFT na STM32 z Cortex-M4
Odbiór sygnału audio z mikrofonu
Kiedy mamy pewność, że wyznaczanie widma częstotliwościowego sygnału działa poprawnie to możemy iść dalej i zamiast przygotowanego wcześniej sygnału wykorzystać sygnał odbierany z mikrofonu. Przedtem jednak konfigurujemy wykorzystywane peryferia:
static void RCC_Configure(void){ /********/ /* AHB1 */ /********/ // Włączenie sygnału taktującego układ CRC jest wymagane przez bibliotekę PDM RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_CRC, ENABLE); /********/ /* APB1 */ /********/ RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // Włączenie sygnału taktującego dla układu I2S RCC_PLLI2SCmd(ENABLE); } static void NVIC_Configure(void){ NVIC_InitTypeDef NVIC_InitStructure; // Konfiguracja grupy priorytetów NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // Konfiguracja przerwań od SPI2 (w tym również dla I2S2) NVIC_InitStructure.NVIC_IRQChannel = SPI2_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); } static void GPIO_Configure(void){ GPIO_InitTypeDef GPIO_InitStructure; // Konfiguracja linii PB10 podłączonej do CLK układu MP45DT02 – funkcja I2S2_CLK GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // Konfiguracja linii PC3 podłączonej do DOUT układu MP45DT02 - funkcja I2S2_DATA GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_SPI2); // Podłączenie PB10 do SPI2 GPIO_PinAFConfig(GPIOC, GPIO_PinSource3, GPIO_AF_SPI2); // Podłączenie PC3 do SPI2 } static void I2S_Configure(void){ I2S_InitTypeDef I2S_InitStructure; SPI_I2S_DeInit(SPI2); I2S_InitStructure.I2S_AudioFreq = OUT_FREQ*2; I2S_InitStructure.I2S_Standard = I2S_Standard_LSB; I2S_InitStructure.I2S_DataFormat = I2S_DataFormat_16b; I2S_InitStructure.I2S_CPOL = I2S_CPOL_High; I2S_InitStructure.I2S_Mode = I2S_Mode_MasterRx; I2S_InitStructure.I2S_MCLKOutput = I2S_MCLKOutput_Disable; I2S_Init(SPI2, &I2S_InitStructure); // Włączenie przerwań od zapełnienia bufora odbiorczego I2S2 SPI_I2S_ITConfig(SPI2, SPI_I2S_IT_RXNE, ENABLE); }
Po skonfigurowaniu peryferii można wykonać inicjalizację biblioteki PDM:
Filter.Fs = OUT_FREQ; // Częstotliwość próbkowania Filter.HP_HZ = 10; // Częstotliwość odcięcia filtru górnoprzepustowego Filter.LP_HZ = 16000; // Częstotliwość odcięcia filtru dolnoprzepustowego Filter.In_MicChannels = 1; // Liczba kanałów wejściowych Filter.Out_MicChannels = 1; // Liczba kanałów wyjściowych PDM_Filter_Init(&Filter); // Inicjalizacja biblioteki PDM
W pliku main.h zdefiniowane są stałe wykorzystywane w programie:
#define DECIMATION_FACTOR 64 #define OUT_FREQ 32000 #define PDM_Input_Buffer_SIZE ((OUT_FREQ/1000)*DECIMATION_FACTOR/8) #define PCM_Output_Buffer_SIZE (OUT_FREQ/1000) #define SPECTRUM_BG_COLOR Black #define SPECTRUM_FREQ_S_kHz 32.0 #define SPECTRUM_HEIGHT 150 #define SPECTRUM_NR_SAMPLES 512 #define SPECTRUM_X_LABEL "[kHz]"
Warto zwrócić uwagę na konfigurację układu I2S oraz biblioteki PDM pod względem wartości częstotliwości. Ponieważ biblioteka PDM oferuje cztery funkcje konwertujące sygnał z postaci PDM na PCM (współczynnik decymacji 64 lub 80, dane w kolejności MSB lub LSB; rysunek 14):
int32_t PDM_Filter_64_MSB(uint8_t* data, uint16_t* dataOut, uint16_t MicGain, PDMFilter_InitStruct * Filter); int32_t PDM_Filter_80_MSB(uint8_t* data, uint16_t* dataOut, uint16_t MicGain, PDMFilter_InitStruct * Filter); int32_t PDM_Filter_64_LSB(uint8_t* data, uint16_t* dataOut, uint16_t MicGain, PDMFilter_InitStruct * Filter); int32_t PDM_Filter_80_LSB(uint8_t* data, uint16_t* dataOut, uint16_t MicGain, PDMFilter_InitStruct * Filter);
to wymaganym jest aby częstotliwość na linii CLK układu MP45DT02 była równa:
FCLK = DecimatorFactor · FS
Tak więc, jeśli chcemy uzyskać częstotliwość próbkowania 32 kHz oraz wykorzystać współczynnik decymacji o wartości 64 to częstotliwość na linii CLK powinna wynosić 2048 kHz. Aby ustawić taką wartość w układzie I2S należy wiedzieć, że układ w trybie normalnej pracy odbiera po 16 bitów danych (najmniejsza możliwa wartość pola I2S_DataFormat) na przemian z kanału pierwszego oraz drugiego. A ponieważ w danej sytuacji będzie on cały czas odbierał dane z jednego źródła to na odbiór „próbki z dwóch kanałów” przeznaczone są 32 bity, czyli 2048 kHz / 32 = 64 kHz – taka też wartość jest ustawiana w polu I2S_AudioFreq.
Rys. 14. Proces obróbki sygnału przez bibliotekę PDM
Po włączeniu odbioru danych przez układ I2S komendą I2S_Cmd(SPI2, ENABLE) dane będą najpierw kopiowane do rejestru odbiorczego, a następnie w przerwaniu zostaną przeniesione do bufora PDM_Input_Buffer. Gdy bufor ten zostanie całkowicie zapełniony to wykona się funkcja PDM_Filter_64_LSB() konwertująca sygnał z postaci PDM do postaci PCM (plik stm32f4xx_it.c):
uint32_t InternalBufferSize = 0; uint32_t Data_Status = 0; void SPI2_IRQHandler(void){ extern PDMFilter_InitStruct Filter; extern uint8_t PDM_Input_Buffer[]; extern uint16_t PCM_Output_Buffer[]; u16 volume; u16 app; // Sprawdź czy są dostępne nowe dane if (SPI_GetITStatus(SPI2, SPI_I2S_IT_RXNE) != RESET){ // Odczytaj dane i zapisz do bufora – najpierw młodszy potem starszy bajt app = SPI_I2S_ReceiveData(SPI2); PDM_Input_Buffer[InternalBufferSize++] = (uint8_t)app; PDM_Input_Buffer[InternalBufferSize++] = (uint8_t)HTONS(app); // Sprawdź czy bufor jest pełny if (InternalBufferSize >= PDM_Input_Buffer_SIZE){ InternalBufferSize = 0; volume = 50; PDM_Filter_64_LSB(PDM_Input_Buffer, PCM_Output_Buffer, volume, &Filter); Data_Status = 1; } } }