Kurs STM32 LL cz. 5. Budowa GPIO i sterowanie wyjściem

Pierwszym programem, który uruchamiamy ucząc się programowanie jest tzw. „Hello world”. W przypadku projektów embedded pod tym hasłem mamy na myśli nie tyle wyświetlanie tekstu, co zaświecenie i zgaszenie diody LED. Do tego potrzebna będzie nam obsługa portów mikrokontrolera, czyli GPIO.

Budowa portu GPIO

GPIO to podstawowy układ peryferyjny w mikrokontrolerach. Stanowi punkt wyjścia do poprawnego skonfigurowania wszystkich pozostałych peryferiów, które używają nóżek mikrokontrolera.

Pin GPIO może pracować w jednej z 4 konfiguracji:

  • Wejście (Input) – pływające (floating), podciągnięcia do VDD (pull-up), podciągnięcie do VSS, czyli GND (pull-down)
  • Wyjście (Output) – z otwartym drenem (Open-Drain) z podciągnięciem pull-up lub pull-down, komplementarne (Push-Pull) z podciągnięciem pull-up lub pull-down
  • Analogowy (Analog)
  • Funkcja alternatywna – z podciągnięciem pull-up lub pull-down

Schemat budowy pinu GPIO przedstawia poniższa grafika.

Struktura GPIO podzielona została na dwie części zawierające obsługę rejestrów: analogową i cyfrową. Trzecia część odpowiada za wykonywanie operacji na pinie IO. W niej znajdziemy przełącznik funkcji analogowych, bufor wejściowy z przerzutnikiem Schmitta oraz bufor wyjściowy z blokiem sterowania i dwoma tranzystorami MOSFET. Poza tym każdy pin ma rezystory podciągające (pull-up i pull-down), zabezpieczenie ESD i diody.

 Pod względem poziomów napięć na wyprowadzeniu GPIO rozróżniamy:

  • Three-volt tolerant (TT) lub Three-volt compliant (TC) – napięcie na pinie nie może przekroczyć VDD + 0,3 V, a dla pinów analogowych lub OPAMP/COMP nie może przekroczyć min(VDD, VREF+) + 0,3 V
  • Five-volt tolerant (FT) – napięcie na pinie nie może przekroczyć VDD + 3,6 V, jednak nie więcej niż 5,5 V. Wyprowadzenie jest FT tylko w trybie input (w trybie output lub analog FT jest nieaktywne)

Każdy pin może pracować z różną szybkością, czyli maksymalną częstotliwością przełączania stanu. Określa ona, jak szybko sygnały na wyjściu narastają oraz opadają, co wiąże się z dostępną maksymalną częstotliwością zmian na wyjściu. Można wybrać jedną z czterech szybkości:

  • Very low speed – do 2 MHz
  • Low Speed – 10 MHz
  • High Speed – 30 MHz
  • Very High Speed – 60 MHz

Warto wiedzieć, że im większa szybkość pracy pinu, tym większe zakłócenia EMI może powodować.

Rejestry GPIO

Podczas konfiguracji GPIO mamy do dyspozycji kilka ustawień, które wiążą się bezpośrednio z poszczególnymi funkcjonalnościami pinów. Konfigurujemy je poprzez wpisanie odpowiednich wartości w rejestrach. 

Rejestr GPIOx_MODER – wybór trybu pracy pinu

Mamy do dyspozycji 4 tryby: wejście, wyjście, funkcja alternatywna oraz tryb analogowy. Po resecie mikrokontrolera rejestr ten przyjmuje wartość 0xFFFFFFFF, co oznacza, że piny domyślnie są w trybie analogowym.

GPIOx_OTYPER – wybór typu wyjścia

Możemy je skonfigurować jako Push-Pull lub Open-Drain. Po resecie mikrokontrolera piny domyślnie są w trybie push-pull.

GPIOx_OSPEEDR – wybór szybkości wyjścia

