Skip to the content.

Torna all’indice

PULSANTE CON INTERRUPT

Gli ingressi rilevati tramite un interrupt vengono sicuramente serviti in maniera più rapida rispetto ad altre soluzioni (delay, polling della millis(), thread), questo perchè l’arrivo di un segnale di interrupt blocca immediatamente l’esecuzione di un task direttamente sull’istruzione che è attualmente in esecuzione al momento dell’arrivo dell’interrupt. Mantiene la complessità (tutto sommato non elevata) di una normale chiamata di funzione ed è attivato a ridosso di un qualunque evento asincrono, del quale, per definizione, non è mai possibile prevedere in anticipo l’accadere.

In definitiva, tra le tecniche più responsive, quella dell’interrupt è certamente la più veloce e quindi non solo garantisce una adeguata responsività ai comandi ma anche la massima velocità di risposta possibile che è un prerequisito necessario per il comando efficace dei dispositivi di sicurezza, cioè di quei dispositivi critici deputati alla protezione di beni e persone da danni irreparabili.

La gestione di un pulsante mediante gli interrupts passa sostanzialmente per due fasi:

Le tecniche individuate nella presente dispensa sono sostanzialmente le stesse per quanto riguarda la prima fase di attivazione mentre differiscono nel modo con cui viene realizzato il ritardo necessario per il debouncing.

Per una discussione più completa sugli interrupt vedi interrupt.

alt text

PULSANTE TOGGLE BASATO SU INTERRUPTS E DEBOUNCE NELLA ISR

Per una discussione generale sugli interrupt si rimanda a interrupt.

Il funzionamento è agevolmente comprensibile alla luce delle seguenti considerazioni:

Un esempio con l’attuazione nel loop del task di accnesione/spegnimento potrebbe essere:

#include "urutils.h"
const unsigned long DEBOUNCETIMERISE = 50;
const unsigned long DEBOUNCETIMEFALL = 50;
const byte ENGINE = 13;
const byte BUTTONPIN = 12;
volatile bool stato = false;
volatile int count1 = 0;
DiffTimer debtimer;

void debounceRise() {
  if (debtimer.get() > DEBOUNCETIMERISE) {// al primo bounce (in rise o in fall) è sempre vero!
    Serial.println(count1);
    count1 = 0;
    stato = !stato;
    Serial.println("I have catched a RISE");
    attachInterrupt(digitalPinToInterrupt(BUTTONPIN), debounceFall, FALLING);
    debtimer.reset();// ogni tipo di fronte resetta il timer
  } else {
    count1++;
  }
}

void debounceFall() {
  if (debtimer.get() > DEBOUNCETIMEFALL) {// al primo bounce (in rise o in fall) è sempre vero!
    Serial.println(count1);
    count1 = 0;
    Serial.println("I have catched a FALL");
    attachInterrupt(digitalPinToInterrupt(BUTTONPIN), debounceRise, RISING);
    debtimer.reset();// ogni tipo di fronte resetta il timer
  } else {
    count1++;
  }
}

void setup ()
{
  Serial.begin(115200);
  pinMode(BUTTONPIN, INPUT);
  pinMode(ENGINE, OUTPUT);  	  // so we can update the LED
  digitalWrite(ENGINE, LOW);
  // attach interrupt handler
  debtimer.start();
  attachInterrupt(digitalPinToInterrupt(BUTTONPIN), debounceRise, RISING);
}  // end of setup

void loop ()
{
  //Serial.println(pressed);
  if (stato) {
    digitalWrite(ENGINE, HIGH);
  } else {
    digitalWrite(ENGINE, LOW);
  }
  delay(10);
}

Le variabili condivise tra una ISR e il loop() andrebbero protette, da accessi paralleli e concorrenti da parte di entrambe, tramite delle corse critiche che rendano l’accesso strettamente sequenziale. Inoltre le variabili condivise devono sempre essere dichiarate con il qualificatore volatile per forzarne la modifica istantanea anche sui registri della CPU.

Gli accessi paralleli non sono un problema quando le istruzioni sono atomiche, cioè non interrompibili. Le istruzioni atomiche o sono eseguite per intero o non sono eseguite affatto. In questo caso gli accessi, sia in lettura che in scrittura, sono in realtà, a basso livello, intrinsecamente sequenziali.

Nei microcontrollori attuali, in genere nessuna istruzione gode della proprietà di essere atomica con una sola eccezione per la lettura e scrittura delle variabili ad 8 bit. Per le variabili codificate con 8 bit l’accesso a basso livello (linguaggio macchina) è intrinsecamente garantito essere atomico. Per queste variabili rimane comunque la necessita dell’uso del qualificatore volatile.

Le modifiche a valori con codifiche maggiori di 8 bit sono in genere non atomiche, pertanto le variabili a 16 o 32 bit andrebbero gestite con gli interrupt disabilitati (sezione critica). Tuttavia, gli interrupt vengono disabilitati di default durante una routine di servizio di interrupt, quindi, non potendo verificarsi il danneggiamento di una variabile multibyte in una ISR, le sezioni critiche vanno inserite soltanto nel loop().

