Robienie 3 rzeczy jednocześnie, czyli jak zrealizować Timer Programowy? | STM32 na Rejestrach #5
Poznajmy zatem jeden ze skutecznych sposobów na to, aby nie blokować pracy procesora oczekiwaniem. Będzie to Timer Programowy. Jest to absolutna podstawa programisty embedded. Takie abecadło.
Często możesz się spotkać ze stwierdzeniem wielozadaniowości. Na przykład filmy tłumaczące ten sam mechanizm na Arduino często właśnie nazywają się pracą wielozadaniową na Arduino. To jest jednocześnie prawdą i kłamstwem.
Kłamstwem, bo na procesorze nie może dziać się więcej niż jedno zadanie, jedna instrukcja w jednym czasie. Nie wrzucamy tutaj dwóch i więcej zadań jednocześnie.
Dlaczego więc to jest prawdą i się udaje? Chodzi o złudzenie wielozadaniowości. Zdania wykonywane są naprzemiennie i w tak szybkim tempie, iż jako ludzie mamy wrażenie jednoczesności ich wykonywania. Ot cała magia.
Sztuka teraz polega na tym, aby te zadania były tak rozłożone czasowo na procesorze, aby cały wielki system sprawiał wrażenie, iż jego wszystkie elementy pracują jednocześnie i poprawnie. W tej serii nie będziemy budowali wielkiego systemu. Pokażę Ci dzisiaj przykład wielozadaniowości i braku blokowania kodu na przykładzie kilku diod LED.
Chcesz nauczyć się programowania STM32 na rejestrach w pełni? Zapraszam do mojego pełnego kursu na ten temat: https://stm32narejestrach.pl

Seria STM32 na Rejestrach na YouTube
Wpisy te powstają równolegle do serii na moim YouTube o tej samej tematyce. jeżeli wolisz wersję video to zapraszam Cię właśnie tam. Artykuły te są skrótem z tego, co pokazuję na YouTube.
Na czym polega Timer Programowy?
Musimy poznać coś takiego jak Timer Programowy.
Timerem Programowym nazywamy technikę programowania polegającą na sprawdzaniu upływającego czasu i odblokowywaniu akcji jeżeli minie go wystarczająco dużo. Pewnie nie mówi nam to zbyt wiele…
Wyobraź sobie linię czasu naszego programu. jeżeli za punkt startowy weźmiemy sobie zero, a chcemy wykonywać akcję co 500 milisekund, to przy jakich wartościach czasu musimy wykonać akcję?
500, 1000, 1500, 2000, 2500 itp. W tych momentach i nigdzie pomiędzy.

Jeśli zegarek, który odmierza czas będzie wskazywał coś innego, to nie robimy nic (albo inne akcje zaplanowane i innym czasie). To jest świat idealny. W naszym nieidealnym świecie moment zrównania się biegnącego czasu z czasem akcji „odblokuje” nam akcję do wykonania i zrobimy to przy najbliższej możliwej okazji.
Co będzie naszym zegarkiem? Już go skonfigurowaliśmy i używaliśmy w poprzednich lekcjach. SysTick Timer. On cały czas odmierza nam czas działania programu. W tle zlicza co 1 milisekundę ile czasu już upłynęło. Nie bez powodu nazywa się on SysTick – bo służy do zliczania „czasu systemowego” czy inaczej mówiąc – do podstawy czasu.
Wystarczy więc sprawdzać ile czasu minęło i porównywać z tym w których momentach chcemy wyzwolić akcję. Jak to się robi?
Od bieżącego globalnego czasu zaznaczonego w jakiś sposób powiedzmy “markerem” czekamy wyznaczony odcinek czasu. Na przykład te nasze 500 ms z początku akapitu. jeżeli czas wykonania akcji “wybił”, to wykonujemy akcję i przesuwamy nasz “marker” do przodu. I tak w kółko.

