W poprzedniej części kursu zainstalowaliśmy środowisko STM32CubeIDE, stworzyliśmy „pusty” projekt i wgraliśmy taki program na mikrokontroler. Teraz przejdziemy do projektów, dzięki którym zapoznamy się z działaniem GPIO. Nie każdy program zadziała od razu – zdarzy się nam specjalnie popełnić błąd, aby za chwilę go „wytknąć” i rozwiązać problem, zupełnie jak podczas normalnego programowania.
Linijka LED, czyli jeden z (pozornie prostych) przykładów z tej części kursu
Uniwersalne wejścia/wyjścia – GPIO w praktyce
Miganie diodą jest podczas nauki programowania mikrokontrolerów odpowiednikiem wyświetlenia „Hello World!” na ekranie komputera, od czego zaczyna się chyba większość kursów programowania. Nie inaczej będzie tym razem. Zamigajmy więc diodą! Aby było to możliwe, musimy poznać GPIO (ang. general-purpose input/output), czyli uniwersalne wejścia/wyjścia.
Jak oznaczone są GPIO w STM32?
Do wykonania tego ćwiczenia niezbędny będzie zestaw NUCLEO-L476RG. Zgodnie z informacją, którą podaliśmy w poprzedniej części kursu, na pokładzie tej płytki znaleźć można m.in. diodę LD2 – jest ona podłączona do pinu PA5. Możemy zatem wykorzystać ją w naszym pierwszym programie.
Dwa przyciski i dioda LD2 na NUCLEO-L476RG
Mikrokontroler STM32L476RG ma cztery porty oznaczone literami: A, B, C oraz D. Porty mogą mieć maksymalnie po 16 pinów (numerowanych od zera, czyli np. od PB0 do PB15). Nie wszystkie porty mają jednak wszystkie piny, np. port D udostępnia tylko jeden pin (PD2). Same litery nie mają też żadnego znaczenia – to tylko sposób na uporządkowanie dużej liczby wyprowadzeń. Na przykład litera A w oznaczeniu PA1 nie oznacza, że jest to wejście analogowe (tak jak w przypadku Arduino UNO).
Nasza dioda jest podłączona do PA5, czyli (licząc od zera) do szóstego pinu portu A.
Pierwsze kroki z Device Configuration Tool
Zaczynamy od utworzenia nowego projektu w STM32CubeIDE. Opis całej procedury znajduje się w poprzedniej części kursu, więc nie będziemy tego tutaj powtarzać. Dla przypomnienia, w największym skrócie: STM32CubeIDE > File > New > STM32 Project. Wybieramy układ STM32L476RG i zostawiamy domyślne ustawienia. Po chwili zobaczymy widok zbliżony do poniższego.
Domyślna perspektywa Device Configuration Tool
Tym razem zatrzymamy się na chwilę na perspektywie Device Configuration Tool, która jest też często nazywana CubeMX, bo narzędzie widoczne w ramach tej perspektywy jest dostępne również jako oddzielny program – STM32CubeMX. Więcej informacji na jego temat (oraz stosowną dokumentację) można znaleźć na stronie tego projektu.
Omówienie interfejsu CubeMX
Graficzny interfejs użytkownika nie jest skomplikowany, ale czasami osoby odpowiedzialne za rozwój tego narzędzia wprowadzają w nim pewne zmiany. Dlatego najlepiej skupić się na lokalizacjach oraz nazwach „rzeczy”, które trzeba klikać – bez zbędnego przywiązywania wagi do tego, jak one wyglądają.
Główna część okna z graficznym konfiguratorem
W górnej części okna widoczne są cztery zakładki:
Pinout & Configuration – narzędzie do konfiguracji pinów; ta zakładka jest domyślnie wybrana, dlatego widzimy schemat mikrokontrolera oraz jego wyprowadzenia.
Clock Configuration – konfiguracja zegarów mikrokontrolera (jest to obszerny temat, któremu poświęcimy w całości osobną część kursu – na razie wystarczą nam domyślne ustawienia).
Project Manager – ustawienia samego projektu (na razie korzystamy z ustawień domyślnych).
Tools – narzędzie przydatne do szacowania poboru mocy przez mikrokontroler.
Zawartość zakładki, która pozwala na konfigurowanie zegarów
Wybranie konkretnej zakładki powoduje zmianę pozostałych części interfejsu. Aktualnie interesuje nas pierwsza z nich, czyli Pinout & Configuration, więc przyjrzyjmy się jej dokładniej. W górnej części okna tego widoku znajdziemy dodatkowe menu, które zawiera dwie pozycje:
Software Packs – pozwala na instalowanie dodatkowych bibliotek, np. związanych ze sztuczną inteligencją, czujnikami MEMS czy komunikacją bezprzewodową.
Pinout – menu zawierające opcje związane z konfiguracją pinów.
Dodatkowe menu wewnątrz zakładki związanej m.in. z GPIO
Poniżej po lewej stronie widoczna jest lista dostępnych modułów peryferyjnych (np. System Core, Analog, Timers itd.). Moduły mogą być wyświetlane w podziale na kategorie lub alfabetycznie. Jest też dostępna wyszukiwarka. Zaznaczenie modułu powoduje wyświetlenie dodatkowego okna z opcjami.
Dodatkowe menu po wybraniu konkretnego peryferium
Centralną część okna zajmuje schemat wyprowadzeń mikrokontrolera. Klikając lewym przyciskiem myszki w wybrany pin, możemy zmienić jego funkcję (zaraz się tym zajmiemy). Z kolei prawy przycisk wywołuje menu z innymi opcjami, dzięki którym można np. przypisać pinowi własną nazwę.
Menu pod prawym przyciskiem myszki dostępne jest dopiero po wybraniu funkcji dla GPIO.
Konfiguracja pinu jako wyjście
Jak już wiemy, do sterowania wbudowaną diodą potrzebujemy pinu PA5. Jeśli nie możemy odnaleźć danej nóżki „ręcznie”, pomocna będzie mała wyszukiwarka, która dostępna jest w dolnej części okna. Wystarczy wpisać PA5 w pole wyszukiwarki, a program podświetli odpowiedni pin.
Po znalezieniu PA5 klikamy w niego lewym przyciskiem myszki. Z rozwijanego menu wybieramy opcję GPIO_Output – taka konfiguracja oznacza wyjście, czyli dokładnie to, czego teraz potrzebujemy. Po tej operacji pin PA5 zostanie odpowiednio oznaczony, abyśmy wiedzieli, że jest już używany.
Oznaczenie wybranego pinu
Nadawanie pinom nazw (etykiet)
Używanie numerów pinów bywa problematyczne, szczególnie jeśli później chcemy zmienić konfigurację sprzętową (zmiana nóżki, do której podłączony jest dany element). Jeśli w programie posługujemy się numerami, to ewentualna zmiana połączeń wymagałaby odnalezienia w kodzie wszystkich fragmentów, które odwoływały się do danego pinu, w celu ich zaktualizowania. Znacznie lepszym rozwiązaniem jest nadanie pinowi nazwy i używanie jej zamiast konkretnego numeru.
Nadanie pinowi nazwy sprawi, że późniejsza zmiana połączeń wymaga jedynie przekonfigurowania projektu w CubeMX (odłączamy nazwę od starego pinu i przypisujemy ją do nowego) – nie ma potrzeby zmian w naszym kodzie.
W celu nadania nazwy pinowi klikamy w niego prawym przyciskiem myszki i wybieramy opcję Enter User Label. Pojawi się wtedy okienko z małym polem tekstowym, w które wpisujemy nazwę (żadnych spacji, polskich znaków i symboli – trzymamy się prostych nazw alfanumerycznych). W tym przypadku posłużymy się nazwą, która jest nadrukowana wprost na płytce Nucleo, czyli LD2.
Nadawanie etykiety dla wybranego pinu
Konfigurację wszystkich pinów możemy sprawdzić (oraz ewentualnie zmienić), wybierając GPIO z listy modułów peryferyjnych mikrokontrolera (po lewej stronie CubeMX). Jeśli używamy widoku z podziałem na kategorie, to GPIO znajdziemy w kategorii System Core. Korzystając z szarych strzałek (prawy górny róg zrzutu) możemy ukrywać graficzny pinout mikrokontrolera, aby maksymalizować widok tabeli.
Menu z podglądem pinów używanych w aktualnym projekcie
To okno daje nam nieco więcej możliwości konfiguracji, ale na razie wartości domyślne w zupełności wystarczą (później wrócimy do tego menu). Teraz warto tylko upewnić się, że wszystkie ustawienia są zgodne ze zrzutem ekranu z kursu, czyli: GPIO output level = Low, GPIO mode = Output Push Pull, GPIO Pull-up/Pull-down = No pull-up and no pull-down, Maximum output speed = Low, User Label = LD2.
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zamów zestaw elementów i wykonaj ćwiczenia z tego kursu! W komplecie płytka NUCLEO-L476RG oraz m.in. wyświetlacz graficzny, joystick, enkoder, czujniki (światła, temperatury, wysokości, odległości), pilot IR i wiele innych.
Masz już zestaw? Zarejestruj go wykorzystując dołączony do niego kod. Szczegóły »
Generowanie kodu
Nasza konfiguracja sprzętowa jest już gotowa, bo wszystkie inne peryferia, które są niezbędne do pracy układu i obsługi GPIO, zostały automatycznie skonfigurowane „w tle”. Nie musimy przejmować się już niczym więcej (oczywiście w dalszych częściach kursu zagłębimy się w konfigurację wielu innych opcji).
Konfiguracja gotowa, więc wystarczy ją zapisać. Wybieramy File > Save (lub klikamy ikonkę z dyskietką). Po uruchomieniu zapisu zostaniemy zapytani o to, czy program ma wygenerować kod – tak, chcemy, aby to zrobił. Przy okazji możemy też zostać zapytani, czy chcemy przełączyć perspektywę na C/C++, na to również wyrażamy zgodę.
Pytania o wygenerowanie kodu i przełączanie perspektywy nie muszą się zawsze wyświetlać – wszystko zależy od tego, czy na jednym z wcześniejszych etapów zaznaczyliśmy opcję, która sprawiła, że IDE zapamiętało już nasz wybór.
Budowa i zawartość projektu
Zapisanie zmian spowoduje automatyczne wygenerowanie szablonu kodu (może to zająć kilkadziesiąt sekund). Gdy program będzie już gotowy i zostaniemy przełączeni do perspektywy C/C++, warto poświęcić chwilę, aby zapoznać się ze strukturą projektu – zerkamy zatem do eksploratora projektu.
Widoczny jest tam m.in. plik z rozszerzeniem „ioc”. Jest to plik z ustawieniami dla CubeMX – gdy go otworzymy, wrócimy do edycji ustawień mikrokontrolera. Z kolei „standardowy kod źródłowy” znajdziemy w dwóch katalogach: Core oraz Drivers.
Drivers to sterowniki, czyli biblioteki dostarczone przez STMicroelectronics. Znajdziemy tam bibliotekę CMSIS, odpowiedzialną za niskopoziomowy dostęp do rejestrów układu, oraz samą bibliotekę HAL, na której bazuje ten kurs (jest ona dostępna w katalogu STM32L4xx_HAL_Driver).
Nas bardziej interesować będzie zawartość katalogu Core. Znajdziemy w nim trzy podkatalogi:
Inc – pliki nagłówkowe,
Src – kod źródłowy naszego programu,
Startup – plik startowy uruchamiany przed funkcją main.
Gdy rozwiniemy katalog Core oraz znajdujący się w nim podkatalog Src, na liście plików znajdziemy szybko main.c. Nasz program będziemy pisać właśnie w tym pliku, warto więc go otworzyć i zapoznać się z jego strukturą.
Lokalizacja pliku main.c
Na samym początku wygenerowanego pliku znajdziemy informację o licencji BSD. Dalej rozpoczyna się już główna część automatycznie generowanego projektu. Raz jeszcze przypominamy, że trzeba uważać na komentarze w postaci USER CODE BEGIN / USER CODE END. Na przykład:
C
1
2
3
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
Ogólna zasada jest taka, że to, co znajdzie się między taką parą komentarzy, to nasza sprawa, ale to, co jest poza nimi, kontroluje CubeMX. Trzeba tutaj wyraźnie podkreślić, że podczas zapisu plików CubeMX bez żadnego uprzedzenia usunie wszystkie zmiany, jakie wprowadziliśmy poza tymi komentarzami!
Pisaliśmy o tym już wcześniej, ale powtarzamy kolejny raz, bo stosunkowo łatwo się pomylić i dopisać z rozpędu własny kod w niepoprawnym miejscu. Wszystko będzie działało do momentu dokonania zmian w konfiguracji mikrokontrolera. Gdy zapiszemy zmiany w CubeMX, szablon kodu zostanie ponownie wygenerowany i nasza praca przepadnie bezpowrotnie.
Zmiany wprowadzane poza przewidzianym do tego rejonem są usuwane przez CubeMX (dzieje się to podczas każdej regeneracji naszego projektu).
Sekcje oznaczone komentarzami, w których możemy wpisywać swój kod, znajdują się w wielu ważnych miejscach. Rozwiązanie to właściwie nas nie ogranicza, bo praktycznie w każdym kluczowym dla nas miejscu będzie opcja, aby dopisać tam swój kod. Trzeba jednak przyzwyczaić się do tego, aby kod wpisywać pomiędzy wspomnianymi komentarzami.
Fragment kodu, który przed chwilą pokazaliśmy, to konkretnie miejsce na dodawanie naszych plików nagłówkowych, więc gdybyśmy chcieli np. dołączyć plik stdio.h, to zrobilibyśmy to tak:
C
1
2
3
4
5
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
Dodanie tego pliku w poniższy sposób sprawi, że nasza linijka „wyparuje” podczas najbliższych zmian, które wprowadzimy w CubeMX:
C
1
2
3
4
5
#include <stdio.h>
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
Przeglądając dalej plik main.c, dojdziemy w końcu do funkcji main:
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
Widzimy w niej wywołania trzech funkcji: HAL_init, SystemClock_Config, MX_GPIO_Init oraz miejsca na pisanie kodu użytkownika. Podczas kursu domyślna inicjalizacja będzie dla nas wystarczająca, więc nie będziemy tutaj nic zmieniać, trzeba jednak pamiętać, że CubeMX będzie dodawał wywołania funkcji konfiguracyjnych dla kolejnych modułów peryferyjnych, gdy będziemy zmieniać konfigurację projektu.
Teraz najważniejsze, czyli pętla główna programu – znajdziemy ją trochę niżej. Widoczne są tam dwa ważne miejsca – przed pętlą główną możemy dodać kod, który będzie wykonany dokładnie raz, natomiast wewnątrz pętli while wpisujemy kod, który będzie wykonywany „w kółko”.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while(1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Pora, aby nareszcie napisać własny program. Do migania diodą potrzebne będą nam dwie funkcje – jedna umożliwi zmianę stanu wyjścia na przeciwny, a druga generowanie opóźnień.
Zmiana stanu pinu na przeciwny
Użyliśmy już CubeMX do wygenerowania konfiguracji sprzętowej. Teraz pora, aby wykorzystać kolejny element wsparcia, które dostarcza STMicroelectronics. Mowa o bibliotece HAL, dzięki której tworzenie programów na tak rozbudowane mikrokontrolery jest stosunkowo proste.
Biblioteka ta zawiera ogrom funkcji, dzięki którym możemy sterować dowolnymi peryferiami układu. Teraz może wydawać się to zupełnie normalne (szczególnie dla osób znających Arduino), ale jeszcze całkiem niedawno (~10 lat temu) pisanie programów na mikrokontrolery wiązało się ze studiowaniem długich not katalogowych i operowaniem na rejestrach. Było to mozolne i na pewno mało przyjazne dla osób początkujących (ale to temat na zupełnie inny artykuł).
Oczywiście nadal można pisać programy, opierając się na tzw. rejestrach, i istnieją zwolennicy tego rozwiązania. Poziom skomplikowania tego zagadnienia rośnie jednak bardzo mocno wraz z możliwościami wybranego układu.
Wróćmy jednak do meritum. Biblioteka HAL posiada funkcję HAL_GPIO_TogglePin, która zmienia stan pinu na przeciwny. Prototyp (nagłówek) tej funkcji wygląda następująco:
Nie musimy się jednak przejmować tymi (pozornie) dziwnie wyglądającymi wartościami, które trzeba podać jako argumenty. Program zdefiniował dla nas automatycznie odpowiednie stałe. Aby zmienić stan diody LD2, wystarczy więc wywołać w pętli while następującą funkcję:
C
1
HAL_GPIO_TogglePin(LD2_GPIO_Port,LD2_Pin);
Podczas konfiguracji GPIO do pinu PA5 przypisaliśmy nazwę LD2. Generator kodu utworzył więc dwie stałe: LD2_GPIO_Port, która automatycznie oznacza port A, oraz LD2_Pin, która odpowiada pinowi oznaczonemu jako 5. Zestawiając te dwie stałe, możemy zatem odwołać się do pinu PA5.
Jeśli zmienimy w CubeMX konfigurację sprzętową i etykietę LD2 przypiszemy do innego wyjścia, to program podmieni wartości stałych na odpowiednie dla danego pinu.
Wprowadzanie prostych opóźnień do programu
Do migania diodą potrzebujemy jeszcze opóźnienia, inaczej częstotliwość migania będzie tak wysoka, że nie będziemy widzieli migania, tylko słabsze świecenie diody. Z pomocą przychodzi nam tutaj funkcja HAL_Delay, dzięki której można wprowadzić najprostsze opóźnienie (ma ono swoje wady, ale podczas pisania pierwszego programu, który miga diodą, taka opcja całkowicie nam wystarczy).
Prototyp (nagłówek) tej funkcji wygląda prosto:
C
1
voidHAL_Delay(uint32_t Delay)
Przyjmuje ona jeden parametr, którym jest opóźnienie wyrażone w milisekundach. Znamy już funkcje, które potrzebne są do migania diodą, więc możemy napisać program. Oczywiście musimy to zrobić w odpowiednim miejscu (patrzymy na komentarze w kodzie).
C
1
2
3
4
5
6
7
8
9
10
11
12
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while(1)
{
HAL_GPIO_TogglePin(LD2_GPIO_Port,LD2_Pin);
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Komentarze, które opisują działanie programu, są ważne – przyzna to (prawie) każdy programista. W tym kursie nie komentujemy jednak każdej linijki kodu, bo są one dokładnie omawiane w treści kursu. Co więcej, nazwy funkcji z biblioteki HAL bardzo często mówią wprost co robi dana funkcja. Nie dodajemy też osobnych komentarzy, które nie są istotne – typu: „Czekaj 500 ms”.
Jeśli jakiś fragment kodu nie jest jasny, to śmiało pytaj w komentarzach – chętnie wyjaśnimy.
Podpowiadanie składni
Jeśli podczas pisania programu zapomnimy, jak dokładnie brzmi nazwa funkcji lub jakie są parametry, które przyjmuje, to możemy skorzystać z podpowiadania składni. W tym celu wystarczy zacząć pisać, np. „HAL_GPIO_”, i nacisnąć kombinację klawiszy CTRL + Spacja. Wyświetlona zostanie wtedy lista funkcji, które zaczynają się od takiej nazwy.
Automatyczne podpowiadanie składni
Kompilacja programu
Gotowy program kompilujemy, klikając ikonę z młotkiem (lub wybierając odpowiednią opcję z menu). Jeśli wszystko poszło poprawnie, to tak jak w poprzedniej części kursu na dole okna zobaczymy odpowiedni komunikat. Oczywiście będą tam również informacje o ewentualnych błędach.
Informacja o poprawnej kompilacji projektu
Teraz możemy uruchomić nasz program na płytce Nucleo. Klikamy przycisk Debug i zatwierdzamy domyślne opcje. Program zostanie uruchomiony i zatrzymany na pierwszej linii kodu funkcji main. Przyciskając ikonę Resume (lub naciskając klawisz F8), uruchomimy program – dioda będzie migać.
Efekt działania pierwszego programu – migająca dioda
Konfiguracja pinu jako wejście
Potrafimy już sterować diodą, możemy teraz rozbudować przykładowy program o obsługę przycisku, który również znajduje się na pokładzie Nucleo – chodzi o niebieski przycisk, opisany jako USER.
Dwa przyciski i dioda LD2 na NUCLEO-L476RG
W dokumentacji płytki Nucleo znajdziemy informację o podłączeniu przycisku do pinu PC13. Nie ma potrzeby, aby tworzyć nowy projekt – możemy zaktualizować obecny. Wystarczy, że w widoku projektu klikniemy 2 razy plik z rozszerzeniem „ioc” – przeniesiemy się wtedy do graficznego konfiguratora.
W uruchomionym edytorze odszukujemy pin PC13, zmieniamy jego tryb na wejście, czyli GPIO_Input. Na koniec dodajemy mu etykietę USER_BUTTON. Tabela z konfiguracją pinów, która widoczna jest w zakładce GPIO, powinna zawierać już dwa wpisy.
Nowy wpis na liście używanych GPIO
Wszystkie zmiany w konfiguracji sprzętu są już gotowe, możemy zapisać projekt i wygenerować nowy kod. Po chwili STM32CubeIDE wyświetli nam zaktualizowaną wersję programu. Na pierwszy rzut oka nie zobaczymy żadnej różnicy. W pętli while nadal powinniśmy widzieć dwie funkcje, które odpowiadają za miganie diody.
Jeśli w nowym programie nie widać tych dwóch funkcji, to znaczy, że zostały poprzednio umieszczone w złym miejscu i CubeMX usunął je podczas generowania nowego kodu.
Do migania używaliśmy funkcji HAL_GPIO_TogglePin, która zmieniała stan pinu na przeciwny. Teraz zmienimy nasz program tak, żeby dioda świeciła tylko wtedy, gdy użytkownik naciska przycisk.
Odczytywanie stanu wejścia cyfrowego
Do odczytu stanu przycisku wykorzystamy funkcję HAL_GPIO_ReadPin, a do zapalania i gaszenia diody – HAL_GPIO_WritePin. Ich nagłówki wyglądają podobnie do HAL_GPIO_Toggle:
Pierwsza funkcja zwraca wartość typu GPIO_PinState, natomiast druga przyjmuje taką wartość jako trzeci parametr. Zobaczmy więc, jak wygląda definicja tego typu.
Jak widzimy, typ ten może przyjmować tylko dwie wartości:
GPIO_PIN_RESET – stan niski; oznacza napięcie bliskie 0 V (zwarcie do masy),
GPIO_PIN_SET – stan wysoki; odpowiada mu napięcie zasilania mikrokontrolera, czyli 3,3 V.
To, jakie napięcia będą włączały diodę albo pojawiały się po naciśnięciu przycisku, zależy od schematu naszego układu. Na płytce Nucleo dioda LD2 jest podłączona tak, że włączamy ją stanem wysokim, a wyłączamy niskim. Natomiast na pinie połączonym z przyciskiem stan niski pojawia się, gdy przycisk zostanie wciśnięty, a wysoki – gdy będzie on zwolniony.
Funkcja HAL_GPIO_ReadPin, podobnie do TogglePin, przyjmuje dwa parametry, czyli port oraz numer pinu, który chcemy odczytać. W związku z tym, że pinowi PC13 nadaliśmy już nazwę USER_BUTTON to możemy wykorzystać zdefiniowane wartości USER_BUTTON_GPIO_Port oraz USER_BUTTON_Pin.
Poprzednio tylko zmienialiśmy stan diody na przeciwny, teraz chcemy dokładniej kontrolować jej stan, użyliśmy więc funkcji HAL_GPIO_WritePin. Jej pierwsze dwa parametry to – tak jak wcześniej – numer portu oraz pinu. Pojawił się też trzeci parametr – to informacja o tym, czy na danym wyjściu ma być ustawiony stan wysoki (GPIO_PIN_SET), czy stan niski (GPIO_PIN_RESET).
Efekt działania programu – dioda świeci po naciśnięciu przycisku
Podłączenie zewnętrznej diody
Umiemy już sterować diodą, która jest na płytce Nucleo, czas podłączyć zewnętrznego LED-a. Przydadzą nam się do tego płytka stykowa oraz elementy z zestawu. Podłączmy więc czerwoną diodę świecącą przez rezystor 330 R do pinu PA0.
Schemat ideowy oraz montażowy do tego przykładu
Możemy teraz edytować konfigurację sprzętową naszego projektu. Robimy to tak samo jak poprzednio. Dodajemy tylko kolejny pin, który będzie skonfigurowany jako wyjście. W poprzednim projekcie diodę na płytce Nucleo nazwaliśmy LD2 (bo tak jest podpisana na Nucleo). Nowej diodzie nadajmy teraz nazwę LED1, aby była zgodna z powyższym schematem ideowym. Po dodaniu drugiej diody na liście użytych GPIO powinny być widoczne już trzy wpisy.
W tym przykładzie nie korzystamy z przycisku oraz diod LD2, więc można byłoby usunąć te dwa wpisy. Nie jest to jednak konieczne – możemy po prostu nie używać ich w programie.
Konfiguracja kolejnego wejścia dla zewnętrznej diody
Wrócimy do migania diodą, ale tym razem zróbmy to za pomocą funkcji HAL_GPIO_WritePin oraz HAL_Delay. Teraz będziemy zapalać diodę na 20% czasu. Chodzi tutaj głównie o możliwość naocznego rozróżnienia stanów logicznych na wyjściu układu.
Takie sterowanie jest też stosowane w praktyce, bo pozwala na zaoszczędzenie prądu. Gdybyśmy włączali diodę na 500 ms, a następnie wyłączali ją na kolejne 500 ms, to nasz prąd średni wynosiłby 50% prądu potrzebnego do ciągłego świecenia diody. Teraz włączamy diodę na 200 ms, a wyłączamy na 800 ms, więc średnie zużycie prądu w takiej sytuacji spadnie do 20%.
Po uruchomieniu przykładu dioda podłączona do Nucleo powinna zacząć migać – 200 ms świecenia oraz 800 ms ciemności. Widać więc wyraźnie, że stanem wysokim (GPIO_PIN_SET) włączamy diodę, a stanem niskim (GPIO_PIN_RESET) gasimy ją.
Długi dodatek (?) na temat linijki LED
W tym miejscu moglibyśmy zakończyć, bo zupełne podstawy GPIO byłyby omówione. Zdecydowaliśmy się jednak na dodanie stosunkowo długiego „dodatku”, w którym pokażemy, w jaki sposób można rozwiązać pozornie prosty temat linijki składającej się z wielu kolorowych diod. Co więcej, temat ten został specjalnie „rozciągnięty”, aby pokazać w praktyce, że jeden problem można rozwiązać na wiele sposobów, które mogą generować różne problemy.
Gorąco zachęcamy, aby wszyscy przeanalizowali ten „dodatek”, nawet jeśli część tego opisu będzie zbyt zawiła od strony programistycznej – pojawią się tutaj przesunięcia bitowe, tablice, struktury i wskaźniki.
Linijka LED, czyli używanie wielu (różnych) wyjść
Skoro potrafimy podłączyć jedną diodę, dlaczego nie podłączyć ich więcej? W zestawie do tego kursu znajdują się diody w różnych kolorach, które można podłączyć na płytce stykowej w formie kolorowej linijki świetlnej.
Na początek podłączmy diody do portu B (od pinu PB5 do PB14). Podłączenie diod do tego samego portu sprawi, że napisanie programu będzie łatwiejsze. Następnie sprawdzimy, co można zrobić, gdy nie będziemy mogli sobie pozwolić na takie „sprzętowe” ułatwienie.
Schemat ideowy i montażowy do przykładu z linijką LED
Podczas podłączania tylu diod, na pewno przyda się mała ściąga z oznaczeniem lokalizacji pinów:
Lokalizacja pinów używanych w ćwiczeniu
Skoro nasz układ jest już gotowy, czas przystąpić do konfiguracji pinów w CubeMX. Na potrzeby tego ćwiczenia można utworzyć nowy projekt, bo finalną wersję programu warto zapisać sobie na później – to będzie przydatny kawałek kodu. Po utworzeniu projektu dla układu STM32L476RG ustawiamy piny PB5-PB14 jako wyjścia; od razu nadajemy im też odpowiednie etykiety (od LED1 do LED10).
Jeśli pomylimy się podczas konfiguracji któregoś z wyjść, to zawsze możemy kliknąć w niego lewym przyciskiem myszki i wybrać opcję Reset_State.
Podczas ustawiania tylu pinów jako wejścia widać od razu, że piny posiadają do wyboru wiele różnych funkcji. Konfiguracja w roli wyjść to tylko mały wycinek ich możliwości – inne tryby pracy pinów omówimy w odpowiednich momentach w trakcie realizacji kolejnych ćwiczeń.
Konfiguracja wielu wejść dla linijki LED
Teraz możemy napisać pierwszą wersję naszego programu. Zacznijmy od prostej pętli, dzięki której na chwilę włączymy każdą z diod. Na pierwszy rzut oka ten program wydaje się prosty: włączamy diodę, czekamy 100 ms, wyłączamy ją, wracamy na początek pętli, włączamy kolejną diodę itd.
Całość działa jednak tylko dlatego, że zastosowano tutaj małą „sztuczkę”. Funkcje HAL_GPIO_WritePin i HAL_GPIO_ReadPin znamy już z poprzedniego przykładu, nowością jest jednak użycie tu przesunięcia bitowego, które sprawia, że „jakoś” włączamy kolejną diodę.
Żeby wyjaśnić, co robi taki zapis, najpierw trzeba odszukać definicję LED1_Pin. Można to zrobić „ręcznie” (sprawdzając plik main.h), a można też w kodzie programu kliknąć lewym przyciskiem myszki w LED1_Pin, jednocześnie trzymając wciśnięty klawisz CTRL.
Podświetlenie stałej, gdy trzymamy wciśnięty klawisz CTRL
Po kliknięciu zostaniemy automatycznie przeniesieni do fragmentu, który zawiera konkretne definicje.
C
1
2
3
4
5
6
7
8
9
10
#define LED1_Pin GPIO_PIN_5
#define LED1_GPIO_Port GPIOB
#define LED2_Pin GPIO_PIN_6
#define LED2_GPIO_Port GPIOB
#define LED3_Pin GPIO_PIN_7
#define LED3_GPIO_Port GPIOB
#define LED4_Pin GPIO_PIN_8
#define LED4_GPIO_Port GPIOB
#define LED5_Pin GPIO_PIN_9
#define LED5_GPIO_Port GPIOB
Jak widzimy, LED1_Pin to odwołanie do GPIO_PIN_5, natomiast LED1_GPIO_Port to GPIOB. Kolejne diody są podłączone do tego samego portu oraz do kolejnych pinów – GPIO_PIN_6, GPIO_PIN_7 itd.
Te definicje to właśnie kod, który automatycznie wygenerował i wstawił CubeMX.
Teraz warto zobaczyć, jak wyglądają definicje tych stałych. Trzymamy więc klawisz CTRL i klikamy w jedną ze stałych (np. GPIO_PIN_5). Tym razem zostaniemy przeniesieni do pliku stm32l4xx_hal_gpio.h.
To już nie jest kod generowany przez CubeMX. Tym razem trafiliśmy do pliku z biblioteki HAL. Widzimy tutaj, że każda stała skrywa pod sobą jedną liczbę zapisaną w systemie szesnastkowym. Gdyby były one zapisane binarnie, to całość wyglądałaby (w uproszczeniu) następująco:
GPIO_PIN_5 = 0000 0010 0000
GPIO_PIN_6 = 0000 0100 0000
GPIO_PIN_7 = 0000 1000 0000
GPIO_PIN_8 = 0001 0000 0000
GPIO_PIN_9 = 0010 0000 0000
Jak widać, każda wartość to 1 na kolejnej pozycji. Wiedząc, że diody mamy podłączone do kolejnych pinów tego samego portu, możemy użyć przesunięcia bitowego, aby „obliczyć” wartość, która w bibliotece HAL odpowiada danemu wyprowadzeniu. Takie rozwiązanie ma swoje plusy, bo wynikowy program jest bardzo krótki i mając odpowiednią wiedzę, można było uzyskać ten efekt bardzo szybko.
Taka wersja programu ma też niestety poważne minusy, ponieważ działa, tylko jeśli podłączymy diody do kolejnych pinów tego samego portu. Spróbujmy więc nieco zmienić nasz program, aby był znacznie łatwiejszy w interpretacji i bardziej uniwersalny.
Własne funkcje pomocnicze
Poprzedni program działał poprawnie i pokazywał często spotykane (w przypadku mikrokontrolerów) metody optymalizacji kodu. Niestety taki kod jest mało czytelny, a konieczność wyboru kolejnych pinów w obrębie jednego portu bywa trudna do realizacji sprzętowej.
Często podłączenie takich diod do kolejnych pinów byłoby wręcz niemożliwe, bo konkretne wyprowadzenie (z uwagi na jego funkcje alternatywne) może być już używane do innego zadania.
Zacznijmy od utworzenia nowej funkcji, która będzie odpowiedzialna za sterowanie świeceniem diody. Dotychczas wywoływaliśmy bezpośrednio HAL_GPIO_WritePin, chcąc zapalić lub zgasić diodę. Teraz ten sam kod przeniesiemy do naszej nowej funkcji pomocniczej.
Załóżmy, że nasza nowa funkcja będzie nazywała się led_set. Następna kwestia to parametry. Musimy jakoś identyfikować diody podłączone do płytki. Wiele osób w takiej sytuacji zapewne zastosowałoby np. typ wyliczeniowy (enum). Na początek wybierzemy jednak tutaj najprostszą opcję i na wzór Arduino będziemy diody numerować od zera zmienną typu int.
Ostatnia decyzja to zastosowanie jednej lub dwóch funkcji, bo możemy mieć oddzielną funkcję do włączania i wyłączania diody albo jedną do obu zadań (ale za to z dodatkowym parametrem). Tym razem wybierzmy jedną funkcję z dodatkowym parametrem typu bool, który będzie miał wartość true, gdy będziemy chcieli włączyć diodę, lub false, gdy będziemy chcieli ją wyłączyć.
Pierwsze podejście do naszej nowej funkcji pomocniczej wygląda więc następująco (funkcję dodajemy w odpowiednim miejscu nad funkcją main):
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Private user code ---------------------------------------------------------*/
W środku funkcji deklarujemy zmienną typu GPIO_PinState, której przypisujemy odpowiednią wartość w zależności od wartości parametru turn_on. Następnie sprawdzamy, czy podany numer diody mieści się w spodziewanym zakresie (0–9), i za pomocą funkcji HAL włączamy lub wyłączamy daną diodę.
Próba kompilacji programu nie uda się jednak, bo kompilator nie rozpoznaje typu bool. Aby naprawić ten błąd, musimy dołączyć nagłówek stdbool.h. Na początku naszego pliku main.c znajdziemy miejsce przewidziane na dodawanie nagłówków – dopisujemy w nim odpowiednią linijkę.
C
1
2
3
4
5
6
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdbool.h>
/* USER CODE END Includes */
Teraz zmieniamy kod w pętli głównej – w taki sposób, aby korzystał z nowej funkcji.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while(1)
{
for(inti=0;i<10;i++){
// zapal diodę
led_set(i,true);
// poczekaj 100 ms
HAL_Delay(100);
// zgaś diodę
led_set(i,false);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Po tej zamianie program powinien działać tak samo dobrze jak poprzednia wersja, a skoro działa, to możemy zabrać się nareszcie za udoskonalanie naszej funkcji led_set.
Diody podłączone do różnych portów
W poprzedniej wersji używaliśmy zawsze stałej LED1_GPIO_Port, bo wiedzieliśmy, że LED1_GPIO_Port, LED2_GPIO_Port, LED2_GPIO_Port i kolejne miały taką samą wartość. Co więcej, diody musiały być podłączone do kolejnych wyprowadzeń, co bardzo ograniczało wybór pinów. Teraz chcemy, żeby nasz program działał poprawnie, nawet jeśli kolejność będzie zupełnie inna.
Jako pierwsze rozwiązanie nasuwa się użycie instrukcji warunkowej switch, bo dla każdej diody można ręcznie napisać odpowiedni przypadek. Taka wersja funkcji mogłaby wyglądać tak jak poniżej (przy okazji: warunek, który przypisuje odpowiednią wartość do zmiennej state, został zapisany w skróconej formie – działa identycznie jak poprzednio).
Jak widać, kod jest długi, ale ma kilka zalet. Przede wszystkim każda dioda może być podłączona do dowolnego portu i pinu. Mamy więc dużą elastyczność, ale za to długi i może niezbyt piękny program. Dla kilku diod to pewnie całkiem dobre rozwiązanie, jednak nawet już przy 10 sztukach widać, że warto byłoby zmniejszyć duplikację kodu.
Używanie tablic zamiast duplikacji kodu
Teraz dla odmiany spróbujmy użyć tablic. Wróćmy do pierwszej wersji programu i zastanówmy się, jak moglibyśmy go poprawić. Najpierw pozbędziemy się przesunięcia bitowego – wystarczy, że użyjemy tu odpowiedniej tablicy.
C
1
2
3
4
staticconstuint16_t LED_PIN[]={
LED1_Pin,LED2_Pin,LED3_Pin,LED4_Pin,LED5_Pin,
LED6_Pin,LED7_Pin,LED8_Pin,LED9_Pin,LED10_Pin,
};
Wartości w tej tablicy będą odpowiadały wartościom, które wcześniej po prostu obliczaliśmy. Możemy więc naszą funkcję led_set zapisać następująco:
Program możemy po raz kolejny przetestować i sprawdzić, czy działa poprawnie. Kod faktycznie działa, ale ma minimum 2 wady: diody nadal muszą być podłączone do tego samego portu, a odwoływanie się do tablicy za pomocą indeksu podanego jako parametr może powodować błędy.
Zacznijmy od sprawdzenia, czy wartość parametru led jest na pewno poprawna (wystarczy warunek):
Zajmijmy się odwołaniem do LED1_GPIO_Port. Jak łatwo się domyślić, potrzebujemy teraz drugiej tablicy, w której będziemy przechowywali odnośniki do użytych portów. Porty GPIO są w bibliotece HAL reprezentowane przez typ GPIO_TypeDef, dodajemy więc tablicę wskaźników do takiego typu.
Dodawanie kolejnych tablic dla parametrów sprawia jednak, że dość trudno jest powiązać ze sobą dane opisujące określoną diodę. Możemy postąpić jeszcze lepiej. Wystarczy zdefiniować strukturę, która opisze podłączenie kolejnych diod.
Tworzymy więc nowy typ o nazwie pin_t i zapisujemy w nim port oraz pin, do którego podłączone są nasze diody. Kolejna wersja funkcji może wyglądać następująco:
Taki zapis sprawi, że nasza pozornie prosta linijka świetlna będzie działała niezależnie od tego, jakie piny i porty wybierzemy do tego, aby podłączyć w praktyce poszczególne diody. Nie trzeba nic już zmieniać w kodzie naszej funkcji.
Wystarczy, że zmienimy ustawienia w konfiguratorze, przypiszemy etykiety do odpowiednich pinów i wygenerujemy od nowa kod.
Zaprezentowana wersja funkcji oczywiście nie jest idealna, należałoby pomyśleć o jej dalszym udoskonalaniu oraz np. o przeniesieniu jej do oddzielnego pliku. W tym przykładzie chcieliśmy jednak pokazać przy okazji też coś innego – funkcje udostępniane przez bibliotekę HAL mogą być używane we własnych funkcjach „pomocniczych” i często znacznie lepiej jest napisać własną małą bibliotekę „opakowującą” HAL, zamiast bezpośrednio odwoływać się do niej w naszym programie.
Sterowanie linijką LED za pomocą przycisku
Wiemy już, jak sterować naszą linijką diod, możemy teraz nieco skomplikować ten przykład i uczynić go bardziej interaktywnym poprzez dodanie obsługi przycisku – wykorzystajmy przycisk, który jest już na Nucleo do tego, aby sterować diodami. Zacznijmy od programu, który będzie włączał kolejne LED-y po naciśnięciu przycisku.
Wracamy więc do widoku CubeMX i konfigurujemy pin PC13 w trybie wejścia. Tak samo jak poprzednio, nazywamy go USER_BUTTON. Teraz możemy pisać nasz program. Zaczniemy od dodania nowej funkcji. Sprawdziliśmy już, że własne funkcje do sterowania diodami poprawiają czytelność programu – możemy to samo zrobić z przyciskiem.
Zaktualizowana konfiguracja projektu
Do odczytu stanu można użyć wywołania funkcji HAL_GPIO_ReadPin, która zwraca GPIO_PIN_RESET, gdy przycisk jest wciśnięty. Proste i skuteczne, ale można uprościć sobie życie, pisząc własną funkcję, np.: is_button_pressed. Funkcja będzie zwracała wartość logiczną: true, jeśli przycisk będzie wciśnięty, i false, jeśli będzie puszczony. Jej kod może wyglądać następująco (dodajemy ją oczywiście nad funkcją main w tym samym bloku komentarzy, w którym jest funkcja włączająca diody):
Skoro napisaliśmy funkcję, to wypadałoby sprawdzić, czy działa. Napiszmy więc program podobny do poprzedniego, ale z nową funkcją do odczytu przycisku oraz własnymi funkcjami sterującymi diodami:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while(1)
{
if(is_button_pressed()){
led_set(0,true);
}else{
led_set(0,false);
}
}
*/
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Teraz możemy przetestować włączanie diod przyciskiem – powinno to działać zgodnie z oczekiwaniami, czyli naciśnięcie niebieskiego przycisku na płytce Nucleo włączy pierwszą diodę (nr 0) w linijce. My jednak chcieliśmy zapalać kolejne diody, a nie ciągle tą samą (i tylko wtedy, gdy wciśnięty jest przycisk).
Zacznijmy od napisania programu w taki sposób, jak dosłownie brzmi zadanie – czyli jeśli jest wciśnięty przycisk, to wyłącz obecnie włączoną diodę i włącz kolejną. Kod może wyglądać następująco (zwróć uwagę na 2 linijki, które zostały dopisane przez początkiem pętli while:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Infinite loop */
/* USER CODE BEGIN WHILE */
intled=0;
led_set(led,true);
while(1)
{
if(is_button_pressed()){
// Po wcisnieciu przycisku wylacz diodę
led_set(led,false);
// Zwieksz zawartosc zmiennej led
led++;
// Sprawdz czy nie przekracza zakresu
if(led>=10){
led=0;
}
// Wlacz kolejna diode
led_set(led,true);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Warto taki program od razu uruchomić. Jego działanie może wydawać się nieco zaskakujące i wygląda to tak, jakby mikrokontroler robił coś zupełnie innego, niż opisuje nasz program: po naciśnięciu przycisku zapalają się wszystkie diody, a po zwolnieniu zapalona pozostaje jedna.
Popatrzmy jeszcze raz na działanie programu: gdy przycisk jest przyciśnięty, włączamy kolejne diody – ale nie czekamy na zwolnienie przycisku, tylko po chwili sprawdzamy ponownie i przechodzimy do kolejnej diody. W związku z tym, że takie przełączanie odbywa się bardzo szybko, wydaje się, że wszystkie diody są włączone – program działałby wtedy nieprawidłowo. Aby sprawdzić, czy takie podejrzenia są prawdziwe, dodajmy opóźnienie po przejściu do kolejnej diody.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* Infinite loop */
/* USER CODE BEGIN WHILE */
intled=0;
led_set(led,true);
while(1)
{
if(is_button_pressed()){
// Po wcisnieciu przycisku wylacz diodę
led_set(led,false);
// Zwieksz zawartosc zmiennej led
led++;
// Sprawdz czy nie przekracza zakresu
if(led>=10){
led=0;
}
// Wlacz kolejna diode
led_set(led,true);
HAL_Delay(500);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Uruchamiamy ten program i testujemy. Widać, że teraz jest dużo lepiej i bliżej naszych oczekiwań – naciskając przycisk, zapalamy kolejne diody. Jednak program ma co najmniej dwa niedociągnięcia. Po pierwsze, jeśli przyciśniemy przycisk i będziemy go trzymać, to zapalane będą kolejne diody, a mieliśmy przechodzić do kolejnej tylko po przyciśnięciu. Druga niedoskonałość będzie widoczna, jeśli szybko będziemy naciskać przycisk – program nie zawsze reaguje na kolejne wciśnięcia.
Problemy w naszym programie wynikały z wywołania HAL_Delay. Jeśli przycisk był naciśnięty na dłużej niż 500 ms, to wykrywaliśmy dwa przyciśnięcia. Co więcej, jeśli w ciągu 500 ms użytkownik nacisnął przycisk więcej niż raz, to kolejne przyciśnięcia były ignorowane. Zmieńmy więc program tak, żeby nie zatrzymywać się na 500 ms, tylko czekać na zwolnienie przycisku.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* Infinite loop */
/* USER CODE BEGIN WHILE */
intled=0;
led_set(led,true);
while(1)
{
// sprawdź czy przycisk jest wciśnięty
if(is_button_pressed()){
// jeśli tak to zgaś aktualną diodę
led_set(led,false);
// wybierz kolejną
if(++led>=10){
led=0;
}
// zapal nową diodę
led_set(led,true);
// czekamy na zwolnienie przycisku
while(is_button_pressed())
{}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Wreszcie program działa dokładnie tak, jak oczekiwaliśmy, ale to jeszcze oczywiście nie koniec.
Podłączanie własnego przycisku
Przykład z diodami działa pięknie, więc warto dodać do niego następną funkcję. Mamy przycisk do włączania kolejnych diod, lecz przydałby się taki, który pozwoli włączać poprzednie. Na płytce Nucleo znajdziemy drugi przycisk, ale działa on jako reset, więc tym razem podłączymy dodatkowy przycisk.
Schemat ideowy oraz montażowy dla wersji z dodatkowym przyciskiem
Po podłączeniu układu na płytce możemy przejść do CubeMX i dodać konfigurację kolejnego pinu. Tak jak poprzednio, używamy pinu PC13, do którego podłączony jest nasz USER_BUTTON, dodajemy tylko informację o kolejnym przycisku, który podłączamy do pinu PC8 – nazwijmy go USER_BUTTON2.
Nowa konfiguracja z drugim przyciskiem
Mamy już funkcję is_button_pressed, która dotychczas nie miała parametrów, bo mieliśmy tylko jeden przycisk. Teraz chcemy obsługiwać dwa przyciski (ale zawsze możemy podłączyć więcej), więc dodamy parametr button – będzie on przechowywał numer przycisku, który chcemy sprawdzić. W związku z tym, że mamy tylko dwa przyciski, możemy na szybko napisać kod z instrukcją switch.
Teraz możemy wrócić do głównego programu. W dotychczasowym kodzie dodajemy jako parametr wywołania is_button_pressed wartość 0 oraz dopisujemy obsługę drugiego przycisku, który będzie zapalał poprzednią diodę.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* Infinite loop */
/* USER CODE BEGIN WHILE */
intled=0;
led_set(led,true);
while(1)
{
if(is_button_pressed(0)){
// Po wcisnieciu przycisku wylacz diodę
led_set(led,false);
// Zwieksz zawartosc zmiennej led
led++;
// Sprawdz czy nie przekracza zakresu
if(led>=10){
led=0;
}
// Wlacz kolejna diode
led_set(led,true);
// czekamy na zwolnienie przycisku
while(is_button_pressed(0)){}
}
if(is_button_pressed(1)){
// Po wcisnieciu przycisku wylacz diodę
led_set(led,false);
// Zmniejsz zawartosc zmiennej led
led--;
// Sprawdz czy nie przekracza zakresu
if(led<0){
led=9;
}
// Wlacz kolejna diode
led_set(led,true);
// czekamy na zwolnienie przycisku
while(is_button_pressed(1)){}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
Program jest gotowy do testów, więc go uruchamiamy i… okazuje się, że nie działa zgodnie z planem. Uzyskany efekt może być dość losowy, zapalone diody mogą dość szybko się zmieniać (tworząc przypadkiem nawet całkiem ładny efekt świetlny). Natomiast samo przyciśnięcie przycisku na płytce stykowej działa, tzn. sprawia, że diody przestają się samoczynnie przełączać.
Powodem naszych problemów jest brak rezystora podciągającego. Podłączyliśmy przełącznik, który zwiera pin wejściowy do masy układu, jednak gdy przycisk nie jest naciśnięty, to wejście naszego układu „wisi w powietrzu” i zbiera zakłócenia. Taka konfiguracja jest najczęściej efektem błędu w projekcie, musimy więc ją poprawić. Mamy co najmniej dwie możliwości – pierwsza to fizyczne podłączenie rezystora. Możemy to zrobić w bardzo prosty sposób.
Schemat ideowy i montażowy z zewnętrznym rezystorem podciągającym
Druga możliwość to aktywowanie rezystora podciągającego, który jest wewnątrz mikrokontrolera. Jest to funkcja, którą spotkać można w większości układów – trzeba tylko wiedzieć, jak ją włączyć. W tym przypadku jest to bardzo proste. Wystarczy, że w CubeMX zmienimy konfigurację pinu PC8 i z listy rozwijanej wybierzemy tryb Pull-up.
Aktywacja rezystora podciągającego na wejściu
Nie musimy nic więcej zmieniać w kodzie – wystarczy zapisać nową konfigurację sprzętową, pozwolić na wygenerowanie kodu, skompilować i uruchomić program. Układ zadziała już (prawie) idealnie.
Drgania styków
Jeśli spędzimy trochę czasu, testując wcześniejszy program, to zauważymy dość ciekawą zależność – wciśnięcie przycisku na płytce Nucleo zawsze przełącza na kolejną diodę, ale nasz przycisk (na płytce stykowej) czasem przeskakuje np. o dwie diody. Ten efekt jest losowy, występuje bardzo rzadko, ale czasem się pojawia. Odpowiedzialne za niego są tzw. drgania styków.
Ich występowanie związane jest z działaniem przełącznika, który sprawia, że podczas przyciskania albo – częściej – zwalniania przycisku pojawiają bardzo szybkie zmiany na wejściu układu. Zmiany te mogą być przez program interpretowane jako dodatkowe wciśnięcia przycisku.
Drgania styków w praktyce – widziane okiem oscyloskopu
Tutaj też mamy wiele możliwości naprawienia naszego programu. Pierwsza to sprzętowa eliminacja przerwań. W tym celu możemy np. wykorzystać tzw. filtr RC. Na płytce Nucleo znajduje się filtr, dlatego używając przycisku USER_BUTTON, nie obserwowaliśmy dodatkowych przyciśnięć.
Filtr RC to hasło, które pojawiło się już w naszym kursie elektroniki. Wspominaliśmy tam, że pewne połączenie rezystora i... Czytaj dalej »
Drgania styków można też eliminować programowo. Jest to nieco trudniejszy temat, ale w najprostszej implementacji wystarczy po wciśnięciu lub zwolnieniu przycisku odczekać kilkadziesiąt milisekund. Ten czas wystarczy na ustabilizowanie odczytów z przełącznika. Na ten moment możemy to rozwiązać za pomocą jednego krótkiego opóźnienia (po puszczeniu przycisku USER_BUTTON2).
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* Infinite loop */
/* USER CODE BEGIN WHILE */
intled=0;
led_set(led,true);
while(1)
{
// sprawdź czy przycisk jest wciśnięty
if(is_button_pressed(0)){
// jeśli tak to zgaś aktualną diodę
led_set(led,false);
// wybierz kolejną
if(++led>=10)
led=0;
// zapal nową diodę
led_set(led,true);
// czekamy na zwolnienie przycisku
while(is_button_pressed(0))
{}
}
// sprawdź czy drugi przycisk jest wciśnięty
if(is_button_pressed(1)){
// jeśli tak to zgaś aktualną diodę
led_set(led,false);
// wybierz kolejną
if(--led<0)
led=9;
// zapal nową diodę
led_set(led,true);
// oczekiwanie na ustanie drgań styków
HAL_Delay(20);
// czekamy na zwolnienie przycisku
while(is_button_pressed(1))
{}
// opóźnienie na wypadek drgań po zwolnieniu przycisku
HAL_Delay(20);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
Teraz nasz program będzie działał już poprawnie:
Sterowanie linijką LED za pomocą dodatkowego przycisku
O co chodzi z tym generowaniem kodu?
Wiele razy wspominaliśmy, że CubeMX generuje kod. Cały proces jest skomplikowany, ale efekt działania tego generatora jest bardzo prosty. CubeMX dosłownie generuje kod programu, który normalnie musielibyśmy pisać sami, i wkleja go za nas do programu. Dowód? Wystarczy zerknąć do funkcji MX_GPIO_Init, która jest w pliku main.c. Znajdziemy tam kod, który powstał na bazie tego, co wyklikaliśmy w CubeMX – warto prześledzić ten kod w ramach ciekawostki.
Wróć do przykładu, w którym dioda włączała się na 200 ms i gasła na 800 ms. Zastanów się, jak odwrócić działanie tego programu bez zmieniania jego kodu.
Podłącz 10 diod do innych, losowo wybranych pinów. Zmień konfigurację w CubeMX i sprawdź, czy program nadal działa zgodnie z naszymi założeniami.
Dodaj do programu kolejny przycisk, który będzie resetował linijkę.
Podsumowanie – co powinieneś zapamiętać?
Za nami pierwsze programy, w których wykorzystaliśmy GPIO – peryferia te od teraz będą towarzyszyły nam prawie zawsze. Zwykłe miganie diodą czy obsługa przycisków to podstawy, dzięki którym można obsłużyć bardzo dużo popularnych peryferiów. Najważniejsze, abyś po lekturze tej części kursu potrafił samodzielnie skonfigurować projekt korzystający z GPIO. Nie musisz samodzielnie z głowy odtwarzać przykładu z linijką, strukturami i wskaźnikami.
Czy wpis był pomocny? Oceń go:
Średnia ocena 4.8 / 5. Głosów łącznie: 150
Nikt jeszcze nie głosował, bądź pierwszy!
Artykuł nie był pomocny? Jak możemy go poprawić? Wpisz swoje sugestie poniżej. Jeśli masz pytanie to zadaj je w komentarzu - ten formularz jest anonimowy, nie będziemy mogli Ci odpowiedzieć!
W następnym artykule zajmiemy się innym bardzo ważnym peryferium – konkretnie UART-em, dzięki któremu możliwe będzie przesłanie informacji z mikrokontrolera do komputera (chociaż to nie jedyne zastosowanie dla UART-u). Przy okazji sprawdzimy również, czym dokładnie jest debugger i do czego może się on przydać podczas testowania programów.
Główny autor kursu: Piotr Bugalski Współautor: Damian Szymański, ilustracje: Piotr Adamczyk Oficjalnym partnerem tego kursu jest firma STMicroelectronics Zakaz kopiowania treści kursów oraz grafik bez zgody FORBOT.pl
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY na bazie Arduino i Raspberry Pi.
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY z Arduino i RPi.
Trwa ładowanie komentarzy...