Kurs STM32 F4 – #4 – Pierwszy projekt, GPIO, przerwania

Kurs STM32 F4 – #4 – Pierwszy projekt, GPIO, przerwania

Stworzenie programu dla mikrokontrolera STM32, wymaga tylko kilkunastu kliknięć! W tej części kursu skonfigurujemy projekt wykorzystując do tego generator kodu CubeMX.

Poznamy podstawowe funkcje do obsługi portów GPIO. Sprawdzimy też jak w praktyce wygląda obsługa przerwań zewnętrznych.

Generowanie kodu konfiguracyjnego - STM32CubeMX

Rozpoczniemy od głównego narzędzia umożliwiającego łatwy start z mikrokontrolerami STM32, czyli generatora kodu CubeMX. Dzięki niemu jesteśmy w stanie skonfigurować wszystkie potrzebne nam do pracy peryferia w bardzo przyjazny i wygodny sposób.

Jak każdy wie, najlepiej uczyć się poprzez praktykę, zatem do dzieła!

Zestaw elementów do kursu

 999+ pozytywnych opinii  Gwarancja pomocy  Wysyłka w 24h

Zestaw elementów do przeprowadzenia wszystkich ćwiczeń z kursu STM32 F4 można nabyć u naszego dystrybutora! Zestaw zawiera m.in. płytkę Discovery, wyświetlacz OLED, joystick oraz enkoder.

Zamów w Botland.com.pl »

Krok 1. Uruchamiamy narzędzie STM32CubeMX.

Jeżeli Cube jest zainstalowany w postaci pluginu do Workbencha, występuje on tam jako dodatkowa perspektywa. Uruchomić można go między innymi wybierając z głównej belki menu Window > Open Perspective > Other > STM32CubeMX. Tym samym w prawym górnym rogu IDE powinna pojawić się dodatkowa ikona z perspektywą Cube'a.

Okno startowe dodatku STMCubeMX do SW4STM32.

Okno startowe dodatku STMCubeMX do SW4STM32.

Krok 2. Aktualizacje.

Pracę z każdym narzędziem warto rozpocząć od sprawdzenia dostępnych aktualizacji. W CubeMX można to zrobić wybierając z głównej belki menu Help > Check for Updates.

Krok 3. Pierwszy projekt.

Tworzymy nowy projekt wybierając z ekranu powitalnego New Project. Naszym oczom ukazuje się panel wyboru mikrokontrolera, na którym będziemy realizować projekt.

Panel wyboru mikrokontrolera w STM32CubeMX.

Panel wyboru mikrokontrolera w STM32CubeMX.

Kolorem zielonym zaznaczono wybór pomiędzy zakładką MCU / Board. Zakładka MCU Selector oznacza wybór samego mikrokontrolera. Z tej opcji korzystać będziemy tworząc projekt na swoją własną płytkę z mikrokontrolerem STM32, którą zaprojektowaliśmy np. do robota. Zakładka Board Selector pozwala na wybór gotowych płytek rozwojowych, takich jak Nucleo, czy Discovery.

Kolorem czerwonym oznaczono sekcję filtrów. Jeżeli wiemy z jakiej rodziny oraz w jakiej obudowie mikrokontrolera szukamy, dzięki tym opcjom jesteśmy w stanie bardzo szybko zawęzić obszar poszukiwań, który na ten moment wynosi aż 836 mikrokontrolerów!

Na czarno zaznaczono obszar dokładniejszych filtrów, konkretnie odnoszących się do peryferiów mikrokontrolera. Jeżeli jesteśmy na etapie poszukiwania układu do projektu i wiemy jakich peryferiów będziemy potrzebować, to dzięki tej sekcji jesteśmy w stanie znaleźć odpowiedni układ.

W obszarze zaznaczonym na niebiesko znajdują się wszystkie mikrokontrolery, które spełniają wybrane wcześniej wymagania.


Przykład wykorzystania wyszukiwarki mikrokontrolerów

  • Załóżmy, że potrzebuję małego mikrokontrolera, najlepiej w obudowie LQFP48.
  • Wiem że będę się musiał komunikować z urządzeniami zewnętrznymi za pomocą 2 interfejsów UART i jednego USB.
  • Chciałbym mieć możliwość dokładnego pomiaru ADC, więc potrzebuję  2 przetworników ADC o rozdzielczości 16 bit.
  • Na płytce mam również układy, z którymi komunikuję się za pomocą I2C oraz SPI.
  • Jednocześnie zamierzam odczytywać parametry sygnałów z aparatury radiowej, oraz sterować bardzo dużą ilością serw, więc potrzebuję jak największej ilości timerów.