Jeszcze nie jest jasne? Będzie jaśniejsze z algorytmem i kodem
Algorytm działania Timera Programowego
Ogólny algorytm działania może być następujący:
- Tworzymy zmienną do zapaniętania bierzącego czasu – “markera”.
- „Karmimy” marker aktualnym czasem (tym systemowym, bo on jest naszym wyznacznikiem).
- Powtarzamy w pętli:
- Sprawdzamy, czy upłynął zadany odcinek czasu
- Jeśli tak:
- Wykonujemy zaplanowaną akcję
- Nadpisujemy marker aktualnym czasem*
* jest kilka możliwości i momentów na to w jaki sposób przeładowywać marker czasem. Możemy to zrobić:
1) Przed akcją / Po akcji. Wtedy decydujemy od którego momentu odliczamy ten “stały” odcinek czasowy
2) Przeładowujemy bieżącym czasem / dodajemy stałą wartość. Decydujemy o tym, czy czas wykonywania akcji wlicza się do czasu oczekiwania na kolejną akcję. Niebezbieczeństwem może być tutaj to, iż czas wykonania się akcji będzie dłuższy niż interwał między kolejnymi akcjami.
Moim ulubionym sposobem jest przeładowanie bieżącym czasem przed wykonaniem akcji i tak też zrobimy. Ten sposób jak do tej pory najlepiej mi się spisywał. Może Tobie podejdzie inny? Spróbuj i się przekonaj. U mnie nie ma żadnych przymusów
Kod Timera Programowego
Teorię mamy za sobą. Musimy więc przejść do praktyki. Naszym ćwiczeniem będzie miganie wieloma diodami z różną częstotliwością.
Więcej diod = więcej konfiguracji pinów. Na szczęście to już potrafimy. Podłączmy diody do pinów sąsiednich do diody na NUCLEO-C031. LD4 którą migaliśmy była na PA5. Podłączmy zatem LD5 na PA6 i LD6 na PA7. One są obok siebie na złączu Arudino Uno. Upewnijmy się na schemacie, iż nie są podłączone do czegoś innego.

Jak widzimy są to wolne piny. Po prostu są wyprowadzone na złącze Arduino, więc z nich skorzytamy. Piny mikrokontrolera podłącz do rezystora ok 330 ohm a rezystor do katody diody LED. Anodę do 3,3V na Nucleo. Dokładny schemat wygląda następująco.

Teraz kod. Możemy w pierwszej kolejności skopiować funkcje dla LD4 i je dostosować.
// LEDs control macros #define LD4_ON GPIOA->BSRR = GPIO_BSRR_BS5 #define LD4_OFF GPIOA->BSRR = GPIO_BSRR_BR5 #define LD4_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD5 #define LD5_ON GPIOA->BSRR = GPIO_BSRR_BS6 #define LD5_OFF GPIOA->BSRR = GPIO_BSRR_BR6 #define LD5_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD6 #define LD6_ON GPIOA->BSRR = GPIO_BSRR_BS7 #define LD6_OFF GPIOA->BSRR = GPIO_BSRR_BR7 #define LD6_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD7 // LEDs Configuration void ConfigureLD4(void); void ConfigureLD5(void); void ConfigureLD6(void); void ConfigureLD4(void) { // Enable Clock for PORTA RCC->IOPENR |= RCC_IOPENR_GPIOAEN; // Configure GPIO Mode - Output GPIOA->MODER |= GPIO_MODER_MODE5_0; GPIOA->MODER &= ~(GPIO_MODER_MODE5_1); // Configure Output Mode - Push-pull GPIOA->OTYPER &= ~(GPIO_OTYPER_OT5); // Configure GPIO Speed - Low GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED5); // Configure Pull-up/Pull-down - no PU/PD GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD5); } void ConfigureLD5(void) // PA6 { // Enable Clock for PORTA RCC->IOPENR |= RCC_IOPENR_GPIOAEN; // Configure GPIO Mode - Output GPIOA->MODER |= GPIO_MODER_MODE6_0; GPIOA->MODER &= ~(GPIO_MODER_MODE6_1); // Configure Output Mode - Push-pull GPIOA->OTYPER &= ~(GPIO_OTYPER_OT6); // Configure GPIO Speed - Low GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED6); // Configure Pull-up/Pull-down - no PU/PD GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD6); } void ConfigureLD6(void) // PA7 { // Enable Clock for PORTA RCC->IOPENR |= RCC_IOPENR_GPIOAEN; // Configure GPIO Mode - Output GPIOA->MODER |= GPIO_MODER_MODE7_0; GPIOA->MODER &= ~(GPIO_MODER_MODE7_1); // Configure Output Mode - Push-pull GPIOA->OTYPER &= ~(GPIO_OTYPER_OT7); // Configure GPIO Speed - Low GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED7); // Configure Pull-up/Pull-down - no PU/PD GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD7); }Zwrócenie bierzącego czasu
Nasze akcje będę nazywał Taskami, czyli zadaniami. Każdy Task powinien wykonywać się w innym interwale. Każde zadanie więc będzie inaczej ten czas traktowało. Potrzebujemy mieć 3 zmienne (”markery”), aby każde zadanie mogło zapamiętać swój punkt odniesienia.
Typ zmiennej Timera powinien być taki sam jak typ zmiennej zliczającej czas systemowy.
// Software Timers variables uint32_t Timer_LD4; uint32_t Timer_LD5; uint32_t Timer_LD6;Powinniśmy załadować bieżący czas do tych zmiennych. Tylko skąd go wziąć? Można byłoby po prostu przypisać zmienną Tick od SysTick Timera, ale TAK SIĘ NIE ROBI!
W przyszłosci będziemy dzielić nasz kod na pliki, więc zacznijmy się na to przygotowywać. Potrzebujemy funkcji, która nam zwróci ten czas. Nie możemy się za każdym razem zastanawiać w jaki sposób uzyskujemy czas globalny. Bo co jak zmienimy metodę pozyskiwania go? Będziemy szperać w całym programie i zmieniać każdą linijkę? No nie. Musi to być opakowane w wygodną funkcję.
// Tick for System Time __IO uint32_t Tick; uint32_t GetSystemTick(void) { return Tick; // Return current System Time }Mając taka funkcję zwracającą czas systemowy będzie dużo łatwiej i poprawniej.
Możemy skorzystać z niej, aby nakarmić stan początkowy naszych zmiennych Timerowych. Zróbmy to zaraz za konfiguracją LEDów.
// Configure LEDs ConfigureLD4(); ConfigureLD5(); ConfigureLD6(); // Software Timers - first fill Timer_LD4 = GetSystemTick(); Timer_LD5 = GetSystemTick(); Timer_LD6 = GetSystemTick();Mamy pierwsze zaznaczenie markerem. Zróbmy najpierw jedno zadanie na znanej nam już diodzie LD4. Blokująco z użyciem Delay zmienialiśmy jej stan co 500 ms. Zróbmy to teraz nieblokująco.
Po pierwsze przydałoby się wygodna defnicja tego czasu oczekiwania.
// Constants for Software Timer's actions #define LD4_TIMER 500Mając ten czas możemy w końcu napisać kod algorytmu wyzwalania Taska.
- Trzeba sprawdzić czy upłynęła zadana ilość czasu. Porównujemy różnicę między bieżącym czasem, a tym “zaznaczonym”. Jesli różnica ta jest większa niż wymagany czas między akcjami, to zaczynamy wywoływać akcjię.
- Możemy od razu przeładować Timer, czyli zaznaczyć kolejny punkt odniesienie na osi czasu od którego chcemy, aby minął wymagany czas do ponownego wykonania naszej akcji. Umieszczenie w tym miejscu przeładowania daje nam to, iż rozpoczęcie akcji będzie zawsze co wyznaczony czas niezależnie od tego ile czau trwa wykonanie zadania.
Zadania mogą być różne i trwać różną ilość czasu w zależności od aktualnych warunków. Takie umieszczenie przeładowania na początku jest bardzo dobrym wyborem. - Czym przeładować? Tak jak mówiłem – ja preferuję aktualnym czasem.
Jak już mamy wszystkie sprawy w okół działania timera załatwione to możemy w końcu przejść do wykonania naszej akcji. My migamy diodą, ale to może być dowolna akcja. Odświeżenie ekranu, odczyt tempratury, odpytanie jakiegoś układu o pomiar. W zasadzie wszystko co potrzebujemy wykonywać cyklicznie.
Kod wykonujący te czynności będzie wyglądał na przykład tak:
// LD4 if((GetSystemTick() - Timer_LD4) > LD4_TIMER) // Check if is time to make action { Timer_LD4 = GetSystemTick(); // Refill action's timer LD4_TOGGLE; // ACTION! }Proste, prawda? Ten mechanizm jest niesamowicie prosty, a jak bardzo użyteczny!
Pozostałe diody
Pozostałe nasze akcje na diody robimy analogicznie zmieniając jedynie czas między wywołaniami.
Możemy śmiało skopiować fragmenty dla pierwszej diody. Musimy pamięctć o tym, aby zmienić im piny i definicje które się ich tyczą. Przecież są to inne piny i z inną częstotliwością chcemy migać.
Gotowy kod naszego programu będzie wyglądał następująco:
/** ****************************************************************************** * @file : main.c * @author : Mateusz Salamon * @brief : STM32 na Rejestrach ****************************************************************************** ****************************************************************************** https://msalamon.pl https://sklep.msalamon.pl https://kursstm32.pl https://stm32narejestrach.pl */ #include "main.h" // LEDs control macros #define LD4_ON GPIOA->BSRR = GPIO_BSRR_BS5 #define LD4_OFF GPIOA->BSRR = GPIO_BSRR_BR5 #define LD4_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD5 #define LD5_ON GPIOA->BSRR = GPIO_BSRR_BS6 #define LD5_OFF GPIOA->BSRR = GPIO_BSRR_BR6 #define LD5_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD6 #define LD6_ON GPIOA->BSRR = GPIO_BSRR_BS7 #define LD6_OFF GPIOA->BSRR = GPIO_BSRR_BR7 #define LD6_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD7 // Constants for Software Timer's actions #define LD4_TIMER 500 #define LD5_TIMER 222 #define LD6_TIMER 147 // Tick for System Time __IO uint32_t Tick; // LEDs Configuration void ConfigureLD4(void); void ConfigureLD5(void); void ConfigureLD6(void); // Get current System Time uint32_t GetSystemTick(void); // Software Timers variables uint32_t Timer_LD4; uint32_t Timer_LD5; uint32_t Timer_LD6; int main(void) { // 1s = 12 000 000 // 0,001 = X SysTick_Config(12000000 / 1000); // Configure LEDs ConfigureLD4(); ConfigureLD5(); ConfigureLD6(); // Software Timers - first fill Timer_LD4 = GetSystemTick(); Timer_LD5 = GetSystemTick(); Timer_LD6 = GetSystemTick(); /* Loop forever */ while(1) { // LD4 if((GetSystemTick() - Timer_LD4) > LD4_TIMER) // Check if is time to make action { Timer_LD4 = GetSystemTick(); // Refill action's timer LD4_TOGGLE; // ACTION! } // LD5 if((GetSystemTick() - Timer_LD5) > LD5_TIMER) { Timer_LD5 = GetSystemTick(); LD5_TOGGLE; } // LD6 if((GetSystemTick() - Timer_LD6) > LD6_TIMER) { Timer_LD6 = GetSystemTick(); LD6_TOGGLE; } } } void ConfigureLD4(void) { // Enable Clock for PORTA RCC->IOPENR |= RCC_IOPENR_GPIOAEN; // Configure GPIO Mode - Output GPIOA->MODER |= GPIO_MODER_MODE5_0; GPIOA->MODER &= ~(GPIO_MODER_MODE5_1); // Configure Output Mode - Push-pull GPIOA->OTYPER &= ~(GPIO_OTYPER_OT5); // Configure GPIO Speed - Low GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED5); // Configure Pull-up/Pull-down - no PU/PD GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD5); } void ConfigureLD5(void) // PA6 { // Enable Clock for PORTA RCC->IOPENR |= RCC_IOPENR_GPIOAEN; // Configure GPIO Mode - Output GPIOA->MODER |= GPIO_MODER_MODE6_0; GPIOA->MODER &= ~(GPIO_MODER_MODE6_1); // Configure Output Mode - Push-pull GPIOA->OTYPER &= ~(GPIO_OTYPER_OT6); // Configure GPIO Speed - Low GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED6); // Configure Pull-up/Pull-down - no PU/PD GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD6); } void ConfigureLD6(void) // PA7 { // Enable Clock for PORTA RCC->IOPENR |= RCC_IOPENR_GPIOAEN; // Configure GPIO Mode - Output GPIOA->MODER |= GPIO_MODER_MODE7_0; GPIOA->MODER &= ~(GPIO_MODER_MODE7_1); // Configure Output Mode - Push-pull GPIOA->OTYPER &= ~(GPIO_OTYPER_OT7); // Configure GPIO Speed - Low GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED7); // Configure Pull-up/Pull-down - no PU/PD GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD7); } void SysTick_Handler(void) { Tick++; // Increase system timer } uint32_t GetSystemTick(void) { return Tick; // Return current System Time }Jeśli mamy wszystko skopiowane, to możemy uruchamić program i zobaczyć czy to działa. To już oczywiście potrafisz
Podsumowanie
Timer Programowy to świetny sposób na realizację zadań w systemie embedded bez blokowania procesora. Działa na zasadzie sprawdzania upływającego czasu i wyzwalania akcji w odpowiednich momentach. Dzięki temu możemy stworzyć złudzenie wielozadaniowości i zapewnić płynne działanie wielu elementów systemu.
Podstawą działania jest SysTick Timer, który w tle zlicza czas w milisekundach. Wystarczy przechowywać „marker” czasu, porównywać go z aktualnym czasem systemowym i w odpowiednich momentach wyzwalać akcje, np. miganie diodą LED. najważniejsze jest odpowiednie przeładowanie znacznika czasu, co pozwala uniknąć błędów związanych z długim czasem wykonywania zadania.
Dzięki tej metodzie możemy niezależnie sterować wieloma diodami LED o różnych częstotliwościach migania, bez konieczności używania blokujących funkcji delay. Mechanizm ten jest uniwersalny i może być używany do sterowania dowolnymi cyklicznymi zadaniami – odczytami czujników, obsługą komunikacji czy odświeżaniem wyświetlacza.
W kolejnym wpisie zajmiemy się kolejnym elementem praktycznego programowania na rejestrach. Będzie to pierwsza komunikacja ze światem zewnętrznym. Dotkniemy interfejsu UART
Daj znać w komentarzu czy Ci się podobał ten wpis! Może masz jakąś propozycję co pokazać w ramach cyklu STM32 na Rejestrach? Podziel się tym artykułem ze znajomymi.
Zapraszam Cię również do mojego sklepu, gdzie kupisz interesujące moduły do eksperymentów, w tym NUCLEO-C031C6, z którego korzystamy w tej serii:
https://sklep.msalamon.pl
Projekt z tego artykułu znajdziesz na:
https://github.com/lamik/stm32narejestrach_5