Kursy • Poradniki • Inspirujące DIY • Forum
Na początek bardzo prosty przykład - miganie diodą LED. Na płytce Nucleo znajduje się dioda podłączona do wyprowadzenia PA5, która pasuje idealnie do pierwszego programu.
STM32 - Miganie diodą
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:
1 2 3 4 |
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 StdPeriph, więc zamiast tego wykorzystamy odpowiednie procedury przez nią dostarczone. 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.
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 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 układach STM32 prawie wszystkie moduły są domyślnie wyłączone, więc te które są nam potrzebne, musimy włączyć zanim będziemy mogli z nich korzystać.
W tym przykładzie chcemy wykorzystać port A, podłączamy więc do niego zegar instrukcją:
1 |
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, 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.
Etap 2 - Konfiguracja GPIO
Kolejny krok to konfiguracja linii PA5 jako wyjścia. Biblioteka StdPeriph 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 program:
1 2 3 4 5 6 |
GPIO_InitTypeDef gpio; // obiekt gpio będący konfiguracją portów GPIO GPIO_StructInit(&gpio); // domyślna konfiguracja gpio.GPIO_Pin = GPIO_Pin_5; // konfigurujemy pin 5 gpio.GPIO_Mode = GPIO_Mode_Out_PP; // jako wyjście GPIO_Init(GPIOA, &gpio); // inicjalizacja modułu GPIOA |
Zmienna gpio przechowuje parametry konfiguracyjne portu I/O. Na początek wywołujemy funkcję GPIO_StructInit, która wypełnia wszystkie pola wartościami domyślnymi (odpowiada to konstruktorowi obiektu w C++). W kolejnych instrukcjach wybieramy pin 5 oraz ustawiamy go jako wyjście (Out_PP – wyjście typu push-pull).
Sama konfiguracja jest ustawiana wywołaniem funkcji 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 wywołania funkcji:
1 2 |
GPIO_SetBits(GPIOA, GPIO_Pin_5); // włączenie diody GPIO_ResetBits(GPIOA, GPIO_Pin_5); // wyłączenie diody |
Mamy już właściwie wszystko, co potrzebne, do napisania pierwszego programu. Wystarczy dodać pętlę główną oraz opóźnienia (żebyśmy widzieli co się dzieje) i możemy uruchomić nasz program:
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 |
#include "stm32f10x.h" void delay(int time) { int i; for (i = 0; i < time * 4000; i++) {} } int main(void) { GPIO_InitTypeDef gpio; // obiekt gpio z konfiguracja portow GPIO RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // uruchomienie zegara modulu GPIO GPIO_StructInit(&gpio); // domyslna konfiguracja gpio.GPIO_Pin = GPIO_Pin_5; // konfigurujemy pin 5 gpio.GPIO_Mode = GPIO_Mode_Out_PP; // jako wyjscie GPIO_Init(GPIOA, &gpio); // inicjalizacja modulu GPIOA while (1) { GPIO_SetBits(GPIOA, GPIO_Pin_5); // zapalenie diody delay(100); GPIO_ResetBits(GPIOA, GPIO_Pin_5); // zgaszenie diody delay(400); } } |
Wartość 4000 w pętli opóźniającej została dobrana eksperymentalnie tak, żeby opóźnienie było ustawiane w milisekundach.
Oczywiście dokładność takich opóźnień jest niewielka, w dalszej części poznamy znacznie lepszą metodę.
Efekt działania programu widoczny jest na poniższym filmie:
Konfiguracja wejść STM32 - przycisk
Mamy migającą diodę, czas odczytać stan przełącznika. Na płytce Nucleo znajdziemy microswitch podłączony do wejścia PC13 (oznaczony jako USER, drugi pełni rolę reset-u).
Mikrokontroler wyposażony jest w porty oznaczone literami od A do E, 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:
1 |
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, 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. Inną możliwością jest uruchomienie np.: wszystkich portów procesora na raz - może będą nam w przyszłości potrzebne?
1 2 |
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, 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:
1 2 3 |
// 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 pullup. Wykorzystamy tą samą zmienną konfiguracyjną, co poprzednio (gpio), zmienimy tylko numer pinu i tryb pracy:
1 2 3 |
gpio.GPIO_Pin = GPIO_Pin_13; // konfigurujemy pin 13 gpio.GPIO_Mode = GPIO_Mode_IPU; // jako wejście z rezystorem pull-up GPIO_Init(GPIOC, &gpio); |
Nie musimy ponownie wywoływać GPIO_StructInit, ponieważ wywołaliśmy tę funkcję wcześniej, podczas konfiguracji portu A. Teraz tylko ustawiamy te parametry, którymi obie konfiguracje się różnią. 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 odpowiednią funkcję. Mamy do wyboru dwie:
- GPIO_ReadInputData() - zwraca stan całego portu (działa jak PINC w AVR)
- GPIO_ReadInputDataBit() - sprawdza pojedynczy bit
Ponieważ interesuje nas stan tylko jednej linii, wykorzystamy drugą funkcję jako wygodniejszą:
1 2 3 4 5 |
if (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == 0) { GPIO_SetBits(GPIOA, GPIO_Pin_5); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_5); } |
Otrzymaliśmy bardzo prosty program zapalający diodę po naciśnięciu przycisku:
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 |
#include "stm32f10x.h" int main(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_13; // konfigurujemy pin 13 gpio.GPIO_Mode = GPIO_Mode_IPU; // jako wejscie z rezystorem pull-up GPIO_Init(GPIOC, &gpio); // port GPIOC while (1) { if (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == 0) { // jesli przycisk jest przycisniety, GPIO_SetBits(GPIOA, GPIO_Pin_5); // zapal diode } else { GPIO_ResetBits(GPIOA, GPIO_Pin_5); } } } |
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?
W najgorszym przypadku może się okazać, że po zmianie kompilatora, albo nawet opcji kompilacji opóźnienie będzie zupełnie inne, albo nawet zupełnie zniknie (optymalizator może uznać, że pętla jest pusta, więc niepotrzebna i usunąć ją z kodu).
Znacznie lepszym rozwiązaniem jest wykorzystanie zegara (timera). Mikrokontrolery z rdzeniem Cortex-M3 posiadają specjalny timer SysTick przeznaczony do odmierzania czasu systemowego.
Dużą zaletą tego timera jest dostępność gotowej funkcji SysTickConfig() - jako parametr podajemy jej, co ile cykli zegara chcemy otrzymywać przerwanie od timera. Aby to przerwanie obsłużyć wystarczy napisać funkcję o nazwie SysTick_Handler() - będzie ona automatycznie wywoływana co zadany czas.
Załóżmy, że chcemy co 1 ms wywoływać naszą funkcję, możemy skonfigurować timer poleceniem:
1 |
SysTick_Config(SystemCoreClock / 1000); |
Zmienna SystemCoreClock została zadeklarowana w pliku system_stm32f10x.h, jest to jeden z plików dodanych do projektu automatycznie. Wartość tej zmiennej to częstotliwość zegara systemowego wyrażona w Hz, powinna więc wynosić 64000000.
Niestety w chwili pisania tego kursu kod generowany przez środowisko nie był poprawny i zmienna posiada wartość 72000000 (typową dla STM32F103, ale nie dla naszego projektu). Zapis 1000 oznacza liczbę wywołań na sekundę, czyli wywołanie co 1ms, tak jak chcieliśmy. Jak widać wykorzystanie tego licznika (timera) jest niezwykle łatwe i wygodne!
Kod generowany przez środowisko System Workbench for STM32 zawiera błąd. Dla płytki Nucleo, częstotliwość pracy zegara wynosi 64MHz, natomiast zmienna SystemCoreClock jest ustawiana na wartość odpowiadającą 72MHz.
Pozostaje napisać procedurę obsługi przerwania:
1 2 3 4 5 6 7 8 |
volatile uint32_t timer_ms = 0; //deklaracja zmiennej 32 bitowej bez znaku void SysTick_Handler() { if (timer_ms) { timer_ms--; } } |
Teraz napisanie procedury opóźniającej jest bardzo proste, wystarczy przypisać zmiennej timer_ms ile czasu chcemy czekać i w pętli oczekiwać na wyzerowanie licznika:
1 2 3 4 5 |
void delay_ms(int time) { timer_ms = time; while(timer_ms > 0){}; } |
Oczywiście nie jest to super precyzyjna metoda, ale będzie działać z dokładnością do 1ms, a to dużo lepiej od naszej pierwszej wersji procedury opóźniającej.
Zmienna timer_ms jest zadeklarowana ze słowem kluczowym volatile. Oznacza ono, że kompilator nie powinien wykonywać optymalizacji związanych z dostępem do tej zmiennej. Jest to ważne ponieważ zarówno kod programu głównego, jak i przerwań modyfikuje jej wartość.
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 |
#include "stm32f10x.h" volatile uint32_t timer_ms = 0; void SysTick_Handler() { if (timer_ms) { timer_ms--; } } void delay_ms(int time) { timer_ms = time; while (timer_ms) {}; } int main(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); SysTick_Config(SystemCoreClock / 1000); while (1) { GPIO_SetBits(GPIOA, GPIO_Pin_5); // zapalenie diody delay_ms(100); GPIO_ResetBits(GPIOA, GPIO_Pin_5); // zgaszenie diody delay_ms(400); } } |
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 led. Pierwszy krok, to wybór linii, do których podłączymy diody.
Najwygodniej jest podłączyć je do jednego portu, o ile się da wykorzystując sąsiednie bity. Wtedy programowanie jest dużo łatwiejsze.
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 więc 10 diod LED do wyprowadzeń PC0 – PC9 (oczywiście przez rezystory 330R).
Połączenie diod w praktyce również nie jest trudne, przykładowe połączenie widoczne jest poniżej. Wymieniłem 5 diod na inne kolory, aby były lepiej widoczne później w kamerze.
Podobnie jak poprzednio pierwszym krokiem będzie konfiguracja wyprowadzeń. Ponieważ wszystkie linie należą do jednego portu oraz będą działały w tym samym trybie, możemy skonfigurować je jednocześnie:
1 2 3 4 |
GPIO_StructInit(&gpio); 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.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &gpio); |
Teraz w pętli głównej możemy sterować linijką diod, na przykład za pomocą pętli for:
1 2 3 4 5 6 7 8 9 |
uint32_t led = 0; while (1) { GPIO_SetBits(GPIOC, 1 << led); //włącz diode delay_ms(150); // poczekaj GPIO_ResetBits(GPIOC, 1 << led); //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:
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 |
#include "stm32f10x.h" volatile uint32_t timer_ms = 0; void SysTick_Handler() { if (timer_ms) { timer_ms--; } } void delay_ms(int time) { timer_ms = time; while (timer_ms) {}; } int main(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); GPIO_StructInit(&gpio); 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.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &gpio); SysTick_Config(SystemCoreClock / 1000); uint32_t led = 0; while (1) { GPIO_SetBits(GPIOC, 1 << led); // zapal diode delay_ms(150); // poczekaj GPIO_ResetBits(GPIOC, 1 << led); // zgas 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 led), 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.
O ile użytkownik może po prostu przytrzymać przycisk dłużej, to w przypadku sygnału od np. czujnika przeszkody, opóźnienie może być katastrofalne w skutkach.
Sposobem na szybką reakcję w takim przypadku jest wykorzystanie przerwań. Mikrokontrolery STM32 mają bardzo rozbudowany układ przerwań, postaram się jednak przygotować 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, przerwanie zewnętrzne (EXTI) oraz kontroler przerwań (NVIC).
Pierwszy krok to włączenie zegara kolejnego układu peryferyjnego. Potrzebujemy wykorzystać piny procesora do innych celów niż domyślne. Uruchamiamy więc moduł funkcji alternatywnych dla pinów I/O (AF - alternative function):
1 |
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); |
Piny konfigurowaliśmy już poprzednio, przejdźmy więc do EXTI. Musimy zadeklarować nową zmienną i wypełnić jej pola:
1 2 3 4 5 6 7 8 |
EXTI_InitTypeDef exti; EXTI_StructInit(&exti); exti.EXTI_Line = EXTI_Line13; exti.EXTI_Mode = EXTI_Mode_Interrupt; exti.EXTI_Trigger = EXTI_Trigger_Rising_Falling; exti.EXTI_LineCmd = ENABLE; EXTI_Init(&exti); |
Schemat jest podobny do poprzedniego, mamy obiekt z konfiguracją, wypełniamy domyślne wartości funkcją EXTI_StructInit(), następnie ustawiamy konfigurację i wywołujemy EXTI_Init().
Nasza konfiguracja to wybór numeru linii (13), tryb – przerwanie, obsługa zbocza narastającego i opadającego (czyli zarówno przyciskania, jak i zwalniania przycisku) oraz ostatnie pole – włączenie modułu.
Pozostaje jeszcze wybór portu C za pomocą wywołania:
1 |
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource13); |
Mamy już linię skonfigurowaną do obsługi przerwań, trzeba jeszcze uruchomić samo przerwanie:
1 2 3 4 5 6 7 |
NVIC_InitTypeDef nvic; nvic.NVIC_IRQChannel = EXTI15_10_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 0x00; nvic.NVIC_IRQChannelSubPriority = 0x00; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); |
Linia 13 należy do grupy 10-15, stąd kanał EXTI15_10_IRQn. Priorytety przerwania są zaawansowanym mechanizmem pozwalającym na ustalanie kolejności obsługi przerwań oraz postępowania w przypadku przerwania zgłoszonego podczas obsługi innego.
Na początek nie musimy się tym przejmować!
Ostatnie co potrzebujemy to procedura obsługi przerwania. Nazwa jej zależy od kanału i w naszym przypadku ma postać:
1 2 3 4 5 6 7 8 9 10 11 12 |
void EXTI15_10_IRQHandler() { if (EXTI_GetITStatus(EXTI_Line13)) { if (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == 0) { GPIO_SetBits(GPIOA, GPIO_Pin_5); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_5); } EXTI_ClearITPendingBit(EXTI_Line13); } } |
Sprawdzamy, czy na pewno obsługujemy linię 13 (jedna procedura obsługuje linie 10-15). Następnie odczytujemy stan linii i zapalamy lub gasimy diodę. Ostania funkcja EXTI_ClearITPendingBit zeruje flagę przerwania.
Cały kod programu:
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 48 49 50 51 |
#include "stm32f10x.h" void EXTI15_10_IRQHandler() { if (EXTI_GetITStatus(EXTI_Line13)) { if (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == 0) { // jesli przycisk jest przycisniety GPIO_SetBits(GPIOA, GPIO_Pin_5); // zapal diode } else { GPIO_ResetBits(GPIOA, GPIO_Pin_5); } EXTI_ClearITPendingBit(EXTI_Line13); } } int main(void) { GPIO_InitTypeDef gpio; EXTI_InitTypeDef exti; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_5; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_13; gpio.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOC, &gpio); GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource13); EXTI_StructInit(&exti); exti.EXTI_Line = EXTI_Line13; exti.EXTI_Mode = EXTI_Mode_Interrupt; exti.EXTI_Trigger = EXTI_Trigger_Rising_Falling; exti.EXTI_LineCmd = ENABLE; EXTI_Init(&exti); nvic.NVIC_IRQChannel = EXTI15_10_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 0x00; nvic.NVIC_IRQChannelSubPriority = 0x00; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); 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_stm32f10x_md.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.
Od linii 134 zdefiniowany jest tzw. wektor przerwań, czyli tablica z adresami procedur obsługi przerwań:
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 48 49 50 51 52 53 54 55 56 57 58 59 60 |
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 RTCAlarm_IRQHandler .word USBWakeUp_IRQHandler |
We fragmencie powyżej widzimy znajomą funkcję SysTick_Handler. W dalszej części pliku znajdziemy EXTI15_10_IRQHandler, jak również wszystkie pozostałe procedury obsługi przerwań.
W przypadku Cortex-M3 pisanie procedur obsługi przerwania jest bardzo proste, wystarczy napisać funkcję w C o takiej nazwie jak zadeklarowana powyżej.
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 linii 70, oznaczonej jako Reset_Handler. Jest to kod wykonywany przed funkcją main! Od linii 100, zobaczymy ciekawy fragment:
1 2 3 4 5 6 |
/* 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 PLL (zegar i taktowanie całego układu), następnie inicjalizacja biblioteki C oraz skok do naszej funkcji main.
Nie ma potrzeby ręcznego wywoływania procedury SystemInit(). W kodzie startowym zostaje ona wywołana przed funkcją main(). Sporo początkujących z niewiadomych przyczyn uporczywie robi to jednak drugi raz, ręcznie.
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 – jaki wzór będzie wyświetlany na diodach?
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.
Nawigacja kursu
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! Jeśli nie chcesz przeoczyć kolejnego odcinka, to skorzystaj z poniższego formularza i zapisz się na powiadomienia o nowych publikacjach!
Autor kursu: Piotr (Elvis) Bugalski
Redakcja: Damian (Treker) Szymański
Powiązane wpisy
GPIO, kursSTM32, przerwania, stm32, wejścia, wyjścia
Trwa ładowanie komentarzy...