KursyPoradnikiInspirujące DIYForum

Kurs STM32 F1 HAL – #4 – sterowanie GPIO w praktyce

Kurs STM32 F1 HAL – #4 – sterowanie GPIO w praktyce

W poprzedniej części kursu STM32 nauczyliśmy się tworzyć projekt, kompilować oraz uruchamiać prosty program. Niestety był on mało atrakcyjny, bo nie komunikował się ze światem zewnętrznym.

Czas więc poznać okno na świat każdego układu, czyli uniwersalne porty wejścia/wyjścia (GPIO).

Na początek bardzo prosty przykład - miganie diodą. Na naszej płytce Nucleo znajduje się dioda podłączona do wyprowadzenia PA5, która pasuje idealnie do pierwszego programu.

Dioda wbudowana w zestaw Nucleo.

Dioda wbudowana w zestaw.

Jak migać diodą na STM32?

W przypadku AVR pierwszym krokiem byłoby ustawienie odpowiedniego pinu jako wyjścia, a następnie sterowanie (zapalanie, gaszenie) diody. Kod wyglądałby mniej więcej tak:

DDRA |= _BV(5);     // PA5 jako wyjście
PORTA |= _BV(5);    // zapal diodę
[...]
PORTA &= ~_BV(5);   // zgaś diodę

Układy STM32 można programować w podobny sposób, tzn. poprzez bezpośredni zapis do rejestrów, jednak ten kurs bazuje na bibliotece HAL, więc zamiast tego wykorzystamy odpowiednie procedury przez nią dostarczane. Na początek kod będzie dłuższy niż bezpośrednie odwołania do rejestrów, ale wykorzystując bardziej skomplikowane peryferia, używanie biblioteki okaże się dużo łatwiejsze.

Etap 1 - taktowanie portów

Pierwszym etapem wykorzystania GPIO jest uruchomienie zegara. W przypadku AVR wszystkie moduły peryferyjne są dostępne od razu. Układy STM32 są natomiast zbudowane w oparciu o inną, bardziej "ekologiczną" filozofię.

W tym przykładzie chcemy wykorzystać port A, "podłączamy" więc do niego zegar instrukcją:

__HAL_RCC_GPIOA_CLK_ENABLE();

Bez powyższej linijki do portu A nie będzie docierało taktowanie całego systemu. W związku z tym będzie on najzwyczajniej nieaktywny.

Gotowe zestawy do kursów Forbota

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

Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!

Zamów w Botland.com.pl »

Etap 2 - Konfiguracja GPIO

Kolejny krok to konfiguracja linii PA5 jako wyjścia. Biblioteka HAL wykorzystuje obiektowy model programowania (chociaż opiera się na języku C, a nie C++). Większość funkcji wymaga najpierw zadeklarowania zmiennej (obiektu) zawierającej konfigurację modułu. Następnie ustawiamy konfigurację wypełniając pola tej zmiennej.

Na koniec wywołujemy funkcję, która ustawi odpowiednią konfigurację w rejestrach mikrokontrolera. Taki model programowania może na początku wydawać się nieco dziwny, ale przestawienie się na ten sposób myślenia jest stosunkowo łatwe.

Warto też pamiętać, że ma to swoje zalety – mamy mniej funkcji do nauczenia, a jedno wywołanie konfiguruje czasem bardzo skomplikowany moduł. Dodatkowo możemy wykorzystać domyślne ustawienia wielu parametrów (zmieniamy tylko te pola, które nas interesują).

Łatwiej będzie to zrozumieć patrząc na przykładowy kod:

GPIO_InitTypeDef gpio; // obiekt gpio będący konfiguracją portów GPIO
 gpio.Pin = GPIO_PIN_5; // konfigurujemy pin 5
 gpio.Mode = GPIO_MODE_OUTPUT_PP; // jako wyjście
 gpio.Pull = GPIO_NOPULL; // rezystory podciągające są wyłączone
 gpio.Speed = GPIO_SPEED_FREQ_LOW; // wystarczą nieskie częstotliwości przełączania
 HAL_GPIO_Init(GPIOA, &gpio); // inicjalizacja modułu GPIOA

