Nella maggior parte degli sketch un pulsante si legge così: nel loop(), a ogni ciclo, si controlla se il pin è alto o basso (polling). Funziona, ma ha due limiti concreti: se il loop() è occupato in quel momento a fare qualcos'altro di lento, l'evento può sfuggire; e se serve reagire in microsecondi (un encoder rotativo che gira veloce, un impulso da un sensore di flusso) il polling semplicemente non è abbastanza rapido. Gli interrupt esterni risolvono il problema: il microcontrollore reagisce all'evento immediatamente, indipendentemente da cosa stesse facendo il loop() in quel momento.
Come funziona un interrupt su GPIO
Configuri un pin per generare un interrupt quando cambia stato secondo una condizione scelta (fronte di salita, di discesa, o entrambi). Quando la condizione si verifica, il microcontrollore interrompe qualunque cosa stia facendo il codice principale, esegue una funzione dedicata — la ISR (Interrupt Service Routine) — e poi riprende esattamente da dove era stato interrotto.
Configurazione base
const int PIN_PULSANTE = 4;
volatile bool evento = false;
void IRAM_ATTR onPulsantePremuto() {
evento = true;
}
void setup() {
Serial.begin(115200);
pinMode(PIN_PULSANTE, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_PULSANTE), onPulsantePremuto, FALLING);
}
void loop() {
if (evento) {
evento = false;
Serial.println("Pulsante premuto!");
}
}
Due dettagli fondamentali in questo esempio, entrambi obbligatori su ESP32:
IRAM_ATTR: forza la ISR a risiedere nella RAM interna invece che nella flash. Necessario perché durante l'accesso alla flash (es. per leggere il codice di altre funzioni) gli interrupt non collegati a IRAM potrebbero non essere eseguibili, causando crash imprevedibili.volatilesulla variabile condivisa: senza, il compilatore potrebbe ottimizzare il codice assumendo cheeventonon cambi mai "da sola" tra un controllo e l'altro nelloop(), ignorando le scritture fatte dalla ISR.
La regola d'oro: ISR corte e senza operazioni lente
La ISR deve fare il minimo indispensabile — impostare un flag, incrementare un contatore — e uscire il prima possibile. Dentro una ISR non vanno mai usati: delay(), Serial.print() in modo massiccio, allocazioni di memoria, o qualunque funzione che potrebbe bloccare. Il lavoro "pesante" (stampare, pubblicare su MQTT, fare calcoli) va sempre spostato nel loop(), che legge il flag impostato dalla ISR e agisce di conseguenza — esattamente come nell'esempio sopra.
Gestire il rimbalzo meccanico (debounce)
Un pulsante fisico non passa in modo pulito da 0 a 1: il contatto meccanico "rimbalza" per qualche millisecondo, generando più interrupt per una singola pressione. Un debounce semplice ed efficace via software:
volatile unsigned long ultimoEvento = 0;
void IRAM_ATTR onPulsantePremuto() {
unsigned long ora = millis();
if (ora - ultimoEvento > 50) { // ignora eventi entro 50ms dal precedente
evento = true;
ultimoEvento = ora;
}
}
Un caso reale: contare impulsi da un sensore di flusso
I sensori di flusso a effetto Hall (comuni in progetti di irrigazione o monitoraggio consumi acqua) generano un impulso per ogni piccola quantità di liquido che passa — a volte decine di impulsi al secondo. Solo un interrupt riesce a contarli tutti in modo affidabile:
volatile unsigned long conteggioImpulsi = 0;
void IRAM_ATTR onImpulsoFlusso() {
conteggioImpulsi++;
}
void setup() {
pinMode(PIN_FLUSSO, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_FLUSSO), onImpulsoFlusso, RISING);
}
Il loop() legge periodicamente conteggioImpulsi, calcola il litraggio in base al fattore di conversione del sensore (specificato nel datasheet) e lo azzera per il ciclo successivo — un pattern che con il solo polling nel loop() perderebbe sistematicamente impulsi ogni volta che il codice principale è occupato anche solo per pochi millisecondi.