Mamy do wyboru 4 wartości: very low, low, high oraz very high. Po resecie mikrokontrolera piny domyślnie mają szybkość very low.

GPIOx_PUPDR – wybór rodzaju podciągnięcia

Mamy do dyspozycji rezystor pull-up lub pull-down. Domyślnie pin nie jest podciągnięty przez żaden z rezystorów. 

GPIOx_IDR – czyli rejestr danych wejściowych

Możemy z niego odczytać aktualny stan na pinie IO.

GPIOx_ODR – czyli rejestr danych wyjściowych

W nim możemy umieścić stan pinów na wyjściu. Można z niego również odczytać ostatnio ustawiony stan wyjścia.

GPIOx_BSRR – rejestr służący do ustawiania stanu na wyjściu

Modyfikuje on rejestr ODR, jeżeli wpisywana jest do danego bitu “1”. Pozwala w atomowy sposób modyfikować stan pojedynczych pinów bez wpływu na pozostałe wyjścia. Możemy za jego pomocą ustawić na wyjściu stan wysoki (bity BS), jak i niski (bity BR).

GPIOx_AFRL i GPIOx_AFRH – rejestry konfigurujące rodzaj funkcji alternatywnej pinu

Odpowiada za użycie pinu jako elementu innego układu peryferyjnego, np USART, SPI czy I2C. Dokładny opis każdej funkcji alternatywnej znajduje się w dokumentacji Datasheet konkretnego mikrokontrolera.

GPIOx_BRR – rejestr służący do ustawiania stanu niskiego na wyjściu

Modyfikuje on rejestr ODR, jeżeli wpisywana jest do danego bitu “1”. Pozwala w atomowy sposób modyfikować stan pojedynczych pinów bez wpływu na pozostałe wyjścia. Stanowi częściową alternatywę dla rejestru BSRR.

Sterowanie wyjściem GPIO – dioda LED

Wstęp ogólny na temat GPIO mamy za sobą. Dzisiaj zajmiemy się jedną z podstawowych funkcji GPIO, czyli pracą jako pin wyjściowy. Nauczymy się konfigurować rejestry oraz sterować pinem, ustawiając na jego wyjściu stan wysoki lub niski.

Budowa GPIO – praca w funkcji wyjścia

Na początku warto zapoznać się z tym, jak zbudowany jest port GPIO w mikrokontrolerze. Pozwoli nam to lepiej zrozumieć poszczególne elementy konfiguracji.

Schemat budowy GPIO w STM32 z zaznaczonymi funkcjonalnościami w trakcie pracy jako wyjście możemy zobaczyć na grafice poniżej.

Część odpowiadająca za sterowanie wyjściem jest umieszczona w dolnym bloku “Output driver”. Składa się z bloku sterującego oraz dwóch tranzystorów MOSFET – PMOS i NMOS. Tranzystor PMOS “łączy” pin IO z dodatnim biegunem zasilania VDD (plusem), a tranzystor NMOS z ujemnym VSS (minusem).

Do bloku sterującego wpływają dane z rejestru “Output data register – GPIOx_ODR”. Górny blok (Input driver) przedstawia część odpowiedzialną za funkcję wejścia. Jest ona również aktywna w momencie pracy pinu jako wyjście, dzięki czemu możemy odczytać z rejestrów wejścia (GPIOx_IDR). Jest to przydatne w niektórych zastosowaniach, np. do dwustronnej komunikacji w interfejsie I2C.

W przypadku wyjścia GPIO mamy do czynienia z dwoma trybami:

  • wyjście komplementarne (push-pull) – inwerter CMOS
  • wyjście z otwartym drenem (open drain)

Wyjście komplementarne wykorzystuje dwa tranzystory PMOS oraz NMOS. W trakcie sterowania wyjściem GPIO zawsze jeden z tranzystorów jest w stanie przewodzenia, a drugi nie.