Zmienna gpio przechowuje parametry konfiguracyjne portu I/O. Jej pola definiują ustawienia, wybieramy pin 5, który pracuje jako wyjście (OUTPUT_PP – wyjście typu push-pull), rezystory podciągające są wyłączone, a prędkość przełączania ustawiamy na niską (FREQ_LOW).

Sama konfiguracja jest ustawiana wywołaniem funkcji HAL_GPIO_Init. Pierwszy parametr informuje o konfiguracji portu A, czyli GPIOA. W drugim parametrze przekazujemy zmienną z konfiguracją, którą chcemy ustawić.

Etap 3 - Sterowanie portami w STM32

Gdy mamy już skonfigurowany port, możemy zająć się sterowaniem diodą. Do zapalania i wygaszania diody wykorzystamy funkcję HAL_GPIO_WritePin:

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // zapalenie diody
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // zgaszenie diody

Pierwsze dwa paramtery to port (GPIOA) oraz numer pinu (PIN_5). Trzeci parametr określa czy chcemy zapalić diodę ustawiając stan wysoki wyjścia (PIN_SET), czy zgasić wystawiając stan niski (PIN_RESET).


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

#include "stm32f1xx.h"
			
void delay(int time)
{
    int i;
    for (i = 0; i < time * 570; i++) {}
}

int main(void)
{
	HAL_Init();

	__HAL_RCC_GPIOA_CLK_ENABLE();

	GPIO_InitTypeDef gpio;				// obiekt gpio będący konfiguracją portów GPIO
	gpio.Pin = GPIO_PIN_5; 				// konfigurujemy pin 5
	gpio.Mode = GPIO_MODE_OUTPUT_PP;	// jako wyjście
	gpio.Pull = GPIO_NOPULL;			// rezystory podciągające są wyłączone
	gpio.Speed = GPIO_SPEED_FREQ_LOW;	// wystarczą nieskie częstotliwości przełączania
	HAL_GPIO_Init(GPIOA, &gpio);		// inicjalizacja modułu GPIOA

	while (1)
	{
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // zapalenie diody
		delay(100);
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // zgaszenie diody
		delay(400);
	}
}

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

Aby uruchomić program naciskamy ikonkę młoteczka, która spowoduje skompilowanie kodu, a następnie klikamy zieloną strzałkę (Run). Efekt działania widoczny jest na poniższym filmie:

Konfiguracja wejść STM32 - przycisk

Mamy migającą diodę, czas odczytać stan prycisku. Na płytce Nucleo znajdziemy tact switch podłączony do wejścia PC13 (oznaczony jako USER, drugi pełni rolę reset-u).

Przycisk użytkownika.

Przycisk użytkownika.

Mikrokontroler wyposażony jest w porty oznaczone literami od A do D, każdy może obsłużyć do 16 linii wejścia wyjścia. Poprzednio używaliśmy portu A, teraz będziemy używać A oraz C. Jako pierwszy krok musimy więc włączyć zegar portu. W tym celu piszemy:

__HAL_RCC_GPIOC_CLK_ENABLE();

Oczywiście jeśli chcemy mieć obsługę diody LED i przycisku jednocześnie, musimy zostawić poprzednio napisaną instrukcję uruchamiającą zegar portu A. Na wszelki wypadek możemy uruchomić również pozostałe porty:

__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();

Zegar portu już działa, czas skonfigurować wejście. W przypadku AVR wszystkie linie domyślnie były skonfigurowane jako wejścia, trzeba było tylko pamiętać o włączeniu rezystora pull-up. Kod dla AVR wyglądałbym więc następująco:

// Przykład na AVR - dla łatwiejszego zrozumienia
DDRC &= ~_BV(13);  // oczywiście 13 to za dużo dla AVR, to tylko przykład
PORTC |= _BV(13);  // włączenie rezystora pullup

Mikrokontroler STM32 posiada dużo więcej możliwości konfiguracji linii wejścia/wyjścia. Poprzednio konfigurowaliśmy wyjście w trybie push-pull, teraz chcemy ustawić pin jako wejście z rezystorem pull-up. Wykorzystamy tą samą zmienną konfiguracyjną, co poprzednio (gpio), zmienimy tylko numer pinu i tryb pracy:

gpio.Pin = GPIO_PIN_13; // konfigurujemy pin 13
 gpio.Mode = GPIO_MODE_INPUT; // jako wejście
 gpio.Pull = GPIO_PULLUP; // włączamy rezystor podciągający
 HAL_GPIO_Init(GPIOC, &gpio); // port GPIOC

Nie musimy ponownie ustawiać wszystkich pól zmiennej gpio, wystarczy, że ustawimy te, które są inne niż podczas konfiguracji portu A. Warto również zwrócić uwagę na odwołanie do portu C (GPIOC), zamiast poprzedniego A.

Odczytywanie wejść w STM32

Czas odczytać stan przycisku. W przypadku AVR wykorzystalibyśmy rejestr PINC, tutaj wywołamy funkcję HAL_GPIO_ReadPin:

if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) {
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
		} else {
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
		}

Otrzymaliśmy bardzo prosty program zapalający diodę po naciśnięciu przycisku:

#include "stm32f1xx.h"
			
int main(void)
{
	HAL_Init();

	__HAL_RCC_GPIOA_CLK_ENABLE();
	__HAL_RCC_GPIOC_CLK_ENABLE();

	GPIO_InitTypeDef gpio;
	gpio.Pin = GPIO_PIN_5;
	gpio.Mode = GPIO_MODE_OUTPUT_PP;
	gpio.Pull = GPIO_NOPULL;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Pin = GPIO_PIN_13; 	// konfigurujemy pin 13
	gpio.Mode = GPIO_MODE_INPUT; 	// jako wejście
	gpio.Pull = GPIO_PULLUP;		// włączamy rezystor podciągający
	HAL_GPIO_Init(GPIOC, &gpio);	// port GPIOC

	while (1)
	{
		if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) { // jesli przycisk jest przycisniety,
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // zapal diode
		} else {
			HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
		}
	}
}

Działanie programu w praktyce:

Opóźnienia w STM32 - SysTick

W pierwszym programie wstawiliśmy prostą pętle opóźniającą. Takie rozwiązanie jest bardzo nieprecyzyjne – jak określić ile dokładnie czasu trwa opóźnienie?

Znacznie lepszym rozwiązaniem jest wykorzystanie zegara (timera). Mikrokontrolery z rdzeniem Cortex-M3 posiadają timer SysTick przeznaczony do odmierzania czasu systemowego.

W przypadku biblioteki HAL właściwie nic nie musimy robić - sami autorzy biblioteki wykorzystali SysTick i udostępnili gotową funkcję HAL_Delay.

void HAL_Delay(__IO uint32_t Delay)

Parametr, który do niej przekazujemy to opóźnienie w milisekundach. Fukcja jest więc bardzo wygodna, ale niestety ma pewną wadę. Jeśli do naszego programu sterującego diodami dodamy nową funkcję to zobaczymy, że opóźnienia są znacznie dłuższe niż zaprogramowane.

Okazuje się, że przyczyną problemów jest stara znajoma - zmienna SystemCoreClock. W poprzedniej edycji kursu mieliśmy z nią problem, ponieważ była domyślnie ustawiana na 72000000, co odpowiada taktowaniu 72MHz, ale płytka Nucleo maksymalnie pracuje z 64MHz.

Teraz problem jest jeszcze poważniejszy. SystemCoreClock jest nadal ustawiana na 720000000, ale nasz mikrokontroler pracuje z domyślnym taktowaniem 8MHz. Więc opóźnienia są 9 razy dłuższe niż byśmy chcieli.

Możemy oczywiście podawać mniejsze wartości do wywołań HAL_Delay, ale zamiast tego znacznie lepiej będzie poprawić wartość SystemCoreClock. Możemy zmienić kod biblioteki, albo przypisać do zmiennej wartość przed wywołaniem HAL_Init.

Program z poprawnymi opóźnieniami wygląda następująco:

#include "stm32f1xx.h"
			
int main(void)
{
	SystemCoreClock = 8000000;	// taktowanie 8Mhz

	HAL_Init();

	__HAL_RCC_GPIOA_CLK_ENABLE();

	GPIO_InitTypeDef gpio;				// obiekt gpio będący konfiguracją portów GPIO
	gpio.Pin = GPIO_PIN_5; 				// konfigurujemy pin 5
	gpio.Mode = GPIO_MODE_OUTPUT_PP;	// jako wyjście
	gpio.Pull = GPIO_NOPULL;			// rezystory podciągające są wyłączone
	gpio.Speed = GPIO_SPEED_FREQ_LOW;	// wystarczą nieskie częstotliwości przełączania
	HAL_GPIO_Init(GPIOA, &gpio);		// inicjalizacja modułu GPIOA

	while (1)
	{
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // zapalenie diody
		HAL_Delay(100);
		HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // zgaszenie diody
		HAL_Delay(100);
	}
}

Linijka LED-ów na STM32

Mając procedurę opóźniającą i wstępne informacje o sterowaniu liniami I/O możemy przygotować pierwszy, bardziej rozbudowany, przykład - linijkę diod świecących. Pierwszy krok, to wybór linii, do których podłączymy diody.

Port A okazuje się częściowo zajęty – pin 5 jest podłączony do diody na płytce, piny 2 i 3 do przejściówki UART, a 13-15 do programatora SWD. Natomiast port B ma zajęte piny 2-4.

Najłatwiej będzie więc wykorzystać port C, gdzie piny 0-12 są wolne. Podłączmy 10 diod do wyprowadzeń PC0 – PC9 (oczywiście przez rezystory 330R).

gpio-leds_bb

Linijka diod LED, przykład na STM32.

Połączenie diod w praktyce również nie jest trudne, przykładowe połączenie:

Linijka diod święcących podłączona do STM32.

Linijka diod święcących podłączona do STM32.

Podobnie jak poprzednio pierwszym krokiem będzie konfiguracja wyprowadzeń. Wszystkie linie należą do jednego portu i będą działały w tym samym trybie, możemy więc skonfigurować je jednocześnie:

GPIO_InitTypeDef gpio;				// obiekt gpio będący konfiguracją portów GPIO
	gpio.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|
			 GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
	gpio.Mode = GPIO_MODE_OUTPUT_PP;	// jako wyjście
	gpio.Pull = GPIO_NOPULL;			// rezystory podciągające są wyłączone
	gpio.Speed = GPIO_SPEED_FREQ_LOW;	// wystarczą nieskie częstotliwości przełączania
	HAL_GPIO_Init(GPIOC, &gpio);		// inicjalizacja modułu GPIOC

Teraz w pętli głównej możemy sterować linijką diod, na przykład za pomocą pętli for:

uint32_t led = 0;
	while (1) {
		HAL_GPIO_WritePin(GPIOC, 1 << led, GPIO_PIN_SET); //włącz diode
		HAL_Delay(150);
		HAL_GPIO_WritePin(GPIOC, 1 << led, GPIO_PIN_RESET); //wyłącz diode
	    if (++led >= 10) { // przejdz do nastepnej
	        led = 0;
	    }
	}

Oczywiście można przygotować zupełnie inny wzór, zachęcam do eksperymentów. Cały kod przykładu dostępny jest poniżej:

#include "stm32f1xx.h"

