Programowanie STM32 M0 z wykorzystaniem bibliotek LL - Cz. 3: Pan mi nie przerywa, bo ja Panu też nie przerywałem

Tu możesz pisać o swoich problemach z pisaniem programów w języku C/C++ dla STM.
Awatar użytkownika
ZbeeGin
User
User
Posty: 492
Rejestracja: sobota 08 lip 2017, 17:16
Lokalizacja: Śląsko-Zagłębiowska Metropolia
Kontaktowanie:

Programowanie STM32 M0 z wykorzystaniem bibliotek LL - Cz. 3: Pan mi nie przerywa, bo ja Panu też nie przerywałem

Postautor: ZbeeGin » niedziela 14 paź 2018, 10:21

W dzisiejszym odcinku zajmiemy się usprawnieniem naszego poprzedniego programu wykorzystując mechanizmy, które obecnie ma każdy mikrokontroler znajdujący się na rynku. Dzięki temu poznamy kilka nowych makr z bibliotek LL oraz nową strukturę programów, które te mechanizmy wykorzystują. Zaczynajmy...


Pamiętacie, że poprzedni program miał sporo, a nawet same wady. Ciężko było przełączyć się na inną szybkość migania diody, bo raz musieliśmy dłużej trzymać przycisk, albo nie byliśmy w stanie go puścić na tyle szybko by z najszybszego migania nie przechodził do najwolniejszego.
Wszystko przez to, że kod w mikrokontrolerach zwykle wykonuje się sekwencyjnie w jednym wątku. Każde opóźnienie zajmujące czas procesora powoduje, że jego reakcja na bodźce, np. zewnętrzne jest uzależniona od tego, kiedy program dojdzie do miejsca, w którym jest ten bodziec sprawdzany.

INFO: Gdyby ktoś nie wiedział, to dawno już są dostępne też mikrokontrolery wielordzeniowe. Pierwszym z nich był prawdopodobnie układ Propeller P8X32A używany w aplikacjach video, mający aż 8 jednostek wykonawczych.

Można było z tą niedogodnością walczyć pisząc program tak, by każde przejście pętli głównej było spowalniane dość krótkim odcinkami czasu - lub nawet wcale - tak aby mikrokontroler miał możliwość częstszego odpytywania stanu przycisku, i gdzieś zapisywał oraz sumował te odcinki czasu by w odpowiednim momencie podjąć akcję zmiany stanu LED. Dzięki temu mruganie diodą było by nadal takie samo jak w przypadku użycia długich opóźnień.

A gdybyśmy tak mieli możliwość natychmiastowej reakcji na przycisk, tak by program na chwilę zostawił mruganie w spokoju, a zajął się przyciskiem...? Otóż takie mechanizmy istnieją już od bardzo dawna. Są nimi przerwania.


Podejście do przerwań w mikrokontrolerach jest dość zróżnicowane. Jest jednakże cecha wspólna: Idea przerwań polega na natychmiastowym przerwaniu działania głównego wątku programu, by mógł on w sytuacjach nagłych szybko wykonać jakiś skończony wątek poboczny. Dalej są już mniejsze lub większe różnice. Np. układy AVR pozwalają na użycie przerwań, ale domyślnie tylko jedno się może w danej chwili wykonać i zakończyć by procesor mógł wykonać drugie - chyba, że sami programowo zezwolimy na wykonanie innego. W układach STM32 stosuje się zaś model, gdzie następne przerwanie może przerwać inne, gdy jego priorytet - czy to programowy, czy sprzętowy - jest wyższy. Co więcej mamy także pewne przerwania, których nie możemy w żaden sposób zamaskować, czyli nie zareagować na nie. Najlepszym przykładem w ekosystemie STM32 jest przerwanie NMI (Non Maskable Interrupt).

INFO: Sygnał RESET też jest w sumie... przerwaniem niemaskowalnym i ma największy priorytet.

Całością mechanizmu przerwań zajmuje się specjalna jednostka zwana NVIC (Nested Vectored Interrupt Controller). W układach STM32 F0 jednostka NVIC może obsługiwać do 32 źródeł przerwań z urządzeń peryferyjnych. Są one ponumerowane od 0-31. Im numer przerwania jest mniejszy, to stoi wyżej w hierarchii. Jest to pewnego rodzaju sprzętowy priorytet. Każde z nich zaś może mieć również przypisany jeden z czterech priorytetów programowych. Im liczbowo ten priorytet jest niższy tym wyższy priorytet ma takie przerwanie. Dlatego np. przerwanie przypisane do grupy o priorytecie "2" będzie mniej ważne od przerwania przypisanego do grupy o priorytecie "0" w sytuacji, gdy oba przerwania by się pojawiły w tym samym czasie lub jedno pojawiło się gdy procesor jest zajęty drugim.
Gdyby w tym samym czasie pojawiły się dwa przerwania o tym samym priorytecie programowym to system arbitrażowy weźmie pod uwagę numer przerwania (priorytet sprzętowy). Tu również będzie obowiązywać zasada, że przerwanie o niższym numerze będzie ważniejsze. Dlatego na przykład przerwania pochodzące z zewnątrz okupują niskie numery.

Istnieją też specjalne przerwania - wyjątki, które są zawsze włączone i znajdują się poza pulą wspomnianych wcześniej 32 źródeł przerwań. Część z nich ma z góry określone priorytety: RESET, NMI, HardFault (Przerwanie związane z błędem sprzętowym). Te przerwania dla odróżnienia mają numery ujemne, a więc stoją najwyżej w hierarchii.
Oprócz nich są też "mniej ważne wyjątki", którym można już ustalać priorytety programowo. Układy z rdzeniem M0 mają ich tylko kilka. Najważniejszy dla nas w późniejszym czasie będzie jeden: SysTick, który jest zgłaszany właśnie jak przepełni się licznik SysTick.


Wiemy już zatem, że mikrokontrolery STM32 mają bogaty system przerwań. Nie wiemy jednak jak zrobić by w przypadku pojawienia się przerwania, nasz program przeszedł do innego miejsca, by móc wykonać operacje, które mu wskażemy. Okazuje się to bardzo proste. Podobnie jak inne mikrokontrolery stosuje się tu system wektorów przerwań.

Na początku pamięci programu znajduje się umowna tablica, gdzie każde 32-bitowe słowo zawiera adres pod jaki ma wskoczyć procesor w przypadku wykrycia żądania przerwania o konkretnym numerze. Treść tej tablicy można odnaleźć w pliku "startup_stm32f072xb.s".

Kod: Zaznacz cały

g_pfnVectors:
  .word  _estack
  .word  Reset_Handler
  .word  NMI_Handler
  .word  HardFault_Handler
  .word  0
  .word  0
  .word  0
  .word  0
  .word  0
  .word  0
  .word  0
  .word  SVC_Handler
  .word  0
  .word  0
  .word  PendSV_Handler
  .word  SysTick_Handler
  .word  WWDG_IRQHandler                   /* Window WatchDog              */
  .word  PVD_VDDIO2_IRQHandler             /* PVD and VDDIO2 through EXTI Line detect */
  .word  RTC_IRQHandler                    /* RTC through the EXTI line    */
  .word  FLASH_IRQHandler                  /* FLASH                        */
  .word  RCC_CRS_IRQHandler                /* RCC and CRS                  */
  .word  EXTI0_1_IRQHandler                /* EXTI Line 0 and 1            */
  .word  EXTI2_3_IRQHandler                /* EXTI Line 2 and 3            */
  .word  EXTI4_15_IRQHandler               /* EXTI Line 4 to 15            */
  (...)



Teraz, aby "podłączyć się" do danego przerwania wystarczy napisać odpowiednią bezparametrową funkcję, której nazwa będzie zgodna z nazwą już predefiniowaną w tablicy wektorów. Na przykład:

Kod: Zaznacz cały

void NMI_Handler(void) {
(...)
}


Podczas budowania programu adres tej funkcji w pamięci zostanie wyliczony i zapisany w tablicy wektorów przerwań. To wszystko.


Pozostaje kwestia jak realizowane są przerwania z wyprowadzeń zewnętrznych, bo próżno szukać na liście wektorów przerwań odpowiedzialnych za porty GPIO. Otóż przerwaniami zewnętrznymi zajmuje się specjalna jednostka EXTI (External Interrupt Module), która w specyficzny dość sposób współpracuje z portami GPIO.

Wszystkie wejścia GPIO trafiają do specjalnego przełącznika - dość rozbudowanego multipleksera - który kieruje sygnały do 16 dostępnych kanałów układu EXTI. I tak. Wszystkie piny 0 każdego portu trafiają do kanału pierwszego EXTI0, wszystkie piny 1 do EXTI1, itd. Ilustruje to obrazek:
exti_mux.png


