[3] Oscyloskop z FFT na STM32F746G-DISCO – biblioteki i aplikacja

Uwaga! Tu znajdziesz pierwszą część artykułu.

Uwaga! Tu znajdziesz drugą część artykułu.

W ostatniej części artykułu poświęconego aplikacji próbkującej i wyświetlającej sygnał z wejścia liniowego zostaną omówione pakiet BSP, biblioteki graficzna STemWin, matematyczna ARM CMSIS DSP oraz prosty moduł do wykrywania podstawowych gestów wykonanych przez użytkownika na panelu dotykowym.

 

BSP

BSP, czyli Board Support Package, jest zbiorem sterowników oraz interfejsów przygotowanych dla określonego mikrokontrolera oraz towarzyszących mu zewnętrznych peryferiów. BSP dostarczany wraz z biblioteką Cube od ST, umożliwia łatwą i szybką konfigurację zestawu ewaluacyjnego  STM32F746G-DISCOVERY i skupienie się na tworzeniu aplikacji.

Konfiguracja biblioteki jest wykonywana w pliku stm32f7xx_hal_conf.h. W przykładzie sprowadza się ona do włączenia odpowiednich modułów w sekcji Module Selection.

Do poprawnego działania, BSP musi dostać informację o czasie, za pośrednictwem funkcji inkrementującej wewnętrzne liczniki. Najprościej można to zrobić wywołując funkcję HAL_IncTick() w regularnych odstępach czasu generowanych przez przerwania sprzętowe. Z uwagi na to, że w projekcie jest używany system FreeRTOS, który może informować aplikację o każdorazowym przerwaniu od zegara systemowego, funkcja HAL_IncTick() jest wywoływana wewnątrz funkcji:

Funkcja vApplicatioTickHook została zdefiniowana w pliku main.c i jest wywoływana z częstotliwością zegara systemowego, ustawianego parametrem configTICK_RATE_HZ, pod warunkiem że wartość configUSE_TICK_HOOK jest ustawiona na 1. Oba te parametry powinny być zdefiniowane w pliku FreeRTOSConfig.h.

Pierwszą czynnością jaką należy wykonać przed skorzystaniem ze sterowników jest wywołanie funkcji inicjalizacyjnej:

Jest ona wywoływana na samym początku funkcji main. Bezpośrednio po niej, w funkcji SystemClock_Config, konfigurowane są zegary mikrokontrolera. Jest ona zdefiniowana w pliku main.c i wykorzystuje funkcje biblioteczne HAL_RCC_OscConfig oraz HAL_RCC_ClockConfig.

Teraz można już przystąpić do konfiguracji peryferiów. Na początek w zadaniu GUI_Task są inicjalizowane zewnętrzna pamięć RAM oraz panel dotykowy. Sterownik panelu dotykowego wymaga podania rozmiaru wyświetlacza w pikselach:

Dopiero wywołaniu drugiej z tych funkcji, można uruchomić licznik programowy odpowiedzialny za odczyt danych z panelu dotykowego. Ostatnią funkcją BSP wywoływana w tym zadaniu jest:

Włącza ona zegar modułu CRC procesora, który jest wymagany przez bibliotekę graficzną.

Dalsza część konfiguracji peryferiów znajduje się w zadaniu Signal_Task. Po otrzymaniu powiadomienia o gotowości interfejsu graficznego, wywoływane są dwie funkcje:

Pierwsza z nich konfiguruje zewnętrzny przetwornik audio, tak aby korzystał z wejścia liniowego (INPUT_DEVICE_INPUT_LINE_1), domyślnego poziomu głośności wynoszącego 64 (na 100) oraz domyślnego próbkowania 16 kHz. Druga funkcja uruchamia pomiary z wykorzystaniem DMA. Pierwszym argumentem jest tablica przeznaczona na próbki sygnału, drugim zaś jej długość. W przykładzie długość tablicy wynosi 16384 elementy, co daje 8192 próbki, ponieważ przetwornik próbkuje i umieszcza w tablicy dane z dwóch kanałów wejściowych.

Użycie DMA wymaga jeszcze zdefiniowania przerwań, aby otrzymać informację o zakończeniu pomiaru. Na początek trzeba dodać bezpośrednią obsługę przerwania od DMA. Odpowiednia funkcja została zdefiniowana w pliku stm32f7xx_it.c:

Jest to w rzeczywistości zwykła funkcja obsługi przerwania DMA, co można sprawdzić w pliku stm32746g_discovery_audio.h:

Funkcja ta z kolei informuje o fakcie wystąpienia przerwania bibliotekę BSP, za pomocą wywołania funkcji HAL_DMA_IRQHandler. Jako argument przyjmuje ona wskaźnik na strukturę SAI_HandleTypeDef, znajdującą się w stm32746g_discovery_audio.c. Należy ją więc zadeklarować jako extern w pliku stm32f7xx_it.c:

Dalsza część przetwarzania przerwania znajduje się już w bibliotece. Aby otrzymać informacje o zakończeniu połowy i całego transferu DMA należy przedefiniować dwie funkcje oznaczone jako __weak w pliku stm32746g_discovery_audio.c:

W projekcie są one zdefiniowanie w pliku main.c jako:

Operacje wykonywane wewnątrz tych funkcji zostały już omówione w poprzedniej części, dotyczącej systemu FreeRTOS. Istotne jest to, że przekazują one informację o końcu transferu lub jego połowy do zadań Signal_Task oraz FFT_Task.

Wywołanie ostatniej z używanych funkcji BSP znajduje się w obsłudze licznika programowego (TouchPanel_TimerCallback), omawianej także w poprzedniej części. Funkcja ta pobiera aktualny stan panelu dotykowego i zapisuje w strukturze typu TS_StateTypeDef, zawierającej m. in. liczbę wykrytych punktów oraz ich współrzędne:

2. Biblioteka graficzna STemWin

Biblioteka graficzna STemWin zawiera zbiór komponentów przydatnych przy tworzeniu interfejsów graficznych w oparciu o wyświetlacze graficzne. Jest ona niezależna od typu użytego mikrokontrolera, wyświetlacza oraz systemu operacyjnego (w tym jego braku).

Biblioteka wymaga kilku zewnętrznych plików konfiguracyjnych zapewniających poprawną obsługę posiadanego sprzętu. Pliki te znajdują się w katalogach src i inc projektu i zostały wzięte z przykładu StemWin_HelloWorld biblioteki STM32Cube pobranej ze strony http://www.st.com/web/en/catalog/tools/PF261909. Poza niżej wymienionymi plikami STemWin nie potrzebuje żadnej dodatkowej konfiguracji:

  • GUIConf.h – zawiera podstawową konfigurację biblioteki StemWin (m. in. domyślną czcionkę, włączenie menadżera okien),
  • GUIConf.c – definiuje funkcję GUI_X_Config odpowiedzialną za alokację pamięci,
  • LCDConf.h – udostępnia strukturę LCD_LayerPropTypedef reprezentującą warstwę interfejsu graficznego i przechowującą takie dane jak rozmiar wyświetlacza i liczbę bajtów przypadającą na jeden piksel,
  • LCDConf.c – definiuje podstawowe parametry i funkcje obsługi wyświetlacza LCD, z których korzysta biblioteka STemWin.

Przed utworzeniem interfejsu graficznego, biblioteka musi zostać zainicjalizowana. Z uwagi na wykorzystanie systemu operacyjnego, czynność ta musi być wykonana wewnątrz jednego z zadań – w tym przypadku GUI_Task, od razu po opisywanej wcześniej inicjalizacji BSP. Pierwszą funkcją STemWin, którą należy wywołać jest GUI_Init. Wszystkie pozostałe funkcje biblioteczne mogą zostać wywołane dopiero po niej. Wyjątek stanowi jedynie funkcja WM_SetCreateFlags, służąca do ustawiania domyślnych flag podczas tworzenia nowych okien interfejsu. W przykładzie jest to tylko jedna flaga: WM_CF_MEMDEV. Powoduje ona, że odrysowywanie obrazu jest wykonywane w buforze w pamięci, a nie bezpośrednio na wyświetlaczu. Dopiero po przygotowaniu bufora, jest on wyświetlany, co zapobiega migotaniu obrazu. Ostatnim krokiem inicjalizacji jest wyczyszczenie wyświetlacza:

W projekcie zaimplementowany został bardzo prosty interfejs graficzny służący do prezentacji danych. Został on oparty o dwa komponenty typu GRAPH – reprezentujące wykresy. Są one tworzone w zadaniu GUI_Task:

Jako pierwszy tworzony jest wykres sygnału za pomocą funkcji GRAPH_CreateEx. Pierwsze dwa argumenty to pozycja lewego górnego rogu okna wykresu na ekranie, kolejne dwa to jego rozmiar. Następnie należy podać okno do którego dodany zostanie wykres – makro WM_HBKWIN zwraca uchwyt do okna Desktop, będącego podstawowym oknem każdego interfejsu graficznego. Następne na liście argumentów są flagi wykorzystywane podczas tworzenia wykresu. WM_CF_SHOW oznacza wyświetlenie okna od razu po jego utworzeniu. Wartości flag podawane w tym polu są wspólne dla wszystkich typów okien, w przeciwieństwie do flag podawanych w przedostatnim argumencie. W projekcie nie są wykorzystywane dodatkowe flagi komponentu. Ostatni argument to ID, które musi być unikalne w obrębie komponentów znajdujących się w obrębie jednego okna. Wartość GUI_ID_GRAPH0 jest zdefiniowana w pliku bibliotecznym GUI.h. W razie konieczności definiowania własnych identyfikatorów, powinny one mieć wartości większe od GUI_ID_USER (0x800). Opisywana funkcja zwraca uchwyt do komponentu wykresu przechowywanego w zmiennej globalnej.

Kolejna funkcja – GRAPH_SetBorder tworzy marginesy wokół pola wykresu. Pierwszym argumentem jest uchwyt do wcześniej utworzonego komponentu. Cztery kolejne parametry oznaczają grubość marginesów: lewego, górnego, prawego i dolnego, wyrażoną w pikselach.

Po utworzeniu okna wykresu należy dołączyć do niego jeden lub więcej obiektów danych. Każdy z nich tworzony jest za pomocą funkcji GRAPH_DATA_YT_Create. Tworzy ona dane reprezentujące m. in. przebiegi czasowe, w których współrzędna X oznacza numery kolejnych próbek, a współrzędna Y – ich wartości. Lista argumentów zawiera kolejno: kolor, maksymalną liczbę punktów na ekranie, tablicę zawierającą dane początkowe oraz jej długość. W tym przypadku dane zostaną dopiero pobrane, dlatego zamiast tablicy podawany jest wskaźnik pusty NULL. Na koniec można dodać dane do okna wykresu za pomocą funkcji GRAPH_AttachData, przyjmującej uchwyty do okna wykresu oraz obiektu danych.

Oba wykresy: sygnału i FFT są tworzone w ten sam sposób, jednak do drugiego z nich dodawana jest jeszcze skala. Jest ona tworzona za pomocą funkcji GRAPH_SCALE_Create. Jej pierwszym argumentem jest pozycja na oknie wykresu – ustawiana na 12 pikseli od jego dolnej krawędzi. Za pomocą drugiego argumentu można ustawić przesunięcie tekstu do lewej, prawej lub jego wyśrodkowanie (gdyby skala dotyczyła osi Y byłoby to przesunięcie tekstu do góry, do dołu lub wyśrodkowanie). Kolejny argument to wybór osi poziomej (GRAPH_SCALE_CF_HORIZONTAL) lub pionowej (GRAPH_SCALE_CF_VERTICAL). Ostatni argument to odległość pomiędzy kolejnymi wartościami w pikselach.

Powyższe wywołania kończą tworzenie interfejsu graficznego w obrębie zadania GUI_Task. Ma ono jednak jeszcze jedną istotną funkcję. Jest nią cykliczne wywoływanie funkcji GUI_Delay, przyjmującej jako argument wartość opóźnienia oraz odpowiedzialnej za aktualizację interfejsu graficznego w razie jakichkolwiek zmian.

Rysowanie wykresów odbywa się w zadaniach Signal_Task i FFT_Task. W obu przypadkach wygląda ono podobnie, jednak w zadaniu FFT_Task zmieniana jest także skala, dlatego właśnie to zadanie posłuży do omówienia aktualizacji danych.

Wykresy aktualizowane są w pętli głównej zadania po otrzymaniu jednej z dwóch notyfikacji: TASK_EVENT_DMA_HALF_DONE lub TASK_EVENT_DMA_DONE. Otrzymanie jej oznacza, że dane w jednej połówce bufora są gotowe do przetworzenia. W pierwszej kolejności wykonywane są obliczenie transformaty oraz skalowanie (omówione w następnym punkcie), a wynikowe dane trafiają do bufora: appGlobals.fftOutput. Dalsze kroki wyglądają następująco:

W pętli for wybierane są punkty, które powinny zostać wyświetlone. Nie wybierane są one od początku bufora danych, ponieważ brany jest od uwagę offset wynikający z możliwego przesunięcia początku wykresu, dodatkowo dzielony przez współczynnik jego skali. Ostateczna lista punktów jest wpisywana do tablicy appGlobals.fftDisplay o stałej długości równej szerokości pola wykresu.

Niestety biblioteka STemWin uniemożliwia dodanie wielu punktów jednocześnie bez odświeżania wykresu po każdym z nich, dlatego obiekt danych appGlobals.fftGraphData jest przed każdą aktualizacją usuwany funkcjami GRAPH_DetachData i GRAPH_DATA_YT_Delete oraz tworzony na nowo za pomocą omawianej już funkcji GRAPH_DATA_YT_Create – tym razem jednak przekazywana jest to niej tablica z wartościami początkowymi. Nowo utworzony obiekt danych jest ponownie dodawany do okna wykresu za pomocą wywołania GRAPH_AttachData. We wszystkich funkcjach używane są komponenty utworzone wcześniej w zadaniu GUI_Task.

Pozostałe notyfikacje służą do aktualizacji skalowania i przesunięcia wykresu. Pierwsze dwie odpowiedzialne są za zmianę offsetu:

Po otrzymaniu notyfikacji TASK_EVENT_CHANGE_VIEW_MOVE_LEFT, offset jest zwiększany po wcześniejszym sprawdzeniu, czy nie zostanie przekroczona wartość maksymalna wynikająca z liczby zebranych próbek. Wartość przesunięcia zależna jest od aktualnego współczynnika skali przechowywanego w zmiennej displayScaleX. Po obliczeniu przesuwana jest także skala wyświetlana pod wykresem. Z uwagi na to, że wykonuje ona automatyczne skalowanie, przekazywana do niej wartość przesunięcia jest najpierw dzielona przez współczynnik skali. Drugi warunek, odpowiedzialny za przesunięcie wykresu w prawo, odpowiednio zmniejsza offset po sprawdzeniu czy nowa wartość nie będzie mniejsza od zera, a następnie w ten sam sposób modyfikuje przesunięcie skali.

W przypadku zbliżenia, pierwszym krokiem jest sprawdzenie czy współczynnik skali może zostać zmniejszony. Następnie modyfikowany jest offset, tak aby punkt środkowy wykresu pozostał bez zmiany. Mnożenie przez wartość scaleFactor, definiowaną na początku kodu zadania:

jest spowodowana początkową inicjalizacją skali wykresu w zadaniu GUI_Task tą samą wartością. Wynika ona z podziałki 2Hz na jeden piksel przy największym zbliżeniu wykresu. Współczynnik skali jest następnie zmniejszany, a offset dzielony przez scaleFactor. Obliczone wartości przekazywane są do funkcji modyfikujących skalę i przesunięcie: GRAPH_SCALE_SetFactor oraz GRAPH_SCALE_SetOff.

Podczas oddalania wykresu, pierwszy warunek sprawdza, czy zmiana skali nie spowoduje wykroczenia poza maksymalna liczbę zbieranych próbek. Następnie, analogicznie do zbliżania, modyfikowane są przesunięcie i współczynnik skali, jednak tym razem konieczne jest jeszcze sprawdzenie czy nie nastąpiło przekroczenie dozwolonych wartości spowodowane zmianą offsetu. Jeżeli tak się stanie, jest on dodatkowo modyfikowany. Na koniec aktualizowana jest skala pod wykresem.

 

3. ARM CMSIS DSP

ARM CMSIS DSP jest biblioteką zawierającą zbiór funkcji matematycznych przydatnych zarówno przy podstawowych operacjach matematycznych, jak i przy podstawowym przetwarzaniu sygnałów. Biblioteka została skompilowana w kilku różnych konfiguracjach:

  • libarm_cortexM7lfdp_math.a – little endian, zmiennoprzecinkowa podwójnej precyzji,
  • ibarm_cortexM7lfsp_math.a – little endian, zmiennoprzecinkowa pojedynczej precyzji,
  • libarm_cortexM7l_math.a – little endian, stałoprzecinkowa.

Pliki można znaleźć w katalogu Drivers/CMSIS/Lib/GCC pakietu STM32Cube dla Cortex-M7. Po dodaniu biblioteki do projektu należy pamiętać o dodaniu symbolu ARM_MATH_CM7 w konfiguracji.

Biblioteka zawiera szereg podstawowych funkcji matematycznych działających na całych tablicach, co znacząco ułatwia implementację bardziej złożonych algorytmów. Listę wszystkich funkcji wraz z dokumentacja można znaleźć na stronie: http://www.keil.com/pack/doc/CMSIS/DSP/html. W przykładzie funkcje biblioteczne zostały użyte do dwóch celów – skalowania wykresów oraz obliczenia FFT.

Obliczanie FFT wykonywane jest wewnątrz zadania FFT_Task, po otrzymaniu notyfikacji o gotowości danych w buforze DMA:

Jedynym parametrem, który należy podać przy jej tworzeniu jest liczba punktów, z których będzie obliczane FFT. Kolejne argumenty funkcji arm_rfft_fast_f32, to tablica zawierająca próbki sygnału i tablica przeznaczona na wynik obliczeń, również typu float32_t. Na końcu znajduje się flaga mówiąca o tym, czy ma być obliczana transformata odwrotna.

Aby otrzymać widmo amplitudowe, należy obliczyć moduł otrzymanego wyniku. Służy do tego funkcja arm_abs_f32, pobierająca tablice wejściową, wyjściową oraz ich długość. W przykładzie wyniki są zapisywane w tym samym miejscu, co dane wejściowe, dlatego pierwsze dwa argumenty są takie same.

Poza obliczeniem FFT biblioteka CMSIS DSP została także użyta do skalowania. Oś X jest modyfikowana, odpowiednio do ustawionego współczynnika skali, w funkcji:

Funkcja ta oblicza średnią arytmetyczną punktów w oknie o długości scale i wpisuje je do kolejnych elementów tablicy wejściowej. Okno jest za każdym razem przesuwane dokładnie o swoja długość. W ten sposób realizowane jest przybliżanie o oddalanie wykresu na ekranie. Uśrednione wartości są następnie skalowane w osi Y tak, aby wypełniały całą wysokość pola wykresu. Jest to realizowane przez funkcję:

Funkcja wyszukuje najpierw wartości minimalnej i maksymalnej w tablicy danych za pomocą funkcji arm_max_f32 oraz arm_min_f32. Obie funkcje maja identyczną listę argumentów: tablica wejściowa, jej długość, wskaźnik pod który zostanie wpisana szukana wartość oraz wskaźnik gdzie zostanie zapisany jej indeks w tablicy. Po znalezieniu wartości min i max, cała tablica jest skalowana, tak aby wartości mieściły się w nowym przedziale <GRAPH_OFFSET_Y; GRAPH_OFFSET_Y+GRAPH_RANGE_Y>, wynikającym z ograniczenia pola wykresu.

Te same operacje uśredniania i skalowania są wykonywane w zadaniu Signal_Task, jednak tam używane są wersje funkcji bibliotecznych działające na liczbach całkowitych.

 

4. Biblioteki rozpoznawania gestów TP – mtouch

Ostatnim opisywanym elementem projektu jest moduł do rozpoznawania gestów wykonanych na panelu dotykowym. Udostępnia on dwie funkcje:

Pierwsza z nich powinna być wywoływana cyklicznie, ponieważ za jej pośrednictwem moduł dostaje informacje o aktualnym stanie panelu dotykowego przekazywanego w strukturze:

Zawiera ona liczbę punktów (obsługiwane są zero, jeden lub dwa) oraz ich współrzędne.

Druga z powyższych funkcji oblicza gest wykryty na podstawie ostatnio przekazanych danych dotyczących dotyku. Gest opisany jest przez strukturę:

Kolejne dwa pola to współrzędne określające położenie gestu na panelu. Jest to konieczne, aby aplikacja mogła zdecydować do którego komponentu interfejsu graficznego się on odnosi.

Gest wykrywany jest na podstawie dwóch ostatnio przekazanych struktur MTOUCH_TouchData_s. Najpierw sprawdzane jest czy liczba punktów zmieniła się, czy pozostała jednakowa. W pierwszym przypadku zapamiętywane są współrzędne początkowe gestu – punkt dotyku lub średnia w przypadku dwóch punktów. Gest oznaczany jest jako TOUCH. Jeżeli natomiast liczba punktów pozostała bez zmian obliczany jest kierunek największego przesunięcia gestów jednopunktowych (MOVE_RIGHT, MOVE_LEFT, MOVE_UP, MOVE_DOWN) lub zmiana odległości między punktami dla gestów dwupunktowych (ZOOM_IN_X, ZOOM_OUT_X, ZOOM_IN_Y, ZOOM_OUT_Y). Kod modułu znajduje się w plikach mtouch.c oraz mtouch.h.

Krzysztof Chojnowski