Le variabili condivise tra ISR e loop() e 8 bit sono stato e count1 che sono state semplicemente dichiarate come volatile senza sezioni critiche su di essa.

PULSANTE TOGGLE BASATO SU INTERRUPTS E DEBOUNCE CON TIMER SW NEL LOOP 1

Per una discussione generale sugli interrupt si rimanda a interrupt.

All’ingresso di una porta digitale, per ottenere la rilevazione sicura (senza rimbalzi) del solo fronte di salita è stata usata la combinazione di due tecniche di schedulazione:

Il rilevatore dei fronti è realizzato campionando il valore del livello al momento dell’arrivo del segnale di interrupt e confrontandolo con il valore del livello campionato in istanti periodici successivi a quello, pianificati (schedulati) tramite un timer HW, allo scadere del quale viene chiamata l’istruzione waitUntilInputChange(). La funzione, di fatto esegue il filtraggio delle segnalazioni di un flag (numberOfButtonInterrupts) impostato dalla ISR chiamata dagli interrupt sulla porta digitale. Le segnalazioni vengono filtrate se troppo in anticipo (funzione di debouncing) e se si riferiscono a letture dello stesso livello (non transizioni). La funzione di debouncing è garantita introducendo un tempo minimo di attesa tra un campionamento e l’altro.

Pur utilizzando gli interrupt, l’efficacia del codice precedente in termini di velocità e responsività è limitata dalla componente nel loop() del meccanismo che purtroppo è sensibile ai ritardi di esecuzione. I ritardi possono essere introdotti da istruzioni delay() o da blocchi di istruzioni troppo lente. E’probabilmente una realizzazione poco pratica, soprattutto per dispositivi di sicurezza, perchè la velocità degli interrupts potrebbe essere vanificata dalla potenziale lentezza del polling del flag.

Una realizzazione di interrupt con debouncing SW che garantisce un intervento immediato è riportata in: interruttore di sicurezza SW

/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
int led = 13;
byte pulsante =12;
byte stato= LOW;  // variabile globale che memorizza lo stato del pulsante
volatile unsigned long previousMillis = 0;
volatile unsigned long lastintTime = 0;
bool started = false;
volatile bool pressed;
#define DEBOUNCETIME 50
 
void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, CHANGE );  
  pressed = false;
}

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  byte val = digitalRead(pulsante);
  if(val == HIGH){
    if(!pressed){ // intervento immediato sul fronte di salita
        pressed = true; // disarmo del pulsante e riarmo del timer
        stato = !stato; 
    }
  }
}  // end of switchPressed

void waitUntilInputChange()
{
    if(pressed){ 
      if(!started){
        started = true;// aggiorna il millis() solo alla prima di molte chiamate consecutive
        lastintTime = millis();
      }
      if((millis() - lastintTime > DEBOUNCETIME ) && digitalRead(pulsante) == LOW){
        pressed = false; // riarmo del pulsante
        started = false; // disarmo del timer
      }
    }
}
// loop principale
void loop() {
	waitUntilInputChange();
	if (stato) {
		digitalWrite(led, !digitalRead(led));   	// inverti lo stato precedente del led
		delay(1000);
	} else {
		digitalWrite(led, LOW);    	// turn the LED off by making the voltage LOW
    delay(10);
	}
}

Le variabili condivise tra una ISR e il loop() andrebbero protette da accessi paralleli da parte di quellew due funzioni tramite delle corse critiche che rendano l’accesso strettamente sequenziale. Inoltre le variabili condivise devono sempre essere dichiarate con il qualificatore volatile per forzarne la modifica istantanea anche sui registri della CPU.

Gli accessi paralleli non sono un problema quando le istruzioni sono atomiche, cioè non interrompibili. Le istruzioni atomiche o sono eseguite per intero o non sono eseguite affatto. In questo caso gli accessi, sia in lettura che in scrittura, sono in realtà, a basso livello, intrinsecamente sequenziali.

Nei microcontrollori attuali, in genere nessuna istruzione gode della proprietà di essere atomica con una sola eccezione per la lettura e scrittura delle variabili ad 8 bit. Per le variabili codificate con 8 bit l’accesso a basso livello (linguaggio macchina) è intrinsecamente garantito essere atomico. Per queste variabili rimane comunque la necessita dell’uso del qualificatore volatile.

Le modifiche a valori con codifiche maggiori di 8 bit sono in genere non atomiche, pertanto le variabili a 16 o 32 bit andrebbero gestite con gli interrupt disabilitati (sezione critica). Tuttavia, gli interrupt vengono disabilitati di default durante una routine di servizio di interrupt, quindi, non potendo verificarsi il danneggiamento di una variabile multibyte in una ISR, le sezioni critiche vanno inserite soltanto nel loop().

Le variabili condivise tra ISR e loop() e 8 bit sono numberOfButtonInterrupts, prevState e lastState che sono stata semplicemente dichiarate come volatile senza sezioni critiche su di essa.

L’unica variabile condivisa tra ISR e loop() e 16 o 32 bit sono previousMillis che è stata dichiarata come volatile e ha nel loop() una sezione critica intorno all’accesso in lettura su di essa.