I tu pojawia się pewna trudność polegająca na tym, że nie możemy wykorzystać na raz np. PA0 i PC0, gdyż przełącznik pozwala nam na użycie jednego z pinów o indeksie 0. Możemy jednak użyć na raz PA0 i PA1, gdyż sygnały z tych pinów przejdą przez dwa multipleksery i trafią do swoich kanałów.

To nie koniec niespodzianek. Na liście wektorów przerwań mamy tylko trzy wektory związane z EXTI, a powinniśmy mieć ich 16. Aby zbytnio nie zajmować tabeli wektorów, konstruktorzy układów STM32 F0 pogrupowali po kilka kanałów w jeden.

  • Przerwania pochodzące z EXTI0 i EXTI1 wywołują przerwanie EXTI0_1_IRQ.
  • Przerwania pochodzące z EXTI2 i EXTI3 wywołują przerwanie EXTI2_3_IRQ.
  • Pozostałe przerwania pochodzące z EXTI4...EXTI15 wywołują przerwanie EXTI4_15_IRQ.

Sprawia to pewną dodatkową trudność, co tak naprawdę zgłosiło żądanie przerwania np. EXTI0. Czy było to PA0 czy jednak PA1? Tu sprawę rozwiązuje specjalny rejestr z flagami przerwań EXTI, gdzie zostanie zaznaczone, które z nich zgłosiło żądanie i będziemy mogli to odczytać odpowiednim makrem.


To może przystąpmy do prezentacji pierwszego kodu w tym odcinku. Częściowo jego treść będzie już znana.

Code: Select all

/* sekcja wprowadzenia wymaganych plikow biblioteki */
#include "stm32f0xx.h"
#include "stm32f0xx_ll_rcc.h"
#include "stm32f0xx_ll_system.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_cortex.h"
#include "stm32f0xx_ll_utils.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_exti.h"

/* tworzymy symbole by kod byl bardziej konfigurowalny */
#define LED_GPIO GPIOA
#define LED_PIN LL_GPIO_PIN_5

#define LED_ON LL_GPIO_SetOutputPin(LED_GPIO, LED_PIN)
#define LED_OFF LL_GPIO_ResetOutputPin(LED_GPIO, LED_PIN)
#define LED_TOGGLE LL_GPIO_TogglePin(LED_GPIO, LED_PIN)

#define BUTTON1_GPIO GPIOC
#define BUTTON1_PIN LL_GPIO_PIN_13
#define BUTTON1_EXTI LL_EXTI_LINE_13
#define BUTTON1_SYSCFG_EXTI_PORT LL_SYSCFG_EXTI_PORTC
#define BUTTON1_SYSCFG_EXTI_LINE LL_SYSCFG_EXTI_LINE13
#define BUTTON1_IRQ EXTI4_15_IRQn


/* sekcja zmiennych programu */

/* rozmiar i maksymalna pozycja w tabeli */
#define DELAY_TABLE_SIZE 4
#define DELAY_TABLE_MAX (DELAY_TABLE_SIZE - 1)

/* deklaracja tabeli opoznien */
static const uint32_t DELAY_TABLE[DELAY_TABLE_SIZE] = { 4, 10, 20, 40 };

/* biezaca pozycja w tabeli skad brane bedzie opoznienie,
* zmieniamy ją w przerwaniu stad argument "volatile" */
static volatile int32_t didx = DELAY_TABLE_MAX;

/* licznik odcinkow po 25ms */
static volatile uint32_t tick_counter = 0;


/* sekcja funkcji pomocniczych */

void SystemClock_Config(void)
{
/* musimy dodac opoznienia w dostepie do Flash, bo zegar przekracza 24MHz */
LL_FLASH_SetLatency(LL_FLASH_LATENCY_1);

/* wlaczamy generator HSI 8MHz */
LL_RCC_HSI_Enable();
while(LL_RCC_HSI_IsReady() != 1) {};
LL_RCC_HSI_SetCalibTrimming(16);

/* teraz mozemy skonfigurowac i uruchomic uklad PLL: 8MHz x 6 = 48MHz */
LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLL_MUL_6, LL_RCC_PREDIV_DIV_1);
LL_RCC_PLL_Enable();
while(LL_RCC_PLL_IsReady() != 1) {};

/* teraz ustalimy dzielniki SYSCLK dla magistral AHB oraz APB1 */
LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);

/* dopiero teraz mozemy przelaczyc zrodlo taktowania na wyjscie z PLL */
LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL) {};

