3.11. Debouncing#

Un interruttore viene rappresentato come un contatto perfettamente aperto o chiuso, ma i contatti di un interruttore reale non passano in modo netto tra i due stati. Essi rimbalzano – stabiliscono e interrompono il contatto elettrico molte volte nell’arco di pochi millisecondi prima di stabilizzarsi. Un ingresso GPIO che legge il pin lo percepisce come una raffica di fronti; un ciclo di polling incauto conta diverse «pressioni» per una sola pressione reale, e un gestore di interrupt viene eseguito più volte per ogni pressione effettiva.

An idealised scope trace showing a switch input signal. The signal starts high (open switch), drops low, bounces back and forth several times within a few milliseconds, then settles low (closed switch).

Un interruttore che rimbalza produce una raffica di transizioni rapide prima di stabilizzarsi.#

Il debouncing è la pratica di filtrare il rimbalzo in modo che ogni pressione fisica venga registrata come un singolo evento. Due approcci risolvono questo problema – software (una regola temporale nel firmware) o hardware (un piccolo filtro sul filo). Non si escludono a vicenda.

3.11.1. Debouncing software#

L’idea è ricordare quando l’ingresso è cambiato l’ultima volta e rifiutare ulteriori cambiamenti entro una breve finestra a partire da quel timestamp. Il rimbalzo dei contatti dura tipicamente meno di 10 ms; una pressione reale richiede 50 – 100 ms; una finestra di 30 – 50 ms cattura tutti i rimbalzi senza bloccare le pressioni reali.

In un ciclo di polling, leggi il pin, confrontalo con l’ultimo valore stabile e accetta un cambiamento solo dopo che è trascorsa la finestra di debounce:

import time
from machine import Pin

button = Pin("P0", Pin.IN, Pin.PULL_UP)
last_state  = 1
last_change = 0
DEBOUNCE_MS = 50

while True:
    now = time.ticks_ms()
    state = button.value()
    if state != last_state and time.ticks_diff(now, last_change) > DEBOUNCE_MS:
        last_change = now
        last_state = state
        if state == 0:
            do_action()
    time.sleep_ms(10)

Per le letture guidate da interrupt, applica la stessa regola temporale all’interno del gestore, quindi passa la pressione reale al contesto principale tramite micropython.schedule() (vedi Ingresso GPIO):

import time
import micropython
from machine import Pin

button = Pin("P0", Pin.IN, Pin.PULL_UP)
last_irq = 0
DEBOUNCE_MS = 50

def handle_press(pin):
    do_action()

def on_press(pin):
    global last_irq
    now = time.ticks_ms()
    if time.ticks_diff(now, last_irq) < DEBOUNCE_MS:
        return
    last_irq = now
    micropython.schedule(handle_press, pin)

button.irq(handler=on_press, trigger=Pin.IRQ_FALLING)

L’ISR filtra i rimbalzi tramite timestamp e accoda il callback; handle_press viene eseguito nuovamente nel contesto principale, dove l’allocazione e l’I/O lento sono sicuri.

3.11.2. Debouncing hardware#

Il debouncing hardware filtra il rimbalzo elettricamente, prima ancora che raggiunga il pin. Lo strumento standard è un condensatore.

Un condensatore è un componente a due terminali che immagazzina carica elettrica. Fisicamente, è costituito da due piastre conduttrici tenute a breve distanza l’una dall’altra, separate da un isolante (il dielettrico).

A capacitor drawn as two parallel horizontal plates with a dielectric (insulator) between them. A lead connects each plate to an external terminal -- A on top, B on bottom. Equal and opposite charges +Q and -Q accumulate on the two plates when a voltage V is applied across the terminals.

Un condensatore a piastre parallele: due conduttori separati da uno strato isolante.#

L’applicazione di una tensione attraverso i suoi terminali spinge cariche uguali e opposte sulle due piastre; la relazione è

Q = C × V

dove Q è la carica immagazzinata (coulomb), V è la tensione ai capi del condensatore e C è la sua capacità (farad). La capacità è fissata dalla costruzione del dispositivo; una maggiore capacità significa più carica immagazzinata alla stessa tensione.

La conseguenza: un condensatore non può cambiare la sua tensione istantaneamente. La carica che entra o esce deve attraversare qualunque resistenza si trovi nel percorso, e tale resistenza determina la velocità con cui la tensione può cambiare.

3.11.2.1. Costante di tempo RC#

Caricare un condensatore attraverso un resistore produce un aumento esponenziale graduale verso la tensione di alimentazione, non un gradino. Il tempo caratteristico di questo aumento è la costante di tempo RC:

τ = R × C

Dopo un τ, il condensatore ha raggiunto circa il 63 % della tensione di alimentazione. Dopo 5 τ, supera il 99 % – «completamente carico» a fini pratici.

A graph showing a capacitor's voltage rising along an exponential curve from 0 V toward the supply rail. The time τ = RC is marked on the x-axis where the curve reaches 63 % of the supply voltage.

Un condensatore si carica lungo una curva esponenziale. τ = RC è il tempo necessario per raggiungere il 63 % della tensione finale.#

La scarica attraverso un resistore segue l’immagine speculare: la tensione cala esponenzialmente dal suo valore iniziale verso zero, scendendo al 37 % della tensione di partenza dopo un τ, e sotto l’1 % dopo 5 τ.

A graph showing a capacitor's voltage falling along an exponential curve from Vmax toward 0 V. The time τ = RC is marked on the x-axis where the curve drops to 37 % of the starting voltage.

Un condensatore si scarica lungo un decadimento esponenziale. τ = RC è il tempo necessario per scendere al 37 % della tensione di partenza.#

3.11.2.2. Il circuito di debounce#

Un condensatore tra un pin di ingresso e la massa, alimentato attraverso un resistore in serie, forma un filtro passa-basso. I picchi rapidi non hanno il tempo di caricare o scaricare il condensatore attraverso quel resistore; il pin rimane vicino alla tensione che aveva prima del picco. I cambiamenti lenti – una pressione deliberata – caricano o scaricano il condensatore e la lettura li segue.

R1 tira il lato alto dell’interruttore verso Vcc, producendo un segnale grezzo dell’interruttore che rimbalza. R2 e C poi filtrano questo segnale in passa-basso verso il pin:

A switch input with hardware debouncing. Vcc connects through a 10 kΩ pull-up resistor down to a junction. That junction connects to ground through the switch on one branch, and through a 10 kΩ series resistor to the Pin on the other branch. A 100 nF capacitor between Pin and ground completes the low-pass filter.

Debouncing hardware: R2 e C filtrano in passa-basso il segnale grezzo dell’interruttore prima che raggiunga il pin.#

Valori tipici: R1 = 10 (pull-up), R2 = 10 (serie), C = 100 nF.

Quando l’interruttore è aperto, la corrente scorre Vcc → R1R2 → condensatore (in serie), caricando il condensatore a Vcc con τ_charge = (R1 + R2) × C = 2 ms.

Quando l’interruttore si chiude, il nodo dell’interruttore viene portato a massa e il condensatore si scarica attraverso il solo R2 verso tale massa con τ_discharge = R2 × C = 1 ms.

Entrambi i fronti sono filtrati RC. Poiché il condensatore si trova sul proprio nodo, a valle di R2 rispetto all’interruttore, oscilla in modo netto tra Vcc (aperto) e 0 V (chiuso) – nessuna corrente deve scorrere attraverso R1 a regime in nessuno dei due casi.

3.11.3. Come scegliere tra i due#

  • Il software è l’opzione predefinita. Non costa nulla in termini di componenti, la soglia è facile da regolare e funziona su qualsiasi pin che la CPU legge.

  • L’hardware vale i componenti aggiuntivi quando il rimbalzo raggiunge qualcosa di diverso dal codice di polling della CPU – un interrupt che non deve attivarsi due volte, un contatore hardware, una periferica priva di un proprio filtro.

Il debouncing software e quello hardware coesistono anche pacificamente: un piccolo filtro RC sopprime i picchi peggiori, e una finestra di debounce software copre ciò che resta.