Gdy wyjście ma stan wysoki (w rejestrze ODR ustawimy “1”), przewodzi tranzystor PMOS, a NMOS nie (blok Output Control neguje sygnał). Powoduje to przepływ napięcia od VDD do wyjścia – na wyjściu pojawia się dodatni biegun zasilania.

Gdy wyjście ma stan niski (w rejestrze ODR ustawimy “0”)i, przewodzi tranzystor NMOS, a PMOS nie (blok Output Control neguje sygnał). Powoduje to przepływ napięcia od wyjścia do GND – na wyjściu pojawia się ujemny biegun zasilania.

Wyjście z otwartym drenem wykorzystuje tylko jeden tranzystor – NMOS. 

Takie rozwiązanie sprawia, że:

  • Gdy wyjście ma stan wysoki, tranzystor NMOS nie przewodzi. Wyjście jest wówczas odłączone zarówno od VDD, jak i GND.
  • Gdy wyjście ma stan niski, tranzystor NMOS przewodzi. Powoduje to przepływ napięcia od wyjścia do GND – na wyjściu pojawia się ujemny biegun zasilania.

Wyjście Open Drain zachowuje się zatem analogicznie jak Push-Pull, gdy jest ustawiony stan niski, a inaczej przy stanie wysokim. Wyjście w trybie Open Drain wymaga podłączenia rezystora podciągającego. Można tutaj zastosować wewnętrzny rezystor Pull Up (zazwyczaj 40 kΩ) lub dodać do obwodu zewnętrzny rezystor o wartości, jakiej potrzebujemy.

Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.

[PROGRAM] Konfiguracja i sterowanie wyjściem GPIO

Znamy już podstawową budowę GPIO. Ta wiedza pozwoli nam lepiej zrozumieć, dlaczego tak a nie inaczej konfigurujemy wyjście oraz dlaczego niektóre rejestry GPIO możemy pominąć.

Na początku musimy uruchomić zegar dla GPIO. Bez tego port nie będzie pracował. W programie będziemy sterowali wyjściem PA5 (oznaczony etykietą LED_GREEN), dlatego włączamy zegar dla GPIOA.

LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);

Teraz powinniśmy ustawić parametry portu. Dla wyjścia kluczową informacją jest to, czy będzie pracowało w trybie Push-Pull, czy Open-Drain. Będziemy chcieli korzystać z obu tranzystorów (móc ustawić zarówno “0”, jak i “1”), zatem wybieramy tryb Push-Pull.

LL_GPIO_SetPinOutputType(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_OUTPUT_PUSHPULL);

Następnie konfigurujemy rezystory podciągające. Możemy ustawić podciągnięcie Pull-up, Pull-down lub odłączyć oba rezystory. Dla wyjścia Push-Pull wybieramy opcję trzecią.

LL_GPIO_SetPinPull(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_PULL_NO);

Następnie wybieramy szybkość (w zasadzie maksymalną częstotliwość pracy) wyjścia

LL_GPIO_SetPinSpeed(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_SPEED_FREQ_LOW);

Na koniec ustawiamy tryb pracy pinu, czyli w naszym wypadku jako wyjście.

LL_GPIO_SetPinMode(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_MODE_OUTPUT);

W tym momencie mamy skonfigurowane wszystkie elementy niezbędne do sterowania pinem jako wyjście. W rejestrach GPIO mamy jeszcze możliwość konfiguracji rodzaju funkcji alternatywnej (Alternate Function), ale przy ustawieniu pinu jako wyjście wartość tego rejestru nie ma znaczenia. Możemy go pominąć.

Dla spostrzegawczych osób niepotrzebne może się wydać konfigurowanie wszystkich rejestrów, które przedstawiłem przed chwilą. Tak właściwie tylko rejestr określający tryb pracy pinu (GPIOx_MODER) jest konieczny – wszystkie pozostałe i tak przyjmują taką wartość domyślną (czyli 0), jaką ustawiliśmy. Nie warto jednak ograniczać kodu o te kilka linijek – unikniemy komplikacji w momencie, gdy będziemy chcieli przekonfigurować pin w trakcie pracy programu na inną funkcjonalność i zostaną stare wartości w rejestrach. Poza tym łatwiej będzie nam skonfigurować pin pod inne zastosowania.

