
Tekst zgodny z esp-idf 5.4+
Przerwania i timery | esp-idf
Przerwania (Interrupts), są czymś co wstrzymuje aktualną pracę mikrokontrolera by wykonać pewne określone zadanie. Dzielimy je na zewnętrzne i wewnętrzne. Zewnętrzne przerwania są wywoływane chociażby zmianą stanu na konkretnym pinie, lub pojawieniem się sygnałów na liniach i2c, spi itp… Natomiast przerwania wewnętrzne pochodzą z timer’ów, czyli wbudowanych w mikrokontroler sprzętowych liczników, które po określonym czasie wywołają przerwanie.
Przerwanie podpinamy do tzw. obsługi przerwania, czyli po prostu funkcji, która zawiera kod jaki ma się wykonać po wystąpieniu przerwania. Przykładowo, jeżeli chcielibyśmy podpiąć do na naszego mikrokontrolera przycisk to chcielibyśmy żeby procesor reagował na jego wciśnięcie błyskawicznie, a nie dopiero gdy w kodzie napotka na sprawdzenie stanu pinu do którego jest on wpięty. Wystarczy do takiego pinu po prostu podpiąć przerwanie i w jego obsłudze kod jaki ma się wykonać po wciśnięciu przycisku. Po jego wykonaniu mikrokontroler powróci do miejsca w kodzie gdzie przerwał swoją pracę.
Podobnie jest z timerami, tylko tutaj definiujemy w kodzie co jaki czas ma występować przerwanie. Jest to bradzo przydatne np. do aktualizacji wyświetlacza co określony czas, czy regularne sprawdzenie wartości jakiegoś czujnika.
Obsługa przerwań zewnętrznych (z pinów gpio) na esp32 i esp8266 wygląda identycznie, ale sterowanie licznikami trochę się różni.
Przrwania zewnętrzne (GPIO)
Najpierw musimy w kodzie włączyć obsługę przerwań, robimy to poleceniem:
Kod nr. 1
gpio_install_isr_service(0);
Jeżeli chcielibyśmy je wyłączyć:
Kod nr. 2
gpio_uninstall_isr_service(0);
Kolejnym krokiem będzie zdefiniowanie typu przerwania, i to robimy podczas konfiguracji pinów GPIO (element intr_type), co było opisane w części o portach gpio.
I ostatecznie podpinamy funkcje przerwania pod pin takim poleceniem:
Kod nr. 3
gpio_isr_handler_add( pin_num, funkcja, *args);
- pin_num – numer pinu gpio
- funkcja – nazwa funkcji, która ma być wywoływana przez przerwanie
- *args – argument/y, które chcemy przekazać funkcji (void *). Brak – NULL.
Przerwania zewnętrzne – przykładowy program
Poniżej zamieszczam program prosty program, który zmienia stan pinu GPIO16 na przeciwny. Wykorzystuję do tego przerwanie zewnętrzne pochodzące z pinu GPIO5, w pętli while umieściłem dwa losowe polecenia tylko po to by procesor się nie resetował.
Kod nr. 4
#include <stdio.h> #include <stdlib.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" static void przerwanie(void *arg) { if (gpio_get_level(16)) gpio_set_level(16, 0); else gpio_set_level(16, 1); } void app_main(void) { gpio_config_t io_conf; io_conf.intr_type = GPIO_INTR_DISABLE; io_conf.mode = GPIO_MODE_INPUT_OUTPUT; io_conf.pin_bit_mask = ((1<<16)); io_conf.pull_down_en = 0; io_conf.pull_up_en = 0; gpio_config(&io_conf); gpio_install_isr_service(0); io_conf.intr_type = GPIO_INTR_POSEDGE; io_conf.mode = GPIO_MODE_INPUT; io_conf.pin_bit_mask = ((1<<5)); io_conf.pull_down_en = 1; io_conf.pull_up_en = 0; gpio_config(&io_conf); gpio_isr_handler_add(5, przerwanie, NULL); while (1) { vTaskDelay(1000 / portTICK_PERIOD_MS); printf("hello\n"); } }
GPIO16 skonfigurowałem jako INPUT_OUTPUT, dlatego że tylko wtedy (w przypadku esp-idf) możemy odczytywać stan portu, który działa jako wyjście. W przypadku esp8266 rtos sdk można odczytywać stan portu nawet gdy ten jest ustawiony jako OUTPUT.
Więc jak widać, na samym początku utworzyłem funkcje o nazwie „przerwanie”, która zawiera kod sprawdzający stan pinu diody i zmienia go na przeciwny. Później, poleceniem „gpio_isr_handler_add(5, przerwanie, NULL)” podpiąłęm tę funkcje pod przerwanie pochodzące z pinu GPIO5. Ostatnia wartość powinna zawierać argumenty jakie bym chciał funkcji tej przekazać. NULL oznacza, że nie przekazujemy żadnych argumentów.
Teraz, za każdym razem gdy na pinie 5 pojawi się zbocze narastające, wywoła ono funkjce „przerwanie”, która zmieni stan diody.
Timery sprzętowe
Zajmiemy się teraz timerami, a naszym celem będzie utworzyć program, który będzie wywoływać tę samą funkcje co w przykładzie z przerwaniami zewnętrznymi, tyle że teraz będzie się ona wywoływać automatycznie co określoną ilość czasu. Timer może także służyć jako zwykły licznik czasu, który można odpytywać o aktualną jego wartość.
Jak wspominałem, obsługa timerów nieco się różni na esp32 i esp8266 więc postanowiłem odseparować konfigurację dla środowiska esp8266 i znajdziesz ją pod tym linkiem: esp8266 timer
Timery ESP-IDF (esp32)
Strona w dokumentacji – esp32 timers
W przypadku esp32 do dyspozycji możemy mieć do 4 czterech niezależnych timerów sprzętowych. Ta liczba jednak ulega czasem zmianie, niektóre z modułów mikrokontrolera używają timerów do swojej pracy (np. Wi-Fi, Bluetooth). W najnowszej implementacji esp-idf nie musimy już wskazywać konkretnego timera jakiego chcemy użyć, zrobi to za nas moduł obsługujący liczniki sprzętowe. Kluczową więc sprawą jest zwalnianie timerów gdy ich nie potrzebujemy, a także obsługa sytuacji, w których brakuje dostępnego licznika w przypadku bardziej złożonych aplikacji wykorzystujących wiele komponentów mikrokontrolera.
Liczniki czasowe w esp-idf możemy skonfigurować na aż 3 sposby. Używając modułu GPTimer (General Purpose Timer), który wykorzystuje wewnętrzne peryferium sprzętowego licznika. Modułu ESP-Timer – ten z kolei całość zliczania i wyzwalania callbacków realizuje programowo, lub z pomocą frameworka FreeRTOS, ten też ma możliwość programowej konfiguracji liczników.
Co więc wykorzystać? Sprzętowy timer nada się wszędzie tam, gdzie potrzebujemy bardzo dokładnego odmierzania czasu, np. przy generowaniu przebiegów, odtwarzaniu protokołów komunikacyjnych i tym podobnych. Jeżeli potrzebujemy co określony czas mrugać diodą, czy odświeżać matrycę wyświetlacza gdzie dokładność co do nanosekundy nie jest dla nas ważna – wystarczy nam obsługa programowa. Także jak zwykle, to wszystko zależy od potrzeb, preferencji i przypadku.
esp32 GPTimer – timery sprzętowe
Na samym początku na warsztat wezmę General Purpose Timer, do jego obsługi potrzebujemy załączyć następujący zasób:
Kod nr. 5
#include "driver/gptimer.h"
Kolejno tworzę uchwyt timera, i strukturę konfiguracyjną:
Kod nr. 6
gptimer_handle_t gptimer1 = NULL; gptimer_config_t timer_conf1 = { .clk_src = GPTIMER_CLK_SRC_DEFAULT, .direction = GPTIMER_COUNT_UP, .resolution_hz = 1 * 1000 * 1000, };
Tutaj ustawiłem po kolei: źródło zegara, kierunek zliczania „ticków” (inkrementacja), oraz rozdzielczość w hercach, w podanym przypadku wartość ta wyniesie 1000000, czyli tyle ticków przypadnie na jedną sekundę.
Kod nr. 7
// Wariant 1 gptimer_new_timer(&timer_conf, &gptimer); // Wariant 2 - bezpieczniejszy esp_err_t err_timer_create = gptimer_new_timer(&timer_conf, &gptimer); if (err_timer_create != ESP_OK) { printf("Brak dostepnych GPTimerow!\n"); }
Ta funkcja (gptimer_new_timer) tworzy timer z użyciem naszej struktury konfiguracyjnej, równocześnie przypisuje do niego wskazany handler (uchwyt). I w tym miejscu podałem dwa warianty wykonania tejże funkcji, drugi z nich wykorzystuje po prostu moduł esp-idf odpowiedzialny za przetwarzanie błędów. W przypadku wariantu numer 1, gdy nie będzie dostępnego wolnego timera sprzętowego wystąpi błąd i mikrokontroler się zresetuje. W wariancie numer 2, możemy na powstały błąd zareagować i uniknąć resetu esp. Gdy tworzymy bardziej złożone urządzenie warto takie przypadki brać pod uwagę.
Gdybyśmy teraz uruchomili skonfigurowany licznik, ten zacząłby działać tj. zliczać ticki i moglibyśmy go odpytywać o aktualny ich stan z pomocą funkcji „gptimer_get_raw_count” przekazując jej handler timera i pointer do zmiennej uint64_t, w której umieszczony zostanie odczyt. Nam jednak zależy na tym by to sam timer generował przerwanie. Skonfigurujmy więc wystąpienie alarmu.
Kod nr. 8
// Konfigiracja alarmu gptimer_alarm_config_t alarm_config = { .alarm_count = 1000000, .flags.auto_reload_on_alarm = true, .reload_count = 0, }; gptimer_set_alarm_action(gptimer, &alarm_config); // Przypisanie funkcji obsługującej alarm (przerwanie) gptimer_event_callbacks_t cbs = { .on_alarm = przerwanie, }; gptimer_register_event_callbacks(gptimer, &cbs, NULL); // Włączenie i wystartowwanie timera gptimer_enable(gptimer); gptimer_start(gptimer);
W sekcji konfiguracji tworzymy strukturę, a w niej ustawiamy wartość, przy której wystąpić ma alarm (wywołane ma zostać przerwanie), potem ustawiamy flagę „auto_reload_on_alarm” na true, a więc po wystąpieniu alarmu, wartość licznika zostanie „przeładowana”. Trzecia zmienna, czyli „reload_count” to wartość licznika, która ma zostać ustawiona po owym przeładowaniu licznika.
Jeśli flagi „auto_reload_on_alarm” nie ustawiłbym jako true, to licznik po wywołaniu alarmu inkrementowałby wartość ticków dalej i alarm wystąpiłby dopiero po przepełnieniu zmiennej zliczającej ticki, a wartość w zmiennej „reload_count” nie miałaby na nic wpływu.
Następnie wystarczy przypisać funkcję, która będzie obsługiwała przerwanie i włączyć, a kolejno wystartować timer. …Oczywiście nic nie stoi na przeszkodzie by timer startować w innym, dowolnym miejscu w kodzie programu.
A tak wygląda definicja funkcji obsługującej przerwanie, zgodnie z dokumentacją musi być ona typu bool.
Kod nr. 9
static bool IRAM_ATTR przerwanie(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) { ets_printf("timer 1\n"); return true; }
Funkcji zostaje przekazany uchwyt GPTimera, dane odnośnie alarmu przekazane przez driver tj. pointer do struktury zawierającej dwie zmienne:
- count_value – aktualny stan licznika
- alarm_value – wartość licznika po „relaodzie”

Mogą się nam czasem te dane przydać w samej obsłudze przerwania. A ostatnia zmienna to wskaźnik do danych, które użytkownik przekazuje obsłudze przerwania. Wskaźnik ten jest przekazywany na etapie „gptimer_register_event_callbacks” (patrz kod nr. 8), w naszym przykładzie nic nie przekazujemy (NULL). Jeśli byśmy te dane jednak przekazywali, to oczywiście można ich wartości dowolnie modyfikować, funkcja posiada tylko wskaźnik i po nim do tych danych sobie dochodzi.
Zwróć też uwagę na polecenie wyświetlające dane na terminalu. Wyświetlanie tekstu jest oczywiście zrealizowane za pomocą funkcji „ets_printf”, czyli funkcji nieblokującej, a to kluczowe w przypadku funkcji typu IRAM_ATTR.
GPTimer przekazywanie argumentów
Na sam koniec zademonstruje kompletny kod implementacji gptimera, który z grubsza pokaże jego ogóle działanie, włączenie z przekazywaniem argumentów obsłudze przerwania.
Kod nr. 10
#include <stdint.h> #include <stdio.h> #include <stdbool.h> #include <unistd.h> #include "driver/gptimer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "rom/ets_sys.h" gptimer_handle_t gptimer = NULL; typedef struct { uint8_t liczba; char *tekst; } dane; static bool IRAM_ATTR przerwanie(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) { dane *dane1 = (dane *)user_ctx; ets_printf("dane_timera: liczba: %d, tekst: %s\n", (uint8_t)dane1->liczba, (char*)dane1->tekst); ets_printf("Aktualny stan licznika: %d, stan licznika po relaodzie: %d\n\n", (uint64_t)edata->count_value, (uint64_t)edata->alarm_value); return true; } void app_main(void) { gptimer_config_t timer_conf = { .clk_src = GPTIMER_CLK_SRC_DEFAULT, .direction = GPTIMER_COUNT_UP, .resolution_hz = 1 * 1000 * 1000, }; gptimer_new_timer(&timer_conf, &gptimer); gptimer_alarm_config_t alarm_config = { .alarm_count = 1000000, .flags.auto_reload_on_alarm = true, .reload_count = 500, }; gptimer_set_alarm_action(gptimer, &alarm_config); gptimer_event_callbacks_t cbs = { .on_alarm = przerwanie, }; dane dane_timera; dane_timera.liczba = 30; dane_timera.tekst = "przykladowy tekst"; gptimer_register_event_callbacks(gptimer, &cbs, &dane_timera); gptimer_enable(gptimer); gptimer_start(gptimer); while (true) { dane_timera.liczba ++; vTaskDelay( 1000 / portTICK_PERIOD_MS); } }
esp32 ESP Timer – timery programowe
Strona w dokumentacji – esp32 esp timer
Jak już wspomniałem wyżej, esp-idf ma także możliwość programowej realizacji liczników odmierzających czas i generujących przerwania. I wszędzie tam gdzie nie jest nam potrzebna jak największa dokładność (wystarczą nam wartości co do mikrosekundy), śmiało możemy z nich korzystać. Co także bym polecał przy większości zastosowań. Ich implementacja jest ponadto znacznie prostsza.
Kod nr. 11
#include <stdio.h> #include <stdbool.h> #include <unistd.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_timer.h" // Deklaracja uchwytu timera esp_timer_handle_t timer = NULL; static void przerwanie(void *args) { printf("przerwanie timera\n"); } void app_main(void) { // Struktura konfiguracyjna esp_timer_create_args_t timer_conf = { .callback = &przerwanie, .name = "Przerwanie_timera", }; // Tworzenie timera esp_timer_create(&timer_conf, &timer); /* Start timera w trybie pracy periodycznej. * istnieje też opcja jednorazowego wywołania timera, * przy pomocy funkcji esp_timer_start_once */ esp_timer_start_periodic(timer, 1006000); while (true) { uint64_t period; // Pobieranie aktualnie ustawionego czasu odliczania timera esp_timer_get_period(timer, &period); printf("period: %lld\n", period); vTaskDelay( 1000 / portTICK_PERIOD_MS); } }
Przez fakt niewielkiego skomplikowania obsługi liczników sprzętowych, postanowiłem cały kod umieścić w ramach jednej sekcji. To co widzisz powyżej to najprostszy sposób uruchomienia tego komponentu.
Zwróć uwagę na definicję funkcji „przerwanie”, nie jest to tak de facto przerwanie, a callback. Używam tych słów zamiennie, aczkolwiek nie jest to do końca poprawne, przerwania wywołuje sam procesor lub jego komponenty. Przez to dodajemy zawsze funkcjom obsługującym przerwania przedrostek „IRAM_ATTR”, co zmusza procesor do umieszczenia tych kawałków kodów w pamięci RAM po to, by szybszy był do nich dostęp.
Callback to po prostu funkcja która ma zostać wywołana przez inną funkcję. Jako, że nie jest to już sekcja krytyczna, mogłem umieścić w niej regularne polecenie „printf”.
Kod opisałem za pomocą komentarzy, nie będę więc powielał tych informacji w tekście. Callbackom możemy przkazywać argumenty, wygląda to bardzo podonie jak w przypadku timerów sprzętowych.
Funkcje, które jeszcze mogą być przydatne:
- esp_timer_stop – stopuje timer
- esp_timer_restart – restartuje timer
- esp_timer_delete – usuwa timer
Po więcej dostępnych funkcji i ich wyjaśnienie zapraszam rzecz jasna do oficjalnej dokumentacji. Nie ma sensu tutaj wszystkiego z niej przytaczać.
Timery sprzętowe w trybach uśpienia
W sytuacji pełnego uśpienia procesora (deep-sleep mode), timery nie działają ani nie zliczają żadnych tików. W przypadku trybu Light-sleep natomiast, tiki zlicza układ RTC, aczkolwiek timery nie wywołują wybudzenia procesora, co za tym idzie – callbacków, w momencie jednak jego wybudzenia w inny sposób, powinny wykonać się zaległe callbacki jeżeli nie doszło do przepełnienia kolejki callbacków. Generalnie jeżeli używamy timerów programowych i mamy zamiar usypiać nasz procesor to musimy dobrze oprogramować wszelkie możliwe do wystąpienia sytuacje. Więcej o tym przeczytasz w dokumentacji.
Pamiętaj by zawsze mieć otwartą dokumentacje środowiska, w którym pracujesz, czy to esp-idf, czy esp8266 rtos sdk i staraj się samemu poznawać resztę funkcji omawianych tutaj zagadnień.
Kolejna część: #4 – UART
Cześć,
czy jest szansa, że dokończysz ten kurs? Byłbym wdzięczny. Niewiele Ci pozostało do ukończenia 🙂
Witaj, kurs będzie skończony. W najbliższym czasie postaram się na tym właśnie skupić.
Świetnie. A czy planujesz może zrealizować chociaż jeden przykład bazujący na na WiFi? Np. transmisja za pomocą protokołu UDP, ewentualnie http?