int main(void)
{
    SystemCoreClock = 8000000; // taktowanie 8Mhz

    HAL_Init();

    __HAL_RCC_GPIOC_CLK_ENABLE();

   GPIO_InitTypeDef gpio; // obiekt gpio będący konfiguracją portów GPIO
   gpio.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|
   GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9;
   gpio.Mode = GPIO_MODE_OUTPUT_PP; // jako wyjście
   gpio.Pull = GPIO_NOPULL; // rezystory podciągające są wyłączone
   gpio.Speed = GPIO_SPEED_FREQ_LOW; // wystarczą nieskie częstotliwości przełączania
   HAL_GPIO_Init(GPIOC, &gpio); // inicjalizacja modułu GPIOC

    uint32_t led = 0;
   while (1) {
      HAL_GPIO_WritePin(GPIOC, 1 << led, GPIO_PIN_SET); //włącz diode
      HAL_Delay(150);
      HAL_GPIO_WritePin(GPIOC, 1 << led, GPIO_PIN_RESET); //wyłącz diode
      if (++led >= 10) { // przejdz do nastepnej
         led = 0;
      }
   }
}

Działanie powyższego programu w praktyce widoczne jest na poniższym filmie:

Przerwania od przycisków w STM32

Często program musi wykonywać swoje zadanie (np. sterować robotem, albo zapalaniem diod), a jednocześnie sprawdzać stan przycisku (lub czujnika). Moglibyśmy oczywiście w pętli głównej co chwila odczytywać stan przycisku. Jednak takie działanie po pierwsze komplikuje program, po drugie czas reakcji na naciśnięcie może być długi.

Sposobem na szybką reakcję w takim przypadku jest wykorzystanie przerwań. Mikrokontrolery STM32 mają bardzo rozbudowany układ przerwań, na początek sprawdzimy jednak jak najprostszy przykład jego wykorzystania.

Załóżmy, że chcemy napisać program, który zapala diodę po naciśnięciu przycisku, a gasi po zwolnieniu, czyli dokładnie taki jak wcześniej, jednak nie chcemy całego czasu procesora poświęcać na sprawdzanie stanu przełącznika. Sterowanie umieścimy w przerwaniu, a nasz program będzie mógł robić coś innego.

Wykorzystamy jak poprzednio przełącznik na płytce Nucleo (pin PC13). Tym razem musimy skonfigurować wejście oraz przerwanie zewnętrzne (EXTI). Na początek konfigurujemy pin PC13 jako wejście sterowane przerwaniami:

gpio.Mode = GPIO_MODE_IT_RISING_FALLING;
	gpio.Pull = GPIO_PULLUP;
	gpio.Pin = GPIO_PIN_13;
	HAL_GPIO_Init(GPIOC, &gpio);

Wybraliśmy GPIO_MODE_IT_RISING_FALLING dzięki czemu przerwanie będzie generowane zarówno podczas naciskania, jak i zwalniania przycisku. Dostępne są jeszcze opcje GPIO_MODE_IT_RISING oraz GPIO_MODE_IT_FALLING, które uruchamiają przerwanie tylko dla jednego zbocza sygnału.

Teraz musimy uruchomić przerwanie zewnętrzne, czyli EXTI. Pinu o numerach 0-4 mają dedykowane przerwania EXTI0_IRQn - EXTI4_IRQn, pozostałe obsługiwane są w grupach. Piny od 5 do 9 dzielą przerwanie EXTI9_5_IRQn, a od 10 do 15 - EXTI15_10_IRQn.

Chcemy obsługiwać pin o numerze 13, więc interesuje nas ostatnia grupa:

HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);

HAL do obsługi przerwań zewnętrznych wykorzystuje funkcję HAL_GPIO_EXTI_Callback. Jest ona wywoływana dla każdego wejścia, a numer pinu jest jej parametrem. Ponieważ chcemy, żeby nasz kod był wykonywany w reakcji na przerwanie, piszemy więc własną wersję tej funkcji:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);	// zmień stan diody
}

