KursyPoradnikiInspirujące DIYForum

Kurs STM32 – #4 – Sterowanie portami GPIO w praktyce

Kurs STM32 – #4 – Sterowanie portami 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ą LED. Na 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 Nucleo.

STM32 - Miganie diodą

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 StdPeriph, więc zamiast tego wykorzystamy odpowiednie procedury przez nią dostarczone. 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.

Gotowe zestawy do kursów Forbota

 Komplet elementów  Gwarancja pomocy  Wysyłka w 24h

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!

Zamów w Botland.com.pl »

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.

Etap 2 - Konfiguracja GPIO

Kolejny krok to konfiguracja linii PA5 jako wyjścia. Biblioteka StdPeriph 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 program:

Zmienna gpio przechowuje parametry konfiguracyjne portu I/O. Na początek wywołujemy funkcję GPIO_StructInit, która wypełnia wszystkie pola wartościami domyślnymi (odpowiada to konstruktorowi obiektu w C++). W kolejnych instrukcjach wybieramy pin 5 oraz ustawiamy go jako wyjście (Out_PP – wyjście typu push-pull).

Sama konfiguracja jest ustawiana wywołaniem funkcji 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 wywołania funkcji:

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

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

Efekt działania programu widoczny jest na poniższym filmie:

Konfiguracja wejść STM32 - przycisk

Mamy migającą diodę, czas odczytać stan przełącznika. Na płytce Nucleo znajdziemy microswitch 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 E, 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. Inną możliwością jest uruchomienie np.: wszystkich portów procesora na raz - może będą nam w przyszłości potrzebne?

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 pullup. Wykorzystamy tą samą zmienną konfiguracyjną, co poprzednio (gpio), zmienimy tylko numer pinu i tryb pracy:

Nie musimy ponownie wywoływać GPIO_StructInit, ponieważ wywołaliśmy tę funkcję wcześniej, podczas konfiguracji portu A. Teraz tylko ustawiamy te parametry, którymi obie konfiguracje się różnią. 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 odpowiednią funkcję. Mamy do wyboru dwie:

  • GPIO_ReadInputData() - zwraca stan całego portu (działa jak PINC w AVR)
  • GPIO_ReadInputDataBit() - sprawdza pojedynczy bit

Ponieważ interesuje nas stan tylko jednej linii, wykorzystamy drugą funkcję jako wygodniejszą:

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ą specjalny timer SysTick przeznaczony do odmierzania czasu systemowego.

Dużą zaletą tego timera jest dostępność gotowej funkcji SysTickConfig() - jako parametr podajemy jej, co ile cykli zegara chcemy otrzymywać przerwanie od timera. Aby to przerwanie obsłużyć wystarczy napisać funkcję o nazwie SysTick_Handler() - będzie ona automatycznie wywoływana co zadany czas.

Załóżmy, że chcemy co 1 ms wywoływać naszą funkcję, możemy skonfigurować timer poleceniem:

Zmienna SystemCoreClock została zadeklarowana w pliku system_stm32f10x.h, jest to jeden z plików dodanych do projektu automatycznie. Wartość tej zmiennej to częstotliwość zegara systemowego wyrażona w Hz, powinna więc wynosić 64000000.

Niestety w chwili pisania tego kursu kod generowany przez środowisko nie był poprawny i zmienna posiada wartość 72000000 (typową dla STM32F103, ale nie dla naszego projektu). Zapis 1000 oznacza liczbę wywołań na sekundę, czyli wywołanie co 1ms, tak jak chcieliśmy. Jak widać wykorzystanie tego licznika (timera) jest niezwykle łatwe i wygodne!

Pozostaje napisać procedurę obsługi przerwania:

Teraz napisanie procedury opóźniającej jest bardzo proste, wystarczy przypisać zmiennej timer_ms ile czasu chcemy czekać i w pętli oczekiwać na wyzerowanie licznika:

Oczywiście nie jest to super precyzyjna metoda, ale będzie działać z dokładnością do 1ms, a to dużo lepiej od naszej pierwszej wersji procedury opóźniającej.

Zmienna timer_ms jest zadeklarowana ze słowem kluczowym volatile. Oznacza ono, że kompilator nie powinien wykonywać optymalizacji związanych z dostępem do tej zmiennej. Jest to ważne ponieważ zarówno kod programu głównego, jak i przerwań modyfikuje jej wartość.

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 led. 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 więc 10 diod LED 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 widoczne jest poniżej. Wymieniłem 5 diod na inne kolory, aby były lepiej widoczne później w kamerze.

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ń. Ponieważ wszystkie linie należą do jednego portu oraz będą działały w tym samym trybie, możemy 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 led), 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ń, postaram się jednak przygotować 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, przerwanie zewnętrzne (EXTI) oraz kontroler przerwań (NVIC).

Pierwszy krok to włączenie zegara kolejnego układu peryferyjnego. Potrzebujemy wykorzystać piny procesora do innych celów niż domyślne. Uruchamiamy więc moduł funkcji alternatywnych dla pinów I/O (AF - alternative function):

Piny konfigurowaliśmy już poprzednio, przejdźmy więc do EXTI. Musimy zadeklarować nową zmienną i wypełnić jej pola:

Schemat jest podobny do poprzedniego, mamy obiekt z konfiguracją, wypełniamy domyślne wartości funkcją EXTI_StructInit(), następnie ustawiamy konfigurację i wywołujemy EXTI_Init().

Nasza konfiguracja to wybór numeru linii (13), tryb – przerwanie, obsługa zbocza narastającego i opadającego (czyli zarówno przyciskania, jak i zwalniania przycisku) oraz ostatnie pole – włączenie modułu.

Pozostaje jeszcze wybór portu C za pomocą wywołania:

Mamy już linię skonfigurowaną do obsługi przerwań, trzeba jeszcze uruchomić samo przerwanie:

Linia 13 należy do grupy 10-15, stąd kanał EXTI15_10_IRQn. Priorytety przerwania są zaawansowanym mechanizmem pozwalającym na ustalanie kolejności obsługi przerwań oraz postępowania w przypadku przerwania zgłoszonego podczas obsługi innego.

Ostatnie co potrzebujemy to procedura obsługi przerwania. Nazwa jej zależy od kanału i w naszym przypadku ma postać:

Sprawdzamy, czy na pewno obsługujemy linię 13 (jedna procedura obsługuje linie 10-15). Następnie odczytujemy stan linii i zapalamy lub gasimy diodę. Ostania funkcja EXTI_ClearITPendingBit zeruje flagę przerwania.

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

Od linii 134 zdefiniowany jest tzw. wektor przerwań, czyli tablica z adresami procedur obsługi przerwań:

We fragmencie powyżej widzimy znajomą funkcję SysTick_Handler. W dalszej części pliku znajdziemy EXTI15_10_IRQHandler, jak również 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 linii 70, oznaczonej jako Reset_Handler. Jest to kod wykonywany przed funkcją main! Od linii 100, zobaczymy ciekawy fragment:

Jest to wywołanie procedury SystemInit, która konfiguruje PLL (zegar i taktowanie całego układu), 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 – jaki wzór będzie wyświetlany na diodach?

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.

Nawigacja kursu

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! Jeśli nie chcesz przeoczyć kolejnego odcinka, to skorzystaj z poniższego formularza i zapisz się na powiadomienia o nowych publikacjach!

Autor kursu: Piotr (Elvis) Bugalski
Redakcja: Damian (Treker) Szymański

GPIO, kursSTM32, przerwania, stm32, wejścia, wyjścia

Trwa ładowanie komentarzy...