/* ustawimy jeszcze specjalna zmienna SystemCoreClock */
LL_SetSystemCoreClock(48000000);
}

void App_Init(void)
{
/* zmieniamy konfigurację taktowania */
SystemClock_Config();

/* konfigurujemy SysTick by uzyc prostej funkcji opozniającej */
LL_Init1msTick(SystemCoreClock);

/* wlaczamy zegar dla GPIOA i GPIOC */
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOC);

/* dokonamy niskopoziomowej konfiguracji PA5 */
LL_GPIO_SetPinMode( LED_GPIO, LED_PIN, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType( LED_GPIO, LED_PIN, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed( LED_GPIO, LED_PIN, LL_GPIO_SPEED_LOW);

/* a teraz PC13 gdzie mamy przycisk */
LL_GPIO_SetPinMode( BUTTON1_GPIO, BUTTON1_PIN, LL_GPIO_MODE_INPUT);
LL_GPIO_SetPinPull( BUTTON1_GPIO, BUTTON1_PIN, LL_GPIO_PULL_UP);

/* włączamy zegar dla SYSCFG odpowiedzialnego m.in za przerwania */
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SYSCFG);

/* przyłączamy odpowiedni port do lini wejścia przerwania i... */
LL_SYSCFG_SetEXTISource(BUTTON1_SYSCFG_EXTI_PORT, BUTTON1_SYSCFG_EXTI_LINE);
/* ... włączamy ją by mogła regowac na zbocze opadajace */
LL_EXTI_EnableIT_0_31(BUTTON1_EXTI);
LL_EXTI_EnableFallingTrig_0_31(BUTTON1_EXTI);

/* pozostaje nam tylko włączyc wlasciwy kanal
* przerwania i ustawic priorytet - tu: najwyzszy*/
NVIC_SetPriority( BUTTON1_IRQ, 0);
NVIC_EnableIRQ( BUTTON1_IRQ);
}


/* sekcja handlera przerwania */

void EXTI4_15_IRQHandler(void)
{
/* sprawdzamy czy zrodlem byl nasz przycisk */
if(LL_EXTI_IsActiveFlag_0_31(BUTTON1_EXTI) != RESET)
{
/* kasujemy flagę */
LL_EXTI_ClearFlag_0_31(BUTTON1_EXTI);
/* przełączymy się w pętli na kolejne opóźnienie */
if(--didx < 0) didx = DELAY_TABLE_MAX;
}
}


/* sekcja z pętlą main() */

int main(void)
{
/* inicjalizacja */
App_Init();

for(;;){
/* opoznienie - robimy to w wiekszych jednostkach */
LL_mDelay(25);
/* sprawdzamy czy przekroczylismy wartosc */
if(++tick_counter > DELAY_TABLE[didx])
{
/* zmiana stanu i od nowa liczenie */
LED_TOGGLE;
tick_counter = 0;
}
}
}


Co się zmieniło? Na początek dodaliśmy parę symboli, które będą związane z dwoma podsystemami wykorzystywanymi w przerwaniach.
1. BUTTON1_EXTILL_EXTI_LINE_13 - Tym symbolem oznaczymy linię wejściową układu EXTI, dla której określimy jakie i czy "zdarzenie" ma wygenerować przerwanie.
2. BUTTON1_SYSCFG_EXTI_PORTLL_SYSCFG_EXTI_PORTC, BUTTON1_SYSCFG_EXTI_LINELL_SYSCFG_EXTI_LINE13 - Tymi zaś symbolami określimy skąd będzie pochodzić sygnał pobudzającą naszą linię wejściową - wstępny system multiplekserów.
3. BUTTON1_IRQEXTI4_15_IRQn - Ten zaś symbol będzie określał, który handler przerwań z EXTI ma zostać uaktywniony. Ponieważ linia EXTI13 w naszym mikrokotrolerze znajduje się w trzeciej grupie musimy użyć właśnie EXTI4_15_IRQn.

Konfigurację zegara zostawimy w spokoju, gdyż jest prawidłowa i nic się tu nie zmienia. Jedynie wyłączymy już opcję kierowania sygnału wzorcowego z układu generatora zegarowego do wyjścia MCO, ponieważ nie będzie już nam to potrzebne. Tak samo postąpimy z konfiguracją tego pinu w App_Init().

W funkcji App_Init() musimy teraz dopisać parę linijek kodu, by skonfigurować nasze przerwania. Na początek jak zwykle musimy włączyć odpowiedni zegar. Tym razem jest to zegar taktujący dla podsystemu SYSCFG. Bez tego nie bylibyśmy w stanie nic skonfigurować, ani uzyskać żadnego efektu pracy przerwań. Ponieważ SYSCFG jest podpięty pod magistralę APB1 obsługującą dalsze peryferia, trzeba skorzystać z makra LL_APB1_GRP2_EnableClock(), któremu przekazujemy parametr LL_APB1_GRP2_PERIPH_SYSCFG.

Teraz powinniśmy dokonać przełączenia wcześniej wspomnianych multiplekserów aby sygnał przerwania mógł w ogóle trafiać do podsystemu przerwań zewnętrznych EXTI. Ponieważ wykorzystujemy pin PC13 musimy zająć się multiplekserem połączonym z kanałem EXTI13 i wybrać by reagował na sygnały pochodzące z portu GPIOC. Wszystko to załatwiamy jedną instrukcją LL_SYSCFG_SetEXTISource przyjmującą tylko dwa parametry: nazwa portu oraz linia docelowa.

Aby jednak EXTI mogło w ogóle reagować na zmiany - i to tylko na jedną konkretną zmianę stanu - musimy jeszcze go skonfigurować. Robimy to dwiema następnymi instrukcjami: LL_EXTI_EnableIT_0_31 ustawi bity zezwalające na zgłaszanie przerwań przez wybraną linię, tu będzie to oczywiście linia 13: LL_EXTI_LINE_13. LL_EXTI_EnableFallingTrig_0_31 ustawi układ rozpoznawania zmiany stanu linii na zbocze opadające. Pamiętajmy, że przycisk zwiera linię PC13 do masy.

Na koniec pozostało nam określić priorytet jaki ma mieć akcja związana z przyciskiem. Ponieważ jest to akcja krytyczna dla naszego programu ustawimy jej najwyższy możliwy priorytet programowy, czyli 0, stąd taka a nie inna postać instrukcji: NVIC_SetPriority( BUTTON1_IRQ, 0). Całą konfigurację zakończymy przez finalne włączenie możliwości zgłaszania przerwań przez grupę EXTI4_15_IRQn funkcją NVIC_EnableIRQ().


Od tej chwili przerwania mogą już być zgłaszane przez przycisk. Teraz trzeba się zastanowić nad postacią procedury obsługującej to przerwanie - handlera przerwania EXTI4_15_IRQHandler. Będzie ona dość prosta i co najważniejsze dość krótka.

PRO TIP: Zawsze należy się starać by przerwania były dość krótkie. Pamiętajmy, że przerwaniem "wyrywamy" procesor z głównego kontekstu zajmując mu czasem cenny czas. Przerwanie ma po prostu zrobić swoje i zakończyć się. Stąd stosowanie jakichkolwiek pętli oczekiwania w przerwaniach nie należy do dobrych nawyków.

Na początku, jako, że mamy wspólne przerwanie dla kilku źródeł EXTI powinniśmy rozpoznać co właściwie zgłosiło to przerwanie. Ktoś może oczywiście zapytać po co, skoro akurat w tym przykładzie mamy włączone tylko jedno źródło? Niby racja, ale powinniśmy już teraz nauczyć się tego na przyszłość. Dlatego też jako pierwszą instrukcję napiszemy prosty warunek, który sprawdzi czy aby na pewno źródłem było to co włączyliśmy. Wykorzystamy makro LL_EXTI_IsActiveFlag_0_31, które zwróci nam stan flagi wyzwolenia przerwana danej linii, jaką podamy mu jako parametr. Jeśli stan flagi się potwierdzi to pierwszą instrukcją jaką wykonamy będzie skasowanie tej flagi przez LL_EXTI_ClearFlag_0_31. Robimy to po to, by nasze przerwanie zostało oznaczone jako obsłużone i by system przerwań nie zapętlił się ciągle zgłaszając nam to przerwanie ponownie zauważając, że flaga "dalej wisi". Co więcej, gdyby już teraz pojawiło się kolejne zbocze opadające, nasz system przerwań zareaguje na nie natychmiast jak opuścimy handler.

Właściwą akcję przerwania wykonuje ostatnia instrukcja, która nie robi nic innego jak tylko zmienia indeks tablicy opóźnień w obrębie dostępnych, by nie doszło do wykroczenia poza obręb tablicy w programie głównym. Załatwiamy to prostym if-em z predekrementacją zmiennej didx. Kod zawarty w main() będzie posłusznie odliczał nam odcinki czasu po 25ms nie musząc wiedzieć nic o tym, że przyciskiem zmieniamy mu ilość tych odcinków czas.


Kod w porównaniu do poprzedniego działa dość sprawnie. Reaguje na przycisk i zmienia prędkość mrugania diodą LED natychmiastowo. Ale skoro już mamy działające przerwania to przecież możemy je również wykorzystać do odliczania czasu przy współpracy z licznikiem SysTick. Takie podejście spowoduje, że procesor nie będzie się zajmował zbędnymi rzeczami i jego rola sprowadzi się tylko do roli "nadzorcy".

Aby to zadziałało to musimy napisać jeszcze jeden handler przerwania, który będzie podpięty do SysTick-a. Tu jednak staną nam na przeszkodzie pewne zalecenia co do pisania kodu przerwań w ekosystemie STM32. Być może zauważyliście, że tworząc nowy projekt w AC6 System Workbench automatycznie pojawiają się w jego strukturze dwa pliki: "stm32f0xx_it.h" oraz "stm32f0xx_it.c".
Do tej pory z nich nie korzystaliśmy, a i one same w sobie nam nie przeszkadzały. Jednak ich przeznaczenie jest ściśle określone: zaleca się, by wszystkie handlery przerwań umieszczać właśnie tam. To doprowadzi nas do sytuacji, gdzie kod powinniśmy już podzielić na moduły.

INFO: Atollic TrueStudio nie tworzy automatycznie tych plików. Trzeba je "wyciągnąć" z gotowych przykładów dołączonych do bibliotek i zmodyfikować.

W zasadzie te dwa pliki to gotowe szablony gdzie już umieszczono zalążki podstawowych handlerów przerwań. Między innymi znajdziemy tam już funkcję SysTick_Handler(). Obecnie jest ona pusta nie licząc treści kompilacji warunkowej, więc naszym zadaniem będzie dopisać jej odpowiednią treść. Nie znajdziemy tam jednak naszego handlera EXTI4_15_IRQHandler(), którego ciało częściowo skopiujemy z poprzedniej postaci kodu i dodamy tylko jego deklarację.


Pójdziemy też o krok dalej. Nasze handlery przerwań będą robić w tych plikach tylko podstawowe operacje. Resztę kodu przerwań przeniesiemy do specjalnych funkcji zwrotnych - callbacków. Dopiero one będą wykonywać pozostałe operacje.

INFO: O callbackach możecie przeczytać więcej w temacie https://microgeek.eu/viewtopic.php?f=29&t=552"%20target="_blank"


Utwórzmy zatem w głównej gałęzi w folderze "Inc" plik "main.h", który w tej wersji będzie przechowywał całą sekcję wprowadzania plików biblioteki, nasze definicje oraz dwa prototypy funkcji zwrotnych. Jego zawartość będzie wyglądała w ten sposób:

Code: Select all

#ifndef MAIN_H_
#define MAIN_H_

/* ---------------------------------------------------------------------------*/
/* sekcja wprowadzenia wymaganych plików biblioteki */

#include "stm32f0xx.h"
#include "stm32f0xx_ll_rcc.h"
#include "stm32f0xx_ll_system.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_cortex.h"
#include "stm32f0xx_ll_utils.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_exti.h"

/* tworzymy symbole by kod był bardziej konfigurowalny */
#define LED_GPIO GPIOA
#define LED_PIN LL_GPIO_PIN_5

#define LED_ON LL_GPIO_SetOutputPin(LED_GPIO, LED_PIN)
#define LED_OFF LL_GPIO_ResetOutputPin(LED_GPIO, LED_PIN)
#define LED_TOGGLE LL_GPIO_TogglePin(LED_GPIO, LED_PIN)

#define BUTTON1_GPIO GPIOC
#define BUTTON1_PIN LL_GPIO_PIN_13
#define BUTTON1_EXTI LL_EXTI_LINE_13
#define BUTTON1_SYSCFG_EXTI_PORT LL_SYSCFG_EXTI_PORTC
#define BUTTON1_SYSCFG_EXTI_LINE LL_SYSCFG_EXTI_LINE13
#define BUTTON1_IRQ EXTI4_15_IRQn

/* ---------------------------------------------------------------------------*/
/* sekcja prototypów funkcji jakie pokazujemy plikom handlera przerwań */

void SysTick_Callback(void);
void Button1_Callback(void);

#endif /* MAIN_H_ */


Teraz zmienimy zawartość pliku "main.c" by odnosiła się do pliku "main.h". Nasze zmienne "uszczelnimy" przez dodanie do nich modyfikatora static. Dzięki temu będą one należeć tylko do "main" i tylko tam będziemy nimi mogli operować.

Ponieważ SysTick jest tak skonfigurowany, że generuje odcinki czasu po 1ms, zmienimy też tabelę DELAY_TABLE by uwzględnić ten fakt.

Code: Select all

#include "main.h"


/* ---------------------------------------------------------------------------*/
/* sekcja zmiennych programu */

/* rozmiar i maksymalna pozycja w tabeli */
#define DELAY_TABLE_SIZE 4
#define DELAY_TABLE_MAX (DELAY_TABLE_SIZE - 1)

/* deklaracja tabeli opóźnień
* zmieniamy ją w przerwaniu zatem argument "volatile"
*/
static const uint32_t DELAY_TABLE[DELAY_TABLE_SIZE] = { 100, 250, 500, 1000 };

/* bieżąca pozycja w tabeli skąd brane będzie
* opóźnienie, zmieniamy ją w przerwaniu
* stąd również argument "volatile" */
static volatile int32_t didx = DELAY_TABLE_MAX;

/* licznik uderzeń licznika SysTick */
static volatile uint32_t tick_counter = 0;


/* ---------------------------------------------------------------------------*/
/* sekcja funkcji pomocniczych */

void SystemClock_Config(void)
{
/* tą funkcję już znamy, więc skrócę ją o komentarze */

LL_FLASH_SetLatency(LL_FLASH_LATENCY_1);

LL_RCC_HSI_Enable();
while(LL_RCC_HSI_IsReady() != 1) {};
LL_RCC_HSI_SetCalibTrimming(16);

LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLL_MUL_6, LL_RCC_PREDIV_DIV_1);
LL_RCC_PLL_Enable();
while(LL_RCC_PLL_IsReady() != 1) {};

LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);

LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL) {};

LL_SetSystemCoreClock(48000000);
}


void App_Init(void)
{
/* zmieniamy konfigurację taktowania */
SystemClock_Config();

/* konfigurujemy SysTick by można użyc przerwań jakie
* będzie generowac co 1ms
* szybkośc taktowania pobierzemy z specjalnej zmiennej
*/
LL_Init1msTick(SystemCoreClock);

/* skonfigurujemy najpierw porty
* włączamy zegar dla GPIOA i GPIOC */
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOC);

/* dokonamy niskopoziomowej konfiguracji PA5 */
LL_GPIO_SetPinMode( LED_GPIO, LED_PIN, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType( LED_GPIO, LED_PIN, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed( LED_GPIO, LED_PIN, LL_GPIO_SPEED_LOW);

/* a teraz PC13 gdzie mamy przycisk */
LL_GPIO_SetPinMode( BUTTON1_GPIO, BUTTON1_PIN, LL_GPIO_MODE_INPUT);
LL_GPIO_SetPinPull( BUTTON1_GPIO, BUTTON1_PIN, LL_GPIO_PULL_UP);


/* włączamy zegar dla SYSCFG odpowiedzialnego m.in za przerwania */
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SYSCFG);

/* przyłączamy odpowiedni port do lini wejścia przerwania
* i... */
LL_SYSCFG_SetEXTISource(BUTTON1_SYSCFG_EXTI_PORT, BUTTON1_SYSCFG_EXTI_LINE);
/* ... włączamy ją by mogła regowac... */
LL_EXTI_EnableIT_0_31(BUTTON1_EXTI);
/* ... na zbocze opadające */
LL_EXTI_EnableFallingTrig_0_31(BUTTON1_EXTI);

/* pozostaje nam tylko włączyc wlasciwy kanał
* przerwania i ustawic priorytet */
NVIC_EnableIRQ( BUTTON1_IRQ);
NVIC_SetPriority(BUTTON1_IRQ, 1);

/* włączymy przerwania z SysTick-a
* bo funkcja LL_Init1msTick tego nie robi
* i też mu ustawimy priorytet */
LL_SYSTICK_EnableIT();
NVIC_SetPriority(SysTick_IRQn, 0);
}


/* ---------------------------------------------------------------------------*/
/* sekcja callbacków z przerwań */

/* przerwanie z przycisku wywoła tą funkcję */
void Button1_Callback(void)
{
/* przełączymy się w pętli na kolejne
* opóźnienie
*/
if(--didx < 0) didx = DELAY_TABLE_MAX;
}


/* przerwania z licznika SysTick wywoła tą
* funkcję */
void SysTick_Callback(void)
{
/* sprawdzamy czy czas już upłynął by zmienic
* stan diody LED na przeciwny */
if(++tick_counter > DELAY_TABLE[didx]){
LED_TOGGLE;
tick_counter = 0;
}
}


/* ---------------------------------------------------------------------------*/
/* sekcja z pętlą main() */

int main(void)
{
/* konfugurację ukryjemy w specjalnej funkcji */
App_Init();

/* wysztko będą realizowac przerwania więc program
* tylko kręci się w kółko
*/
for(;;){
}
}



Teraz otwórzmy plik "stm32f0xx_it.h" gdzie w sekcji Includes dopiszemy dyrektywę dołączającą nasze definicje z pliku "main.h".

Code: Select all

/* Includes ------------------------------------------------------------------*/
#include "main.h"


I pod koniec pozostałych prototypów handlerów dodajmy jeszcze nasz, związany z użytym przerwaniem EXTI4_15IRQ:

Code: Select all

void EXTI4_15_IRQHandler(void);



W pliku "stm32f0xx_it.c" zaś musimy umieścić już nasze handlery, które będą wywoływać odpowiednie funkcje zwrotne zadeklarowane w "main.h".

Code: Select all

void SysTick_Handler(void)
{
#ifdef USE_RTOS_SYSTICK
osSystickHandler();
#endif
/* wywołujemy specjalną funkcję z main */
SysTick_Callback();
}


/**
* @brief This function handles EXTI from line 4..15.
* @param None
* @retval None
*/
void EXTI4_15_IRQHandler(void)
{
/* sprawdzamy czy źródłem był nasz przycisk */
if(LL_EXTI_IsActiveFlag_0_31(BUTTON1_EXTI) != RESET)
{
/* kasujemy flagę */
LL_EXTI_ClearFlag_0_31(BUTTON1_EXTI);

/* i wywołujemy specjalną funkcję z main */
Button1_Callback();
}
}



Po tych operacjach wykonujemy ponowną kompilację, która powinna przebiec prawidłowo. Sam program również powinien pracować tak samo jak poprzedni kod.

Jedyna różnica, to to, że nie angażujemy już tak procesora w sam proces odliczania czasu i zbędne stało się użycie blokującej funkcji opóźniającej. Ale przecież nie ma programów bez błędów lub niedoskonałości. Tu też jest jedna, która ujawnia się właśnie w takiej konfiguracji. Jeśli zauważyliście to, to doskonale!.

O co chodzi? Zmieniamy indeks tablicy w dół, co powoduje, że zmieniamy wartość opóźnienia także w dół. Zaś licznik "ticków" liczy do przodu. Załóżmy, że mamy teraz wybrane najdłuższe opóźnienie i odliczoną już wartość 750. Teraz pojawia się przerwanie od przycisku, które przesuwa indeks tablicy na pozycję gdzie zapisano 500.
Nasz callback z przerwania licznika SysTick w pierwszym warunku stwierdza - słusznie zresztą - że 750 to więcej niż 500 i resetuje licznik oraz zmienia stan diody. Jednakże czas od ostatniej zmiany jaki się tu pojawia wynosi właśnie 750ms i nijak się ma do poprzedniej wartości opóźnienia równej 1000, ani obecnej wartości 500 zapisanej w tablicy. Takie "impulsy" pośrednie nazywane są "glitch" i były częstym zjawiskiem w pierwszych rozwiązaniach układów PWM (Pulse Width Modulation).
W naszej aplikacji to nie ma znaczenia, ale gdyby zależności czasowe były krytyczne to trzeba by było temu przeciwdziałać. Z reguły robi się to buforując nową wartość, która zostaje wpisana dopiero gdy pojawia się następne przepełnienie licznika "ticków".



Co w części czwartej? Tak naprawdę... jeszcze nie wiem. Wszystko przez to, że "gołe" Nucleo nie ma zbyt wiele do zaoferowania. Zatem dalsze części tekstu aspirującego na miano kursu pojawią się dopiero jak rozwiąże się problem platformy na jakiej prowadzić go dalej.
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Wróć do „Programowanie STM w C/C++”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 5 gości