Kurs esp32/esp8266 | #3 – przerwania i timery

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:

gpio_install_isr_service(0);

Jeżeli chcielibyśmy je wyłączyć:

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:

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ł.

#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_RATE_MS);
       ets_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 podzielę to na małe podrozdziały i wpierw opiszę obsługę liczników na esp8266, a następnie na esp32.

Timery ESP8266 RTOS SDK (esp8266)

Strona w dokumentacji – hw_timer esp8266

Tutaj obsługa timerów jest bardzo prosta. Niestety jednak esp8266 zawiera dwa timery sprzętowe, z czego jeden służy procesorowi do obsługi modułu wi-fi więc do naszej dyspozycji pozostaje tylko jeden timer.

Do naszego projektu musimy dołączyć bibliotekę „hw_timer.h”:

#include "driver/hw_timer.h"

Kolejny krok to inicjalizacja timera i przypisanie do niego funkcji obsługującej przerwanie.

hw_timer_init(funkcja, *args)
  • funkcja – nazwa funkcji obsługującej przerwanie
  • *args – argumenty przekazywane funkcji (void *)

Teraz ustawiamy czas co jaki timer ma wywoływać przerwanie:

hw_timer_alarm_us(value, reload)
  • value – czas w us (mikrosekundy) z przedziału 50-1677721
  • reload – true/false (1/0) zależnie czy timer ma wykonywać się cyklicznie, czy tylko raz

Jeżeli chcemy włączyć/wyłączyć timer używamy polecenia „hw_timer_enable( true/false )”.

Przykład użycia HW Timer – esp8266

#include <stdio.h>
#include <stdlib.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/hw_timer.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);

    hw_timer_init(przerwanie, NULL);
    hw_timer_alarm_us(1000000, true);
    

    while (1) 
    {
       vTaskDelay(1000 / portTICK_RATE_MS);
       ets_printf("hello\n");
    }
}

Po załadowaniu tego kodu do mikrokontrolera, dioda podłączona do pinu GPIO16 migać będzie z częstotliwością 1Hz. Jak więc widać podstawowe uruchomienie licznika sprzętowego w środowisku ESP8266 RTOS SDK to zaledniwe dwie dodatkowe linie kodu. Oczywiście praca licznika nie wstrzymuje wykonywania się kodu zawartego w głównej pętli programu.


Timery ESP-IDF (esp32)

Strona w dokumentacji – esp32 timers

W przypadku esp32 mamy dwie grupy timerów, w których się znajdują po dwa timery. Na początek załączamy bibliotekę:

#include "driver/timer.h"

Kolejno w kodzie programu tworzymy zmienną struktry „timer_config_t” i konfigurujemy nasz timer:

timer_conf_t config = {
	.divider = 16,
	.counter_dir = TIMER_COUNT_UP,
	.counter_en = TIMER_PAUSE,
	.alarm_en = TIMER_ALARM_EN,
	.auto_reload = 1,
};
  • divider – dzielnik zegara timera
  • counter_dir – kierunek licznika (increment/decrement)
  • counter_en – licznik włączony/wyłączony od razu po przekazaniu struktury funkcji ustawiającej
  • alarm_en – włączony/wyłączony alarm
  • auto_reload – 1 – licznik odlicza do przepełnienia i się resetuje, 0 – licznik liczy tylko do przepełnienia

W dokumentacji możesz znaleźć wszystkie możliwe makrodefinicje jakie można podać poszczególnym elementom.

Przekażę teraz wskaźnik tej struktury funkcji inicjalizującej timer:

timer_init(0, 0, &config);

Jak jednak widać, poza strukturą przekazujemy jeszcze dwa argumenty. Pierwszy to grupa timera, a drugi to timer. Wyżej wspomniałem, że esp32 ma dwie grupy timerów, z czego obie mają po dwa timery. My tutaj wybraliśmy grupę TIMERG0 oraz jej hw_timer[0].

W konfiguracji nie włączyliśmy licznika więc zrobimy to tym poleceniem:

timer_set_counter_value(0, 0, 0);

Przekazujemy mu grupę timera, jego numer oraz wartość, z której ma zaczynać liczyć. Będzie tak liczył do momentu przepełnienia, lub wystąpienia alarmu i zacznie od nowa ponieważ ustawiliśmy w konfiguracji „auto_reload”.

Teraz ustawimy i włączymy alarm. Przypiszemy go naszej funkcji, która zmienia stan portu GPIO. Podajemy wartość licznika, po osiągnięciu której ma nastąpić alarm, następującym poleceniem:

timer_set_alarm_value( grupa, timer, value);

Wiadomo, grupa – 0, timer – 0. Jednak co wpisać w value? Powinniśmy tutaj umieścić liczbę znajdującą się w zakresie zmiennej typu uint64_t. Skąd jednak mamy wiedzieć jaka liczba reprezentuje przykładowo – sekunde? W przykładzie dostarczanym przez producenta możemy znaleźć makrodefinicje:

#define TIMER_SCALE	(TIMER_BASE_CLK / 16) // convert counter to seconds

TIMER_BASE_CLK to domyślny zegar, według którego pracują timery, wystarczy go podzielić przez, przez nas zdefioniowany dzielnik (w naszym przypadku 16) by otrzymać wartość licznika odpowiadająca jednej sekundzie.

Te makrodefinicje pozostawmy sobie w programie i w następujący sposób wywołajmy funkcje „timer_set_alarm_value”:

timer_set_alarm_value(0, 0, 3 * TIMER_SCALE);
timer_enable_intr(0, 0);

Więc ustawiłem by alarm wywoływał się co 3 sekundy, a kolejnym poleceniem włączyłem przerwania z timera0 (grupy 0).

Wystarczy przypisać teraz by przerwanie wywoływało naszą funkcje od zmiany stanu GPIO i włączyć timer.

timer_isr_callback_add(0, 0, przerwanie, NULL, 0);
timer_start(0, 0);

Funkcji „timer_isr_callback_all” przekazujemy numer grupy oraz timera, nazwę funkcji obsługującej przerwanie, argument i na końcu, flaga odpowiedzialnia za miejsce alokacji przerwania (wstawmy tutaj po prostu 0).

Przykład użycia Timera – esp32

#include <stdio.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "driver/gpio.h"
#include "driver/timer.h"

#define TIMER_SCALE (TIMER_BASE_CLK / 16)

static void przerwanie(void * arg)
{
	if (gpio_get_level(4)) gpio_set_level(4, 0);
  	else gpio_set_level(4, 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<<5) | (1<<4));
    io_conf.pull_down_en = 0;
    io_conf.pull_up_en = 0;

    gpio_config(&io_conf);
  
    timer_config_t config = {
     	 .divider = 16,
		.counter_dir = TIMER_COUNT_UP,
      	.counter_en = TIMER_PAUSE,
      	.alarm_en = TIMER_ALARM_EN,
      	.auto_reload = 1,
    };
	
	timer_init(0, 0, &config);
	timer_set_alarm_value(0, 0, 3*TIMER_SCALE);
	timer_enable_intr(0, 0);
	
	timer_isr_callback_add(0, 0, przerwanie, NULL, 0);
	timer_start(0, 0);
  
	while (1)
    {
    	vTaskDelay(500 / portTICK_RATE_MS);
    	gpio_set_level(5, 1);
    	vTaskDelay(500 / portTICK_RATE_MS);
    	gpio_set_level(5, 0);
    }
}

Do procesora podłączoną mam jeszcze inną diodę (gpio5), która miga z częstotliwością 0.5Hz i bardzo dobrze widać, że 3 „mrugnięcia” (pełne on-off) to jedno mrugnięcie tej podpiętnej do timera, czyli dioda zmienia stan co 3 sekundy – tak jak tego chcieliśmy.

Trochę bardziej złożone niżeli w przypadku esp8266, to z powodu tego że tutaj do dyspozycji mamy nie jeden – a cztery timery.


Z timerów to by było na tyle. Mam świadomość, że temat opisałem mocno pobieżnie jednak po przeczytaniu tego tekstu powinieneś móc bez większych trudności, w podstawowy sposób obsłużyć timery esp.

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

Kurs esp32 i esp8266 – spis treści

3 komentarze

      1. Ś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?

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *