Lezione 12
🎓 PIC16 — From Zero to Hero · Lezione 12 · Modulo 2
Lettura pulsanti e debounce ⏱️
Il rimbalzo meccanico dei pulsanti genera decine di transizioni spurie in pochi millisecondi.
Come calcolare un delay software preciso a 4 MHz e come costruire un debounce robusto in assembly.
🎯 Cosa impari in questa lezione
- Cos’è il rimbalzo meccanico di un pulsante e perché il PIC lo vede come centinaia di pressioni
- Come calcolare la durata di un loop software in µs a 4 MHz
- Il pattern delay a doppio loop: formula cicli e come scegliere i valori
- Debounce per pressione: rileva solo il fronte di discesa, ignora i rimbalzi
- Debounce per rilascio: aspetta che il pin si stabilizzi a 1 prima di riabilitare
- Pattern completo press/release con stato macchina a 2 stati
- Come osservare il timing nel simulatore con la modalità Step
📋 Scheda lezione
| Modulo | 2 — I/O Digitale |
| Lezione | 12 di 42 |
| Livello | ⭐⭐ Principiante+ |
| Tempo | ~40 minuti |
| Tecnica chiave | Delay software + macchina a stati |
| Prerequisiti | Lezione 11 (PORTB, pull-up, BTFSC/BTFSS) |
🧠 Il problema: il rimbalzo meccanico
Un pulsante meccanico non passa istantaneamente da aperto a chiuso. I contatti metallici rimbalzano per 5–20 ms prima di stabilizzarsi. Durante questo tempo il pin alterna rapidamente tra 0 e 1 decine di volte. Il PIC16 a 4 MHz esegue un’istruzione ogni microsecondo: in 20 ms esegue 20.000 istruzioni. Se il tuo loop di polling è veloce, vedrà ogni singolo rimbalzo come una pressione separata.
⚠️ Caso concreto: il tuo programma incrementa un contatore ad ogni pressione. Con rimbalzo, una singola pressione fisica può incrementare il contatore di 15 o 20. Il bug sembra casuale e irriproducibile — ogni pressione conta un numero diverso, dipendente dalla velocità meccanica del dito. Il debounce elimina completamente questo problema.
⏱️ Fondamento: calcolare un delay software a 4 MHz
Prima di costruire il debounce serve capire come calcolare un ritardo preciso. A 4 MHz il PIC16 esegue 1 milione di istruzioni al secondo: ogni istruzione dura 1 µs (tranne GOTO, CALL, RETURN, DECFSZ-skip che durano 2 µs).
Un singolo contatore a 8 bit arriva a massimo 256×3 µs ≈ 0.77 ms. Per un debounce servono almeno 10–20 ms: servono due contatori annidati.
; Subroutine delay ≈ 10 ms @ 4 MHz
; Formula: 14 × 238 × 3µs = 10.01 ms
DLY_HI EQU 0x0C
DLY_LO EQU 0x0D
DELAY_10MS:
MOVLW .14 ; D_HI = 14
MOVWF DLY_HI
DOUT:
MOVLW .238 ; D_LO = 238
MOVWF DLY_LO
DIN:
DECFSZ DLY_LO, F ; 1 ciclo (2 se ultimo)
GOTO DIN ; 2 cicli
DECFSZ DLY_HI, F ; 1 ciclo (2 se ultimo)
GOTO DOUT ; 2 cicli
RETURN ; 2 cicli
💡 Notazione decimale: .14 e .238 con il punto davanti indicano la base decimale nell’assembler MPASM. Il simulatore accetta sia .14 sia 0x0E sia d'14'. Senza prefisso, l’assembler del simulatore interpreta i numeri come decimali per default.
🔧 Tecniche di debounce software
Tecnica 1 — Attesa fissa (semplice ma efficace)
Al primo fronte di discesa (pin va a 0), aspetta 20 ms e poi rileggi. Se il pin è ancora a 0, la pressione è confermata. È la tecnica più semplice e funziona bene per la maggior parte dei pulsanti.
; Debounce tecnica 1: attesa fissa 20 ms
; RB7 = pulsante con pull-up (premuto = 0)
MAIN:
; Polling: aspetta fronte di discesa su RB7
BTFSC PORTB, 7 ; salta se RB7=0 (premuto)
GOTO MAIN ; RB7=1 → rilasciato → riprova
; Fronte rilevato! Aspetta fine rimbalzo
CALL DELAY_20MS ; 20 ms — più lungo del rimbalzo peggiore
; Riconferma: pin ancora basso?
BTFSS PORTB, 7 ; salta se RB7=1 (era rumore, ignora)
GOTO PREMUTO ; RB7=0 → confermato!
GOTO MAIN ; falso positivo, ritorna al polling
PREMUTO:
; — Esegui azione UNA SOLA VOLTA —
CALL AZIONE ; incrementa, toggle, ecc.
; Aspetta rilascio (evita ripetizioni)
ATTESA_RILASCIO:
BTFSS PORTB, 7 ; salta se RB7=1 (rilasciato)
GOTO ATTESA_RILASCIO ; ancora premuto, aspetta
CALL DELAY_20MS ; debounce anche sul rilascio
GOTO MAIN ; pronto per prossima pressione
Tecnica 2 — Macchina a stati (più flessibile)
La tecnica 1 blocca il programma durante il delay. Se hai altro lavoro da fare nel frattempo (aggiornare display, controllare altri pin), serve una macchina a stati che non usi CALL con delay bloccanti ma gestisca il debounce “di lato” ad ogni ciclo del main loop.
💻 Programma completo — contatore con debounce
Un contatore a 4 bit (0–15) mostrato su RB0–RB3. Ogni pressione confermata di RB7 incrementa il contatore. Il valore viene mostrato sui LED tramite shadow register. Usa la tecnica 1 (attesa fissa) che è la più leggibile per iniziare.
; ============================================================
; Lezione 12 — Contatore con debounce @ 4 MHz
; RB0–RB3: display contatore 0–15 (LED)
; RB7: pulsante + (pull-up, premuto=0)
; ============================================================
LIST p=16F84A
#include <p16f84a.inc>
CBLOCK 0x0C
CONTA ; → 0x0C: valore corrente 0–15
PB_SHADOW ; → 0x0D: shadow PORTB
DLY_HI ; → 0x0E: delay esterno
DLY_LO ; → 0x0F: delay interno
ENDC
BTN EQU 7 ; RB7 = pulsante
MASK4 EQU 0x0F ; maschera 4 bit bassi
ORG 0x0000
GOTO START
ORG 0x0004
RETFIE
START:
BSF STATUS, RP0 ; Bank 1
MOVLW 0x80 ; RB7=input, RB0-6=output
MOVWF TRISB
CLRF TRISA
BCF OPTION_REG, 7 ; RBPU=0 → pull-up abilitato su RB7
BCF STATUS, RP0 ; Bank 0
CLRF CONTA
CLRF PB_SHADOW
CLRF PORTB
MAIN:
; Attende fronte di discesa RB7
BTFSC PORTB, BTN ; salta se RB7=0 (premuto)
GOTO MAIN ; ancora rilasciato
; Debounce: attesa 20 ms
CALL DELAY_20MS
; Riconferma pressione
BTFSS PORTB, BTN ; salta se RB7=1 (era rumore)
CALL INCREMENTA ; confermato! incrementa
; Attende rilascio + debounce
WAIT_REL:
BTFSS PORTB, BTN
GOTO WAIT_REL ; ancora premuto
CALL DELAY_20MS ; debounce rilascio
GOTO MAIN
; — Incrementa contatore e aggiorna LED —
INCREMENTA:
INCF CONTA, F ; CONTA++
MOVLW MASK4 ; 0x0F
ANDWF CONTA, F ; mantieni solo 4 bit (0–15, poi torna a 0)
; Aggiorna LED via shadow (mantieni bit alti intatti)
MOVLW 0xF0 ; maschera bit alti shadow
ANDWF PB_SHADOW, W ; W = bit 4-7 del shadow
IORWF CONTA, W ; OR con i 4 bit bassi del contatore
MOVWF PB_SHADOW ; aggiorna shadow
MOVWF PORTB ; applica ai LED
RETURN
; — Delay ≈ 20 ms @ 4 MHz —
; Formula: 28 × 238 × 3µs ≈ 20 ms
DELAY_20MS:
MOVLW .28
MOVWF DLY_HI
D_OUT:
MOVLW .238
MOVWF DLY_LO
D_IN:
DECFSZ DLY_LO, F
GOTO D_IN
DECFSZ DLY_HI, F
GOTO D_OUT
RETURN
END
💡 Nel simulatore — usa la modalità Step: durante il DELAY_20MS il PC cicla migliaia di volte tra D_IN e D_OUT: non usare Run continuo oppure imposta la velocità bassa (Animate). Per testare il debounce, clicca rapidamente il checkbox di RB7 durante il delay: il programma deve ignorare i click successivi finché non torna al polling in MAIN. Nel simulatore i “rimbalzi” fisici non esistono, ma puoi simulare click multipli rapidi per verificare che il debounce funzioni.
🏋️ Prova tu!
- Carica il programma completo e verifica che il contatore incrementi di esattamente 1 ad ogni click di RB7. Senza debounce (togli le CALL DELAY_20MS) quante volte incrementa per click? Conta la differenza tra i due comportamenti.
- Modifica il delay per ottenere esattamente 5 ms. Usa la formula T ≈ D_HI × D_LO × 3 µs. Quali valori scegli per D_HI e D_LO? Verifica che D_HI e D_LO siano entrambi ≤ 255.
- Sfida: aggiungi un secondo pulsante su RB6 che decrementa il contatore. Usa lo stesso delay per il debounce. Gestisci il caso CONTA=0: la decrementazione deve fermarsi a 0 (oppure ciclare a 15, a tua scelta). Usa BTFSC STATUS,Z dopo DECF per rilevare lo zero.
❓ Quiz rapido
1. A 4 MHz, quanti µs dura un GOTO? E un DECFSZ quando non salta?
Mostra risposta
GOTO = 2 cicli macchina = 2 µs (è un’istruzione che cambia il PC, come CALL/RETURN). DECFSZ quando non salta = 1 ciclo = 1 µs. DECFSZ quando salta (ultimo conteggio, risultato = 0) = 2 cicli = 2 µs, per la stessa ragione del GOTO: deve “consumare” il prossimo opcode come NOP per aggiornare il PC.
2. Con D_HI=20 e D_LO=167, quanto dura il delay in ms? (usa la formula T ≈ D_HI × D_LO × 3 µs)
Mostra risposta
T ≈ 20 × 167 × 3 µs = 10.020 µs = 10,02 ms. È un ottimo valore di debounce, leggermente più preciso di quello usato nel programma (28×238 ≈ 20 ms). Il 3 µs per iterazione è un’approssimazione: l’ultima iterazione del loop interno vale 4 cicli invece di 3 (DECFSZ skip=2 + non esegue GOTO), quindi il delay reale è leggermente più corto, ma l’errore è trascurabile per il debounce.
3. Perché il debounce va fatto anche sul rilascio del pulsante, non solo sulla pressione?
Mostra risposta
Per lo stesso motivo meccanico: quando il pulsante viene rilasciato, i contatti rimbalzano di nuovo generando transizioni spurie da 0 a 1. Senza debounce sul rilascio, il programma potrebbe rilevare il fronte di salita come nuova “pressione” nella successiva iterazione del polling, oppure uscire da WAIT_REL troppo presto durante un rimbalzo e immediatamente rilevare un nuovo fronte di discesa nella stessa pressione fisica, contando 2 pressioni invece di 1.
4. La tecnica 1 (attesa fissa) “blocca” il programma per 20 ms. In quali casi questo è un problema? Quando invece non lo è?
Mostra risposta
È un problema quando il programma deve fare altro durante quei 20 ms: aggiornare un display multiplexato (richiede refresh ogni ~2 ms), generare PWM, controllare un secondo pulsante in parallelo, o rispondere a eventi veloci. In questi casi serve la tecnica a macchina a stati o l’uso di interrupt. Non è un problema se il programma ha solo la funzione di leggere quel pulsante: contatori, semafori, menu di configurazione. Per applicazioni semplici come questa lezione, la tecnica 1 è perfetta e molto più leggibile.
Prossima lezione →
Lezione 13 — Display a 7 segmenti
Come pilotare un display a 7 segmenti con lookup table.
Multiplexing di più cifre con delay e aggiornamento continuo.
© Prof. D. Bertolino · PIC16 From Zero to Hero · WebPicSimulator · Lezione 12