Simulazione online su ESP32 del codice precedente con Wowki: https://wokwi.com/projects/388450490165203969

Simulazione online su ESP32 del codice precedente con Wowki: https://wokwi.com/projects/350016534055223891

PULSANTE TOGGLE BASATO SU INTERRUPTS E DEBOUNCE CON TIMER SW NEL LOOP 2

/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
#include "urutils.h"
int led = 13;
byte pulsante =12;
byte stato= LOW;  // variabile globale che memorizza lo stato del pulsante
volatile bool pressed;
#define DEBOUNCETIME 50
DiffTimer debounce;
 
void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, CHANGE );  
  pressed = false;
}

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  byte val = digitalRead(pulsante);
  if(val == HIGH){
    if(!pressed){ // intervento immediato sul fronte di salita
        pressed = true; // disarmo del pulsante e riarmo del timer
        stato = !stato; 
    }
  }
}  // end of switchPressed

void waitUntilInputChange()
{
    if (pressed){ 
      debounce.start();// aggiorna il millis() interno solo alla prima di molte chiamate consecutive
      if(debounce.get() > DEBOUNCETIME  && digitalRead(pulsante) == LOW){
        pressed = false; // riarmo del pulsante
        debounce.stop(); // disarmo del timer
        debounce.reset();
      }
    }
}
// loop principale
void loop() {
	waitUntilInputChange();
	if (stato) {
		digitalWrite(led, !digitalRead(led));   	// inverti lo stato precedente del led
		delay(1000);
	} else {
		digitalWrite(led, LOW);    	// turn the LED off by making the voltage LOW
    delay(10);
	}
}

Simulazione online su ESP32 del codice precedente con Wowki: https://wokwi.com/projects/388481409829351425

Variante che disarma gli interrupt spuri fino al rilascio del pulsante:

/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
#include "urutils.h"
int led = 13;
byte pulsante =12;
byte stato= LOW;  // variabile globale che memorizza lo stato del pulsante
volatile bool pressed;
#define DEBOUNCETIME 50
DiffTimer debounce;
 
void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING );  
  pressed = false;
}

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  detachInterrupt(digitalPinToInterrupt(pulsante));
  pressed = true; // disarmo del pulsante e riarmo del timer
  stato = !stato; // logica da attivare sul fronte (toggle)
}  // end of switchPressed

void waitUntilInputChange()
{
    if (pressed){ 
      debounce.start();// aggiorna il millis() interno solo alla prima di molte chiamate consecutive
      if(debounce.get() > DEBOUNCETIME  && digitalRead(pulsante) == LOW){// disarmo del timer al timeout
        attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING ); 
        pressed = false; // riarmo del pulsante
        debounce.stop(); // disarmo del timer
        debounce.reset();
      }
    }
}
// loop principale
void loop() {
	waitUntilInputChange();
	if (stato) {
		digitalWrite(led, !digitalRead(led));   	// inverti lo stato precedente del led
		delay(1000);
	} else {
		digitalWrite(led, LOW);    	// turn the LED off by making the voltage LOW
    		delay(10);
	}
}

Simulazione online su ESP32 del codice precedente con Wowki: https://wokwi.com/projects/390288516762524673

Pulsante toggle basato su interrupts e timer HW

Per una discussione generale sugli interrupt si rimanda a interrupt.

All’ingresso di una porta digitale viene associata una callback che viene invocata alla ricezione di un segnale di interrupt attivo su entrambi i fronti. Il fronte di salita, selezionato prendendo solo i valori HIGH, potrebbe essere rilevato molte volte consecutivamente a causa del fenomeno dei rimbalzi. Per evitare la rilevazione dei fronti spuri successivi al primo, viene disabilitata, dentro la ISR, la loro rilevazione disarmando gli interrupt mediante l’istruzione detachInterrupt(digitalPinToInterrupt(pulsante)).

Il tempo per la riabilitazione (riarmo) dell’interrupt non deve essere ne troppo presto, cioè minore di 50 msec, altrimenti si finisce per leggere dei rimbalzi ma neppure troppo tardi, cioè dopo la pressione di un tasto, altrimenti si perdono degli input dell’utente. Il momento migliore per riabilitare gli interrupt potrebbe essere il momento del rilascio del pulsante, dato che precede sempre una eventuale successiva pressione. In ogni caso, un timer impedisce quei tentativi di riabilitazione che potrebbero avvenire prima dei 50 msec utili ad evitare i rimbalzi.

Il timer è di tipo one shot e viene riarmato solo se un polling della porta del tasto fornisce ancora valore alto (tasto premuto se in pull down). Se invece fornisce valore basso, non viene riarmato il timer ma viene riarmato al suo posto l’interrupt del tasto mediante l’istruzione attachInterrupt().

Allo scadere del timeout viene eseguita la callback waitUntilInputLow() all’interno della ISR del timer. La funzione esegue una nuova lettura del valore della porta:

La funzione di debouncing è garantita introducendo un tempo minimo di attesa tra un campionamento e l’altro.

Per mantenere la ISR chiamante il più veloce possibile, viene spostato nel loop() l’algoritmo di blink basato sui delay(), dove può fare il suo lavoro industurbato essendo l’unico task (suscettibile ai ritardi) presente.

Le attese sono tutte non bloccanti e realizzate tramite un timer HW che adopera esso stesso gli interrupt per richiamare la funzione di servizio (callback) da eseguire allo scadere del timeout. Il timer, utilizzando gli interrupt, è in grado di intervenire in tempo in tutte le situazioni, eventualmente anche interrompendo l’esecuzione di istruzioni che impegnino intensamente il loop(). Si tratta sicuramente di una realizzazione che, avendo la massima efficacia possibile in tutte le situazioni, si presta alla realizzazione di dispositivi di sicurezza.

#include <Ticker.h>
/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
int led = 13;
byte pulsante =12;
volatile bool stato;
#define DEBOUNCETIME 50
Ticker debounceTicker;
 
void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING );
  stato = false;
}

void switchPressed ()
{
  detachInterrupt(digitalPinToInterrupt(pulsante));
  debounceTicker.once_ms(50, waitUntilInputLow);  
  Serial.println("SALITA disarmo pulsante");
  stato = !stato; 	 // logica da attivare sul fronte (toggle)

}  // end of switchPressed

void waitUntilInputLow()
{
    if (digitalRead(pulsante) == HIGH)//se coincide con il valore di un polling
    { 
        Serial.print("Aspetto");
        debounceTicker.once_ms(50, waitUntilInputLow);  
    }else{
        Serial.print("DISCESA riarmo pulsante\n");
        attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING );
    }
}

// loop principale
void loop() {
	if (stato) {
		digitalWrite(led, !digitalRead(led));   	// inverti lo stato precedente del led
		delay(500);
	} else {
		digitalWrite(led, LOW);    	// turn the LED off by making the voltage LOW
    		delay(10);
	}
}

Di seguito il link della simulazione online con ESP32 su Wokwi: https://wokwi.com/projects/390289622147259393

Pulsante toggle basato su interrupts e con debounce basato sui delay()

All’ingresso di una porta digitale viene associata una callback che viene invocata alla ricezione di un segnale di interrupt attivo su entrambi i fronti. Il fronte di salita, selezionato prendendo solo i valori HIGH, potrebbe essere rilevato molte volte consecutivamente a causa del fenomeno dei rimbalzi.

Per evitare la rilevazione dei fronti spuri successivi al primo, viene disabilitata, dentro la ISR, la loro rilevazione disarmando gli interrupt mediante l’istruzione detachInterrupt(digitalPinToInterrupt(pulsante)). Contemporaneamente viene asserito un flag di segnalazione, pressed, che comunica ad un loop() di attivare il timer per il riarmo dell’interrupt per rispondere a nuove pressioni dell’utente.

Il tempo per la riabilitazione (riarmo) dell’interrupt non deve essere ne troppo presto, cioè minore di 50 msec, altrimenti si finisce per leggere dei rimbalzi ma neppure troppo tardi, altrimenti si perdono degli input dell’utente. Il momento migliore per riabilitare gli interrupt potrebbe essere il momento del rilascio del pulsante, dato che precede sempre una eventuale successiva pressione. In ogni caso, un timer impedisce quei tentativi di riabilitazione che potrebbero avvenire prima dei 50 msec utili ad evitare i rimbalzi.

/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
#include "urutils.h"
int led = 13;
byte pulsante =12;
byte stato= LOW;  // variabile globale che memorizza lo stato del pulsante
volatile bool pressed;
#define DEBOUNCETIME 50
DiffTimer debounce;
 
void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING );  
  pressed = false;
}

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  detachInterrupt(digitalPinToInterrupt(pulsante));
  pressed = true; // disarmo del pulsante e riarmo del timer
  stato = !stato; // logica da attivare sul fronte (toggle)
}  // end of switchPressed

// loop principale
void loop() {
  if(pressed){
    waitUntilInputLow(pulsante, 50);
    attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING );  
    pressed = false;
  }
  digitalWrite(led, stato);   	// inverti lo stato precedente del led
  delay(10);
}

Simulazione online su ESP32 del codice precedente con Wowki: https://wokwi.com/projects/390288516762524673

Pulsante toggle basato su interrupts e timer debounce con timer SW get()

All’ingresso di una porta digitale viene associata una callback che viene invocata alla ricezione di un segnale di interrupt attivo su entrambi i fronti. Il fronte di salita, selezionato prendendo solo i valori HIGH, potrebbe essere rilevato molte volte consecutivamente a causa del fenomeno dei rimbalzi.

Per evitare la rilevazione dei fronti spuri successivi al primo, viene disabilitata, dentro la ISR, la loro rilevazione disarmando gli interrupt mediante l’istruzione detachInterrupt(digitalPinToInterrupt(pulsante)). Contemporaneamente viene asserito un flag di segnalazione, pressed, che comunica ad un loop() di attivare il timer per il riarmo dell’interrupt per rispondere a nuove pressioni dell’utente.

Il tempo per la riabilitazione (riarmo) dell’interrupt non deve essere ne troppo presto, cioè minore di 50 msec, altrimenti si finisce per leggere dei rimbalzi ma neppure troppo tardi, altrimenti si perdono degli input dell’utente. Il momento migliore per riabilitare gli interrupt potrebbe essere il momento del rilascio del pulsante, dato che precede sempre una eventuale successiva pressione. In ogni caso, un timer impedisce quei tentativi di riabilitazione che potrebbero avvenire prima dei 50 msec utili ad evitare i rimbalzi.

Variante che disabilita gli interrupt spuri fino al rilascio del pulsante:

/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
#include "urutils.h"
int led = 13;
byte pulsante =12;
byte stato= LOW;  // variabile globale che memorizza lo stato del pulsante
volatile bool pressed;
#define DEBOUNCETIME 50
DiffTimer debounce;
 
void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING );  
  pressed = false;
}

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  detachInterrupt(digitalPinToInterrupt(pulsante));
  pressed = true; // disarmo del pulsante e riarmo del timer
  stato = !stato; // logica da attivare sul fronte (toggle)
}  // end of switchPressed

void waitUntilInputChange()
{
    if (pressed){ 
      debounce.start();// aggiorna il millis() interno solo alla prima di molte chiamate consecutive
      if(debounce.get() > DEBOUNCETIME  && digitalRead(pulsante) == LOW){// disarmo del timer al timeout
        attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING ); 
        pressed = false; // riarmo del pulsante
        debounce.stop(); // disarmo del timer
        debounce.reset();
      }
    }
}
// loop principale
void loop() {
	waitUntilInputChange();
	if (stato) {
		digitalWrite(led, !digitalRead(led));   	// inverti lo stato precedente del led
		delay(1000);
	} else {
		digitalWrite(led, LOW);    	// turn the LED off by making the voltage LOW
    		delay(10);
	}
}

Simulazione online su ESP32 del codice precedente con Wowki: https://wokwi.com/projects/390288516762524673

Variante che ignora gli interrupt spuri fino al rilascio del pulsante:

All’ingresso di una porta digitale viene associata una callback che viene invocata alla ricezione di un segnale di interrupt attivo su entrambi i fronti. Il fronte di salita, selezionato prendendo solo i valori HIGH, potrebbe essere rilevato molte volte consecutivamente a causa del fenomeno dei rimbalzi.

Per evitare la rilevazione dei fronti spuri successivi al primo, viene disabilitata, dentro la ISR, la loro rilevazione non disabilitando gli interrupt ma disabilitando la loro gestione mediante la condizione if(!pressed) che, dopo il primo fronte, non è più soddisfatta. Il flag di segnalazione, pressed, oltre a disabilitare la gestione degli interrupt, comunica ad un loop() di attivare il timer per il riarmo dell’interrupt per rispondere a nuove pressioni dell’utente.

Il tempo per la riabilitazione (riarmo) dell’interrupt non deve essere ne troppo presto, cioè minore di 50 msec, altrimenti si finisce per leggere dei rimbalzi ma neppure troppo tardi, cioè dopo la pressione di un tasto, altrimenti si perdono degli input dell’utente. Il momento migliore per riabilitare gli interrupt potrebbe essere il momento del rilascio del pulsante, dato che precede sempre una eventuale successiva pressione. In ogni caso, un timer impedisce quei tentativi di riabilitazione che potrebbero avvenire prima dei 50 msec utili ad evitare i rimbalzi.

/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
#include "urutils.h"
int led = 13;
byte pulsante =12;
byte stato= LOW;  // variabile globale che memorizza lo stato del pulsante
volatile bool pressed;
#define DEBOUNCETIME 50
DiffTimer debounce;
 
void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, RISING );  
  pressed = false;
}

// Interrupt Service Routine (ISR)
void switchPressed ()
{
    if(!pressed){ // intervento immediato ma sul primo fronte di salita soltanto (causa disarmo pulsante)
        pressed = true; // disarmo del pulsante e riarmo del timer di debouncing
        stato = !stato; // logica da attivare sul fronte (toggle)
    }
}   // end of switchPressed

void waitUntilInputChange()
{
    if (pressed){ 
      debounce.start();// aggiorna il millis() interno solo alla prima di molte chiamate consecutive
      if(debounce.get() > DEBOUNCETIME  && digitalRead(pulsante) == LOW){// disarmo del timer al timeout
        pressed = false; // riarmo del pulsante
        debounce.stop();
	debounce.reset();
      }
    }
}
// loop principale
void loop() {
	waitUntilInputChange();
	if (stato) {
		digitalWrite(led, !digitalRead(led));   	// inverti lo stato precedente del led
		delay(1000);
	} else {
		digitalWrite(led, LOW);    	// turn the LED off by making the voltage LOW
   		 delay(10);
	}
}

Simulazione online su ESP32 del codice precedente con Wowki: https://wokwi.com/projects/388481409829351425

Pulsante toggle basato su interrupts e debounce nella ISR

Per una discussione generale sugli interrupt si rimanda a interrupt.

Il funzionamento è agevolmente comprensibile alla luce delle seguenti considerazioni:

Un esempio con l’attuazione nel loop del task di accenesione/spegnimento potrebbe essere:

#include "urutils.h"
const unsigned long DEBOUNCETIMERISE = 50;
const unsigned long DEBOUNCETIMEFALL = 50;
const byte ENGINE = 13;
const byte BUTTONPIN = 12;
volatile bool stato = false;
volatile int count1 = 0;
DiffTimer debtimer;

void debounceRise() {
  if (debtimer.get() > DEBOUNCETIMERISE) {// al primo bounce (in rise o in fall) è sempre vero!
    Serial.println(count1);
    count1 = 0;
    stato = !stato; // logica da attivare sul fronte (toggle)
    Serial.println("I have catched a RISE");
    attachInterrupt(digitalPinToInterrupt(BUTTONPIN), debounceFall, FALLING);// pulsante disarmato!
    debtimer.reset();// ogni tipo di fronte resetta il timer
  } else {
    count1++;
  }
}

void debounceFall() {
  if (debtimer.get() > DEBOUNCETIMEFALL) {// al primo bounce (in rise o in fall) è sempre vero!
    Serial.println(count1);
    count1 = 0;
    Serial.println("I have catched a FALL");
    attachInterrupt(digitalPinToInterrupt(BUTTONPIN), debounceRise, RISING);// pulsante riarmato!
    debtimer.reset();// ogni tipo di fronte resetta il timer
  } else {
    count1++;
  }
}

void setup ()
{
  Serial.begin(115200);
  pinMode(BUTTONPIN, INPUT);
  pinMode(ENGINE, OUTPUT);  	  // so we can update the LED
  digitalWrite(ENGINE, LOW);
  // attach interrupt handler
  debtimer.start();
  attachInterrupt(digitalPinToInterrupt(BUTTONPIN), debounceRise, RISING);
}  // end of setup

void loop ()
{
  //Serial.println(pressed);
  if (stato) {
    digitalWrite(ENGINE, HIGH);
  } else {
    digitalWrite(ENGINE, LOW);
  }
  delay(10);
}

Le variabili condivise tra una ISR e il loop() andrebbero protette, da accessi paralleli e concorrenti da parte di entrambe, tramite delle corse critiche che rendano l’accesso strettamente sequenziale. Inoltre le variabili condivise devono sempre essere dichiarate con il qualificatore volatile per forzarne la modifica istantanea anche sui registri della CPU.

Gli accessi paralleli non sono un problema quando le istruzioni sono atomiche, cioè non interrompibili. Le istruzioni atomiche o sono eseguite per intero o non sono eseguite affatto. In questo caso gli accessi, sia in lettura che in scrittura, sono in realtà, a basso livello, intrinsecamente sequenziali.

Nei microcontrollori attuali, in genere nessuna istruzione gode della proprietà di essere atomica con una sola eccezione per la lettura e scrittura delle variabili ad 8 bit. Per le variabili codificate con 8 bit l’accesso a basso livello (linguaggio macchina) è intrinsecamente garantito essere atomico. Per queste variabili rimane comunque la necessita dell’uso del qualificatore volatile.

Le modifiche a valori con codifiche maggiori di 8 bit sono in genere non atomiche, pertanto le variabili a 16 o 32 bit andrebbero gestite con gli interrupt disabilitati (sezione critica). Tuttavia, gli interrupt vengono disabilitati di default durante una routine di servizio di interrupt, quindi, non potendo verificarsi il danneggiamento di una variabile multibyte in una ISR, le sezioni critiche vanno inserite soltanto nel loop().

Le variabili condivise tra ISR e loop() e 8 bit sono stato e count1 che sono state semplicemente dichiarate come volatile senza sezioni critiche su di essa.

PULSANTE DI SICUREZZA CON DEBOUNCER BASATO SUI DELAY NEL LOOP

Il codice precedente, per quanto molto reponsivo, non è adatto a realizzare un blocco di sicurezza per via del ritardo nell’intervento di attivazione e disattivazione dell’uscita causato dalll’algoritmo di debouncing (antirimbalzo). Per adattarlo a quest’ultimo scopo, il codice va modificato in modo da avere un intervento immediato su uno dei fronti (quello che comanda lo sblocco dell’alimentazione) ed uno ritardato (per realizzare il debouncing) sull’altro (quello che comanda il riarmo).

Il ritardo per il debouncing è realizzato con la waitUntilInputLow() nel loop principale, che utilizza internamente i delay(). Il ritardo, utilizzando i delay(), è un’operazione bloccante e quindi potenzialmente potrebbe interferire negativamente con tutti i task, all’interno del loop che hanno bisogno di essere eseguiti in parallelo al task del debouncer.

#include "urutils.h"
const unsigned long DEBOUNCETIME = 50;
const byte ENGINE = 13;
const byte safetystop = 12;
volatile unsigned short numberOfButtonInterrupts = 0;
bool pressed;

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  numberOfButtonInterrupts++; // contatore rimbalzi
  byte val = digitalRead(safetystop); // lettura stato pulsante
  if(val==HIGH){ // fronte di salita
    pressed = true; // disarmo il pulsante
    digitalWrite(ENGINE, LOW); // blocco subito il motore
  }
}  

bool waitUntilInterruptLOW(int pin)
{   
  if (pressed)//se il pulsante è ancora premuto
  { 
    waitUntilInputLow(pin, 50);
    pressed = false; // riarmo il pulsante
    Serial.print("HIT: "); Serial.print(numberOfButtonInterrupts);
    numberOfButtonInterrupts = 0; // reset del flag
    Serial.println(" in DISCESA riarmo pulsante");
    return true;
  }
  return false;
}

void setup ()
{
  Serial.begin(115200);
  pinMode(ENGINE, OUTPUT);  	  // so we can update the LED
  digitalWrite(ENGINE, HIGH);
  digitalWrite(safetystop, LOW); 
  // attach interrupt handler
  attachInterrupt(digitalPinToInterrupt(safetystop), switchPressed, CHANGE);  
  numberOfButtonInterrupts = 0;
  pressed = false;
}  // end of setup

void loop ()
{
  if(waitUntilInterruptLOW(safetystop)){
	digitalWrite(ENGINE, HIGH); // riattivo il motore
  }
  delay(10);
}

Simulazione su Esp32 con Wowki: https://wokwi.com/projects/382390727185717249

PULSANTE DI SICUREZZA CON DEBOUNCER BASATO SUI DELAY SU UN THREAD A PARTE

Il codice precedente, per quanto molto reponsivo, non è adatto a realizzare un blocco di sicurezza per via del ritardo nell’intervento di attivazione e disattivazione dell’uscita causato dalll’algoritmo di debouncing (antirimbalzo). Per adattarlo a quest’ultimo scopo, il codice va modificato in modo da avere un intervento immediato su uno dei fronti (quello che comanda lo sblocco dell’alimentazione) ed uno ritardato (per realizzare il debouncing) sull’altro (quello che comanda il riarmo).

Il ritardo per il debouncing è realizzato con la waitUntilInputLow() ma stavolta non nel loop principale. Il ritardo, utilizza i delay(),in un thread separato che non può interferire con altri task.

#include "urutils.h"
#include <pthread.h> //libreria di tipo preemptive
const unsigned long DEBOUNCETIME = 50;
const byte ENGINE = 13;
const byte safetystop = 12;
volatile unsigned short numberOfButtonInterrupts = 0;
bool volatile pressed;
pthread_t t_debounce;

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  numberOfButtonInterrupts++; // contatore rimbalzi
  byte val = digitalRead(safetystop); // lettura stato pulsante
  if(val==HIGH){ // fronte di salita
    pressed = true; // disarmo il pulsante
    digitalWrite(ENGINE, LOW); // blocco subito il motore
  }
}  
void * taskDebounce(void *)
{
  while(true){    
    if (pressed)//se il pulsante è ancora premuto
    { 
      waitUntilInputLow(safetystop, 50);
      pressed = false; // riarmo il pulsante
      Serial.print("HIT: "); Serial.print(numberOfButtonInterrupts);
      numberOfButtonInterrupts = 0; // reset del flag
      Serial.println(" in DISCESA riarmo pulsante");
    }
  }
}

void setup ()
{
  Serial.begin(115200);
  pinMode(ENGINE, OUTPUT);  	  // so we can update the LED
  digitalWrite(ENGINE, HIGH);
  digitalWrite(safetystop, LOW); 
  // attach interrupt handler
  attachInterrupt(digitalPinToInterrupt(safetystop), switchPressed, CHANGE);  
  numberOfButtonInterrupts = 0;
  pressed = false;
  pthread_create(&t_debounce, NULL, taskDebounce, NULL);
}  // end of setup

void loop ()
{
  if(!pressed){// riavvio, senza fretta, il motore nel loop()
	digitalWrite(ENGINE, HIGH); // riattivo il motore
  }
  delay(10);
}

Simulazione su Esp32 con Wowki: https://wokwi.com/projects/382393152775960577

PULSANTE TOGGLE CON DEBOUNCER BASATO SU TIMER HW PER ARDUINO

#include <TimerOne.h>
/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
int led = 13;
byte pulsante =2;
volatile unsigned short numberOfButtonInterrupts = 0;
volatile bool pressed;
volatile bool stato;
#define DEBOUNCETIME 50
 
void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, CHANGE );
  Timer1.initialize(50000);
  numberOfButtonInterrupts = 0;
  pressed = false;
  stato = false;
}

void switchPressed ()
{
  numberOfButtonInterrupts++; // contatore rimbalzi
  bool val = digitalRead(pulsante); // lettura stato pulsante
  if(val && !pressed){ // fronte di salita
    pressed = true; // disarmo il pulsante
    Timer1.start(); 
    Timer1.attachInterrupt(waitUntilInputLow);
    Serial.println("SALITA disarmo pulsante");
    stato = !stato; 	  // logica toggle  
  }
}  // end of switchPressed

void waitUntilInputLow()
{
    // sezione critica
    if (digitalRead(pulsante) == HIGH)// se il pulsante è ancora premuto
    { 
        Serial.print("Aspetto");
        Serial.print("HIT: "); Serial.println(numberOfButtonInterrupts);
    }else{
        Timer1.stop(); 
        Timer1.detachInterrupt();
        Serial.print("DISCESA riarmo pulsante\n");
        Serial.print("HIT: "); Serial.println(numberOfButtonInterrupts);
        numberOfButtonInterrupts = 0; // reset del flag
        pressed = false; // riarmo il pulsante
    }
}

// loop principale
void loop() {
	if (stato) {
		digitalWrite(led, !digitalRead(led));   	// inverti lo stato precedente del led
		delay(500);
	} else {
		digitalWrite(led, LOW);    	// turn the LED off by making the voltage LOW
		delay(10);
	}
}

Simulazione su Arduino con Wowki: https://wokwi.com/projects/351244710765920855

PULSANTE TOGGLE CON DEBOUNCER BASATO SU TIMER HW PER ARDUINO 2

Versione eseguibile senza l’ausilio di librerie adatta per il simulatore Tinkercad:

/*Alla pressione del pulsante si attiva o disattiva il lampeggo di un led*/
int led = 13;
byte pulsante =2;
volatile unsigned short numberOfButtonInterrupts = 0;
volatile bool pressed;
volatile bool stato;
#define DEBOUNCETIME 500
volatile bool knockKnock = false;
volatile int timer2Counter = 0;
#define WATCH 0
#define TIMER 1
#define TIMER2_POSTSCALER 1000

bool knockMeAfter(int t);  // t is the time in ms
void switchPressed ();
void waitUntilInputLow();

// impostazione prescaler timer HW monostabile
bool knockMeAfter(int t)       // t is the time in ms
{
  static int t_prev = 0;
  byte prescaler = 0;
  if (TIMSK2 == 0b00000010)
    return false;              // Timer2 is busy now
  TCNT2 = 0;                 // Reset counter
  if (t_prev != t) {           // Find the appropriate prescaler taking t in us
    if (t > 16320)
      t = 16320;
    if (t > 4080)
      prescaler = 0b00000111;  // f_cpu / 1024
    else if (t > 2040)
      prescaler = 0b00000110;  // f_cpu / 256
    else if (t > 1020)
      prescaler = 0b00000101;  // f_cpu / 128
    else if (t > 510)
      prescaler = 0b00000100;  // f_cpu / 64
    else if (t > 127)
      prescaler = 0b00000011;  // f_cpu / 32
    else if (t > 15)
      prescaler = 0b00000010;  // f_cpu / 8
    else
      prescaler = 0b00000001;  // No prescaling

    TCCR2A = 0b00000010;       // Mode 2: CTC Mode
    TCCR2B = prescaler;
    OCR2A = (float)t / ((float)(prescaler == 0b00000111 ? 1024 : prescaler == 0b00000110 ? 256 : prescaler == 0b00000101 ? 128 : prescaler == 0b00000100 ? 64 : prescaler == 0b00000011 ? 32 : prescaler == 0b00000010 ? 8 : 1) * 0.0625);
    t_prev = t;
  }
  TIMSK2 = 0b00000010;         // Interrupt on overflow
  sei();                       // Enable global interrupts
  return true;
}

// ISR interrupt tasto
void switchPressed ()
{
  numberOfButtonInterrupts++; // contatore rimbalzi
  bool val = digitalRead(pulsante); // lettura stato pulsante
  if(val && !pressed){ // fronte di salita
    pressed = true; // disarmo il pulsante
    knockMeAfter(DEBOUNCETIME);
    Serial.println("SALITA disarmo pulsante");
    stato = !stato; 	  // logica toggle  
  }
}  // end of switchPressed

// ISR interrupt timer HW
ISR(TIMER2_COMPA_vect)
{
  timer2Counter++;
  if (timer2Counter >= TIMER2_POSTSCALER) {
    timer2Counter = 0;
    TIMSK2 = 0b00000000;   // resetto il timer inibendo ulteriori interrupt
    waitUntilInputLow();
  }
}

void waitUntilInputLow()
{
    if (digitalRead(pulsante) == HIGH)// se il pulsante è ancora premuto
    { 
        Serial.print("Aspetto");
        Serial.print("HIT: "); Serial.println(numberOfButtonInterrupts);
        knockMeAfter(DEBOUNCETIME);
    }else{
        Serial.print("DISCESA riarmo pulsante\n");
        Serial.print("HIT: "); Serial.println(numberOfButtonInterrupts);
        numberOfButtonInterrupts = 0; // reset del flag
        pressed = false; // riarmo il pulsante
    }
}

void setup() {
  Serial.begin(115200);
  pinMode(led, OUTPUT);
  pinMode(pulsante, INPUT);
  attachInterrupt(digitalPinToInterrupt(pulsante), switchPressed, CHANGE );
  numberOfButtonInterrupts = 0;
  pressed = false;
  stato = false;
}

// loop principale
void loop() {
	if (stato) {
		digitalWrite(led, !digitalRead(led));   	// inverti lo stato precedente del led
		delay(500);
	} else {
		digitalWrite(led, LOW);    	// turn the LED off by making the voltage LOW
    		delay(10);
	}
}

Simulazione su Arduino con Tinkercad: https://www.tinkercad.com/embed/iui5Fd9MYtU?editbtn=1

Simulazione su Arduino con Wowki: https://wokwi.com/projects/351242718844813912

Torna all’indice