Wprowadzając te wszystkie informacje do Cube, ograniczyłem liczbę mikrokontrolerów z 836 do 3, które spełniają moje wymagania.

Wyszukiwanie odpowiedniego mikrokontrolera znając wymagania dotyczące peryferii.

Wyszukiwanie odpowiedniego mikrokontrolera znając wymagania dotyczące peryferiów.


Wróćmy jednak do naszej płytki.

Krok 4. Wybór mikrokontrolera.

Wybieramy odpowiedni mikrokontroler do naszego projektu. Wiedząc, że układ znajdujący się na płytce STM32F411E-Discovery, to STM32F411VET6, wybierając odpowiednie filtry z łatwością odszukamy interesujący nas model.

ybór mikrokontrolera do płytki STM32F411E-Discovery

ybór mikrokontrolera do płytki STM32F411E-Discovery

Po zaznaczeniu mikrokontrolera przechodzimy dalej wybierając OK.

Krok 5. Wybór aktywnych peryferiów.

Naszym oczom powinien ukazać się ekran główny z podglądem wybranego mikrokontrolera.

Główny ekran programu STM32CubeMX

Główny ekran programu STM32CubeMX.

W sekcji zaznaczonej na czerwono znajduje się kilka przydatnych opcji (poza oczywistymi, typu zapisz itp.), z których nauczymy się korzystać przy okazji innego artykułu. Na ten moment wystarczy nam zaznaczona na niebiesko wyszukiwarka, dzięki której można bardzo szybko odnaleźć interesujące nas piny mikrokontrolera. Dla obudowy która ma 100 lub 144 wyprowadzenia potrafi to być nie lada wyzwaniem!

Obszar zaznaczony na zielono pozwala na wybór zakładki odpowiedzialnej za konkretne typy konfiguracji mikrokontrolera. W dalszych częściach kursu na pewno zajrzymy do każdej z nich.

Sekcja zaznaczona na brązowo zawiera wszystkie dostępne w mikrokontrolerze peryferia. Z tego poziomu możemy je aktywować oraz wybrać ich główny tryb działania.

W centralnej części ekranu umiejscowiony jest graficzny podgląd wyprowadzeń mikrokontrolera wraz z ich aktywnymi funkcjami. Jest to niezwykle przydatne przy ustalaniu optymalnych wyprowadzeń wszystkich peryferiów podczas projektowania płytki.

Widać, że na ten moment żadne z wyprowadzeń mikrokontrolera nie zostało skonfigurowane. Te na, które użytkownik nie ma wpływu, już na wstępie oznaczone są odpowiednimi kolorami.

Krok 6. Konfiguracja portu GPIO.

Spróbujemy teraz napisać standardowe mikrokontrolerowe Hello World, a więc zaświecić diodą! W widoku mikrokontrolera należy znaleźć i kliknąć na pin PD15 (można posłużyć się wspomnianą wcześniej wyszukiwarką, wpisując w nią frazę PD15). Rozwinie to listę funkcji, które mogą być przypisane do tego pinu.

Możliwości konfiguracji pinu PD15

Możliwości pinu PD15.

Widzimy, że pin PD15 może być między innymi:

  • wejściem przetwornika ADC,
  • wyjściem timera,
  • standardowym wejściem oraz wyjściem GPIO,
  • można na nim również skonfigurować przerwanie zewnętrzne.

Chcemy sterować diodą, czyli wystawiać na pinie stan niski lub wysoki, należy więc skonfigurować go jako GPIO_Output. Po wybraniu trybu, pin podświetli się na zielono, a obok pojawi się jego aktualna funkcja.

Pin PD15 skonfigurowany w trybie GPIO_Output

Pin PD15 skonfigurowany w trybie GPIO_Output.

Krok 7. Generowanie kodu konfiguracyjnego.

Część konfiguracyjną mamy już za sobą. Kwestie związane z zegarami zostały skonfigurowane automatycznie (oczywiście możemy mieć na to wpływ, ale zajmiemy się tym później).

Aby wygenerować kod na podstawie stworzonej konfiguracji mikrokontrolera, należy wybrać z menu głównego Project > Settings, a następnie wypełnić niezbędne pola.

Interesują nas tylko trzy pozycje:

  1. nazwa projektu,
  2. jego lokalizacja,
  3. IDE, pod które wygenerowany zostanie cały kod.
