Kursy • Poradniki • Inspirujące DIY • Forum
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!
Gotowe zestawy do kursów Forbota
Komplet elementów 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.
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.
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.
Chociaż podczas trwania kursu korzystać będziemy z płytki Discovery, na początku będzie nam dużo łatwiej jeśli wybierzemy sam mikrokontroler.
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.
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.
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.
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.
Na ekranie znajduje się również zupełnie niepotrzebna nam w tej chwili pozioma sekcja na samym dole. Można ją wyłączyć poprzez odznaczenie na głównej belce menu widoku Window > Outputs, żeby nie zajmowała niepotrzebnie miejsca.
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.
Widok mikrokontrolera można przybliżać i oddalać za pomocą standardowego Ctrl + scroll lub przy użyciu przycisków znajdujących się na prawo od okna wyszukiwarki. Można go również przesuwać na boki, przeciągając mikrokontroler z przyciśniętym prawym lub lewym klawiszem myszy.
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.
Piny zasilania zaznaczone są kolorem żółtym,
a piny specjalne takie jak reset i pin BOOT zaznaczono na zielono.
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.
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.
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:
- nazwa projektu,
- jego lokalizacja,
- IDE, pod które wygenerowany zostanie cały kod.
Na zrzucie poniżej jest to niewidoczne,
ale nazwa projektu nie zawiera spacji i wygląda następująco: 01_GPIO
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:
Udało nam się stworzyć i wygenerować
pierwszą konfigurację na mikrokontroler STM32!
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.
Krok 3. Importowanie projektu.
Klikamy prawym przyciskiem myszy w obszarze Project Explorer i wybieramy Import.
Następnie wybieramy pozycję Existing Projects into Workspace z zakładki General.
Zatwierdzamy wybór przyciskiem Next. W następnym panelu wybieramy lokalizację wcześniej stworzonego przez Cube projektu (w tym wypadku wybieramy folder 01_GPIO).
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.
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.
Zawartość pliku main.c
Otwórzmy zatem plik main.c i sprawdźmy co się w nim znajduje.
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.
- SystemClock_Config - jak sama nazwa wskazuje, odpowiada za konfigurację zegara, którym taktowany jest mikrokontroler.
- 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.
Dzięki używanym tu narzędziom, nie jest to problemem!
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.
Wspomnianych wyżej znaczników nie można dodawać samemu! Należy się trzymać tych wygenerowanych przez program. Na szczęście jest ich na tyle dużo, że zawsze znajdzie się odpowiedni w miejscu, w którym chcielibyśmy coś dopisać.
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.
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.
Jeśli kogoś interesuje więcej szczegółów, wystarczy najechać kursorem na funkcję,
a wyświetli się jej podstawowa dokumentacja.
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.
Poniżej funkcji main można znaleźć deklaracje wspomnianych wcześniej funkcji inicjalizujących. Jeśli ktoś chciałby zobaczyć co dzieje się na nieco niższym poziomie, to powinien tam zajrzeć.
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.
Jeżeli w edytorze nie wyświetlają się numery linii, można je włączyć wybierając Window > Preferences > General > Editors > Text Editors, a następnie zaznaczyć opcję Show line numbers.
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.
Numer linii, która akurat wypada pomiędzy znacznikami USER CODE BEGIN/END 2, będzie się różnił ze względu na wybrany mikrokontroler i włączone peryferia. Należy się kierować umiejscowieniem wspomnianych wcześniej znaczników.
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.
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ę.
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:
HAL_GPIO_WritePin
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.
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).
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.
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_SET);
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.
Ze schematu wynika, że aby zapalić diodę
należy wystawić stan wysoki na pin sterujący.
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.
Plik binarny znajduje się w nastepującej lokalizacji (zaczynając od głównego folderu projektu): 01_GPIOSW4STM32 1_GPIODebug 1_GPIO.bin
Krok 5. Wgrywamy program wybierając Start. Jeżeli wszystko przebiegło pomyślnie, na płytce Discovery powinna się zaświecić niebieska dioda.
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.
Na szczęście można to zrealizować w dużo prostszy,
aczkolwiek mniej optymalny sposób.
Do tego celu wykorzystamy funkcję opóźniającą, przyjmującą jako parametr czas opóźnienia w milisekundach.
HAL_Delay(delay_time_in_miliseconds);
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ę:
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_SET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_RESET);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
Po zbudowaniu i wgraniu tego programu, niebieska dioda powinna się zaświecić, a następnie po sekundzie zgasnąć.
Przed wgraniem nowego programu w ST-Link Utility trzeba ponownie ręcznie wybrać plik binarny, aby załadował do pamięci jego nową wersję.
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.
Funkcja której użyjemy to HAL_GPIO_Toggle_Pin. Od poprzedniej różni się tym, że nie przyjmuje ostatniego parametru, ponieważ zmienia obecny na przeciwny.
Przykładowy program realizujący to zadanie przedstawiono poniżej.
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_15);
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
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).
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.
Widać, że stan przycisku odczytywać będziemy na pinie PA0.
Warto zauważyć, że w układzie przycisku na płytce Discovery znajdują się elementy RC mające sprzętowo zapobiec tzw. drganiu styków. Dzięki temu nie musimy się przejmować tym efektem.
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.
Po wybraniu GPIO_Input pin powinien podświetlić się na zielono.
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.
Dodajmy etykietę do obydwu pinów, które będziemy wykorzystywać w tym programie.
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
Wszystkie powyższe operacje możemy wybrać również z rozwijanego menu, klikając prawym przyciskiem myszy na nazwę projektu w panelu Project Explorer.
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:
HAL_GPIO_ReadPin()
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.
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.
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if(HAL_GPIO_ReadPin(Button_GPIO_Port, Button_Pin) == GPIO_PIN_SET){
HAL_GPIO_WritePin(LED_Blue_GPIO_Port, LED_Blue_Pin, GPIO_PIN_SET);
}else{
HAL_GPIO_WritePin(LED_Blue_GPIO_Port, LED_Blue_Pin, GPIO_PIN_RESET);
}
/* USER CODE END WHILE */
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:
HAL_GPIO_WritePin(LED_Blue_GPIO_Port, LED_Blue_Pin, HAL_GPIO_ReadPin(Button_GPIO_Port, Button_Pin));
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.
Znaczy to tyle, że bardzo często odpytujemy urządzenie (w tym wypadku - sprawdzamy stan na pinie) w celu pobrania informacji.
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.
Tu z pomocą przychodzą nam przerwania!
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:
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.
Po zmianie funkcji pinu jego etykieta zostanie automatycznie zmieniona na domyślną.
Nie zapomnij o ponownym zdefiniowaniu własnej, wygodnej etykiety!
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.
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.
W sekcji zaznaczonej na zielono możemy wewnętrznie podciągnąć pin do masy lub do zasilania. Normalnie w tej sytuacji byłoby to wskazane, ale na płytce Discovery pin PA0 jest sprzętowo podciągnięty do masy (widać to na wyżej wklejonym schemacie przycisku).
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.
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.
Jak na razie najwygodniejszy jaki udało mi się odkryć przedstawiam poniżej.
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.
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.
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:
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
}
/* USER CODE END PFP */
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.
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
HAL_GPIO_WritePin(LED_Blue_GPIO_Port, LED_Blue_Pin, HAL_GPIO_ReadPin(Button_GPIO_Port, Button_Pin));
}
/* USER CODE END PFP */
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.
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if(GPIO_Pin == Button_Pin){
HAL_GPIO_WritePin(LED_Blue_GPIO_Port, LED_Blue_Pin, HAL_GPIO_ReadPin(Button_GPIO_Port, Button_Pin));
}
}
/* USER CODE END PFP */
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:
Warto wgrać na płytkę dostępny tu plik binarny, przetestować jego działanie i następnie napisać program realizujący prezentowaną funkcjonalność. Dopiero w przypadku poważnych problemów z implementacją należy się posiłkować dostępnym w załączniku kodem.
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
Powiązane wpisy
F4, kurs, kursSTM32F4, przerwania, stm32
Trwa ładowanie komentarzy...