W tej chwili obsługujemy tylko jeden pin wejściowy, wiec nie musimy sprawdzać, czy to na pewno pin 13. W przypadku bardziej rozbudowanego programu musielibysmy sprawdzić, który pin jest przyczyną wystąpienia przerwania.

Okazuje się, że taki program jeszcze nie zadziała. Zanim będziemy mogli cieszyć się obsługą przerwań musimy dodać jedną funkcję do pliku stm32f1xx_it.c. Gdy tworzyliśmy nasz projekt, plik ten został automatycznie utworzony. Znajdziemy w nim jedną funkcję, która przekierowuje przerwanie SysTick do biblioteki HAL:

void SysTick_Handler(void)
{
	HAL_IncTick();
	HAL_SYSTICK_IRQHandler();
#ifdef USE_RTOS_SYSTICK
	osSystickHandler();
#endif
}

Plik ten jest bardzo ważny - łączy on niskopoziomową obsługę przerwań z biblioteką HAL. Funkcja SysTick_Handler jest wywoływana w reakcji na przerwanie od timera SysTick. Dopiero ona wywołuje HAL_IncTick(), dzięki której funkcja HAL_Delay() działa poprawnie.

Musimy dopisać podobną funkcję, która połączy przerwanie sprzętowe EXTI15_10_IRQn z HAL. Kod takiej funkcji wygląda następująco:

void EXTI15_10_IRQHandler(void)
{
	HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
}

Gdybyśmy obsługiwali więcej pinów z tej grupy, powinniśmy dodać kolejne wywołania funkcji HAL_GPIO_EXTI_IRQHandler. Resztą zajmie się biblioteka HAL. Cały kod programu:

#include "stm32f1xx.h"
			
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);	// zmień stan diody
}

int main(void)
{
	SystemCoreClock = 8000000;	// taktowanie 8Mhz

	HAL_Init();

	__HAL_RCC_GPIOA_CLK_ENABLE();
	__HAL_RCC_GPIOC_CLK_ENABLE();

	GPIO_InitTypeDef gpio;
	gpio.Mode = GPIO_MODE_OUTPUT_PP;
	gpio.Pull = GPIO_NOPULL;
	gpio.Pin = GPIO_PIN_5;
	gpio.Speed = GPIO_SPEED_FREQ_LOW;
	HAL_GPIO_Init(GPIOA, &gpio);

	gpio.Mode = GPIO_MODE_IT_RISING_FALLING;
	gpio.Pull = GPIO_PULLUP;
	gpio.Pin = GPIO_PIN_13;
	HAL_GPIO_Init(GPIOC, &gpio);

	HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);

	while (1)
	{
	}
}

Więcej informacji o przerwaniach

Widzieliśmy już dwie procedury obsługi przerwania - dla zegara SysTick oraz dla obsługi przycisku. Jeśli ktoś byłby ciekaw skąd brały się nazwy tych procedur, albo jakie jeszcze procedury mogą być użyte, najlepiej jest przeanalizować plik startup_stm32f103xb.s. Plik ten został automatycznie dodany do naszego projektu i zawiera kod w asemblerze. Na szczęście nawet nie znając tego języka łatwo domyślić się o co w nim chodzi.

Znajdziemy w nim tzw. wektor przerwań, czyli tablicę z adresami procedur obsługi przerwań:

g_pfnVectors:
  .word _estack
  .word Reset_Handler
  .word NMI_Handler
  .word HardFault_Handler
  .word MemManage_Handler
  .word BusFault_Handler
  .word UsageFault_Handler
  .word 0
  .word 0
  .word 0
  .word 0
  .word SVC_Handler
  .word DebugMon_Handler
  .word 0
  .word PendSV_Handler
  .word SysTick_Handler
  .word WWDG_IRQHandler
  .word PVD_IRQHandler
  .word TAMPER_IRQHandler
  .word RTC_IRQHandler
  .word FLASH_IRQHandler
  .word RCC_IRQHandler
  .word EXTI0_IRQHandler
  .word EXTI1_IRQHandler
  .word EXTI2_IRQHandler
  .word EXTI3_IRQHandler
  .word EXTI4_IRQHandler
  .word DMA1_Channel1_IRQHandler
  .word DMA1_Channel2_IRQHandler
  .word DMA1_Channel3_IRQHandler
  .word DMA1_Channel4_IRQHandler
  .word DMA1_Channel5_IRQHandler
  .word DMA1_Channel6_IRQHandler
  .word DMA1_Channel7_IRQHandler
  .word ADC1_2_IRQHandler
  .word USB_HP_CAN1_TX_IRQHandler
  .word USB_LP_CAN1_RX0_IRQHandler
  .word CAN1_RX1_IRQHandler
  .word CAN1_SCE_IRQHandler
  .word EXTI9_5_IRQHandler
  .word TIM1_BRK_IRQHandler
  .word TIM1_UP_IRQHandler
  .word TIM1_TRG_COM_IRQHandler
  .word TIM1_CC_IRQHandler
  .word TIM2_IRQHandler
  .word TIM3_IRQHandler
  .word TIM4_IRQHandler
  .word I2C1_EV_IRQHandler
  .word I2C1_ER_IRQHandler
  .word I2C2_EV_IRQHandler
  .word I2C2_ER_IRQHandler
  .word SPI1_IRQHandler
  .word SPI2_IRQHandler
  .word USART1_IRQHandler
  .word USART2_IRQHandler
  .word USART3_IRQHandler
  .word EXTI15_10_IRQHandler
  .word RTC_Alarm_IRQHandler
  .word USBWakeUp_IRQHandler

We fragmencie powyżej widzimy znajomą funkcję SysTick_Handler. W dalszej części pliku znajdziemy EXTI15_10_IRQHandler oraz wszystkie pozostałe procedury obsługi przerwań.

Co dzieje się po starcie STM32?

W tym pliku znajdziemy też kod wykonywany po resecie procesora (czyli również po uruchomieniu). Zaczyna się on od oznaczonej jako Reset_Handler. Jest to kod wykonywany przed funkcją main! W okolicy linii numer 100 zobaczymy również poniższy fragment:

/* Call the clock system intitialization function.*/
    bl  SystemInit
/* Call static constructors */
    bl __libc_init_array
/* Call the application's entry point.*/
    bl	main

Jest to wywołanie procedury SystemInit, która konfiguruje zegar i taktowanie całego układu (domyślnie na 8MHz), następnie inicjalizacja biblioteki C oraz skok do naszej funkcji main.

Zadanie domowe 4.1

Przygotuj program, który zmienia kolejność zapalania diod w linijce LED po naciśnięciu przycisku.

Zadanie domowe 4.2

Napisz program, który zapala kolejną diodę z linijki LED po naciśnięciu przycisku.

Zadanie domowe 4.3

Przygotuj program, który zapala kolejne diody, a po naciśnięciu przycisku zapala diodę na płytce Nucleo – natychmiast i nie przerywając sekwencji sterowania diodami.

Zadanie domowe 4.4 (dodatkowe)

Wykonaj licznik Johnsona z użyciem dioda świecących.

Podsumowanie

W tej części kursu STM32 opisane zostały najważniejsze informacje dotyczące portów GPIO. Oczywiście mechanizmy te oferują znacznie więcej możliwości, będziemy jeszcze później wracać do bardziej zaawansowanych opcji, gdy zajdzie taka potrzeba.

W kolejnej części przyjdzie pora na nawiązanie połączenia płytki Nucleo z komputerem za pomocą interfejsu UART, który będzie nam towarzysz do samego końca kursu!

Nawigacja kursu

Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk
Redakcja: Damian Szymański

f1, HAL, kurs, kursSTM32F1HAL, led, przerwania, przycisk, stm32

Trwa ładowanie komentarzy...