Przykład wypełnienia opcji dla generowanego kodu.

Przykład opcji dla generowanego kodu.

Po zmianieToolchain/IDE na SW4STM32 pojawi się zaznaczone pole "Generate Under Root", które trzeba odznaczyć. Na lokalizację projektu warto wybrać łatwo dostępne miejsce, ponieważ kilka razy będzie się tam trzeba przeklikać. Należy unikać nazw projektu zawierających spacje i polskie znaki. Może to być przyczyną późniejszych problemów.

Całą resztę zostawiamy domyślnie i klikamy Ok. W tym momencie Cube może zakomunikować, że nie pobrano pakietów dotyczących tego układu. Zgadzamy się na ich ściągnięcie, czekamy na zakończenie procesu.


Po poprawnym pobraniu pakietów dotyczących serii F4 jesteśmy gotowi na wygenerowanie kodu. Aby to uczynić, należy wybrać Project > Generate Code lub ikonę zębatki znajdującą się pod belką głównego menu. Jeżeli wszystko pójdzie bez problemów, naszym oczom powinien ukazać się następujący komunikat:

Komunikat o poprawnym wygenerowaniu kodu.

Komunikat o poprawnym wygenerowaniu kodu.

Importowanie projektu i pisanie programu - SW4STM32

Teraz zajmiemy się importem programu do IDE, zapoznamy się ze strukturą projektu i napiszemy swój pierwszy program na STM32.

Krok 1. Uruchamiamy SW4STM32.

Krok 2. Przygotowujemy Workbench do pracy.

Upewniamy się, że jesteśmy w perspektywie C/C++ oraz, że panel Project Explorer jest widoczny. Jeżeli tak nie jest, można go włączyć wybierając Window > Show View > Project Explorer.

KursF4_4_10

Uruchamianie panelu Project Explorer.

Krok 3. Importowanie projektu.

Klikamy prawym przyciskiem myszy w obszarze Project Explorer i wybieramy Import.

Importowanie nowego projektu

Importowanie nowego projektu.

Następnie wybieramy pozycję Existing Projects into Workspace z zakładki General.

KursF4_4_12

Wybór opcji importu projektu.

Zatwierdzamy wybór przyciskiem Next. W następnym panelu wybieramy lokalizację wcześniej stworzonego przez Cube projektu (w tym wypadku wybieramy folder 01_GPIO).

Wskazanie odpowiedniego folderu.

Wskazanie odpowiedniego folderu.

Po wybraniu i zatwierdzeniu odpowiedniego folderu w sekcji Projects pokaże się znaleziony do zaimportowania projekt. Należy się upewnić, że jest on zaznaczony oraz, że pozycja Copy projects into workspace jest odznaczona.

KursF4_4_14

Ostatni krok przed importem projektów.

Klikamy Finish, zakańczając tym samym proces importowania projektu.

Struktura wygenerowanego projektu

Wygenerowany projekt składa się z bardzo wielu plików. Nie jest dla nas istotnym rozróżniać, który z nich do czego służy. Co powinniśmy wiedzieć na ten moment?

Folder Application > User zawiera najważniejsze dla nas pliki, a konkretnie zaznaczone na zrzucie poniżej main.c oraz stm32f4xx_it.c. Plik main.c stanowi oczywiście główny plik programu, zawierający konfigurację peryferiów oraz pętlę główną.

W pliku stm32f4xx_it.c można znaleźć informacje na temat aktualnie obsługiwanych przerwań. Trzecią zaznaczoną sekcją są biblioteki HAL. Tu będziemy szukać odpowiedzi na pytania związane obsługą oraz działaniem tych bibliotek.

KursF4_4_15

Najważniejsze pliki projektu.

Zawartość pliku main.c

Otwórzmy zatem plik main.c i sprawdźmy co się w nim znajduje.

Początek pliku main.c

Początek pliku main.c

W sekcji zaznaczonej na czerwono znajdują się informacje dotyczące licencji i praw do pliku. Linijka zaznaczona na niebiesko dołącza do projektu biblioteki HAL. W pomarańczowym prostokącie znajdują się prototypy dwóch funkcji.

  1. SystemClock_Config - jak sama nazwa wskazuje, odpowiada za konfigurację zegara, którym taktowany jest mikrokontroler.
  2. MX_GPIO_Init - ta funkcja została wygenerowana, ponieważ konfigurując mikrokontroler w Cube postanowiliśmy korzystać z portu GPIO.

