Lezione 13 di 43

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

Modulo2 — I/O Digitale
Lezione12 di 42
Livello⭐⭐ Principiante+
Tempo~40 minuti
Tecnica chiaveDelay software + macchina a stati
PrerequisitiLezione 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.

Segnale su RB7 durante la pressione di un pulsante (con pull-up) 1 0 tempo → ideale (senza rimbalzo) BOUNCE 5–20 ms decine di transizioni spurie stabile a 0 (premuto) rilasciato premuto e stabile
Figura 1 — Segnale reale su un pin con pull-up durante la pressione. Il PIC a 4 MHz vede ogni 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).

Loop delay a singolo contatore — analisi cicli DELAY_MS: MOVLW N MOVWF COUNT LOOP: DECFSZ COUNT, F ; 1 ciclo (2 se skip) GOTO LOOP ; 2 cicli Analisi cicli (N iterazioni) MOVLW + MOVWF = 2 cicli (1 volta) N-1 iterazioni: DECFSZ(1) + GOTO(2) = 3 cicli × (N-1) ultima iter.: DECFSZ skip = 2 cicli Totale ≈ 3N cicli  →  a 4 MHz: T = 3N × 1µs = 3N µs
Figura 2 — Il loop DECFSZ+GOTO a singolo contatore vale ≈ 3N µs a 4 MHz. Per N=256: 768 µs ≈ 0.77 ms.

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.

Delay a doppio loop annidato — formula e calcolo DELAY_20MS: MOVLW D_HI ; contatore esterno MOVWF DLY_HI OUTER: MOVLW D_LO ; reset loop interno MOVWF DLY_LO INNER: DECFSZ DLY_LO, F GOTO INNER DECFSZ DLY_HI, F GOTO OUTER Formula approssimata T ≈ D_HI × D_LO × 3 µs Esempi pratici a 4 MHz: D_HI=7, D_LO=238 → ≈ 5 ms D_HI=14, D_LO=238 → ≈ 10 ms ← debounce D_HI=28, D_LO=238 → ≈ 20 ms ← debounce sicuro
Figura 3 — Delay doppio loop: T ≈ D_HI × D_LO × 3 µs. Valori concreti per debounce da 10–20 ms.

; 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 per attesa fissa — flowchart polling loop pin=0 CALL DELAY_20MS attendi 20 ms pin=0? (ancora?) sì pressione confermata! no → era rimbalzo, ignora aspetta rilascio (pin torna a 1)
Figura 4 — Debounce per attesa fissa: rileva primo fronte, aspetta 20 ms, riconferma. Se il pin è ancora basso, la pressione è reale.

; 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.

Macchina a 3 stati per debounce non-bloccante STATO 0 IDLE (attesa) STATO 1 DEBOUNCING (conta decrementa) STATO 2 PREMUTO (stabile) pin=0 (carica conta) pin=1 (rimbalzo) conta=0 (confermato) pin=1 (rilascio) pin=1 → rilasciato → torna idle
Figura 5 — Macchina a 3 stati: IDLE → DEBOUNCING (conta cicli) → PREMUTO. Il rilascio riporta a IDLE. Nessun delay bloccante.

💻 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!

  1. 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.
  2. 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.
  3. 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