Konfigurację trybu pracy mamy gotową. Pozostaje nam wysterować stan na wyjściu. Możemy to zrobić na trzy sposoby:

  • Ustawić stan wysoki na wyjściu (rejestr BSRR)
LL_GPIO_SetOutputPin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
  • Ustawić stan niski na wyjściu (rejestr BRR)
LL_GPIO_ResetOutputPin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
  • Zmienić stan na wyjściu na przeciwny (rejestr BSRR na podstawie odczytu z ODR)
LL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);

Na koniec rozważań warto zastanowić się, dlaczego biblioteka LL do zmiany stanu wyjścia wykorzystuje rejestry BSRR i BRR, a nie rejestr ODR? Bo te rejestry zostały dodane w strukturze GPIO dla ułatwienia pracy programisty. Dlaczego? Ponieważ chcąc ustawić stan wyjścia na np. na pinie 5, musielibyśmy za każdym razem odczytać wartość rejestru ODR, nałożyć maskę na bicie 5 i wpisać wartość znowu do rejestru (tak jak to robi funkcja TogglePin). Inaczej dokonalibyśmy niekontrolowanych zmian także na innych pinach. Rejestry BSRR i BRR umożliwiają nam ustawienie wartości “0” lub “1” na konkretnym pinie bez konieczności sprawdzania, co jest na pozostałych.

A co, jeśli wybralibyśmy wyjście w trybie Open-Drain? Jak to wpłynie na sterowanie diodą LED?

W trybie Open-Drain jesteśmy w stanie ustawić wyjście w stanie niskim lub odciąć pin od obu stanów, co spowoduje, że będziemy mieli na nim stan nieustalony. Dioda mogłaby zachowywać się w sposób niekontrolowany i nie bylibyśmy w stanie poprawnie nią sterować. Moglibyśmy dodać rezystor podciągający “do plusa” (wewnętrzny lub zewnętrzny), co dałoby nam zbliżone możliwości do trybu Push-Pull.

LL_GPIO_SetPinOutputType(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_OUTPUT_OPENDRAIN);
LL_GPIO_SetPinPull(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_PULL_UP);

Timer systemowy – SysTick

Timer systemowy (SysTick) jest dedykowany dla systemów operacyjnych czasu rzeczywistego, ale może być również używany jako standardowy licznik zliczający w dół.

Funkcje zegara SysTick:

  • 24-bitowy licznik w dół
  • Możliwość automatycznego przeładowania
  • Generowanie przerwań systemu, gdy licznik osiągnie 0
  • Programowalne źródło zegara

W przypadku SysTick-a mamy do skonfigurowania rejestr kontrolny, czyli SysTick->CTRL. Znajdziemy w nim następujące bity:

  • CLKSOURCE – ustawienie sprawia, że licznik używa bezpośrednio zegara systemowego (AHB), wyzerowanie włącza dzielnik /8
  • TICKINT – włącza generowanie przerwania
  • ENABLE – uruchamia licznik

Zanim uruchomimy licznik, musimy jeszcze ustawić częstotliwość generowania przerwań. W tym celu do rejestru SysTick->LOAD wpisujemy pomniejszoną o 1 liczbę cykli zegara po których ma pojawić się przerwanie.

W przypadku bibliotek Low Layer cała konfiguracja SysTick-a odbywa się za pomocą funkcji LL_Init1msTick(), którą w naszych ćwiczeniach wywołujemy zawsze po konfiguracji zegarów mikrokontrolera. Jeżeli korzystamy z przerwań od SysTicka, musimy również włączyć je w kontrolerze NVIC funkcją LL_SYSTICK_EnableIT().

Warto wspomnieć, że SysTick jest elementem rdzenia ARM, dlatego identyczny timer znajdziemy w każdym mikrokontrolerze z tym rdzeniem. Dokładny jego opis znajdziemy w dokumentacji “Programming manual”.

[PROGRAM] Konfiguracja i sterowanie wyjściem + timer programowy z SysTick

Wiemy już jak sterować wyjściem GPIO oraz czym jest SysTick. Przejdźmy w takim razie do przykładu, w którym zaimplementujemy programowy timer w oparciu o SysTick. Kod jest na tyle uniwersalny, że bardzo łatwo jest go zastosować do innego timera w przypadku, gdybyśmy potrzebowali jakiegoś niestandardowego przedziału czasowego.

Do obsługi timera programowego stworzymy strukturę. Będziemy w niej przechowywali ilość ticków (ilość wystąpień przerwań timera SysTick, czyli ilość milisekund) oraz czas zadania (co ile milisekund ma się wykonać zadanie).

typedef struct
{
	uint32_t ms_tick;
	uint32_t task_time;
}software_timer_t;

W funkcji inicjalizującej zerujemy licznik milisekund oraz ustawiamy czas zadania.

void software_timer_task_init(software_timer_t *timer, uint32_t time)
{
	timer->ms_tick = software_timer_get_ms_tick();
	timer->task_time = time;
}

W funkcji software_timer_inc_ms_tick inkrementujemy wartość zliczaną przez SysTick. ms_tick jest zmienną statyczną.

static uint32_t ms_tick = 0;

void software_timer_inc_ms_tick(void)
{
	ms_tick++;
}

Funkcję software_timer_inc_ms_tick musimy umieścić w przerwaniu od timera sprzętowego SysTick.

void SysTick_Handler(void)
{
	software_timer_inc_ms_tick();
}

Ilość zliczonych “tick-ów” zwracamy przez funkcję software_timer_get_ms_tick.

uint32_t software_timer_get_ms_tick(void)
{
	return ms_tick;
}

Aby skonfigurować SysTick, należy wywołać dwie funkcje: LL_Init1msTick(), która ustawia rejestry licznika oraz LL_SYSTICK_EnableIT(), czyli włączenie przerwań.

LL_Init1msTick(64000000);
LL_SYSTICK_EnableIT();

W teraz w funkcji main() możemy zaimplementować timer programowy do zadania, jakim będzie miganie diodą.

software_timer_t led_timer;
software_timer_task_init(&led_timer, LED_TASK_TIME);

gdzie

#define LED_TASK_TIME 100

W pętli while(1) dodajemy obsługę timera dla naszego zadania. W przypadku gdy mamy kilka takich zadań, warto ten kod umieścić w oddzielnej funkcji, aby zwiększyć przejrzystość programu.

while (1)
  {
	  if((software_timer_get_ms_tick() - led_timer.ms_tick) >= led_timer.task_time)
	  {
		  led_timer.ms_tick = software_timer_get_ms_tick();
		  LL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
	  }
}

W ten sposób uruchomiliśmy miganie diodą z użyciem timera programowego. Takie zastosowanie SysTick-a pozwala nam uniknąć używanie funkcji LL_mDelay(), która blokuje wykonywanie programu. Efekt jest taki sam, a procesor „w międzyczasie” może wykonywać inne operacje.

Chciałbyś otrzymywać na bieżąco informacje o nowych artykułach z kursu? Zapisz się do newslettera!

