In un precedente articolo ho iniziato a spiegarvi come utilizzare i pin del chip esp32 per attività di Input/Output. Oggi vediamo come utilizzare gli interrupts che tali pin ci offrono.
Interrupts
In informatica, un interrupt è un segnale che indica l’accadere di un evento che richiede immediata attenzione. Il segnale di interrupt interrompe quindi la normale esecuzione del programma e manda in esecuzione una particolare funzione, detta interrupt service routine (ISR) che ha il compito di reagire all’evento che si è scatenato.
Vi sono due tipi di interrupt: hardware e software.
[checklist]
- un interrupt hardware è generato da una periferica esterna alla cpu (ad esempio la pressione di un tasto su una tastiera)
- un interrupt software è generato da un programma in esecuzione sulla cpu (ad esempio un driver che ha disponibili nuovi dati)
[/checklist]
GPIO interrupts
In modalità GPIO, i pin del chip esp32 offrono diverse condizioni nelle quali possono generare un interrupt:
interrupts generati da pin in modalità I/O, gpio.h
Gli interrupt vengono generati in base alla variazione del segnale presente sul pin. Ad esempio, se il pin è configurato in modalità GPIO_INTR_POSEDGE, un interrupt sarà generato ogni volta che il segnale passa dallo stato logico 0 allo stato logico 1 (rising edge, ovvero sul “fronte di salita” del segnale). Al contrario, se configurato come GPIO_NEGEDGE, l’interrupt sarà generato sul fronte di discesa del segnale (ovvero nel passaggio 1->0).
Per configurare la modalità di interrupt di un pin possiamo utilizzare la funzione:
esp_err_t gpio_set_intr_type(gpio_num_t gpio_num, gpio_int_type_t intr_type); |
a cui va indicato il pin (gpio_num) e la modalità (intr_type).
In alternativa, se stiamo configurando i pin usando un template, possiamo specificare la modalità di interrupt tra i parametri del template stesso:
typedef struct { [...] gpio_int_type_t intr_type; /*!< GPIO interrupt type */ } gpio_config_t; |
ISR e framework
Come la gestione del pin, anche la gestione degli interrupt nel chip esp32 è abbastanza complessa: il chip infatti offre fino a 32 interrupts per ogni core, con diverse priorità… Fortunatamente il framework semplifica molto il loro utilizzo.
Per prima cosa dobbiamo installare il servizio che gestisce gli interrupts per i pin di I/O:
#define ESP_INTR_FLAG_DEFAULT 0 [...] gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT); |
Quindi possiamo indicare, per ogni pin, quale sarà la interrupt service routine che la cpu dovrà eseguire alla generazione dell’interrupt configurato per quel pin. E’ inoltre possibile passare dei parametri (args) alla ISR:
gpio_isr_handler_add(gpio_num, isr_handler, void* args); |
E’ molto importante sapere che la interrupt service routine deve avere una breve durata di esecuzione, avendo interrotto il normale programma.
ISR e tasks
Ho preparato un esempio per mostrare l’utilizzo degli interrupts e la corretta gestione della interrupt service routine. Il programma, che trovate su Github, riceve – tramite interrupt – l’evento di pressione di un tasto e cambia di conseguenza lo stato di un led.
Avendo utilizzato, come per gli altri esempi di questo tutorial, la scheda di sviluppo ESP32 DevKit-C di Espressif, ho potuto sfruttare il pulsante BOOT già presente sulla scheda e collegato al pin GPIO 0:
Come si vede dallo schema, alla pressione del tasto il pin viene collegato a massa; dovrò quindi utilizzare la modalità di interrupt GPIO_NEGEDGE.
Sia il pin a cui è collegato il pulsante, sia quello a cui è collegato il led sono configurabili tramite menuconfig:
Poco sopra vi ho spiegato che la routine che gestisce un interrupt deve essere più breve possibile; per questo tutte le azioni da compiere alla pressione del tasto – cambio di stato del led e visualizzazione di una stringa sul monitor – sono inserite in un task dedicato. E’ quindi necessario capire come poter far comunicare la ISR con questo task, per notificargli l’avvenuta pressione del tasto. FreeRTOS mette a disposizione diversi meccanismi per la comunicazione tra tasks, dai più semplici (semafori, mutex…) a quelli più complessi (code…). In questo caso è sufficiente una notifica binaria (“tasto premuto”), quindi ho scelto di utilizzare un semaforo binario.
Lo schema di funzionamento del programma è il seguente:
Per prima cosa, è necessario creare il semaforo:
SemaphoreHandle_t xSemaphore = NULL; [...] xSemaphore = xSemaphoreCreateBinary(); |
Alla pressione del tasto, viene eseguita la funzione:
void IRAM_ATTR button_isr_handler(void* arg) { xSemaphoreGiveFromISR(xSemaphore, NULL); } |
La ISR non fa altro che “dare” il semaforo, ovvero cambiare il suo stato in modo che un altro task, in attesa, possa essere sbloccato e continuare la sua esecuzione.
Il task che gestisce led e console seriale infatti ha il seguente codice:
void button_task(void* arg) { for(;;) { if(xSemaphoreTake(xSemaphore,portMAX_DELAY) == pdTRUE) { printf("Button pressed!\n"); led_status = !led_status; gpio_set_level(CONFIG_LED_PIN, led_status); } } } |
Un loop infinito che attende la disponibilità del semaforo (portMAX_DELAY indica al task di attendere un tempo infinito) e, una volta ottenuto il semaforo, visualizza il messaggio Button pressed! su console e cambia lo stato al led.
Hi, just found your website and tutorials – they’ve been extremely helpful! Been looking for a resource like this for weeks. Much appreciated!
Luca, it means CONFIG_BUTTON_PIN and CONFIG_LED_PIN are defined for the board by default, right? Because there is barely any information about it. Could you please tell more where it is defined etc?
Adam, those are variables you can configure in menuconfig. Please give a look to my other tutorial about this functionality.
Hello Luca,
I see many of your tutorial related to esp32 which is quite interesting and informative too.
I try to understand your points mentioned on the blog specific to the topic. I really appreciate it.
What I thought would be great if you take a example and put a whole code together.
Thanks
Bhavik
Hi Bhavik, do you mean a “real” project based on my tutorials?