Kurs STM32 F1 HAL – #4 – sterowanie GPIO w praktyce

Kurs STM32 F1 HAL – #4 – sterowanie GPIO w praktyce

W poprzedniej części kursu STM32 nauczyliśmy się tworzyć projekt, kompilować oraz uruchamiać prosty program. Niestety był on mało atrakcyjny, bo nie komunikował się ze światem zewnętrznym.

Czas więc poznać okno na świat każdego układu, czyli uniwersalne porty wejścia/wyjścia (GPIO).

Na początek bardzo prosty przykład - miganie diodą. Na naszej płytce Nucleo znajduje się dioda podłączona do wyprowadzenia PA5, która pasuje idealnie do pierwszego programu.

Dioda wbudowana w zestaw Nucleo.

Dioda wbudowana w zestaw.

Jak migać diodą na STM32?

W przypadku AVR pierwszym krokiem byłoby ustawienie odpowiedniego pinu jako wyjścia, a następnie sterowanie (zapalanie, gaszenie) diody. Kod wyglądałby mniej więcej tak:

Układy STM32 można programować w podobny sposób, tzn. poprzez bezpośredni zapis do rejestrów, jednak ten kurs bazuje na bibliotece HAL, więc zamiast tego wykorzystamy odpowiednie procedury przez nią dostarczane. Na początek kod będzie dłuższy niż bezpośrednie odwołania do rejestrów, ale wykorzystując bardziej skomplikowane peryferia, używanie biblioteki okaże się dużo łatwiejsze.

Etap 1 - taktowanie portów

Pierwszym etapem wykorzystania GPIO jest uruchomienie zegara. W przypadku AVR wszystkie moduły peryferyjne są dostępne od razu. Układy STM32 są natomiast zbudowane w oparciu o inną, bardziej "ekologiczną" filozofię.

W tym przykładzie chcemy wykorzystać port A, "podłączamy" więc do niego zegar instrukcją:

Bez powyższej linijki do portu A nie będzie docierało taktowanie całego systemu. W związku z tym będzie on najzwyczajniej nieaktywny.

Zestaw elementów do kursu

Gwarancja pomocy na forum Błyskawiczna wysyłka

Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!

Kup w Botland.com.pl

Etap 2 - Konfiguracja GPIO

Kolejny krok to konfiguracja linii PA5 jako wyjścia. Biblioteka HAL wykorzystuje obiektowy model programowania (chociaż opiera się na języku C, a nie C++). Większość funkcji wymaga najpierw zadeklarowania zmiennej (obiektu) zawierającej konfigurację modułu. Następnie ustawiamy konfigurację wypełniając pola tej zmiennej.

Na koniec wywołujemy funkcję, która ustawi odpowiednią konfigurację w rejestrach mikrokontrolera. Taki model programowania może na początku wydawać się nieco dziwny, ale przestawienie się na ten sposób myślenia jest stosunkowo łatwe.

Warto też pamiętać, że ma to swoje zalety – mamy mniej funkcji do nauczenia, a jedno wywołanie konfiguruje czasem bardzo skomplikowany moduł. Dodatkowo możemy wykorzystać domyślne ustawienia wielu parametrów (zmieniamy tylko te pola, które nas interesują).

Łatwiej będzie to zrozumieć patrząc na przykładowy kod:

Zmienna gpio przechowuje parametry konfiguracyjne portu I/O. Jej pola definiują ustawienia, wybieramy pin 5, który pracuje jako wyjście (OUTPUT_PP – wyjście typu push-pull), rezystory podciągające są wyłączone, a prędkość przełączania ustawiamy na niską (FREQ_LOW).

Sama konfiguracja jest ustawiana wywołaniem funkcji HAL_GPIO_Init. Pierwszy parametr informuje o konfiguracji portu A, czyli GPIOA. W drugim parametrze przekazujemy zmienną z konfiguracją, którą chcemy ustawić.

Etap 3 - Sterowanie portami w STM32

Gdy mamy już skonfigurowany port, możemy zająć się sterowaniem diodą. Do zapalania i wygaszania diody wykorzystamy funkcję HAL_GPIO_WritePin:

Pierwsze dwa paramtery to port (GPIOA) oraz numer pinu (PIN_5). Trzeci parametr określa czy chcemy zapalić diodę ustawiając stan wysoki wyjścia (PIN_SET), czy zgasić wystawiając stan niski (PIN_RESET).