Gdzie można pisać kod?

Pozostają jeszcze obszary zaznaczone na zielono. Każdy z nich rozpoczyna się frazą USER CODE BEGIN, a kończy USER CODE END. Co to oznacza?

Wyobraźmy sobie następującą sytuację.

  • Skonfigurowaliśmy projekt w Cube, wygenerowaliśmy kod i wciągnęliśmy go do Workbencha.
  • Napisaliśmy trochę kodu i zaczęliśmy testować jego działanie.
  • Po pewnym czasie stwierdziliśmy jednak, że powinniśmy nieco zmodyfikować konfigurację włączonych peryferiów, dodać kilka nowych i usunąć te, z których nie korzystamy.
  • Nie chcemy jednak przepisywać całego napisanego już kodu do nowego projektu, który będziemy musieli wygenerować ze względu na zmiany w konfiguracji mikrokontrolera.

STM32CubeMx generuje kod w taki sposób, aby wszystkie zmiany można było przeprowadzić bezboleśnie. Krótko mówiąc: generując projekt wielokrotnie, nie jest on generowany zupełnie od nowa, lecz jest scalany z tym już istniejącym.

Znaczniki USER CODE BEGIN oraz USER CODE END wyznaczają dla tego narzędzia obszar, który nie zostanie zmodyfikowany i zachowa swoją zawartość po scaleniu projektów. Wszystko oczywiście dzieje się automatycznie, więc jedyne czym musi się przejmować programista, to pisanie kodu w wyznaczonych do tego sekcjach.

Działanie tego mechanizmu przetestujemy jeszcze w tym artykule. Zanim jednak do tego przejdziemy, przyjrzyjmy się dalszej części pliku main.c, a konkretnie samej funkcji main.

Funkcja main.

Funkcja main.

Na czerwono zaznaczono funkcję, która musi być wywołana w programie jako pierwsza (tzn. przed jakąkolwiek inną funkcją z biblioteki HAL). HAL_Init jak można się domyślić, inicjalizuje bibliotekę HAL.

W sekcji pomarańczowej wywołana jest kolejna podstawowa funkcja, odpowiadająca za konfigurację zegara taktującego mikrokontroler - SystemClock_Config.

W obszarze zaznaczonym na niebiesko pojawiać się będą funkcje inicjalizujące działanie wszystkich peryferiów wybranych wcześniej przez programistę. Na razie korzystamy tylko z portu GPIO, więc tylko to peryferium jest w tym miejscu inicjalizowane.

Na samym końcu część dla nas najbardziej interesująca, czyli zaznaczona na fioletowo pętla główna. Tu będziemy (przynajmniej na razie) umieszczać większość naszego kodu.

Obsługa bibliotek HAL - Sterowanie portem GPIO

Prawie wszystkie użyteczne dla nas funkcje będą zaczynać się od prefiksu HAL_. Znajdźmy więc odpowiednie miejsce i zacznijmy pisać! Pamiętając o wcześniejszych obwarowaniach, a więc:

  • kod umieszczamy tylko w wyznaczonych do tego miejscach,
  • korzystanie z bibliotek HAL rozpoczynamy dopiero po inicjalizacji peryferiów.

Szukamy właściwej linii, w której powinniśmy umieścić nasze instrukcje. Ponieważ chcemy, aby zapalenie diody odbyło się tylko raz, na pewno będzie to przed rozpoczęciem pętli głównej. Aby wszystko poprawnie działało, musi to być również za wszystkimi funkcjami inicjalizującymi. Tym samym pozostaje nam niżej zaznaczony obszar, znajdujący się w liniach 78-83.

Funkcja main.

Funkcja main.

Włączanie wyświetlania linii kodu w edytorze

Włączanie wyświetlania linii kodu w edytorze.

Stosując się do ostatniego kryterium, pozostaje nam jedno miejsce, w którym powinniśmy umieścić naszą instrukcję. Jest to linia znajdująca się pomiędzy znacznikami USER CODE BEGIN/END 2, czyli w tym wypadku linia 79.

Jaką funkcje użyć do...?

W tym miejscu zamiast podać wam gotową funkcję, spróbuję przedstawić proces, dzięki któremu można znaleźć instrukcje oraz informacje których szukamy.

Zaczynamy!

Wykorzystując potęgę narzędzi uzupełniania tekstu oraz wiedzę, że instrukcja prawie na pewno rozpoczyna się od prefiksu HAL_, spróbujmy znaleźć naszą funkcję. Zaczynamy od wpisania do edytora frazy HAL_. Następnie wciskamy Ctrl + spacja. Naszym oczom ukazuje się bardzo długa lista wyrażeń, których możemy użyć w tym miejscu programu.

KursF4_4_19

Podpowiedzi wyświetlone przez edytor.

Nie przybliża nas to zbyt mocno do rozwiązania problemu, ale na szczęście zostało nam jeszcze kilka wartościowych informacji. Po pierwsze - będziemy korzystać z GPIO, co też wpisujemy od razu za wcześniej wpisanym prefiksem. Warto zwrócić uwagę na to, jak auto uzupełnianie reaguje na każdą nowo wpisana literę.

KursF4_4_20

Podpowiedzi wyświetlone przez edytor.

W tym miejscu robi się już dużo prościej. Zostało nam mniej niż 10 pozycji, z których możemy wybrać. Wiedząc co chcemy zrobić, a więc ustawić konkretny stan pinu, wybieramy odpowiednią funkcję, czyli:

Wystarczy przejść strzałkami na wybraną funkcję i nacisnąć enter. Wszystko automatycznie się uzupełni, dodane zostaną nawiasy, w których umieszczony zostanie kursor oraz pojawi się podpowiedź odnośnie wymaganych parametrów.

KursF4_4_21

Podpowiedź dotycząca argumentów funkcji.

Widzimy, że funkcja przyjmuje 3 parametry. Aby dowiedzieć się co oznaczają oraz w jakim formacie należy je wprowadzić, wystarczy za pomocą myszki najechać kursorem na funkcję, aby wyświetlić jasno opisującą wszystko dokumentację (Sekcje @param zawierają istotne dla nas informacje).

KursF4_4_22

Podpowiedź dotycząca wykorzystania funkcji.

Zgodnie ze wskazówkami z dokumentacji, wiedząc, że chcemy wystawić stan na pinie PD15, wypełniamy parametry funkcji. Przy wpisywaniu każdego z nich, już po pierwszej wpisanej literce polecam wcisnąć magiczny skrót Ctrl + spacja i obserwować jakie podpowiedzi otrzymujemy.

Finalnie powinniśmy dojść do postaci funkcji przedstawionej poniżej.

Pierwsze dwa parametry wydają się być oczywiste. Skąd jednak wiemy jaki stan powinniśmy wystawić, aby dioda zaświeciła? Taką informację można znaleźć w schematach umieszczonych w dokumentacji płytki STM32F411E-Discovery.

Schemat podłączenia diod LED na płytce STM32F411E-Discovery

Schemat podłączenia diod LED na płytce STM32F411E-Discovery.

Kompilacja programu

Aby skompilować program, należy zapisać dokonane zmiany (ctrl + s), a następnie zbudować projekt (ctrl + b). Jeżeli nie popełniliśmy żadnych błędów, kompilacja powinna przebiec bez większych problemów. Szczegóły wykonanych operacji można podejrzeć w zakładce Console w dolnej sekcji ekranu.

Wgranie programu na płytkę

Rozpoczniemy od sposobu pewnego, bezpiecznego i generującego najmniej problemów. Aby wgrać program na mikrokontroler, wykorzystamy zainstalowany wcześniej program ST-Link Utility.

Krok 1. Uruchamiamy ST-Link Utility.

Krok 2. Podłączamy płytkę do portu USB za pomocą złącza mini USB.

Krok 3. Nawiązujemy połączenie wybierając w programie Target > Connect.

Krok 4. Program, który ma być wgrany, wybieramy za pomocą Target > Program. Następnie przechodzimy do lokalizacji naszego projektu i odszukujemy plik z rozszerzeniem bin.

Przygotowanie do wgrania programu.

Przygotowanie do wgrania programu.

Krok 5. Wgrywamy program wybierając Start. Jeżeli wszystko przebiegło pomyślnie, na płytce Discovery powinna się zaświecić niebieska dioda.

Pierwszy programdziała - włączenie diody!

Pierwszy programdziała - włączenie diody!

Miganie diodą - funkcja opóźniająca

Spróbujmy teraz doprowadzić do migania z określoną częstotliwością. Będzie nam potrzebna umiejętność gaszenia i zapalenia diody - to już mamy. Będzie też trzeba w jakiś sposób uzależnić nasz system od czasu. Timery, to zagadnienie przedstawione w późniejszym artykule, dlatego nie skorzystamy z nich dzisiaj.

Do tego celu wykorzystamy funkcję opóźniającą, przyjmującą jako parametr czas opóźnienia w milisekundach.

Wiedząc jak zapalić i jak zgasić diodę, możemy dodać kolejne dwie linijki do naszego programu, uzyskując tym samym jego następującą formę:

Po zbudowaniu i wgraniu tego programu, niebieska dioda powinna się zaświecić, a następnie po sekundzie zgasnąć.

Cykliczna zmiana stanu wyjścia

Spróbujmy teraz uzyskać miganie diody z określoną częstotliwością. Zamiast wpisywać konkretny stan pinu, możemy do tego użyć funkcji zmieniającej aktualny stan na przeciwny.

Przykładowy program realizujący to zadanie przedstawiono poniżej.

Warto zauważyć, że kod został przeniesiony do pętli nieskończonej, ponieważ ma się wykonywać cyklicznie. Efektem działania programu powinna być zmiana stanu diody (co pół sekundy).

Efekt działania programu - animacja.

Efekt działania programu - animacja.

Odczytywanie stanu na pinie

Spróbujmy teraz obsłużyć niebieski przycisk znajdujący się na płytce Discovery. Ze schematu dowiemy się w jaki sposób jest on podłączony do mikrokontrolera.

Schemat podłączenia przycisku użytkownika na płytce STM32F411E-Discovery

Schemat podłączenia przycisku użytkownika na płytce STM32F411E-Discovery.

Widać, że stan przycisku odczytywać będziemy na pinie PA0.

Krok 1. Modyfikacja konfiguracji w Cube.

Aby odczytać stan na pinie PA0, należy go skonfigurować w odpowiednim trybie. W naszym wypadku, ponieważ potrzebujemy rozróżnić tylko dwa stany (niski i wysoki) będzie to GPIO_Input.

Klikamy na pin PA0 i wybieramy odpowiedni tryb.

KursF4_4_37

Konfiguracja pinu wejściowego.

Po wybraniu GPIO_Input pin powinien podświetlić się na zielono.

Pin PA0 skonfigurowany w trybie GPIO_Input

Pin PA0 skonfigurowany w trybie GPIO_Input.

W tym momencie wykorzystamy także możliwość dodania własnej etykiety do pinu mikrokontrolera. Aby to zrobić, należy kliknąć prawym przyciskiem na pin, a następnie wybrać Enter user Label.

Dodawanie etykiety do pinu

Dodawanie etykiety do pinu.

Dodajmy etykietę do obydwu pinów, które będziemy wykorzystywać w tym programie.

KursF4_4_28

Przypisanie etykiet do używanych pinów.

Taka konfiguracja w zupełności nam wystarczy, aby odczytywać stan przycisku.

Krok 2. Generujemy kod. Ponieważ informacje na temat nazwy i lokalizacji projektu się nie zmieniają, aby wygenerować nowy projekt i scalić go ze starym wystarczy, że wybierzemy opcję:

Project > Generate Code

Krok 3. Aktualizujemy pliki projektu. Eclipse zazwyczaj automatycznie wykrywa zmiany w plikach. Czasami jednak pojawiają się błędy lub projekt nie chce się skompilować. Aby mieć pewność, że na pewno operujemy na nowych plikach, warto dokonać procedury naprawczej. Składają się na nią następujące czynności:

  • Project > Clean
  • Project > C/C++ Index > Rebuild
  • Project > C/C++ Index > Freshen All Files

Po tych działaniach powinniśmy być w stanie zbudować projekt w oparciu o zmodyfikowane pliki.

Krok 4. Odczytanie stanu pinu wejściowego. Funkcję, która będzie temu służyła, możemy znaleźć tak jak to zrobiliśmy w przypadku funkcji ustawiającej konkretny stan pinu. Będzie to oczywiście:

Funkcja przyjmuje dwa argumenty. Zamiast jednak wpisywać nazwę konkretnego portu, tak jak w poprzednim przykładzie, spróbujmy pierwszy argument rozpocząć od zdefiniowanej wcześniej w Cube etykiety Button.  Następnie sprawdźmy, co podpowie nam magiczny skrót Ctrl + spacja.

KursF4_4_29

Korzystanie z podpowiedzi edytora.

Okazuje się, że dodana na poziomie konfiguracji pinów etykieta została przeniesiona do kodu właściwego i można z niej skorzystać. Jest to bardzo wygodny mechanizm, pozwalający na łatwiejsze zarządzanie wyprowadzeniami mikrokontrolera.


Z dokumentacji funkcji ReadPin (trzeba najechać myszką na funkcję) wiemy, że zwraca ona stan pinu w postaci GPIO_PIN_SET lub GPIO_PIN_RESET. Z tą informacją jesteśmy w stanie napisać prosty program, który odzwierciedla stan przycisku na diodzie. Jedną z możliwości napisania tego programu przedstawiłem poniżej.

Zauważmy, że do obsługi diody LED również wykorzystałem zdefiniowaną wcześniej etykietę. Oczywiście powyższy program można zamknąć też w jednej linijce:

W praktyce program działa następująco:

Świetnie! Potrafimy już obsługiwać podstawowe funkcje portów wejścia - wyjścia. Robimy to jednak nieoptymalnie. W napisanych dotąd programach dokonujemy tzw. pollingu.

Następnie po każdym takim odczycie na podstawie zebranych danych podejmujemy konkretne decyzje. Po co jednak sprawdzać stan przycisku miliony razy na sekundę, jeśli jest klikany co najwyżej dwa, trzy razy na sekundę?

Obsługa przerwań zewnętrznych dzięki Cube i HAL

O wiele naturalniejszą formą obsługi takiej sytuacji byłoby wysłanie do systemu informacji o stanie przycisku tylko w przypadku jego zmiany. Wtedy funkcja podejmująca decyzje byłaby wywoływana tylko wtedy, gdy do systemu zostanie dostarczona nowa informacja.

Krok 1. Musimy skonfigurować pin tak, aby przy zmianie stanu generował przerwanie, które programista może obsłużyć. Należy zacząć od wybrania odpowiedniej funkcji pinu:

Konfiguracja pinu w trybie przerwania zewnętrznego

Konfiguracja pinu w trybie przerwania zewnętrznego.

Rozwinięcie skrótu GPIO_EXTI0, to General Purpose Input Output External Interrupt 0 - czyli przerwanie zewnętrzne od portu wejścia - wyjścia na pinie 0.

Krok 2. Musimy teraz skonfigurować szczegóły działania tego przerwania. Otwieramy zakładkę Configuration. W obszarze zaznaczonym na pomarańczowo, w formie modułów pojawiać się będą wszystkie włączane przez nas peryferia.

W sekcji System znajdują się moduły dotyczące obsługi bazowych części mikrokontrolera, czyli pamięci, portów, przerwań i głównego zegara.

Panel konfiguracji poszczególnych peryferii

Panel konfiguracji poszczególnych peryferiów.

Otwieramy moduł GPIO. Są tam wymienione wszystkie wybrane przez nas piny. Wybieramy ten od przycisku (PA0).

Chcemy otrzymywać informację zarówno przy przyciśnięciu, jak i zwolnieniu przycisku, więc w sekcji GPIO mode wybieramy External Interrupt Mode with Rising/Falling edge trigger detection.

KursF4_4_32

Panel konfiguracji portów wejścia - wyjścia.

Zatwierdzamy zmiany przyciskiem Ok. Następnie otwieramy moduł kontrolera przerwań - NVIC (Nested Vector Interrupt Controller). W poniższym panelu pokazane są wszystkie przerwania, które zostały skonfigurowane (nie tylko przez użytkownika) i mogą zostać uaktywnione.

Odnajdujemy przerwanie odpowiedzialne za nasz przycisk, czyli EXTI line0 interrupt. Uaktywniamy je zaznaczając pozycję Enabled.         

KursF4_4_33

Panel konfiguracji kontrolera przerwań.

Zatwierdzamy wprowadzone zmiany przyciskiem Ok. Na tym etapie nie musimy się przejmować niczym więcej. Generujemy kod i aktualizujemy go w Workbenchu.

Obsługa przerwania w kodzie

Przerwania programowo obsługuje się za pomocą tzw. callbacków. Są to funkcje wywoływane po obsłużeniu przez system podstawowych elementów związanych z przerwaniem (wyczyszczenie flagi przerwania itp). Na ten moment te informacje nam wystarczą.

Funkcje callback przeznaczone są do własnej implementacji przez programistę, tak aby można je było zdefiniować w wygodnym dla siebie miejscu. Próbowałem znaleźć sposób na dotarcie do odpowiednich postaci funkcji callback bez dokumentacji.

Wiemy że wszystkie funkcje Callback przeznaczone do własnej implementacji będą miały w nazwie słowo weak oraz callback. Taką frazę dla wyszukiwarki zapisać możemy jako weak*callback. Z menu wybieramy Search > File. W pole wyszukiwania wpisujemy podaną frazę i wciskamy Search.

Wyszukiwanie funkcji w programie.

Wyszukiwanie funkcji w programie.

Na dole ekranu pojawią się wyniki wyszukiwania. Wiemy, że funkcji callback szukać będziemy zawsze w plikach bibliotek HAL, dlatego rozwijamy folder STM32F4xx_HAL_Driver. W tym folderze wyświetlą się pliki odpowiedzialne za konkretne moduły i peryferia mikrokontrolera.

Interesuje nas tutaj przerwanie zewnętrzne od pinu, więc odpowiedzialnej za to funkcji szukać będziemy w pliku dotyczącym GPIO.

Miejsce poszukiwanej funkcji.

Miejsce poszukiwanej funkcji.

Funkcja zdefiniowana jest z użyciem atrybutu __weak, aby uniknąć błędów linkera i pozwolić na późniejszą redefinicję funkcji przez użytkownika. Nasza implementacja funkcji callback w pliku main wyglądać będzie zatem następująco:

Teoretycznie w tym miejscu powinniśmy umieścić jedynie prototyp funkcji, co sugerują nawet znaczniki USER CODE BEGIN, a pełną definicję przenieść poniżej pliku main. Jednak ze względu na to, że funkcja ta nie zajmuje wielu linii, pozostawię ją w tym miejscu.

Efektem działania powyższej funkcji będzie wpisanie nowego stanu diody przy każdej zmianie stanu przycisku. Nic nowego, prawda? Różnica polega na tym, że dzieje się to tylko wtedy, gdy zmieni się stan przycisku, bez zbędnego odczytywania jego stanu miliony razy na sekundę.


Zauważmy jeszcze jedną istotną rzecz. Zdefiniowana przez nas funkcja jest wywoływana po przerwaniu pochodzącym od dowolnego pinu. Aktualnie mamy skonfigurowane tylko jedno przerwanie, więc nie jest to problemem. Co jeżeli jednak byłoby ich więcej i chcielibyśmy rozróżnić od którego pinu pochodzi przerwanie?

Widać, że jedynym parametrem definiowanej funkcji jest uint16_t GPIO_Pin. Jest to nic innego, tylko numer pinu, od którego pochodzi przerwanie. Chcąc w takim razie obsłużyć przerwanie pochodzące konkretnie od pinu PA0, wystarczy sprawdzić, czy to właśnie on wywołał przerwanie.

Zadanie do przećwiczenia

W celu utrwalenia nabytych umiejętności proponuję napisać program, który będzie włączał po kolei wszystkie diody, a następnie je wyłączał. Zapalanie i gaszenie ma się odbywać pojedynczo i ma być wyzwalane wciśnięciem niebieskiego przycisku na płytce. Projekt realizujący tę funkcjonalność znajdziecie w załączniku.

W praktyce efekt powinien wyglądać następująco:

Podsumowanie

W tym artykule nauczyliśmy się podstawowej obsługi narzędzia konfiguracyjnego STM32CubeMX. Potrafimy wygenerować kod na podstawie stworzonej konfiguracji, zaimportować projekt do IDE, zbudować oraz wgrać go na mikrokontroler. Poznaliśmy także niezbędne funkcje do obsługi portów GPIO. Wiemy również jak obsłużyć przerwanie zewnętrzne pochodzące np. od przycisku.

W załączniku znajdziecie archiwum ze stworzonym w trakcie artykułu projektem. W następnym artykule zajmiemy się pomiarem napięcia, a więc przetwornikiem analogowo-cyfrowym (ADC). Poznamy także narzędzie umożliwiające proste i wygodne obserwowanie tego, co dzieje się w mikrokontrolerze, czyli STMStudio.

Nawigacja kursu

Dzięki za uwagę! Jeżeli masz pytania, to pisz śmiało w komentarzach! Nie chcesz przeoczyć kolejnych części kursu? Skorzystaj z poniższego formularza i zapisz się na powiadomienia o nowych artykułach!

Autor kursu: Bartek (Popeye) Kurosz
Redakcja: Damian (Treker) Szymański

Załączniki

GPIO (zip, 8 MB)

Paczka zawierająca projekt tworzony podczas artykułu, projekt z zadaniem do przećwiczenia oraz jego pliki binarne

F4, kurs, kursSTM32F4, przerwania, stm32

Trwa ładowanie komentarzy...