TO NIE TYLKO MAIL Z INFORMACJĄ O NOWEJ LEKCJI, ALE TAKŻE DODATKOWE MATERIAŁY. NIE PRZEGAP NOWEJ TREŚCI I DODATKOWYCH BONUSÓW. PRZEJDŹ DO STRONY KURSU I PODAJ SWÓJ ADRES E-MAIL. NIE ZAPOMNIJ POTWIERDZIĆ CHĘCI DOŁĄCZENIA W PIERWSZEJ WIADOMOŚCI!
Repozytorium GitHub

  1. Co jeżeli ms_tick się przekręci ?? Wystąpi to po 25 dniach:
    2^31/1000/60/60/24 = 24.8551348148
    Znaczy to, że takie rozwiązanie nie może być zastosowane do jakiegoś projektu, w którym płytka będzie pracować bez przerwy przez kilka mies. W takim razie jak zapobiec takiemu przekręceniu? Są jakieś inne „techniki” na obsługiwanie przerwań?

  2. Cześć Piotrze,
    Fajnie, że w końcu przyklady z LL, mniej w Internecie niz HAL. Kawa postawiona!

    Mam pytanie co do obsługi przerwania. Czy to jest tzw. „dobra praktyka” umieszczanie handlerów przerwań w pliku 'stm32g0xx_it.c’ czy tak sobie poprostu wymyśliłeś? Ja wrzucałem (w AVRach) tam gdzie obsługiwałem dane peryferia, czyli np. przerwania od Uart -w pliku .c od uart, timer w pliku od timera itd.

    1. Cześć, dzięki za pozytywny odbiór. Zarówno jedno, jak i drugie rozwiązanie jest dobre. Ja akurat użyłem sposobu prezentowanego w bibliotekach od ST – takie przyzwyczajenie. Mam wtedy lepszy przegląd wszystkich przerwań używanych w projekcie. Ale wrzucanie handlerów do poszczególnych plików tez jest ok, wydaje mi się ze to kwestia osobistych preferencji.

  3. Witam. Typo (dwa razy „podłoga”) w funkcji
    software_timer__task_init(&led_timer, LED_TASK_TIME);

    1. Brakuje też informacji, że trzeba wywołać
      LL_SYSTICK_EnableIT();

      oraz na githubie LED_TASK_TIME to 1000 a na blogu 100 🙂

      1. „Zanim uruchomimy licznik, musimy jeszcze ustawić częstotliwość generowania przerwań. W tym celu do rejestru SysTick->LOAD wpisujemy pomniejszoną o 1 liczbę cykli zegara po których ma pojawić się przerwanie.”

        Dlaeczgo nigdzie tego nie ustawiamy tak jak wspomniałeś?

        1. W tym rozdziale opisywałem SysTick jako element rdzenia mikrokontrolera. Konfigurujemy go później w programach za pomocą funkcji z biblioteki Low Layer, w tym przypadku LL_Init1msTick(). Teraz czytając widzę, że trochę za duży skrót myślowy zastosowałem i nie jest to jasne. Dzięki za uwagę, uzupełnię materiał 🙂

          1. Daję dużo komentarzy, bo widzę w tym ogromna wartość merytoryczną 🙂 Możesz potem wydać z tego ebooka 🙂

          2. Dzięki za każdą uwagę i komentarz! Sam nie jestem w stanie wszystkiego „wyłapać”, nawet jak materiał przeczytam kilka razy, a dzięki takim komentarzom mogę tworzyć lepsze poradniki:)

  4. Tak chociaż kursik robie na boardzie nucleo64-l476RG, jednak nie pamietam by w erracie było coś o ograniczeniach 🙂

  5. Witam. Zastosowałem wariant wyjścia jako open drain i pull up :
    // Initialize Led
    LL_GPIO_SetPinOutputType(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_OUTPUT_OPENDRAIN);
    LL_GPIO_SetPinPull(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_PULL_UP);
    LL_GPIO_SetPinSpeed(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_SPEED_FREQ_LOW);
    LL_GPIO_SetPinMode(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_MODE_OUTPUT);

    while (1)
    {
    LL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
    LL_mDelay(1000);
    }

    i powiem że wynik marny 😀 diodia miga co 1 sekunde ale dostaje tak mały prąd ze jej świecenie jest ledwo zauważalne. Co może być tutaj przyczyną? wbudowany rezystor podciągający ma zbyt mały opór?

    1. Korzystasz z diody na Nucleo? Jesteś pewien, że nic więcej w konfiguracji nie zmieniałeś? Powinna świecić dokładnie tak samo jak przy wyjściu Push-Pull.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *