Basi · 14 min di lettura

Interrupt Arduino: attachInterrupt, regole ISR e debounce

Come usare gli interrupt su Arduino: attachInterrupt, pin 2 e 3 della Uno, RISING/FALLING/CHANGE, volatile, debounce, encoder e giri/min, ESP32 e Pico.

Un interrupt su Arduino fa sì che il processore lasci ciò che sta facendo nell'istante in cui su un pin avviene il cambiamento che stai aspettando (o scatta un evento di un timer) ed esegua una funzione speciale scritta da te. In breve: invece di chiedere di continuo dentro loop() "il pulsante è stato premuto?", dici ad Arduino "avvisami quando viene premuto". Lo fai con la funzione attachInterrupt(); sulla Arduino Uno i pin di interrupt sono D2 e D3, mentre sull'ESP32 e sul Raspberry Pi Pico quasi ogni GPIO può generare un interrupt.

Gli interrupt sono indispensabili per contare impulsi molto veloci senza perderne nessuno (encoder, sensori di velocità, flussimetri), cogliere un evento breve nel mezzo di un compito lungo e risvegliare una scheda dalla modalità sleep. In questa guida vedrai passo passo come funziona un interrupt a livello hardware, quali pin puoi usare su ogni scheda, la differenza tra le modalità RISING, FALLING e CHANGE, le regole per scrivere una routine di servizio dell'interrupt (ISR), come eliminare i rimbalzi di un pulsante ed esempi con un pulsante, un encoder e un contagiri. Alla fine scoprirai anche IRAM_ATTR sull'ESP32 e l'uso di irq con MicroPython sul Pico.

Pin di interrupt della Arduino Uno D2 (INT0) e D3 (INT1)
La Arduino Uno ha solo due pin di interrupt esterno: D2 (INT0) e D3 (INT1).

Che cos'è un interrupt e come funziona?

Pensa a un campanello: senza, dovresti andare alla porta ogni minuto per controllare se è arrivato qualcuno. Questo si chiama polling (interrogazione continua), ed è esattamente ciò che fai quando leggi un pin con digitalRead() dentro loop(). Il campanello invece è un interrupt: suona mentre sei occupato con altro, ti fermi, guardi alla porta e torni a ciò che stavi facendo. Un microcontrollore funziona allo stesso modo. Quando sul pin avviene il cambiamento che hai scelto, l'hardware alza un flag di interrupt; il processore termina l'istruzione in corso, salva i registri, salta tramite il vettore di interrupt alla tua ISR (Interrupt Service Routine, routine di servizio dell'interrupt) e, terminata la ISR, riprende loop() esattamente dal punto in cui si era fermato. Sulla Uno questo passaggio richiede solo pochi microsecondi.

CaratteristicaPollingInterrupt
Come funzionaloop() legge il pin in continuazioneL'hardware rileva il cambiamento ed esegue la ISR
Tempo di rispostaPiù loop() è lento, maggiore è il ritardoPochi microsecondi
Con delay() nel codiceGli eventi possono andare persiGli eventi vengono comunque catturati
Impulsi molto velociVengono persiVengono contati uno per uno
Complessità del codiceSempliceServono volatile e letture atomiche
Ideale perPulsanti, sensori lentiEncoder, sensori di velocità e di flusso, risveglio dallo sleep

Quali pin supportano gli interrupt? Arduino, ESP32 e Pico

SchedaPin di interrupt esternoNota
Arduino Uno R3, Nano, Pro MiniD2, D3D2 = INT0, D3 = INT1
Arduino Uno R4 (Minima / WiFi)D2, D3I pin indicati nella reference ufficiale di Arduino
Arduino Mega 25602, 3, 18, 19, 20, 2120 e 21 non sono utilizzabili come interrupt mentre servono per l'I2C
Arduino Leonardo, Micro0, 1, 2, 3, 70 e 1 sono anche i pin della porta seriale
Arduino Nano EveryTutti i pin digitaliProcessore ATmega4809
ESP32Tutti i GPIOI GPIO 34–39 sono solo ingressi e non hanno pull-up interna
ESP8266 (NodeMCU)Tutti tranne GPIO 16 (D0)Le ISR vanno marcate con IRAM_ATTR
Raspberry Pi Pico / Pico WTutti, da GP0 a GP28I pin lavorano a 3,3 V, non applicare mai 5 V

Nel codice non passare direttamente il numero del pin, ma convertilo nel numero di interrupt con digitalPinToInterrupt(pin), perché questo numero cambia da una scheda all'altra: sulla Uno D2 è l'interrupt 0 e D3 l'interrupt 1, mentre sulla Leonardo l'interrupt 0 corrisponde a D3 e l'interrupt 1 a D2. Se sulla Uno ti servono più di due interrupt, i pin change interrupt (PCINT) funzionano su quasi tutti i pin; però segnalano solo che "qualcosa è cambiato" e tutti i pin della stessa porta condividono un'unica ISR. Le librerie PinChangeInterrupt ed EnableInterrupt ne semplificano l'uso. Trovi la piedinatura completa della tua scheda nello strumento Pinout e le funzioni dei pin della Uno nell'articolo pin della Arduino Uno.

L'errore più comune è scrivere attachInterrupt(2, funzione, FALLING). Qui il 2 viene letto come numero di interrupt e non come numero di pin; la Uno non ha un interrupt 2, quindi non succede nulla e non compare nemmeno un messaggio di errore. La forma corretta è attachInterrupt(digitalPinToInterrupt(2), funzione, FALLING).

Come usare attachInterrupt e le modalità di interrupt

La sintassi è attachInterrupt(digitalPinToInterrupt(pin), ISR, modalità). Il secondo parametro è il nome della funzione eseguita quando arriva l'interrupt; questa funzione non riceve parametri e non restituisce nulla (void). Il terzo parametro stabilisce quale evento fa scattare l'interrupt. Per disattivare un interrupt si usa detachInterrupt(digitalPinToInterrupt(pin)), per sospendere brevemente tutti gli interrupt noInterrupts() e per riattivarli interrupts().

ModalitàQuando scatta?Uso tipico
LOWRipetutamente finché il pin resta LOWRisveglio dallo sleep
CHANGEA ogni cambiamento (entrambi i fronti)Encoder, misura della durata di un impulso, debounce
RISINGNel passaggio da LOW a HIGH (fronte di salita)Inizio di un impulso del sensore, pulsante con pull-down
FALLINGNel passaggio da HIGH a LOW (fronte di discesa)Pressione di un pulsante con pull-up, sensori open collector
HIGHFinché il pin resta HIGHSolo su Due, Zero e MKR1000; sull'ESP32 ONHIGH

Scegli la modalità in base al circuito. Se hai collegato il pulsante con INPUT_PULLUP, il pin è normalmente HIGH e scende a LOW quando lo premi, quindi la pressione viene rilevata da FALLING. Usa la modalità LOW con cautela: finché il pin resta LOW, la ISR viene eseguita in continuazione e loop() quasi non avanza. Per questo di solito si usa solo per risvegliare la scheda; per far uscire la Uno dalla modalità sleep più profonda (power-down), il datasheet consiglia un interrupt sul livello LOW o un pin change interrupt. Sull'ESP32 le costanti LOW e HIGH non funzionano come ti aspetteresti come modalità di interrupt: LOW non attiva affatto l'interrupt e HIGH si comporta come un fronte di salita. Per gli interrupt di livello lì si scrive ONLOW e ONHIGH.

Primo esempio: contare le pressioni del pulsante con un interrupt

  1. Collega un piedino del pulsante a D2 e l'altro a GND. Non serve una resistenza esterna: useremo la pull-up interna.
  2. Carica il codice qui sotto e apri il Monitor seriale a 9600 baud.
  3. Premi il pulsante qualche volta e guarda il contatore salire. Nonostante il delay(500) dentro loop(), nessuna pressione va persa.
  4. Non stupirti se a volte il numero sale di 2 o 3 in un colpo: è il rimbalzo dei contatti, e lo risolveremo tra poco.
C++ (Arduino IDE)
1const byte BUTTON_PIN = 2;                // kesme pini D2 (INT0) / interrupt pin D2 (INT0)
2volatile unsigned int pressCount = 0;     // ISR ile paylaşılan sayaç / counter shared with the ISR
3
4void onPress() {                          // ISR: kısa ve hızlı / ISR: short and fast
5  pressCount++;
6}
7
8void setup() {
9  Serial.begin(9600);
10  pinMode(BUTTON_PIN, INPUT_PULLUP);      // dahili pull-up, buton GND'ye / internal pull-up, button to GND
11  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
12}
13
14void loop() {
15  noInterrupts();                         // 2 baytlık değeri güvenle kopyala / copy the 2-byte value safely
16  unsigned int count = pressCount;
17  interrupts();
18
19  Serial.print(F("Basis / Presses: "));
20  Serial.println(count);
21  delay(500);                             // loop yavaş olsa da basış kaçmaz / presses are not missed even if loop is slow
22}

In questo codice ci sono tre punti importanti. Primo, onPress() aggiunge solo uno al contatore e termina subito: il compito di una ISR è annotare l'evento, non svolgere il lavoro. Secondo, il contatore è dichiarato volatile, così il compilatore non lo tiene in un registro rischiando di leggere un valore vecchio, ma prende ogni volta il valore aggiornato dalla memoria. Terzo, loop() disattiva per un attimo gli interrupt mentre legge il contatore. Un unsigned int sulla Uno occupa 2 byte e il processore a 8 bit lo legge in due passaggi. Quando il contatore passa da 255 a 256, cioè da 0x00FF a 0x0100, loop() legge prima il byte basso (0xFF); se proprio in quel momento arriva l'interrupt e incrementa il contatore, poi legge il byte alto (0x01) e il risultato è 0x01FF, cioè 511. Poiché questo errore compare molto di rado, è anche difficile da individuare. Sulle schede AVR la macro ATOMIC_BLOCK(ATOMIC_RESTORESTATE) di util/atomic.h offre la stessa protezione; le variabili di un solo byte come bool e byte non hanno questo problema.

Regole della ISR: breve, con volatile e senza delay

  • Tienila breve e veloce: mentre una ISR è in esecuzione, gli altri interrupt aspettano. Sulla Uno anche il contatore di millis() e la porta seriale funzionano con gli interrupt, quindi una ISR lunga altera la temporizzazione e può farti perdere i dati seriali in arrivo. Una ISR ideale termina in pochi microsecondi.
  • Non usare delay(): delay() misura il tempo con contatori aggiornati dall'interrupt del Timer0; dentro una ISR gli interrupt sono disattivati, quindi non funziona. Puoi leggere millis(), ma durante la ISR non avanza, e micros() inizia a restituire valori errati dopo 1–2 ms. Se ti serve davvero un'attesa brevissima, delayMicroseconds() funziona.
  • Non usare Serial.print(): anche l'invio seriale dipende dagli interrupt; allunga la ISR e si blocca se il buffer si riempie. Lascia la stampa a loop().
  • Rendi volatile le variabili condivise: ogni variabile usata sia nella ISR sia in loop() deve essere dichiarata volatile.
  • Leggi in modo atomico le variabili a più byte: in loop() copia le variabili int, long, float e unsigned long tra noInterrupts() e interrupts().
  • Niente parametri né valore di ritorno: una ISR deve essere void e non ricevere parametri; i dati si scambiano tramite variabili globali.
  • Niente lavoro pesante nella ISR: scrivere su un display I2C, salvare su una scheda SD o chiamare librerie che a loro volta hanno bisogno degli interrupt, come Wire, provoca blocchi dentro una ISR.
Lo schema più robusto è "alza un flag, fai il lavoro in loop": la ISR imposta soltanto a true una variabile bool volatile o incrementa un contatore, e loop() si occupa del lavoro vero, come aggiornare un display, pilotare un motore o fare calcoli, quando vede quel flag. Se scrivi loop() con millis() invece di delay, l'intero sistema resta veloce e affidabile.

Leggere un pulsante con un interrupt ed eliminare i rimbalzi

Quando premi un pulsante meccanico, i contatti metallici non si assestano subito; si aprono e si chiudono più volte per circa 0,1–10 millisecondi. loop() potrebbe essere troppo lento per accorgersene, ma un interrupt cattura ogni fronte, quindi una sola pressione può generare 3–5 interrupt. Inoltre il rimbalzo avviene non solo quando premi ma anche quando rilasci: un codice che usa solo FALLING con un blocco temporale può quindi contare due volte se tieni premuto il pulsante per un po' e poi lo lasci. Il codice qui sotto usa la modalità CHANGE e accetta una pressione solo se il pin è LOW e la linea era rimasta tranquilla per almeno 50 ms. Così vengono filtrati sia i rimbalzi alla pressione sia quelli al rilascio.

C++ (Arduino IDE)
1const byte BUTTON_PIN = 2;                // kesme pini / interrupt pin
2const byte LED_PIN = 13;                  // dahili LED / onboard LED
3const unsigned long QUIET_MS = 50;        // gereken sessizlik süresi / required quiet time
4
5volatile bool ledState = false;           // ISR ile paylaşılır / shared with the ISR
6
7void onButtonChange() {                   // iki kenarda da çalışır / runs on both edges
8  static unsigned long lastEdgeMs = 0;
9  unsigned long now = millis();           // ISR'de okunur ama artmaz / readable in an ISR but does not advance
10  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
11
12  if (pressed && now - lastEdgeMs > QUIET_MS) {   // hat sessizdi: gerçek basış / line was quiet: real press
13    ledState = !ledState;
14  }
15  lastEdgeMs = now;                       // her kenar süreyi yeniden başlatır / every edge restarts the timer
16}
17
18void setup() {
19  pinMode(LED_PIN, OUTPUT);
20  pinMode(BUTTON_PIN, INPUT_PULLUP);
21  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
22}
23
24void loop() {
25  digitalWrite(LED_PIN, ledState ? HIGH : LOW);   // bool tek bayt, okuması atomik / bool is one byte, atomic read
26  // burada uzun işler olsa da basış kaçmaz / long tasks here will not make you miss a press
27}

Oltre al software, puoi eliminare i rimbalzi anche via hardware: se metti un condensatore da 100 nF tra i due terminali del pulsante e una resistenza di pull-up da 10 kΩ sul pin, la costante di tempo diventa τ = R × C = 10 kΩ × 100 nF = 1 ms, che smussa i rimbalzi brevi; il trigger di Schmitt sugli ingressi della Uno ripulisce inoltre il fronte. Puoi calcolare altri valori con lo strumento costante di tempo RC. A essere onesti, per i pulsanti premuti a mano spesso un interrupt non serve; se loop() è veloce, il polling basta. Per pull-up, pull-down e debounce con millis(), leggi come collegare un pulsante ad Arduino. Riserva gli interrupt ai segnali veloci e brevi.

Leggere un encoder rotativo con gli interrupt

Quando lo ruoti, un encoder rotativo (come il KY-040) genera su due pin chiamati CLK e DT delle onde quadre sfasate tra loro. Se lo giri velocemente, gli impulsi durano meno di un millisecondo e con il polling è facile perdere dei passi. Il metodo più semplice è collegare CLK a un pin di interrupt e leggere il livello di DT nell'istante in cui CLK scende: se DT è HIGH la rotazione è avvenuta in un verso, se è LOW nell'altro. Questa versione conta un passo per ogni scatto; se il verso risulta invertito, scambia le righe con ++ e --. Anche gli encoder economici rimbalzano, quindi aggiungere un condensatore da 10–100 nF tra CLK e GND e tra DT e GND migliora nettamente il risultato. Per una versione che collega entrambi i pin agli interrupt e usa una tabella di stati per non perdere nemmeno un passo, dai un'occhiata alla guida all'encoder rotativo KY-040.

C++ (Arduino IDE)
1const byte PIN_CLK = 2;                   // kesme pini / interrupt pin
2const byte PIN_DT = 4;                    // normal dijital giriş / regular digital input
3
4volatile long position = 0;               // 4 baytlık konum / 4-byte position
5
6void onClkFall() {
7  if (digitalRead(PIN_DT) == HIGH) {      // yön DT seviyesinden / direction from the DT level
8    position++;
9  } else {
10    position--;
11  }
12}
13
14void setup() {
15  Serial.begin(115200);
16  pinMode(PIN_CLK, INPUT_PULLUP);
17  pinMode(PIN_DT, INPUT_PULLUP);
18  attachInterrupt(digitalPinToInterrupt(PIN_CLK), onClkFall, FALLING);
19}
20
21void loop() {
22  static long lastPos = 0;
23
24  noInterrupts();                         // atomik kopya / atomic copy
25  long pos = position;
26  interrupts();
27
28  if (pos != lastPos) {
29    Serial.println(pos);
30    lastPos = pos;
31  }
32}

Contare i giri: misurare i giri/min di un motore

Misurare la velocità di rotazione di un motore o di una ruota è l'uso classico degli interrupt. Come sensore si usa un sensore ottico a forcella con LM393 e un disco encoder a 20 fessure, oppure un sensore a effetto Hall come l'A3144 con un magnete incollato sull'albero. Ogni fessura o ogni passaggio del magnete produce un impulso. Dal numero di impulsi contati in un secondo, la velocità è: giri/min = impulsi al secondo × 60 / impulsi per giro. Per esempio, se un disco a 20 fessure dà 100 impulsi al secondo, il motore gira a 100 × 60 / 20 = 300 giri/min.

C++ (Arduino IDE)
1const byte SENSOR_PIN = 3;                // kesme pini D3 (INT1) / interrupt pin D3 (INT1)
2const byte PULSES_PER_REV = 20;           // diskteki yarık sayısı / slots on the encoder disc
3
4volatile unsigned long pulseCount = 0;
5
6void onPulse() {
7  pulseCount++;
8}
9
10void setup() {
11  Serial.begin(9600);
12  pinMode(SENSOR_PIN, INPUT_PULLUP);
13  attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), onPulse, FALLING);
14}
15
16void loop() {
17  static unsigned long lastMs = 0;
18
19  if (millis() - lastMs >= 1000) {        // her saniye hesapla / compute once per second
20    lastMs += 1000;
21
22    noInterrupts();                       // kopyala ve sıfırla / copy and reset
23    unsigned long pulses = pulseCount;
24    pulseCount = 0;
25    interrupts();
26
27    float rpm = pulses * 60.0 / PULSES_PER_REV;
28    Serial.print(F("RPM: "));
29    Serial.println(rpm, 1);
30  }
31}

Qui loop() copia e azzera il contatore ogni secondo; entrambe le operazioni stanno nello stesso blocco noInterrupts(), così nessun impulso va perso se tra le due arriva un interrupt. Con un disco a 20 fessure la risoluzione è di 3 giri/min. Se a bassa velocità vuoi risultati più precisi, invece di contare gli impulsi misura nella ISR il tempo tra due impulsi con micros() e usa giri/min = 60.000.000 / (periodo_µs × impulsi per giro). Lo stesso metodo vale per il flussimetro YF-S201, per la misura di velocità con un sensore a effetto Hall e per i giri della ruota di un motoriduttore DC.

Una breve nota sugli interrupt dei timer

Gli interrupt non arrivano solo dai pin. Anche i timer interni del microcontrollore possono generarli a intervalli fissi, così una funzione viene eseguita esattamente ogni 1 ms o ogni 500 ms, qualunque cosa stia facendo loop(). La Uno ha tre timer, e alcuni sono già usati dalle funzioni di Arduino:

TimerDimensioneUsato daPin PWM
Timer08 bitmillis(), delay(), micros()5, 6
Timer116 bitLibreria Servo, TimerOne9, 10
Timer28 bittone(), IRremote3, 11

Sulla Uno la strada più semplice è la libreria TimerOne: Timer1.initialize(500000) imposta il periodo in microsecondi e Timer1.attachInterrupt(funzione) collega la ISR eseguita ogni 500 ms. Poiché usa il Timer1, va in conflitto con la libreria Servo e con analogWrite() sui pin 9 e 10. Sull'ESP32 con il core Arduino 3.x, un interrupt del timer si configura con timerBegin(1000000), timerAttachInterrupt() e timerAlarm(); timerAlarmWrite() e timerAlarmEnable() della versione 2.x sono stati rimossi, perciò i vecchi esempi falliscono con l'errore "was not declared in this scope". Sul Pico, MicroPython crea un interrupt periodico in una sola riga con Timer(period=500, mode=Timer.PERIODIC, callback=funzione). Per il legame tra timer e PWM leggi Che cos'è il PWM?

Interrupt sull'ESP32: IRAM_ATTR e portMUX

Sull'ESP32 non devi cercare un pin particolare: ogni GPIO può generare un interrupt. Solo i GPIO 34–39 non hanno pull-up interna, quindi un pulsante lì richiede una pull-up esterna da 10 kΩ; inoltre l'errata di Espressif segnala che i GPIO 36 e 39 possono produrre interrupt spuri mentre si usa l'ADC. La vera differenza sull'ESP32 è l'attributo IRAM_ATTR scritto davanti alla ISR. Colloca la funzione nella RAM interna veloce (IRAM) invece che nella flash. Durante una scrittura in flash (Preferences, SPIFFS, OTA) la cache viene disattivata; a seconda della configurazione, un interrupt che in quel momento accede a codice in flash può far riavviare la scheda con "Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed)". L'ESP8266 è ancora più severo: una ISR senza IRAM_ATTR blocca la scheda con il messaggio "ISR not in IRAM!" appena viene chiamato attachInterrupt.

C++ (Arduino IDE)
1const int BUTTON_PIN = 27;                // her GPIO kesme verebilir / any GPIO can interrupt
2const int LED_PIN = 2;                    // çoğu DevKit'te dahili LED / onboard LED on most DevKits
3const uint32_t QUIET_MS = 50;             // gereken sessizlik süresi / required quiet time
4
5volatile uint32_t pressCount = 0;
6portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;   // paylaşılan veri kilidi / lock for shared data
7
8void IRAM_ATTR onButtonChange() {         // ISR IRAM'de çalışır / ISR runs from IRAM
9  static uint32_t lastEdgeMs = 0;
10  uint32_t now = millis();
11  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
12
13  if (pressed && now - lastEdgeMs > QUIET_MS) {
14    portENTER_CRITICAL_ISR(&mux);
15    pressCount++;
16    portEXIT_CRITICAL_ISR(&mux);
17  }
18  lastEdgeMs = now;
19}
20
21void setup() {
22  Serial.begin(115200);
23  pinMode(LED_PIN, OUTPUT);
24  pinMode(BUTTON_PIN, INPUT_PULLUP);
25  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonChange, CHANGE);
26}
27
28void loop() {
29  static uint32_t lastCount = 0;
30
31  portENTER_CRITICAL(&mux);               // loop tarafında kilit / lock on the loop side
32  uint32_t count = pressCount;
33  portEXIT_CRITICAL(&mux);
34
35  if (count != lastCount) {
36    lastCount = count;
37    digitalWrite(LED_PIN, (count % 2) ? HIGH : LOW);   // her basışta LED değişir / LED toggles on every press
38    Serial.printf("Basis / Press: %lu\n", (unsigned long)count);
39  }
40}

Poiché l'ESP32 esegue FreeRTOS e ha due core, le variabili condivise si proteggono con un blocco portMUX_TYPE invece che con il noInterrupts() della Uno: dentro la ISR si usa portENTER_CRITICAL_ISR(), in loop() portENTER_CRITICAL(). Leggere una variabile a 32 bit sull'ESP32 è già un'operazione unica, ma il blocco è indispensabile per operazioni come leggere e azzerare un valore o aggiornare più variabili insieme. Se vuoi collegare la stessa funzione a più pin, attachInterruptArg() ti permette di passare alla ISR un parametro, come il numero del pin. Per le basi dell'ESP32 leggi Che cos'è l'ESP32?, per task e core ESP32 FreeRTOS e dual core.

Interrupt sul Raspberry Pi Pico: irq in MicroPython

Sul Raspberry Pi Pico tutti i pin da GP0 a GP28 possono generare un interrupt. In MicroPython lo si configura con il metodo irq() dell'oggetto pin: per trigger passi Pin.IRQ_FALLING, Pin.IRQ_RISING o entrambi combinati, mentre handler è la funzione eseguita all'arrivo dell'interrupt e riceve come parametro il pin che l'ha generato. Python non ha volatile: basta dichiarare global le variabili che modifichi nella ISR. Per misurare il tempo si usano time.ticks_ms() e time.ticks_diff(), che gestisce l'overflow. Il codice seguente porta sul Pico la logica del tempo di quiete dell'esempio Arduino:

MicroPython
1from machine import Pin
2import time
3
4led = Pin("LED", Pin.OUT)                 # dahili LED (Pico ve Pico W) / onboard LED (Pico and Pico W)
5button = Pin(14, Pin.IN, Pin.PULL_UP)     # buton GP14 ile GND arasında / button between GP14 and GND
6
7QUIET_MS = 50                             # gereken sessizlik süresi / required quiet time
8presses = 0
9last_edge = time.ticks_ms()
10
11def on_change(pin):                       # handler tetikleyen pini alır / handler receives the pin
12    global presses, last_edge
13    now = time.ticks_ms()
14    if pin.value() == 0 and time.ticks_diff(now, last_edge) > QUIET_MS:
15        presses += 1
16        led.toggle()
17    last_edge = now
18
19button.irq(trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, handler=on_change)
20
21while True:
22    print("Basis / Presses:", presses)    # ana döngü uyurken bile basışlar sayılır / presses are counted while the loop sleeps
23    time.sleep(1)

Sul Pico, irq funziona di default come interrupt soft: MicroPython mette la funzione in coda e la esegue il prima possibile, quindi usare print o creare una lista al suo interno non crea problemi, ma la latenza aumenta un po'. Se passi hard=True, la latenza scende a pochi microsecondi; in cambio non puoi allocare memoria dentro la ISR, per cui una nuova lista, una stringa e persino un calcolo con float possono generare un "MemoryError". Se programmi il Pico con l'Arduino IDE (core di Earle Philhower), attachInterrupt() funziona allo stesso modo su tutti i GPIO. Per l'installazione leggi Che cos'è il Raspberry Pi Pico?, per i comandi di base le basi di MicroPython.

Idee di progetti con gli interrupt

  • Misuratore di riflessi: misura quanto ci metti a premere il pulsante dopo che si accende un LED, usando micros() nella ISR con una precisione inferiore al millisecondo.
  • Tachimetro per bici: metti un magnete sulla ruota e un sensore a effetto Hall sul telaio; calcola velocità e distanza dal tempo per giro.
  • Contatore d'acqua: conta gli impulsi di un flussimetro YF-S201 e mostra i litri consumati.
  • Auto robot che va dritta: misura la velocità dei due motori con gli encoder sulle ruote e rendile uguali.
  • Controllo dei menu: scorri un menu sul display e modifica le impostazioni con un encoder rotativo.
  • Allarme a batteria: lascia che l'uscita di un sensore PIR risvegli la scheda mentre dorme; per l'ESP32 leggi l'articolo sul deep sleep.

Errori frequenti e soluzioni

SintomoCausaSoluzione
L'interrupt non scatta maiA attachInterrupt(2, ...) è stato passato un numero di pin oppure il pin scelto non supporta gli interruptUsa digitalPinToInterrupt(pin); sulla Uno collegati a D2 o D3
Una pressione fa salire il contatore di 2–5Rimbalzo dei contattiFai il debounce via software e/o aggiungi un condensatore da 100 nF
La variabile non cambia mai in loop()Manca volatileDichiara volatile la variabile condivisa
Ogni tanto valori assurdamente grandiVariabile a più byte letta in modo non atomicoCopiala tra noInterrupts() e interrupts()
Il programma si blocca o millis() va alla derivadelay(), Serial o un'operazione lunga dentro la ISRNella ISR aggiorna solo un flag o un contatore
Arrivano interrupt senza toccare il pulsantePin flottante, nessuna pull-upUsa INPUT_PULLUP o una resistenza esterna da 10 kΩ
La ISR viene eseguita subito una volta dopo un nuovo attachInterruptUn fronte arrivato a interrupt disattivato ha alzato il flag (AVR)Sulla Uno scrivi EIFR = bit(INTF0); prima di attachInterrupt (INTF1 per D3)
L'ESP32 si riavvia a casoLa ISR non è in IRAM o svolge lavoro pesanteAggiungi IRAM_ATTR e accorcia la ISR

Gli interrupt sono uno strumento potente che ti permette di cogliere ogni evento anche quando loop() è impegnato in altro; ti basta ricordare tre regole: tieni la ISR breve, dichiara volatile le variabili condivise e leggile in modo atomico in loop(). Nell'app BlueGrays trovi contatori con pulsante, un test dei riflessi, progetti con pulsanti per ESP32 e un progetto per il Pico che confronta il debounce tramite interrupt (IRQ) e tramite polling software, ognuno con schema del circuito e codice passo passo.

Domande frequenti

Quali pin della Arduino Uno supportano gli interrupt?

Arduino Uno R3, Nano e Pro Mini hanno solo due pin di interrupt esterno: D2 (INT0) e D3 (INT1). Sulla Mega 2560 puoi usare 2, 3, 18, 19, 20 e 21, sulla Leonardo 0, 1, 2, 3 e 7. Sugli altri pin sono disponibili solo i pin change interrupt (PCINT), che segnalano soltanto che il pin è cambiato.

Perché delay() non funziona dentro un interrupt (ISR)?

delay() misura il tempo con contatori aggiornati dall'interrupt del Timer0. Mentre una ISR è in esecuzione, gli altri interrupt aspettano, quindi quei contatori non avanzano e delay() non può funzionare correttamente. Se qualcosa deve attendere, nella ISR alza solo un flag e fai l'attesa in loop() con millis().

A cosa serve volatile e quando va usato?

volatile dice al compilatore che la variabile può cambiare in qualsiasi momento al di fuori del normale flusso del programma, per esempio in una ISR; il suo valore quindi non viene tenuto in un registro ma riletto dalla memoria ogni volta. Ogni variabile usata sia in una ISR sia in loop() dovrebbe essere volatile. Tuttavia volatile non rende atomiche le letture a più byte: per questo si usa noInterrupts().

Si possono usare più di 2 interrupt su una Arduino Uno?

Sì. Grazie ai pin change interrupt (PCINT), quasi tutti i pin della Uno possono generare un interrupt. Segnalano solo un cambiamento, e i pin della stessa porta (D0–D7, D8–D13, A0–A5) condividono un'unica ISR. Con le librerie PinChangeInterrupt o EnableInterrupt il loro uso è molto simile ad attachInterrupt.

Meglio usare gli interrupt o il polling?

Per pulsanti premuti a mano e sensori che cambiano lentamente, il polling in loop() di solito basta ed è più semplice. Usa gli interrupt per impulsi veloci e brevi, come quelli di encoder, sensori di velocità e flussimetri, oppure per risvegliare la scheda dallo sleep. Se loop() è pieno di lunghi delay(), sostituirli con millis() risolve anche gran parte dei problemi.

Cosa succede se non scrivo IRAM_ATTR sull'ESP32?

Di solito il codice compila e sembra funzionare, ma la ISR resta in flash, parte più lentamente e, a seconda della configurazione, può andare in crash con "Cache disabled but cached memory region accessed" durante una scrittura in flash. Sull'ESP8266, senza IRAM_ATTR la scheda si ferma subito con "ISR not in IRAM!". Per questo sulle schede ESP metti IRAM_ATTR davanti a ogni ISR.

Vedi tutti i progetti
Accendere e spegnere un LED con un pulsante
#10Principiante

Accendere e spegnere un LED con un pulsante

Premi il pulsante una volta e la luce si accende, premilo di nuovo e si spegne, proprio come l'interruttore della tua stanza. L'Arduino si accorge di ogni pressione e porta il LED nello stato opposto,

4.8(100)
167
Raspberry Pi Pico Pulsante Debounce: IRQ vs Software Polling
#159Intermedio
Premium

Raspberry Pi Pico Pulsante Debounce: IRQ vs Software Polling

Una pressione e il codice conta 3-4? È contact bounce! Confronta 2 metodi di conteggio in Serial: polling RAW (cattura bounce, conta male) vs IRQ + software debounce (preciso). Impara fisica del bounce, software debounce, Pin.irq() interrupt, IRQ_FALLING trigger, ISR handler, sicurezza delle variabili globali ISR.

0.0(0)
12
Controllo LED ESP32 con pulsante (Toggle)
#105Principiante

Controllo LED ESP32 con pulsante (Toggle)

Ogni volta che si preme il pulsante, lo stato del LED cambia: se è acceso si spegne, se è spento si accende. Classico schema toggle. Con il debounce puliamo il rumore meccanico del pulsante per garantire una singola commutazione con una sola pressione.

4.6(39)
57
Raspberry Pi Pico Pulsante Toggle LED
#156Principiante

Raspberry Pi Pico Pulsante Toggle LED

Pulsante + LED — livello successivo! A differenza del progetto pulsante di base, non serve tenere premuto. Premi e rilascia → LED acceso. Premi di nuovo → LED spento (toggle). Stesso circuito, grande differenza di codice. Impara edge detection, state tracking e debounce.

0.0(0)
9
Contatore con display a 7 segmenti e pulsanti
#43Avanzato
Premium

Contatore con display a 7 segmenti e pulsanti

Un contatore semplice con tre pulsanti: con uno la cifra sul display sale di uno, con un altro scende di uno, e il terzo la riporta a zero. Il display a 7 segmenti a una sola cifra conta da 0 a 9, e q

4.6(35)
47
Arduino Test di Reazione con Segnale Acustico: Misura i tuoi Riflessi in Millisecondi (LCD + Buzzer + 3 LED)
#97Intermedio
Premium

Arduino Test di Reazione con Segnale Acustico: Misura i tuoi Riflessi in Millisecondi (LCD + Buzzer + 3 LED)

Quanto sono veloci i tuoi riflessi? Premi il pulsante: il LED rosso dice ai vostri posti, il giallo pronti e parte un'attesa casuale da 1,5 a 3,5 secondi. Appena si accende il LED verde e il buzzer suona, premi di nuovo; il tuo tempo di reazione compare in millisecondi sul display LCD I2C, con il record della sessione e la media sotto. Se premi prima del verde, il display mostra TOO EARLY! e senti un suono di sconfitta. Tieni premuto: nuovo giocatore, stesso ordine di attese.

4.6(33)
50
Contatore WebSocket ESP32 (2 pulsanti + LCD + Web in tempo reale)
#130Avanzato
Premium

Contatore WebSocket ESP32 (2 pulsanti + LCD + Web in tempo reale)

In questo progetto due pulsanti collegati all'ESP32 (GPIO 5 incrementa, GPIO 4 decrementa) modificano un contatore. Il valore si aggiorna all'istante su un display LCD I2C 16x2 e, grazie al supporto WebSocket di ESPAsyncWebServer, in tutti i browser collegati senza ricaricare la pagina. Al posto del polling HTTP si usa un'unica connessione persistente e l'ESP32 invia il nuovo valore a tutti con ws.textAll().

4.5(14)
23
Gioco di memoria con Arduino (4 LED + 4 pulsanti + buzzer)
#93Avanzato
Premium

Gioco di memoria con Arduino (4 LED + 4 pulsanti + buzzer)

Ti fidi della tua memoria? Quattro LED colorati si accendono in sequenza, ognuno con la sua nota, e tu ripeti lo stesso ordine premendo i pulsanti. Ogni turno giusto aggiunge un nuovo colore alla sequ

4.8(46)
69