Mamy już właściwie wszystko, co potrzebne, do napisania programu. Wystarczy dodać pętlę główną oraz opóźnienia (żebyśmy widzieli co się dzieje) i możemy uruchomić program.

Wartość 570 w pętli opóźniającej została dobrana eksperymentalnie tak, żeby opóźnienie było ustawiane w milisekundach.

Aby uruchomić program naciskamy ikonkę młoteczka, która spowoduje skompilowanie kodu, a następnie klikamy zieloną strzałkę (Run). Efekt działania widoczny jest na poniższym filmie:

Konfiguracja wejść STM32 - przycisk

Mamy migającą diodę, czas odczytać stan prycisku. Na płytce Nucleo znajdziemy tact switch podłączony do wejścia PC13 (oznaczony jako USER, drugi pełni rolę reset-u).

Przycisk użytkownika.

Przycisk użytkownika.

Mikrokontroler wyposażony jest w porty oznaczone literami od A do D, każdy może obsłużyć do 16 linii wejścia wyjścia. Poprzednio używaliśmy portu A, teraz będziemy używać A oraz C. Jako pierwszy krok musimy więc włączyć zegar portu. W tym celu piszemy:

Oczywiście jeśli chcemy mieć obsługę diody LED i przycisku jednocześnie, musimy zostawić poprzednio napisaną instrukcję uruchamiającą zegar portu A. Na wszelki wypadek możemy uruchomić również pozostałe porty:

Zegar portu już działa, czas skonfigurować wejście. W przypadku AVR wszystkie linie domyślnie były skonfigurowane jako wejścia, trzeba było tylko pamiętać o włączeniu rezystora pull-up. Kod dla AVR wyglądałbym więc następująco:

Mikrokontroler STM32 posiada dużo więcej możliwości konfiguracji linii wejścia/wyjścia. Poprzednio konfigurowaliśmy wyjście w trybie push-pull, teraz chcemy ustawić pin jako wejście z rezystorem pull-up. Wykorzystamy tą samą zmienną konfiguracyjną, co poprzednio (gpio), zmienimy tylko numer pinu i tryb pracy:

Nie musimy ponownie ustawiać wszystkich pól zmiennej gpio, wystarczy, że ustawimy te, które są inne niż podczas konfiguracji portu A. Warto również zwrócić uwagę na odwołanie do portu C (GPIOC), zamiast poprzedniego A.

Odczytywanie wejść w STM32

Czas odczytać stan przycisku. W przypadku AVR wykorzystalibyśmy rejestr PINC, tutaj wywołamy funkcję HAL_GPIO_ReadPin:

Otrzymaliśmy bardzo prosty program zapalający diodę po naciśnięciu przycisku:

Działanie programu w praktyce:

Opóźnienia w STM32 - SysTick

W pierwszym programie wstawiliśmy prostą pętle opóźniającą. Takie rozwiązanie jest bardzo nieprecyzyjne – jak określić ile dokładnie czasu trwa opóźnienie?

Znacznie lepszym rozwiązaniem jest wykorzystanie zegara (timera). Mikrokontrolery z rdzeniem Cortex-M3 posiadają timer SysTick przeznaczony do odmierzania czasu systemowego.

W przypadku biblioteki HAL właściwie nic nie musimy robić - sami autorzy biblioteki wykorzystali SysTick i udostępnili gotową funkcję HAL_Delay.

Parametr, który do niej przekazujemy to opóźnienie w milisekundach. Fukcja jest więc bardzo wygodna, ale niestety ma pewną wadę. Jeśli do naszego programu sterującego diodami dodamy nową funkcję to zobaczymy, że opóźnienia są znacznie dłuższe niż zaprogramowane.

Okazuje się, że przyczyną problemów jest stara znajoma - zmienna SystemCoreClock. W poprzedniej edycji kursu mieliśmy z nią problem, ponieważ była domyślnie ustawiana na 72000000, co odpowiada taktowaniu 72MHz, ale płytka Nucleo maksymalnie pracuje z 64MHz.

Teraz problem jest jeszcze poważniejszy. SystemCoreClock jest nadal ustawiana na 720000000, ale nasz mikrokontroler pracuje z domyślnym taktowaniem 8MHz. Więc opóźnienia są 9 razy dłuższe niż byśmy chcieli.

Możemy oczywiście podawać mniejsze wartości do wywołań HAL_Delay, ale zamiast tego znacznie lepiej będzie poprawić wartość SystemCoreClock. Możemy zmienić kod biblioteki, albo przypisać do zmiennej wartość przed wywołaniem HAL_Init.

Program z poprawnymi opóźnieniami wygląda następująco:

Linijka LED-ów na STM32

Mając procedurę opóźniającą i wstępne informacje o sterowaniu liniami I/O możemy przygotować pierwszy, bardziej rozbudowany, przykład - linijkę diod świecących. Pierwszy krok, to wybór linii, do których podłączymy diody.

Port A okazuje się częściowo zajęty – pin 5 jest podłączony do diody na płytce, piny 2 i 3 do przejściówki UART, a 13-15 do programatora SWD. Natomiast port B ma zajęte piny 2-4.

Najłatwiej będzie więc wykorzystać port C, gdzie piny 0-12 są wolne. Podłączmy 10 diod do wyprowadzeń PC0 – PC9 (oczywiście przez rezystory 330R).

gpio-leds_bb

Linijka diod LED, przykład na STM32.

Połączenie diod w praktyce również nie jest trudne, przykładowe połączenie:

Linijka diod święcących podłączona do STM32.

Linijka diod święcących podłączona do STM32.

Podobnie jak poprzednio pierwszym krokiem będzie konfiguracja wyprowadzeń. Wszystkie linie należą do jednego portu i będą działały w tym samym trybie, możemy więc skonfigurować je jednocześnie:

Teraz w pętli głównej możemy sterować linijką diod, na przykład za pomocą pętli for:

Oczywiście można przygotować zupełnie inny wzór, zachęcam do eksperymentów. Cały kod przykładu dostępny jest poniżej:

Działanie powyższego programu w praktyce widoczne jest na poniższym filmie:

Przerwania od przycisków w STM32

Często program musi wykonywać swoje zadanie (np. sterować robotem, albo zapalaniem diod), a jednocześnie sprawdzać stan przycisku (lub czujnika). Moglibyśmy oczywiście w pętli głównej co chwila odczytywać stan przycisku. Jednak takie działanie po pierwsze komplikuje program, po drugie czas reakcji na naciśnięcie może być długi.

Sposobem na szybką reakcję w takim przypadku jest wykorzystanie przerwań. Mikrokontrolery STM32 mają bardzo rozbudowany układ przerwań, na początek sprawdzimy jednak jak najprostszy przykład jego wykorzystania.

Załóżmy, że chcemy napisać program, który zapala diodę po naciśnięciu przycisku, a gasi po zwolnieniu, czyli dokładnie taki jak wcześniej, jednak nie chcemy całego czasu procesora poświęcać na sprawdzanie stanu przełącznika. Sterowanie umieścimy w przerwaniu, a nasz program będzie mógł robić coś innego.

Wykorzystamy jak poprzednio przełącznik na płytce Nucleo (pin PC13). Tym razem musimy skonfigurować wejście oraz przerwanie zewnętrzne (EXTI). Na początek konfigurujemy pin PC13 jako wejście sterowane przerwaniami:

Wybraliśmy GPIO_MODE_IT_RISING_FALLING dzięki czemu przerwanie będzie generowane zarówno podczas naciskania, jak i zwalniania przycisku. Dostępne są jeszcze opcje GPIO_MODE_IT_RISING oraz GPIO_MODE_IT_FALLING, które uruchamiają przerwanie tylko dla jednego zbocza sygnału.

Teraz musimy uruchomić przerwanie zewnętrzne, czyli EXTI. Pinu o numerach 0-4 mają dedykowane przerwania EXTI0_IRQn - EXTI4_IRQn, pozostałe obsługiwane są w grupach. Piny od 5 do 9 dzielą przerwanie EXTI9_5_IRQn, a od 10 do 15 - EXTI15_10_IRQn.

Chcemy obsługiwać pin o numerze 13, więc interesuje nas ostatnia grupa:

HAL do obsługi przerwań zewnętrznych wykorzystuje funkcję HAL_GPIO_EXTI_Callback. Jest ona wywoływana dla każdego wejścia, a numer pinu jest jej parametrem. Ponieważ chcemy, żeby nasz kod był wykonywany w reakcji na przerwanie, piszemy więc własną wersję tej funkcji:

W tej chwili obsługujemy tylko jeden pin wejściowy, wiec nie musimy sprawdzać, czy to na pewno pin 13. W przypadku bardziej rozbudowanego programu musielibysmy sprawdzić, który pin jest przyczyną wystąpienia przerwania.

Okazuje się, że taki program jeszcze nie zadziała. Zanim będziemy mogli cieszyć się obsługą przerwań musimy dodać jedną funkcję do pliku stm32f1xx_it.c. Gdy tworzyliśmy nasz projekt, plik ten został automatycznie utworzony. Znajdziemy w nim jedną funkcję, która przekierowuje przerwanie SysTick do biblioteki HAL:

Plik ten jest bardzo ważny - łączy on niskopoziomową obsługę przerwań z biblioteką HAL. Funkcja SysTick_Handler jest wywoływana w reakcji na przerwanie od timera SysTick. Dopiero ona wywołuje HAL_IncTick(), dzięki której funkcja HAL_Delay() działa poprawnie.

Musimy dopisać podobną funkcję, która połączy przerwanie sprzętowe EXTI15_10_IRQn z HAL. Kod takiej funkcji wygląda następująco:

Gdybyśmy obsługiwali więcej pinów z tej grupy, powinniśmy dodać kolejne wywołania funkcji HAL_GPIO_EXTI_IRQHandler. Resztą zajmie się biblioteka HAL. Cały kod programu:

Więcej informacji o przerwaniach

Widzieliśmy już dwie procedury obsługi przerwania - dla zegara SysTick oraz dla obsługi przycisku. Jeśli ktoś byłby ciekaw skąd brały się nazwy tych procedur, albo jakie jeszcze procedury mogą być użyte, najlepiej jest przeanalizować plik startup_stm32f103xb.s. Plik ten został automatycznie dodany do naszego projektu i zawiera kod w asemblerze. Na szczęście nawet nie znając tego języka łatwo domyślić się o co w nim chodzi.

Znajdziemy w nim tzw. wektor przerwań, czyli tablicę z adresami procedur obsługi przerwań:

We fragmencie powyżej widzimy znajomą funkcję SysTick_Handler. W dalszej części pliku znajdziemy EXTI15_10_IRQHandler oraz wszystkie pozostałe procedury obsługi przerwań.

Co dzieje się po starcie STM32?

W tym pliku znajdziemy też kod wykonywany po resecie procesora (czyli również po uruchomieniu). Zaczyna się on od oznaczonej jako Reset_Handler. Jest to kod wykonywany przed funkcją main! W okolicy linii numer 100 zobaczymy również poniższy fragment:

Jest to wywołanie procedury SystemInit, która konfiguruje zegar i taktowanie całego układu (domyślnie na 8MHz), następnie inicjalizacja biblioteki C oraz skok do naszej funkcji main.

Zadanie domowe 4.1

Przygotuj program, który zmienia kolejność zapalania diod w linijce LED po naciśnięciu przycisku.

Zadanie domowe 4.2

Napisz program, który zapala kolejną diodę z linijki LED po naciśnięciu przycisku.

Zadanie domowe 4.3

Przygotuj program, który zapala kolejne diody, a po naciśnięciu przycisku zapala diodę na płytce Nucleo – natychmiast i nie przerywając sekwencji sterowania diodami.

Zadanie domowe 4.4 (dodatkowe)

Wykonaj licznik Johnsona z użyciem dioda świecących.

Podsumowanie

W tej części kursu STM32 opisane zostały najważniejsze informacje dotyczące portów GPIO. Oczywiście mechanizmy te oferują znacznie więcej możliwości, będziemy jeszcze później wracać do bardziej zaawansowanych opcji, gdy zajdzie taka potrzeba.

W kolejnej części przyjdzie pora na nawiązanie połączenia płytki Nucleo z komputerem za pomocą interfejsu UART, który będzie nam towarzysz do samego końca kursu!

Nawigacja kursu

Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk
Redakcja: Damian Szymański

f1, HAL, kurs, kursSTM32F1HAL, led, przerwania, przycisk, stm32

Komentarze

Trwa przerwa techniczna - komentarze do tego wpisu są dostępne na forum: