Pulsanti, interruttori e contatti NO/NC

Un pulsante o un interruttore modifica la continuità elettrica tra due conduttori. Il componente non produce autonomamente una tensione e non genera direttamente un livello logico: apre o chiude un circuito in funzione della propria posizione meccanica.

Lo stato di un contatto viene indicato rispetto alla condizione di riposo, cioè quando il dispositivo non è azionato.

Un contatto normalmente aperto, indicato con NO dall’espressione inglese Normally Open, è aperto nella condizione di riposo. Quando il pulsante viene premuto, il contatto si chiude e permette il passaggio della corrente.

Un contatto normalmente chiuso, indicato con NC da Normally Closed, è invece chiuso nella condizione di riposo. Quando il dispositivo viene azionato, il contatto si apre e interrompe il circuito.

Alcuni dispositivi dispongono di un contatto in scambio formato da tre terminali:

  • COM, terminale comune;
  • NO, collegato al comune soltanto durante l’azionamento;
  • NC, collegato al comune nella condizione di riposo.

Nel passaggio da una posizione all’altra, il terminale comune viene quindi trasferito dal contatto NC al contatto NO.

Occorre inoltre distinguere tra dispositivi momentanei e dispositivi a posizione mantenuta. Un pulsante momentaneo ritorna nella posizione iniziale quando viene rilasciato. Un interruttore a posizione mantenuta conserva invece il proprio stato fino a una nuova azione meccanica.

Le sigle NO e NC descrivono esclusivamente lo stato meccanico del contatto a riposo. Non stabiliscono quale livello logico verrà letto dal microcontrollore: questa relazione dipende dal collegamento elettrico utilizzato.

Ingresso digitale e stato logico

Un ingresso digitale dell’ATmega328P rileva due condizioni logiche: livello basso e livello alto. Nel normale funzionamento a 5 V, un ingresso collegato a massa viene interpretato come livello basso, mentre un ingresso collegato a una tensione sufficientemente vicina a 5 V viene interpretato come livello alto.

Il pulsante non genera direttamente un livello logico. È un contatto meccanico che, quando viene azionato, collega oppure separa due punti del circuito. Il livello letto dal microcontrollore dipende quindi dal modo in cui il pulsante e le resistenze di polarizzazione sono collegati.

Il pin configurato come ingresso presenta un’impedenza elevata e assorbe una corrente molto piccola. Questa caratteristica permette di leggere un segnale senza caricare sensibilmente il circuito, ma rende necessario assegnare al pin uno stato elettrico definito.

Un ingresso lasciato scollegato è detto flottante. In questa condizione il livello logico non è determinato con certezza e può cambiare a causa di disturbi elettrici, capacità parassite, vicinanza di altri conduttori o contatto con il corpo umano. Il programma può quindi rilevare variazioni che non corrispondono a una reale pressione del pulsante.

Per evitare questa condizione si utilizza una resistenza di pull-up o di pull-down. La resistenza mantiene il pin a un livello logico noto quando il contatto è aperto, mentre il pulsante porta il pin al livello opposto quando viene chiuso.

Resistenze di pull-up e pull-down

Una resistenza di pull-up collega il pin di ingresso alla tensione positiva di alimentazione. Quando il contatto è aperto, la resistenza mantiene il pin a livello alto. Quando il pulsante collega il pin a massa, il livello diventa basso.

In questa configurazione il pulsante è quindi considerato premuto quando l’ingresso assume valore logico basso. La relazione tra azione meccanica e livello logico risulta invertita:

  • pulsante rilasciato: livello alto;
  • pulsante premuto: livello basso.

Una resistenza di pull-down collega invece il pin a massa. Con il contatto aperto, il livello rimane basso. Quando il pulsante collega il pin alla tensione positiva, l’ingresso assume livello alto:

  • pulsante rilasciato: livello basso;
  • pulsante premuto: livello alto.

Il valore della resistenza deve essere sufficientemente basso da mantenere stabile il livello logico e sufficientemente alto da limitare la corrente quando il pulsante viene chiuso. Un valore esterno di 10 kΩ è comunemente adatto per ingressi digitali alimentati a 5 V.

Con una resistenza da 10 kΩ, quando il pulsante chiude il collegamento tra 5 V e massa attraverso la resistenza, la corrente vale:

I = V / R = 5 V / 10 000 Ω = 0,5 mA

L’ATmega328P dispone inoltre di resistenze interne di pull-up, attivabili via software. In questo caso non è necessaria una resistenza esterna: il pulsante viene collegato tra il pin e massa e il programma abilita la polarizzazione interna.

Nel framework Wiring la configurazione si esegue con:

pinMode(pin_pulsante, INPUT_PULLUP);

Con INPUT_PULLUP, l’ingresso legge HIGH quando il pulsante è rilasciato e LOW quando viene premuto.

L’ATmega328P non dispone invece di una resistenza interna di pull-down selezionabile nello stesso modo. Quando si desidera una polarizzazione verso massa occorre utilizzare una resistenza esterna.

Confronto tra ingresso con resistenza di pull-up e ingresso con resistenza di pull-down

Rimbalzo meccanico dei contatti

Quando un pulsante o un interruttore cambia stato, i contatti metallici non passano istantaneamente da circuito aperto a circuito chiuso, o viceversa. Durante il movimento meccanico possono urtarsi, separarsi e richiudersi più volte prima di raggiungere una posizione stabile.

Questo fenomeno è chiamato rimbalzo dei contatti, o contact bounce. Dal punto di vista elettrico, una singola pressione può quindi produrre una rapida sequenza di transizioni tra livello alto e livello basso.

Il rimbalzo può verificarsi sia durante la pressione sia durante il rilascio. La sua durata dipende dal tipo di componente, dalla struttura meccanica, dall’usura e dalla velocità di azionamento; può estendersi per alcuni millisecondi e, in determinati casi, per un intervallo maggiore.

Il microcontrollore esegue il programma molto più rapidamente del movimento meccanico del pulsante. Senza un sistema di filtraggio, può quindi interpretare i rimbalzi come più pressioni e più rilasci distinti.

Un semplice conteggio potrebbe produrre, per esempio, il seguente risultato:

una pressione reale → quattro transizioni rilevate

Il problema non si risolve inserendo un ritardo arbitrario dopo ogni lettura, perché un’attesa bloccante impedirebbe al programma di gestire contemporaneamente altre temporizzazioni, ingressi o uscite.

È quindi necessario applicare un debounce, cioè una procedura che accetta il nuovo stato soltanto dopo che il segnale è rimasto stabile per un intervallo definito.

Debounce software non bloccante

Il debounce software confronta la lettura istantanea del pin con uno stato considerato stabile. Quando viene rilevata una variazione, il programma non la accetta immediatamente, ma avvia una temporizzazione.

Se il segnale rimane invariato per tutto l’intervallo stabilito, il nuovo valore viene considerato valido. Se invece cambia nuovamente prima della scadenza, la temporizzazione viene riavviata.

Per gestire correttamente il processo sono necessari almeno tre valori distinti:

  • la lettura istantanea del pin;
  • l’ultimo valore letto;
  • lo stato stabile già accettato dal programma.

È inoltre necessario memorizzare l’istante in cui è stata rilevata l’ultima variazione.

Un intervallo di debounce di circa 20–50 ms è comunemente adatto ai normali pulsanti meccanici. Il valore effettivo può essere modificato in funzione del componente utilizzato e della risposta richiesta.

La temporizzazione deve essere non bloccante. Il programma continua quindi a eseguire le altre operazioni mentre controlla il tempo trascorso mediante un contatore o una funzione temporale come millis().

La logica generale è la seguente:

lettura diversa dall’ultima lettura
→ memorizzazione del nuovo valore
→ azzeramento della temporizzazione

lettura invariata per il tempo di debounce
→ aggiornamento dello stato stabile

In questo modo una sequenza rapida di oscillazioni prodotta dal rimbalzo meccanico genera una sola variazione valida.

Rilevamento di pressione e rilascio

Dopo il debounce, il programma dispone di uno stato stabile affidabile. A questo punto non è sufficiente sapere se il pulsante è premuto o rilasciato: in molte applicazioni è necessario rilevare il momento preciso in cui avviene il cambiamento.

Con una configurazione pull-up:

  • stato stabile HIGH: pulsante rilasciato;
  • transizione da HIGH a LOW: pressione valida;
  • stato stabile LOW: pulsante mantenuto premuto;
  • transizione da LOW a HIGH: rilascio valido.

Il programma deve quindi confrontare il nuovo stato stabile con quello precedentemente memorizzato. Soltanto quando i due valori sono diversi viene generato un evento.

La logica generale è:

nuovo stato stabile diverso dal precedente
→ aggiornamento dello stato precedente
→ identificazione della transizione

Per una configurazione pull-up:

HIGH → LOW = pressione
LOW → HIGH = rilascio

Questo metodo impedisce che il programma consideri come nuove pressioni le letture ripetute effettuate mentre il pulsante rimane abbassato.

Il rilevamento della transizione consente di separare chiaramente tre condizioni:

  • pulsante appena premuto;
  • pulsante mantenuto premuto;
  • pulsante appena rilasciato.

Una pressione può quindi incrementare un contatore, commutare un’uscita o avviare una funzione una sola volta, indipendentemente dalla durata dell’azionamento.

Conteggio delle pressioni valide e commutazione di un’uscita

Una pressione deve essere conteggiata soltanto quando il debounce conferma una transizione stabile da pulsante rilasciato a pulsante premuto.

Con collegamento pull-up, l’incremento avviene quindi esclusivamente durante la transizione:

HIGH → LOW

Il contatore non deve essere modificato mentre il pulsante rimane premuto. In questo modo una pressione lunga produce un solo incremento, anche se il programma legge il pin migliaia di volte prima del rilascio.

La logica può essere rappresentata così:

pressione stabile rilevata
→ incremento del contatore
→ eventuale commutazione dell’uscita

Un’uscita digitale può essere commutata a ogni pressione valida. Se inizialmente è spenta, la prima pressione la accende, la seconda la spegne, la terza la riaccende e così via.

Per ottenere questo comportamento il programma memorizza lo stato dell’uscita in una variabile booleana:

false = uscita disattivata
true  = uscita attivata

A ogni pressione valida lo stato viene invertito:

nuovo stato = contrario dello stato precedente

Il contatore delle pressioni e lo stato dell’uscita rappresentano due informazioni distinte. Il primo registra quante pressioni valide sono state rilevate; il secondo indica la condizione attuale dell’uscita.

Il rilascio del pulsante viene comunque riconosciuto e memorizzato, ma non incrementa il contatore e non modifica l’uscita, salvo che il progetto richieda esplicitamente una funzione associata anche al rilascio.

Collegamento utilizzato nei programmi

Negli esempi successivi viene utilizzato un pulsante con contatto normalmente aperto collegato tra l’ingresso digitale e massa.

Il pin di ingresso viene mantenuto a livello alto mediante la resistenza interna di pull-up dell’ATmega328P. Non è quindi necessaria una resistenza esterna.

I collegamenti sono:

pulsante NO:
un terminale → pin PD2
altro terminale → GND

uscita:
pin PB0 → resistenza da 330 Ω → LED → GND

Sulla scheda Arduino UNO R3, le corrispondenze sono:

PD2 → pin digitale 2
PB0 → pin digitale 8

Il comportamento dell’ingresso è quindi:

pulsante rilasciato → HIGH
pulsante premuto   → LOW

Il LED collegato al pin digitale 8 della Arduino UNO R3, corrispondente a PB0 dell’ATmega328P, viene commutato a ogni pressione valida del pulsante.

Un contatto normalmente chiuso può essere collegato allo stesso ingresso. In questo caso, con il dispositivo a riposo il pin viene mantenuto a massa dal contatto chiuso e assume livello LOW; quando il contatto viene azionato e si apre, la resistenza di pull-up porta il pin a livello HIGH.

La logica associata all’azione meccanica risulta quindi invertita:

contatto NC a riposo  → LOW
contatto NC azionato  → HIGH

Programma in C

Il programma seguente utilizza il pin digitale 2 della Arduino UNO R3, corrispondente a PD2 dell’ATmega328P, come ingresso con resistenza interna di pull-up. Il pin digitale 8 della Arduino UNO R3, corrispondente a PB0 dell’ATmega328P, viene configurato come uscita per il LED.

Il debounce viene eseguito senza attese bloccanti mediante un contatore incrementato ogni millisecondo dal Timer1.

#define F_CPU 16000000UL

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdbool.h>
#include <stdint.h>

#define TEMPO_DEBOUNCE_MS 30UL

volatile uint32_t millisecondi = 0;

bool ultima_lettura = true;
bool stato_stabile = true;
bool uscita_attiva = false;

uint32_t tempo_ultima_variazione = 0;
uint32_t numero_pressioni = 0;

ISR(TIMER1_COMPA_vect)
{
    millisecondi++;
}

uint32_t leggi_millisecondi(void)
{
    uint8_t stato_sreg;
    uint32_t valore;

    stato_sreg = SREG;
    cli();

    valore = millisecondi;

    SREG = stato_sreg;

    return valore;
}

void configura_timer1(void)
{
    TCCR1A = 0;

    /*
     * Timer1 in modalità CTC.
     * Clock: 16 MHz
     * Prescaler: 64
     * Frequenza timer: 250 kHz
     * OCR1A = 249 → interrupt ogni 1 ms
     */
    TCCR1B =
        (1 << WGM12) |
        (1 << CS11) |
        (1 << CS10);

    OCR1A = 249;
    TIMSK1 = (1 << OCIE1A);
}

int main(void)
{
    /*
     * Pin digitale 2 della Arduino UNO R3,
     * corrispondente a PD2 dell’ATmega328P,
     * configurato come ingresso con pull-up interna.
     */
    DDRD &= ~(1 << DDD2);
    PORTD |= (1 << PORTD2);

    /*
     * Pin digitale 8 della Arduino UNO R3,
     * corrispondente a PB0 dell’ATmega328P,
     * configurato come uscita per il LED.
     * LED inizialmente spento.
     */
    DDRB |= (1 << DDB0);
    PORTB &= ~(1 << PORTB0);

    configura_timer1();
    sei();

    ultima_lettura = (PIND & (1 << PIND2)) != 0;
    stato_stabile = ultima_lettura;
    tempo_ultima_variazione = leggi_millisecondi();

    while (1)
    {
        bool lettura_attuale;
        uint32_t tempo_attuale;

        lettura_attuale = (PIND & (1 << PIND2)) != 0;
        tempo_attuale = leggi_millisecondi();

        /*
         * Una variazione della lettura riavvia
         * la temporizzazione del debounce.
         */
        if (lettura_attuale != ultima_lettura)
        {
            ultima_lettura = lettura_attuale;
            tempo_ultima_variazione = tempo_attuale;
        }

        /*
         * Il nuovo stato viene accettato soltanto
         * se rimane invariato per almeno 30 ms.
         */
        if (
            lettura_attuale != stato_stabile &&
            tempo_attuale - tempo_ultima_variazione >= TEMPO_DEBOUNCE_MS
        )
        {
            stato_stabile = lettura_attuale;

            /*
             * Con pull-up, LOW indica
             * una pressione valida.
             */
            if (stato_stabile == false)
            {
                numero_pressioni++;

                uscita_attiva = !uscita_attiva;

                if (uscita_attiva)
                {
                    PORTB |= (1 << PORTB0);
                }
                else
                {
                    PORTB &= ~(1 << PORTB0);
                }
            }
        }
    }
}

Spiegazione del programma in C

La costante:

#define F_CPU 16000000UL

indica una frequenza di clock di 16 MHz, corrispondente a quella utilizzata dalla Arduino UNO R3.

La durata del debounce è definita in un solo punto:

#define TEMPO_DEBOUNCE_MS 30UL

Il valore può quindi essere modificato senza intervenire nella logica del programma.

La variabile:

volatile uint32_t millisecondi = 0;

contiene il tempo trascorso dall’avvio, espresso in millisecondi. È dichiarata volatile perché viene modificata all’interno della routine di interrupt del Timer1:

ISR(TIMER1_COMPA_vect)
{
    millisecondi++;
}

L’interrupt viene eseguito ogni millisecondo e incrementa il contatore.

Le tre variabili booleane svolgono funzioni differenti:

bool ultima_lettura = true;
bool stato_stabile = true;
bool uscita_attiva = false;

ultima_lettura memorizza l’ultimo livello elettrico letto sul pin.
stato_stabile contiene invece il livello già accettato dopo il debounce.
uscita_attiva rappresenta lo stato corrente del LED.

Poiché viene utilizzata una resistenza interna di pull-up, il valore iniziale true corrisponde al livello HIGH presente quando il pulsante è rilasciato.

La variabile:

uint32_t tempo_ultima_variazione = 0;

memorizza l’istante in cui la lettura del pulsante ha cambiato valore. Da questo momento viene misurato l’intervallo durante il quale il segnale rimane invariato.

La variabile:

uint32_t numero_pressioni = 0;

conta esclusivamente le pressioni confermate dal debounce.

La funzione:

uint32_t leggi_millisecondi(void)

copia in modo sicuro il valore del contatore temporale. Sull’ATmega328P, che utilizza un’architettura a 8 bit, una variabile a 32 bit richiede più operazioni per essere letta.

Se l’interrupt modificasse millisecondi durante la copia, il programma potrebbe ottenere un valore incoerente. Per evitarlo, la funzione salva il registro di stato, disabilita temporaneamente gli interrupt, copia il valore e infine ripristina lo stato precedente:

stato_sreg = SREG;
cli();

valore = millisecondi;

SREG = stato_sreg;

La funzione configura_timer1() imposta Timer1 in modalità CTC. Con clock a 16 MHz e prescaler 64, il timer riceve:

16 000 000 / 64 = 250 000 impulsi al secondo

Un millisecondo corrisponde quindi a 250 impulsi. Poiché il conteggio inizia da zero, il registro di confronto viene impostato a 249:

OCR1A = 249;

Il bit OCIE1A abilita l’interrupt quando il contatore raggiunge il valore contenuto in OCR1A.

Nel main(), il pin digitale 2 della Arduino UNO R3 viene configurato come ingresso:

DDRD &= ~(1 << DDD2);

La resistenza interna di pull-up viene attivata impostando a uno il bit corrispondente nel registro PORTD:

PORTD |= (1 << PORTD2);

Il pin digitale 8 viene configurato come uscita:

DDRB |= (1 << DDB0);

Il LED viene inizialmente spento portando PB0 a livello basso:

PORTB &= ~(1 << PORTB0);

Dopo la configurazione del timer, la funzione:

sei();

abilita globalmente gli interrupt.

All’inizio vengono acquisiti il livello reale del pulsante e il tempo corrente:

ultima_lettura = (PIND & (1 << PIND2)) != 0;
stato_stabile = ultima_lettura;
tempo_ultima_variazione = leggi_millisecondi();

Questo evita di presumere arbitrariamente quale sia lo stato iniziale dell’ingresso.

Nel ciclo principale, il registro PIND viene utilizzato per leggere il pin digitale 2:

lettura_attuale = (PIND & (1 << PIND2)) != 0;

Se la lettura è diversa dalla precedente, il programma memorizza il nuovo valore e registra l’istante della variazione:

if (lettura_attuale != ultima_lettura)
{
    ultima_lettura = lettura_attuale;
    tempo_ultima_variazione = tempo_attuale;
}

Ogni ulteriore oscillazione del contatto aggiorna nuovamente tempo_ultima_variazione. Il conteggio del debounce riparte quindi dall’ultima transizione rilevata.

Il nuovo livello viene accettato soltanto quando soddisfa entrambe le condizioni:

lettura_attuale != stato_stabile &&
tempo_attuale - tempo_ultima_variazione >= TEMPO_DEBOUNCE_MS

La prima condizione verifica che esista un cambiamento ancora non confermato. La seconda verifica che il segnale sia rimasto invariato per almeno 30 ms.

La sottrazione tra valori senza segno:

tempo_attuale - tempo_ultima_variazione

mantiene corretta la misura dell’intervallo anche quando il contatore a 32 bit supera il proprio valore massimo e riparte da zero.

Quando il nuovo stato viene confermato, il programma aggiorna stato_stabile:

stato_stabile = lettura_attuale;

Con pull-up interna, una pressione valida corrisponde al livello basso:

if (stato_stabile == false)

Soltanto in questo momento viene incrementato il contatore:

numero_pressioni++;

Lo stato dell’uscita viene poi invertito:

uscita_attiva = !uscita_attiva;

Se il nuovo stato è attivo, PB0 viene portato a livello alto e il LED si accende:

PORTB |= (1 << PORTB0);

In caso contrario, PB0 viene portato a livello basso e il LED si spegne:

PORTB &= ~(1 << PORTB0);

Il rilascio viene comunque confermato dal debounce e memorizzato in stato_stabile, ma non incrementa il contatore e non commuta il LED.

Programma equivalente in C++

Il programma seguente utilizza ancora il pin digitale 2 della Arduino UNO R3, corrispondente a PD2 dell’ATmega328P, come ingresso con pull-up interna. Il pin digitale 8, corrispondente a PB0, controlla il LED.

L’accesso all’hardware avviene direttamente tramite i registri dell’ATmega328P. La gestione del debounce è raccolta nella classe PulsanteDebounce.

#define F_CPU 16000000UL

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdint.h>

constexpr uint32_t TEMPO_DEBOUNCE_MS = 30UL;

volatile uint32_t millisecondi = 0;

ISR(TIMER1_COMPA_vect)
{
    millisecondi++;
}

uint32_t leggi_millisecondi()
{
    uint8_t stato_sreg;
    uint32_t valore;

    stato_sreg = SREG;
    cli();

    valore = millisecondi;

    SREG = stato_sreg;

    return valore;
}

void configura_timer1()
{
    TCCR1A = 0;

    /*
     * Timer1 in modalità CTC.
     * Clock: 16 MHz
     * Prescaler: 64
     * Frequenza timer: 250 kHz
     * OCR1A = 249 → interrupt ogni 1 ms
     */
    TCCR1B =
        (1 << WGM12) |
        (1 << CS11) |
        (1 << CS10);

    OCR1A = 249;
    TIMSK1 = (1 << OCIE1A);
}

class PulsanteDebounce
{
public:
    PulsanteDebounce(
        bool stato_iniziale,
        uint32_t tempo_iniziale
    )
        : ultima_lettura(stato_iniziale),
          stato_stabile(stato_iniziale),
          tempo_ultima_variazione(tempo_iniziale)
    {
    }

    bool aggiorna(
        bool lettura_attuale,
        uint32_t tempo_attuale
    )
    {
        /*
         * Ogni variazione della lettura riavvia
         * la temporizzazione del debounce.
         */
        if (lettura_attuale != ultima_lettura)
        {
            ultima_lettura = lettura_attuale;
            tempo_ultima_variazione = tempo_attuale;
        }

        /*
         * Il nuovo stato viene accettato soltanto
         * dopo almeno 30 ms di stabilità.
         */
        if (
            lettura_attuale != stato_stabile &&
            tempo_attuale - tempo_ultima_variazione >=
                TEMPO_DEBOUNCE_MS
        )
        {
            stato_stabile = lettura_attuale;
            return true;
        }

        return false;
    }

    bool premuto() const
    {
        /*
         * Con pull-up interna:
         * LOW = pulsante premuto.
         */
        return stato_stabile == false;
    }

private:
    bool ultima_lettura;
    bool stato_stabile;
    uint32_t tempo_ultima_variazione;
};

int main()
{
    /*
     * Pin digitale 2 della Arduino UNO R3,
     * corrispondente a PD2 dell’ATmega328P,
     * configurato come ingresso con pull-up interna.
     */
    DDRD &= ~(1 << DDD2);
    PORTD |= (1 << PORTD2);

    /*
     * Pin digitale 8 della Arduino UNO R3,
     * corrispondente a PB0 dell’ATmega328P,
     * configurato come uscita per il LED.
     */
    DDRB |= (1 << DDB0);
    PORTB &= ~(1 << PORTB0);

    configura_timer1();
    sei();

    bool lettura_iniziale =
        (PIND & (1 << PIND2)) != 0;

    PulsanteDebounce pulsante(
        lettura_iniziale,
        leggi_millisecondi()
    );

    bool uscita_attiva = false;
    uint32_t numero_pressioni = 0;

    while (true)
    {
        bool lettura_attuale =
            (PIND & (1 << PIND2)) != 0;

        uint32_t tempo_attuale =
            leggi_millisecondi();

        /*
         * aggiorna() restituisce true soltanto
         * quando viene confermata una transizione stabile.
         */
        if (
            pulsante.aggiorna(
                lettura_attuale,
                tempo_attuale
            )
        )
        {
            /*
             * Il contatore e il LED vengono modificati
             * soltanto durante una pressione valida.
             */
            if (pulsante.premuto())
            {
                numero_pressioni++;
                uscita_attiva = !uscita_attiva;

                if (uscita_attiva)
                {
                    PORTB |= (1 << PORTB0);
                }
                else
                {
                    PORTB &= ~(1 << PORTB0);
                }
            }
        }
    }
}

Spiegazione del programma in C++

La configurazione del Timer1, la lettura atomica del contatore temporale e l’accesso diretto ai registri dell’ATmega328P restano equivalenti a quelli utilizzati nel programma in C.

La principale differenza consiste nell’organizzazione della logica di debounce all’interno della classe:

class PulsanteDebounce

La classe mantiene al proprio interno tre informazioni:

bool ultima_lettura;
bool stato_stabile;
uint32_t tempo_ultima_variazione;

Questi membri sono dichiarati private, quindi possono essere modificati soltanto attraverso le funzioni della classe. Il resto del programma non accede direttamente allo stato interno del debounce.

Il costruttore:

PulsanteDebounce(
    bool stato_iniziale,
    uint32_t tempo_iniziale
)

riceve il livello iniziale dell’ingresso e il tempo corrente. La lista di inizializzazione:

: ultima_lettura(stato_iniziale),
  stato_stabile(stato_iniziale),
  tempo_ultima_variazione(tempo_iniziale)

assegna i valori iniziali ai membri della classe prima dell’esecuzione del corpo del costruttore.

La funzione:

bool aggiorna(
    bool lettura_attuale,
    uint32_t tempo_attuale
)

riceve la lettura istantanea del pulsante e il tempo corrente.

Quando la lettura cambia, vengono aggiornati ultima_lettura e tempo_ultima_variazione:

if (lettura_attuale != ultima_lettura)
{
    ultima_lettura = lettura_attuale;
    tempo_ultima_variazione = tempo_attuale;
}

Ogni nuovo rimbalzo riavvia quindi la temporizzazione.

Il nuovo stato viene accettato soltanto quando è diverso dallo stato stabile precedente ed è rimasto invariato per almeno 30 ms:

if (
    lettura_attuale != stato_stabile &&
    tempo_attuale - tempo_ultima_variazione >=
        TEMPO_DEBOUNCE_MS
)

Quando la transizione viene confermata, la funzione aggiorna stato_stabile e restituisce true:

stato_stabile = lettura_attuale;
return true;

Se non è stata confermata alcuna nuova transizione, restituisce false.

La funzione:

bool premuto() const

indica se lo stato stabile corrisponde a un pulsante premuto. Con la pull-up interna, il livello basso rappresenta la pressione:

return stato_stabile == false;

La parola chiave const, posta dopo le parentesi della funzione, indica che premuto() non modifica i membri dell’oggetto.

Nel main(), dopo la configurazione dei pin e del timer, viene acquisito lo stato reale dell’ingresso:

bool lettura_iniziale =
    (PIND & (1 << PIND2)) != 0;

Viene quindi creato l’oggetto:

PulsanteDebounce pulsante(
    lettura_iniziale,
    leggi_millisecondi()
);

L’oggetto conserva autonomamente le informazioni necessarie al debounce.

Nel ciclo principale, la funzione aggiorna() viene richiamata continuamente:

if (
    pulsante.aggiorna(
        lettura_attuale,
        tempo_attuale
    )
)

Il blocco interno viene eseguito soltanto quando è stata confermata una nuova transizione stabile.

La verifica:

if (pulsante.premuto())

distingue la pressione dal rilascio. Il contatore e il LED vengono modificati soltanto quando la transizione confermata corrisponde alla pressione.

La classe separa quindi la gestione del contatto dalla funzione applicativa. La stessa struttura può essere riutilizzata per più pulsanti creando oggetti distinti, ognuno dotato del proprio stato e della propria temporizzazione.

Programma equivalente con framework Wiring

Il programma seguente utilizza il pin digitale 2 della Arduino UNO R3, corrispondente a PD2 dell’ATmega328P, come ingresso con resistenza interna di pull-up. Il pin digitale 8, corrispondente a PB0, controlla il LED.

La temporizzazione del debounce viene gestita mediante millis(), senza utilizzare delay().

const int pin_pulsante = 2;
const int pin_led = 8;

const unsigned long tempo_debounce_ms = 30;

bool ultima_lettura;
bool stato_stabile;
bool stato_led = false;

unsigned long tempo_ultima_variazione = 0;
unsigned long numero_pressioni = 0;

void setup()
{
    /*
     * Pin digitale 2 della Arduino UNO R3,
     * corrispondente a PD2 dell’ATmega328P,
     * configurato come ingresso con pull-up interna.
     */
    pinMode(pin_pulsante, INPUT_PULLUP);

    /*
     * Pin digitale 8 della Arduino UNO R3,
     * corrispondente a PB0 dell’ATmega328P,
     * configurato come uscita per il LED.
     */
    pinMode(pin_led, OUTPUT);
    digitalWrite(pin_led, LOW);

    /*
     * Acquisizione dello stato reale
     * presente all’avvio.
     */
    ultima_lettura = digitalRead(pin_pulsante);
    stato_stabile = ultima_lettura;
    tempo_ultima_variazione = millis();
}

void loop()
{
    bool lettura_attuale = digitalRead(pin_pulsante);
    unsigned long tempo_attuale = millis();

    /*
     * Ogni variazione della lettura riavvia
     * la temporizzazione del debounce.
     */
    if (lettura_attuale != ultima_lettura)
    {
        ultima_lettura = lettura_attuale;
        tempo_ultima_variazione = tempo_attuale;
    }

    /*
     * Il nuovo stato viene accettato soltanto
     * dopo almeno 30 ms di stabilità.
     */
    if (
        lettura_attuale != stato_stabile &&
        tempo_attuale - tempo_ultima_variazione >=
            tempo_debounce_ms
    )
    {
        stato_stabile = lettura_attuale;

        /*
         * Con INPUT_PULLUP:
         * LOW = pulsante premuto.
         */
        if (stato_stabile == LOW)
        {
            numero_pressioni++;

            stato_led = !stato_led;

            digitalWrite(
                pin_led,
                stato_led ? HIGH : LOW
            );
        }
    }
}

Spiegazione del programma con framework Wiring

Le costanti:

const int pin_pulsante = 2;
const int pin_led = 8;

identificano i pin utilizzati sulla Arduino UNO R3.

Il pin digitale 2 corrisponde a PD2 dell’ATmega328P e viene utilizzato per leggere il pulsante. Il pin digitale 8 corrisponde a PB0 e controlla il LED.

La durata del debounce è definita da:

const unsigned long tempo_debounce_ms = 30;

Il tipo unsigned long è lo stesso utilizzato dalla funzione millis() per rappresentare il tempo trascorso dall’avvio.

Le variabili:

bool ultima_lettura;
bool stato_stabile;
bool stato_led = false;

memorizzano rispettivamente:

  • l’ultimo livello letto sul pin;
  • il livello già confermato dal debounce;
  • lo stato corrente del LED.

La variabile:

unsigned long tempo_ultima_variazione = 0;

contiene l’istante in cui è stata rilevata l’ultima variazione del segnale.

Il contatore:

unsigned long numero_pressioni = 0;

viene incrementato soltanto quando una pressione è stata confermata come valida.

Nella funzione setup(), il pulsante viene configurato come ingresso con resistenza interna di pull-up:

pinMode(pin_pulsante, INPUT_PULLUP);

Con questa configurazione:

pulsante rilasciato → HIGH
pulsante premuto   → LOW

Il LED viene configurato come uscita:

pinMode(pin_led, OUTPUT);

e inizialmente viene spento:

digitalWrite(pin_led, LOW);

Le istruzioni:

ultima_lettura = digitalRead(pin_pulsante);
stato_stabile = ultima_lettura;
tempo_ultima_variazione = millis();

acquisiscono lo stato reale presente all’avvio e inizializzano la temporizzazione.

La funzione:

digitalRead(pin_pulsante)

restituisce HIGH oppure LOW in funzione del livello presente sul pin.

Nel ciclo loop(), la lettura corrente e il tempo corrente vengono acquisiti mediante:

bool lettura_attuale = digitalRead(pin_pulsante);
unsigned long tempo_attuale = millis();

La funzione millis() restituisce il numero di millisecondi trascorsi dall’avvio del programma. Non interrompe l’esecuzione e consente quindi di gestire il debounce senza utilizzare delay().

Quando la lettura cambia:

if (lettura_attuale != ultima_lettura)

il nuovo valore viene memorizzato e la temporizzazione viene riavviata:

ultima_lettura = lettura_attuale;
tempo_ultima_variazione = tempo_attuale;

Il nuovo stato viene accettato soltanto quando è diverso dallo stato stabile precedente ed è rimasto invariato per almeno 30 ms:

if (
    lettura_attuale != stato_stabile &&
    tempo_attuale - tempo_ultima_variazione >=
        tempo_debounce_ms
)

La sottrazione tra valori di tipo unsigned long mantiene corretta la misura dell’intervallo anche quando il contatore interno di millis() supera il proprio valore massimo e riparte da zero.

Quando la transizione viene confermata:

stato_stabile = lettura_attuale;

il programma verifica se il nuovo stato corrisponde a una pressione:

if (stato_stabile == LOW)

Con INPUT_PULLUP, il livello LOW indica che il pulsante ha collegato il pin a massa.

Il contatore viene quindi incrementato:

numero_pressioni++;

Lo stato del LED viene invertito:

stato_led = !stato_led;

e applicato all’uscita:

digitalWrite(
    pin_led,
    stato_led ? HIGH : LOW
);

L’operatore condizionale seleziona HIGH quando stato_led è vero e LOW quando è falso.

Il rilascio del pulsante viene riconosciuto e confermato dal debounce, ma non modifica né il contatore né il LED.

Debounce hardware

Il rimbalzo dei contatti può essere ridotto anche mediante un circuito hardware posto tra il pulsante e l’ingresso del microcontrollore.

La soluzione più semplice utilizza una resistenza e un condensatore. I due componenti formano una rete RC che rallenta le variazioni rapide della tensione prodotte dai rimbalzi meccanici.

La costante di tempo del circuito è:

τ = R × C

Per esempio, con:

R = 10 kΩ
C = 1 µF

si ottiene:

τ = 10 000 Ω × 0,000001 F = 0,01 s = 10 ms

Il condensatore non elimina fisicamente il rimbalzo, ma attenua le rapide oscillazioni del segnale. La tensione sul pin varia in modo più graduale invece di seguire ogni apertura e chiusura momentanea del contatto.

Un semplice filtro RC può però produrre una transizione lenta attraverso la zona in cui il livello logico non è chiaramente alto né basso. Per ottenere un segnale digitale più netto si può aggiungere un ingresso con isteresi, per esempio una porta logica con trigger di Schmitt.

Il trigger di Schmitt utilizza due soglie differenti:

  • una soglia per il passaggio da livello basso a livello alto;
  • una soglia diversa per il passaggio da livello alto a livello basso.

Questa isteresi impedisce che piccole oscillazioni della tensione provochino molte commutazioni dell’uscita.

Il debounce hardware è utile quando il segnale deve essere stabilizzato indipendentemente dal programma o quando l’ingresso viene utilizzato da circuiti che richiedono fronti ben definiti.

Il debounce software rimane comunque necessario o conveniente in molti sistemi, perché non richiede componenti aggiuntivi e permette di modificare facilmente il tempo di stabilizzazione. Le due tecniche possono anche essere utilizzate insieme.

Scelta tra contatto NO e contatto NC

La scelta tra un contatto normalmente aperto e un contatto normalmente chiuso dipende dal comportamento richiesto al circuito.

Un contatto NO mantiene il circuito aperto nella condizione di riposo e lo chiude soltanto durante l’azionamento. È adatto, per esempio, a un pulsante utilizzato per impartire un comando momentaneo.

Con un collegamento pull-up:

contatto NO aperto     → ingresso HIGH
contatto NO azionato   → ingresso LOW

Un contatto NC mantiene invece il circuito chiuso nella condizione di riposo e lo apre durante l’azionamento.

Con il contatto collegato tra ingresso e massa e la pull-up attiva:

contatto NC a riposo   → ingresso LOW
contatto NC azionato   → ingresso HIGH

Il contatto NC consente inoltre di rilevare alcune anomalie del collegamento. Se un filo viene interrotto, il circuito assume lo stesso stato elettrico prodotto dall’apertura del contatto. Il programma può quindi rilevare sia l’azionamento sia un’interruzione della linea, anche se non può distinguere automaticamente le due cause senza ulteriori circuiti di controllo.

Questa caratteristica rende i contatti NC utili nei circuiti in cui la continuità della linea deve essere verificata durante il funzionamento.

La sigla NC non rende però il circuito automaticamente sicuro. La sicurezza dipende dall’intera architettura: tipo di ingresso, alimentazione, diagnostica, cablaggio, gestione dei guasti e comportamento previsto in caso di perdita del segnale.

Disturbi elettrici e lunghezza dei collegamenti

Un pulsante collegato direttamente a un ingresso digitale può funzionare correttamente con conduttori corti e in un ambiente elettricamente poco disturbato. Quando il collegamento diventa più lungo, il conduttore può invece raccogliere disturbi elettromagnetici provenienti da relè, motori, alimentatori switching, cavi di potenza o scariche elettrostatiche.

La resistenza di pull-up interna dell’ATmega328P ha un valore relativamente elevato e non è sempre la soluzione più robusta per collegamenti esterni o lunghi. In questi casi può essere preferibile utilizzare una resistenza esterna con valore più basso, per esempio 4,7 kΩ o 10 kΩ, in modo da rendere il livello logico meno sensibile ai disturbi.

I conduttori del pulsante dovrebbero essere mantenuti lontani dai cavi che alimentano carichi induttivi o dispositivi ad alta corrente. Quando possibile, il segnale e il relativo conduttore di massa possono essere disposti vicini oppure intrecciati.

Un condensatore posto vicino all’ingresso può contribuire a filtrare variazioni rapide, ma il suo valore deve essere scelto considerando la resistenza di pull-up o pull-down e il tempo di risposta richiesto.

Per collegamenti particolarmente lunghi o installati in ambienti disturbati possono essere necessari ulteriori accorgimenti:

  • filtro RC;
  • ingresso con trigger di Schmitt;
  • protezione contro sovratensioni e scariche elettrostatiche;
  • cavo schermato;
  • isolamento galvanico;
  • ingresso industriale progettato per tensioni superiori ai livelli logici del microcontrollore.

Un pin dell’ATmega328P non deve essere collegato direttamente a tensioni superiori a quelle ammesse dal componente. Contatti provenienti da macchine, impianti o circuiti alimentati a 12 V o 24 V richiedono un’interfaccia elettrica adeguata.

Schemi e collegamenti

L’immagine reti.png confronta i due collegamenti fondamentali utilizzati per assegnare uno stato definito a un ingresso digitale:

  • resistenza di pull-up tra ingresso e +5 V, con pulsante verso massa;
  • resistenza di pull-down tra ingresso e massa, con pulsante verso +5 V.

Nel circuito utilizzato dai programmi dell’articolo viene adottata la configurazione pull-up interna dell’ATmega328P.

I collegamenti completi sono:

Pulsante NO:
un terminale → pin digitale 2 della Arduino UNO R3
altro terminale → GND

Il pin digitale 2 corrisponde a PD2 dell’ATmega328P. La resistenza di pull-up viene attivata dal programma e non deve quindi essere aggiunta esternamente.

LED:
pin digitale 8 della Arduino UNO R3
→ resistenza da 330 Ω
→ anodo del LED

catodo del LED
→ GND

Il pin digitale 8 corrisponde a PB0 dell’ATmega328P.

Con il pulsante rilasciato, la pull-up mantiene l’ingresso a livello HIGH. Durante la pressione, il contatto collega il pin a massa e il livello diventa LOW.

La resistenza da 330 Ω limita la corrente che attraversa il LED e protegge sia il componente sia l’uscita del microcontrollore.

Nel caso di un contatto NC collegato tra il pin digitale 2 e massa, il collegamento resta invariato, ma il comportamento logico diventa:

contatto a riposo  → LOW
contatto azionato  → HIGH

Il programma deve quindi associare l’evento di azionamento alla transizione da LOW a HIGH.

Adattamento del programma a un contatto NC

I programmi precedenti utilizzano un pulsante normalmente aperto collegato tra il pin digitale 2 e massa. Con la pull-up interna:

rilasciato → HIGH
premuto   → LOW

Utilizzando un contatto normalmente chiuso, il circuito elettrico può rimanere identico, ma gli stati risultano invertiti:

contatto a riposo → LOW
contatto azionato → HIGH

La procedura di debounce non cambia. Devono essere modificate soltanto le condizioni con cui il programma identifica l’azionamento.

Nel programma in C, per considerare valida l’apertura del contatto NC, la condizione:

if (stato_stabile == false)

diventa:

if (stato_stabile == true)

Nel programma in C++ la funzione:

bool premuto() const
{
    return stato_stabile == false;
}

può essere sostituita con:

bool azionato() const
{
    return stato_stabile == true;
}

Nel programma con framework Wiring, la condizione:

if (stato_stabile == LOW)

diventa:

if (stato_stabile == HIGH)

Il debounce continua a riconoscere sia l’apertura sia la richiusura del contatto. Il contatore e la commutazione del LED vengono però eseguiti soltanto durante la transizione associata all’azionamento.

Con un contatto NC, anche l’interruzione di un conduttore porta generalmente l’ingresso a livello HIGH, perché la pull-up non viene più contrastata dal collegamento a massa. Il programma rileva quindi un’apertura della linea, ma senza circuiti aggiuntivi non può distinguere tra azionamento volontario, filo interrotto e morsetto scollegato.

Fonti principali

Microchip Technology, ATmega328P Data Sheet — porte di ingresso e uscita digitali, registri DDRx, PORTx e PINx, resistenze interne di pull-up, caratteristiche elettriche e Timer1.

Arduino, Arduino UNO R3 Pinout — corrispondenza tra i pin digitali della scheda e le porte dell’ATmega328P.

Arduino, Arduino UNO R3 Schematic — collegamenti elettrici della scheda e identificazione del microcontrollore.

Arduino, Language Reference — funzioni pinMode(), digitalRead(), digitalWrite() e millis().

Microchip Technology, documentazione tecnica sugli ingressi digitali, trigger di Schmitt e reti RC applicate ai microcontrollori AVR.

Documentazione tecnica dei produttori di pulsanti e interruttori relativa a contatti NO/NC, rimbalzo meccanico e durata elettrica dei contatti.