W artykule przedstawiamy przykład implementacji analizatora widma FFT sygnału pochodzącego z cyfrowego mikrofonu MEMS zamontowanego na płytce zestawu STM32F4DISCOVERY. Prezentowany przykład ilustruje w jaki sposób odczytywać sygnał z mikrofonu, jak go przetwarzać oraz jak wyświetlić wynik na kolorowym wyświetlaczu LCD. W projekcie przyjęto zakres analizowanych częstotliwości do 16 kHz, jednak całość stanowi podstawę, którą można rozwijać i modyfikować w zależności od wymagań. Projekt został utworzony w środowisku CooCox CoIDE z wykorzystaniem kompilatora GNU Tools ARM Embedded.
Fot. 1. Testowanie układu analizatora widma sygnałem audio z głośnika na który podawany jest prostokątny sygnał o częstotliwości 6,76 kHz
Urządzenie, które na wejściu przyjmuje sygnał opisany w czasie, a na wyjściu przedstawia ten sam sygnał, ale już opisany w częstotliwości, nazywamy analizatorem widma. Aby wykonać taki układ należy na początku określić etapy jego realizacji:
- konwersja sygnału akustycznego na elektryczny,
- odbiór sygnału elektrycznego przez mikrokontroler,
- wyciągnięcie danych przenoszonych przez sygnał elektryczny i zapis ich do bufora,
- wyznaczenie widma zebranych próbek sygnału, czyli transformacja x(t) ? X(f),
- przedstawienie widma w formie graficznej.
Fot. 2. Umiejscowienie czujnika dźwięku MP45DT02 na płytce STM32F4DISCOVERY
Na płytce STM32F4DISCOVERY znajduje się mikrofon MEMS typu MP45DT02 (fotografia 2) i to właśnie on będzie realizował pierwszy etap, czyli zamieniał sygnał akustyczny na elektryczny.
Rys. 3. Schemat podłączenia mikrofonu MP45DT02 do mikrokontrolera STM32F407VG w zestawie STM32F4DISCOVERY
Podłączenie mikrofonu do mikrokontrolera przedstawiono na rysunku 3. Ma on 6 wyprowadzeń z czego 3 są od zasilania, pozostałe 3 są sygnałowe. Te ostatnie to kolejno: wybór kanału lewy/prawy, wejściowy sygnał taktujący CLK oraz wyjściowy sygnał danych.
Odbierane przez układ I2S dane to tylko zbiór pewnych sekwencji bitów i przed oddaniem ich do etapu 4 należy je jeszcze przekonwertować z postaci PDM do postaci PCM, czyli chodzi głównie o to by uzyskać zbiór 16-bitowych wartości opisujących kolejne próbki sygnału w różnych chwilach czasu (kodowanie PCM). W tym celu można wykorzystać gotową bibliotekę PDM Audio Software Decoding Library, która jest dostępna w pliku archiwalnym pod nazwą STSW-STM32068 w katalogu Utilities/STM32F4-Discovery/) – są to 2 pliki: libPDMFilter_GCC.a oraz pdm_filter.h.
Kiedy mamy już zebrane próbki sygnału to możemy przejść do etapu czwartego i poddać je transformacji Fouriera. We wspomnianym już pliku STSW-STM32068 w katalogu Libraries/CMSIS/DSP_Lib/ można znaleźć kody źródłowe różnych funkcji często stosowanych przy przetwarzaniu sygnałów w tym również gotową funkcje szybkiej transformaty Fouriera (Fast Fourier Transform). Szczegółowy ich opis można znaleźć w pliku Libraries/CMSIS/index.htm w kategorii CMSIS DSP Software Library.
Ostatni etap to wyświetlenie otrzymanego widma sygnału na wyświetlaczu LCD i można to zrobić wykorzystując kilka plików biblioteki STM32 embedded GUI library do pobrania na stronie (wymagają one pewnych modyfikacji).
Widma częstotliwościowe prostych sygnałów
Tworzenie aplikacji analizatora widma najlepiej rozpocząć od wykonania czegoś prostego. Ponieważ interesuje nas widmo częstotliwościowe to warto sprawdzić jak wygląda ono dla kilku prostych sygnałów. W tym celu możemy wygenerować cztery sygnały, które zawierają od jednej do czterech składowych harmonicznych:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
if(mode == 1){ for(i=0; i<512; ++i){ buffer_input[i] = (float32_t) 30*sin(2*PI*freq*i*dt); } } else if(mode == 2){ for(i=0; i<512; ++i){ buffer_input[i] = (float32_t) 20*sin(2*PI*freq*i*dt) + 10*sin(2*PI*2*freq*i*dt); } } else if(mode == 3){ for(i=0; i<512; ++i){ buffer_input[i] = (float32_t) 15*sin(2*PI*freq*i*dt) + 10*sin(2*PI*2*freq*i*dt) + 5*sin(2*PI*3*freq*i*dt); } } else if(mode == 4){ for(i=0; i<512; ++i){ buffer_input[i] = (float32_t) 15*sin(2*PI*freq*i*dt) + 10*sin(2*PI*2*freq*i*dt) + 3*sin(2*PI*3*freq*i*dt) + 2*sin(2*PI*4*freq*i*dt); } } |
Generowanych jest 512 próbek sygnału i liczba ta wynika z wymagań narzuconych przez funkcję wyznaczającą widmo i chęci przedstawienia jak najdłuższego przebiegu na wyświetlaczu (wykorzystane będą tylko pierwsze 256). Wartość zmiennej freq w toku działania aplikacji demonstracyjnej jest zmieniana od 1 do 1024, a zmienna dt jest wirtualnym okresem próbkowania, który wynosi 0,001. Dalej można wyświetlić wygenerowane sygnały:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// Usunięcie poprzedniego przebiegu sygnału LCD_SetForegroundColor(Black); for(i=0; i<255; ++i){ x_LCD_DrawLine_2(i + 32, (uint16_t)(buffer_input_copy[i] + 50), i + 33, (uint16_t)(buffer_input_copy[i+1] + 50)); } // Rysowanie nowego przebiegu sygnału LCD_SetForegroundColor(Yellow); for(i=0; i<255; ++i){ x_LCD_DrawLine_2(i + 32, (uint16_t)(buffer_input[i] + 50), i + 33, (uint16_t)(buffer_input[i+1] + 50)); } // Wyświetlenie informacji o częstotliwościach składowych harmonicznych sygnału if(mode == 1){ sprintf(text,"F=%4d [1]",freq); x_LCD_DrawText(32, 100, text, LeftJustify, NormalOrientation); } else if(mode == 2){ sprintf(text,"F1=%4d [1], F2=%4d [1]",freq,freq*2); x_LCD_DrawText(32, 100, text, LeftJustify, NormalOrientation); } else if(mode == 3){ sprintf(text,"F1=%4d [1], F2=%4d [1]",freq,freq*2); x_LCD_DrawText(32, 100, text, LeftJustify, NormalOrientation); sprintf(text,"F3=%4d [1]",freq*3); x_LCD_DrawText(32, 110, text, LeftJustify, NormalOrientation); } else if(mode == 4){ sprintf(text,"F1=%4d [1], F2=%4d [1]",freq,freq*2); x_LCD_DrawText(32, 100, text, LeftJustify, NormalOrientation); sprintf(text,"F3=%4d [1], F4=%4d [1]",freq*3,freq*4); x_LCD_DrawText(32, 110, text, LeftJustify, NormalOrientation); } // Zachowanie kopii pierwszej połowy próbek aktualnego sygnału for(i=0; i<256; ++i){ buffer_input_copy[i] = buffer_input[i]; buffer_output_mag_copy[i] = buffer_output_mag[i]; } |
Mając przygotowany sygnał wykorzystujemy trzy funkcje z biblioteki DSP i wyznaczamy najpierw transformatę Fouriera danego sygnału, a następnie obliczamy wartości modułów dla każdej pary liczb rzeczywistej i urojonej. Na koniec możemy wyszukać składową sygnału (częstotliwość), która ma największą amplitudę i przeskalować tablicę modułów do zakresu nadającego się do wyświetlenia:
1 2 3 4 5 6 7 8 9 10 |
// Wyznaczenie transformaty Fouriera arm_rfft_f32(&S, buffer_input, buffer_output); // Obliczenie modułów arm_cmplx_mag_f32(buffer_output, buffer_output_mag, 512); // Znalezienie składowej harmonicznej sygnału o największej amplitudzie arm_max_f32(buffer_output_mag, 512, &maxvalue, &maxvalueindex); // Skalowanie wartości modułów for(i=0; i<512; ++i){ buffer_output_mag[i] = 100*buffer_output_mag[i]/maxvalue; } |
Pierwsza funkcja przyjmuje trzy argumenty: wskaźnik do struktury typu arm_rfft_instance_f32 opisującej sposób działania transformaty FFT, wskaźnik do buforu wejściowego z wartościami próbek sygnału oraz wskaźnik do buforu wyjściowego w którym będą zapisane zespolone wartości transformaty sygnału. Ważne jest aby w buforze wejściowym były zapisane rzeczywiste wartości (uwaga: zostaną one zmodyfikowane po wykonaniu się funkcji), czyli:
1 |
{real[0], real[1], real[2], real[3], ..} |
Natomiast w buforze wyjściowym, który powinien być dwa razy większy od wejściowego, oprócz wartości rzeczywistych będą znajdować się również wartości urojone:
1 |
{real[0], imag[0], Real[1], imag[1], ...} |
W przypadku struktury przekazywanej w pierwszym argumencie nie ma potrzeby ręcznego uzupełniania jej, gdyż można do tego celu wykorzystać funkcję arm_rfft_init_f32():
1 2 3 4 5 6 7 8 9 |
// Wystarczą deklaracje dwóch struktur arm_rfft_instance_f32 S; arm_cfft_radix4_instance_f32 S_CFFT; // oraz inicjalizacja, gdzie: // arg#3 – liczba próbek w buforze wejściowym; możliwe wartości to: 128, 512, 2048 // arg#4 – kierunek transformacji: 0 – prosta, 1 – odwrotna // arg#5 – uporządkowanie wartości w buforze wyjściowym: 0 – odwrócone, 1 - normalne arm_rfft_init_f32(&S, &S_CFFT, 512, 0, 1); |
Druga funkcja przyjmuje również trzy argumenty: wskaźnik do buforu wejściowego z wartościami par liczb rzeczywistych i urojonych, wskaźnik do buforu wyjściowego do którego zostaną zapisane wartości rzeczywiste oraz ostatni parametr – ilość liczb zespolonych. Funkcja oblicza moduł dla każdej pary wartości liczby rzeczywistej i urojonej zgodnie ze wzorem:
Trzecia funkcja znajduje maksymalną wartość w tablicy buffer_output_mag o długości 512 elementów i zapisuje ją do zmiennej maxvalue, a indeks komórki w jakiej się ona znajduje jest zapisywany w zmiennej maxvalueindex.
Fot. 4. Sygnały i ich widma: od jednej składowej do czterech składowych
Na koniec, po przeskalowaniu wartości modułów, można wyświetlić otrzymane wyniki (fotografia 4). Wystarczy przedstawić pierwsze 256 elementów tablicy buffer_output_mag, gdyż druga połowa stanowi lustrzane odbicie pierwszej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Usunięcie poprzedniego widma LCD_SetForegroundColor(Black); for(i=0; i<256; ++i){ x_LCD_DrawLine_2(i + 32, 130 + 100, i + 32, 130 + (uint16_t)(100 - (uint16_t)buffer_output_mag_copy[i+1])); } // Rysowanie nowego widma LCD_SetForegroundColor(Green); for(i=0; i<256; ++i){ x_LCD_DrawLine_2(i + 32, 130 + 100, i + 32, 130 + (uint16_t)(100 - (uint16_t)buffer_output_mag[i+1])); } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
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:
1 2 3 4 5 6 |
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:
1 2 3 4 5 6 7 8 9 10 |
#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):
1 2 3 4 |
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.
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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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; } } } |
Widmo częstotliwościowe sygnału audio z mikrofonu
Ostatni etap projektu to wyświetlenie widma sygnału jaki otrzymujemy z mikrofonu. Do tego celu przygotowano dwie funkcje DrawSpectrum_Prepare(), która rysuje tylko raz stałe elementy wykresu oraz DrawSpectrum_Update(), która rysuje samo widmo lub go usuwa w zależności od przekazanego jako argument koloru.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
void DrawSpectrum_Prepare(uint16_t xpos, uint8_t ypos){ int i; g_SPECTRUM_Xpos = xpos; g_SPECTRUM_Ypos = ypos; // Rysowanie obramowania LCD_SetForegroundColor(White); x_LCD_DrawRectangle(xpos + 10, ypos + 10, SPECTRUM_NR_SAMPLES/2 + 2, SPECTRUM_HEIGHT+2); // Wypełnienie tła x_LCD_DrawFilledRectangle(xpos + 11, ypos + 11, SPECTRUM_NR_SAMPLES/2, SPECTRUM_HEIGHT, SPECTRUM_BG_COLOR); // Rysowanie podziałek osi poziomej LCD_SetFont(&Font8x8); LCD_SetTextColor(Grey); for(i=0; i<=16; ++i){ sprintf(text,"%.2f",i*16*SPECTRUM_FREQ_S_kHz/SPECTRUM_NR_SAMPLES); x_LCD_DrawLine(xpos + 10 + i*16, ypos + 10 + SPECTRUM_HEIGHT+2, 5, Vertical); x_LCD_DrawText(xpos + 10 + i*16 + 4, ypos + 10 + SPECTRUM_HEIGHT+2 + 10, text, LeftJustify, RightOrientation); } x_LCD_DrawText(xpos + 10 + i*16 + 4, ypos + 10 + SPECTRUM_HEIGHT+2 + 10, SPECTRUM_X_LABEL, LeftJustify, RightOrientation); // Rysowanie podziałek osi pionowej for(i=0; i<=6; ++i){ x_LCD_DrawLine(xpos + 10 - 5, ypos + 10 + SPECTRUM_HEIGHT+2 - 1 - i*25, 5, Horizontal); } } void DrawSpectrum_Update(uint16_t color){ int i; // Rysowanie szpilek LCD_SetForegroundColor(color); for(i=0; i<256; ++i){ x_LCD_DrawLine_2(g_SPECTRUM_Xpos + 11 + i, g_SPECTRUM_Ypos + 160, g_SPECTRUM_Xpos + 11 + i, g_SPECTRUM_Ypos + (uint16_t)(160 - buffer_output_mag[i+1])); } } |
Poniżej przedstawiono kod który wykorzystuje wszystkie podane dotąd informacje. Komentarza mogą wymagać dwa miejsca. Ponieważ w funkcje obsługi przerwań od odbioru danych od I2S maksymalny rozmiar paczki zapisanej w buforze PCM_Output_Buffer wynosi w danej sytuacji 32 elementy to trzeba zebrać ich 16 aby otrzymać tablicę 512 wartości rzeczywistych przekazywanych do funkcji wyznaczającej transformatę. Po drugie zamiast skalować widmo wykorzystując maksymalną jej wartość jest ono skalowane z wartością stałą wyznaczoną doświadczalnie tak aby wyświetlanie odbywało się poprawnie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// Rysowanie stałych elementów wykresu; cały obiekt rozpoczyna się w punkcie x=21, y=20 DrawSpectrum_Prepare(21,20); // Włączenie odbioru danych przez układ I2S I2S_Cmd(SPI2, ENABLE); while(1){ // Jeśli wciśnięto przycisk to zakończ wyświetlanie widma if(STM_EVAL_PBGetState(BUTTON_USER) == SET){ while(STM_EVAL_PBGetState(BUTTON_USER) == SET); break; } // Jeśli nowa paczka danych w postaci sygnału PCM jest gotowa if(Data_Status){ // Przekopiuj dane paczki do bufora for(i=0; i<(OUT_FREQ/1000); i++){ buffer_input[i+(OUT_FREQ/1000)*z] = (float32_t) PCM_Output_Buffer[i]; } ++z; if(z > 512/(OUT_FREQ/1000)){ z = 0; // ************************************************************ // Usunięcie poprzedniego widma DrawSpectrum_Update(Black); // ************************************************************ // Wyznaczenie transformaty Fouriera arm_rfft_f32(&S, buffer_input, buffer_output); // Obliczenie modułów arm_cmplx_mag_f32(buffer_output, buffer_output_mag, 512); // Znalezienie składowej harmonicznej sygnału o największej amplitudzie arm_max_f32(&(buffer_output_mag[1]), 512, &maxvalue, &maxvalueindex); // Skalowanie wartości modułów for(i=0; i<512; ++i){ buffer_output_mag[i+1] = 140*buffer_output_mag[i+1]/20000000; } // ************************************************************ // Rysowanie nowego widma DrawSpectrum_Update(Green); } Data_Status = 0; } // Wyświetl częstotliwość składowej harmonicznej o największej amplitudzie sprintf(text,"F=%2.2f[kHz] ",(maxvalueindex+1)*32.0/512); x_LCD_DrawText(100, 15, text, LeftJustify, NormalOrientation); // Czekaj jakiś czas, aby widmo było lepiej widoczne for(i=0; i<0x10000; ++i); } // Wyłącz odbiór danych I2S_Cmd(SPI2, DISABLE);<br><br><br> |
Rys. 5. Struktura prezentowanego projektu
Ostatecznie struktura projektu wygląda tak jak na rysunku 5, a na rysunkach 6 i 7 przedstawiono konfigurację w zakładce Compile oraz Link środowiska CooCox CoIDE.
Rys. 6. Konfiguracja w zakładce Compile
Rys. 7. Konfiguracja w zakładce Link
W pliku system_stm32f4xx.c można znaleźć wartości użyte do konfiguracji sygnałów taktujących zarówno samego systemu jak również przekazywanego do peryferii I2S. Sprzętowa jednostka FPU nie została włączona gdyż biblioteka PDM nie jest przystosowana do jej obsługi (kompilator wyrzuca błędy).
Jan Szemiet