Lezione 15
🎓 PIC16 — From Zero to Hero · Lezione 15 · Modulo 2 — Progetto Finale
Progetto I/O Completo — Cronometro FSM ⏱
Pulsanti, display multiplexato e macchina a stati finiti in un unico sistema.
Il primo firmware embedded completo: cronometro 00–59 con Start/Stop e Reset.
🏁 Questa lezione conclude il Modulo 2 — I/O Digitale. Integra tutto ciò che hai imparato nelle lezioni 11–14 in un sistema funzionante completo.
🎯 Cosa impari in questa lezione
- Progettare un sistema I/O completo con schema hardware
- Implementare una FSM a 3 stati: IDLE, RUNNING, PAUSED
- Integrare debounce, multiplexing e temporizzazione in un loop unico
- Debounce non-bloccante: contatore di campionamento invece di delay fisso
- Gestire due pulsanti con azioni diverse (Start/Stop e Reset)
- Strutturare il codice in subroutine coese con responsabilità distinte
- Il pattern time-slicing per sistemi embedded senza RTOS
📋 Scheda lezione
Lezione: 15 / 42
Modulo: 2 — I/O Digitale
Device: PIC16F84A
Prerequisiti: Lez. 11–14
Difficoltà: ★★★★☆
Istruzioni nuove: — (integrazione)
Pattern chiave: FSM, time-slicing
1 · Specifica e hardware
Il sistema implementa un cronometro 00–59 con due pulsanti. RB0–RB6 → segmenti a–g dei display. RA0 → abilita DIG0 (unità). RA1 → abilita DIG1 (decine). RA2 → BTN Start/Stop (pull-up, logica invertita). RA3 → BTN Reset (pull-up). Comportamento: BTN_SS alterna tra avvio, pausa e ripresa; BTN_RST azzera sempre il contatore.
2 · La macchina a stati finiti (FSM)
Una FSM ha un numero finito di stati, transizioni causate da eventi e azioni associate. In memoria occupa una sola variabile SYS_STATE con tre valori: 0=IDLE, 1=RUNNING, 2=PAUSED. Garantisce che il sistema sia sempre in uno stato ben definito — non possono esistere combinazioni illegali come con flag separati.
3 · Architettura: loop a time-slicing
Senza sistema operativo, ogni task riceve “fette di tempo” nel loop principale. Ogni ciclo dura ~10ms (dominato dalla scansione display 2×5ms). Il contatore dei secondi conta 100 cicli = 1 secondo. Il debounce non-bloccante campiona i pulsanti ogni ciclo invece di fermare tutto con un delay.
Debounce non-bloccante: invece del delay fisso da 20ms della Lezione 12, un contatore viene incrementato ogni ciclo (10ms) finché il pulsante è premuto. A 2 campioni stabili (20ms) scatta l’evento. Il display non si ferma mai.
4 · Codice completo — Cronometro 00–59
; ============================================================ ; Lezione 15 — Cronometro 00-59, macchina a stati ; PIC16F84A @ 4 MHz ; ; Hardware: ; RB0-RB6 → segmenti a-g (Common Cathode) ; RA0 → abilita DIG0 (unità) ; RA1 → abilita DIG1 (decine) ; RA2 → BTN_SS (Start/Stop, pull-up, 0=premuto) ; RA3 → BTN_RST (Reset, pull-up, 0=premuto) ; ; Architettura: ; Loop ≈ 10ms (scan display 2×5ms) ; Secondi: 100 cicli da 10ms ; Debounce: 2 campioni stabili × 10ms = 20ms ; FSM: IDLE=0, RUNNING=1, PAUSED=2 ; ============================================================ LIST P=16F84A #INCLUDE <p16f84a.inc> __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF ; --- Costanti stato FSM --- #DEFINE ST_IDLE 0x00 #DEFINE ST_RUNNING 0x01 #DEFINE ST_PAUSED 0x02 CBLOCK 0x0C SYS_STATE ; stato FSM (ST_IDLE/ST_RUNNING/ST_PAUSED) SECONDS ; valore corrente 0-59 DIGIT0 ; cifra unità (0-9) DIGIT1 ; cifra decine (0-5) SEC_CNT ; contatore cicli per 1 secondo (0-99) BTN_SS_CNT ; debounce counter BTN_SS BTN_RST_CNT ; debounce counter BTN_RST BTN_SS_PREV ; stato precedente BTN_SS (per edge detect) BTN_RST_PREV ; stato precedente BTN_RST TEMP D_LO ENDC ORG 0x0000 GOTO INIT ; ============================================================ ; INIT ; ============================================================ ORG 0x0005 INIT BSF STATUS,RP0 CLRF TRISB ; RB0-RB7 output (segmenti + abilitazioni) MOVLW 0b00001100 ; RA2=input(BTN_SS), RA3=input(BTN_RST) MOVWF TRISA ; RA0,RA1 output (abilitazioni display) BCF STATUS,RP0 CLRF PORTA CLRF PORTB ; Azzera tutte le variabili CLRF SYS_STATE CLRF SECONDS CLRF DIGIT0 CLRF DIGIT1 MOVLW D'100' MOVWF SEC_CNT CLRF BTN_SS_CNT CLRF BTN_RST_CNT MOVLW 0xFF MOVWF BTN_SS_PREV ; FF = non premuto (pull-up) MOVWF BTN_RST_PREV ; ============================================================ ; MAIN LOOP — ciclo ≈ 10ms ; ============================================================ MAIN_LOOP ; ---- TASK 1: Scan display (2 × 5ms = 10ms) ---- CALL SCAN_DISPLAY ; ---- TASK 2: Leggi e debounce pulsanti ---- CALL READ_BUTTONS ; ---- TASK 3: Aggiorna FSM ---- CALL UPDATE_FSM ; ---- TASK 4: Conta secondi (solo se RUNNING) ---- MOVF SYS_STATE,W SUBLW ST_RUNNING BTFSS STATUS,Z ; skip se RUNNING GOTO SKIP_COUNT CALL COUNT_SECONDS SKIP_COUNT ; ---- TASK 5: Aggiorna digit display ---- CALL SPLIT_DIGITS GOTO MAIN_LOOP ; ============================================================ ; SCAN_DISPLAY: mostra DIG0 per 5ms, poi DIG1 per 5ms ; ============================================================ SCAN_DISPLAY ; DIG0 (unità) CLRF PORTA CLRF PORTB MOVF DIGIT0,W CALL SEG_TABLE MOVWF PORTB BSF PORTA,0 BCF PORTA,1 CALL DELAY_5MS ; blanking CLRF PORTA CLRF PORTB ; DIG1 (decine) MOVF DIGIT1,W CALL SEG_TABLE MOVWF PORTB BCF PORTA,0 BSF PORTA,1 CALL DELAY_5MS CLRF PORTA CLRF PORTB RETURN ; ============================================================ ; READ_BUTTONS: debounce non-bloccante per BTN_SS e BTN_RST ; ; Algoritmo per ogni pulsante: ; Se pin=0 (premuto): incrementa contatore debounce ; Se pin=1 (libero): azzera contatore debounce ; Se contatore >= 2: pulsante stabile → setta flag evento ; ; Risultato: BTN_SS_PREV e BTN_RST_PREV valgono 0 solo ; quando il pulsante è stabile per 2 cicli (20ms) ; ============================================================ READ_BUTTONS ; --- BTN_SS (RA2) --- BTFSC PORTA,2 ; skip se RA2=0 (premuto) GOTO BTN_SS_FREE ; RA2 = 0: pulsante premuto INCF BTN_SS_CNT,F MOVF BTN_SS_CNT,W SUBLW D'2' ; == 2? BTFSS STATUS,Z GOTO BTN_SS_DONE ; stabile per 20ms: registra evento (BTN_SS_PREV = 0) CLRF BTN_SS_PREV GOTO BTN_SS_DONE BTN_SS_FREE CLRF BTN_SS_CNT MOVLW 0xFF MOVWF BTN_SS_PREV ; pulsante libero BTN_SS_DONE ; --- BTN_RST (RA3) --- BTFSC PORTA,3 GOTO BTN_RST_FREE INCF BTN_RST_CNT,F MOVF BTN_RST_CNT,W SUBLW D'2' BTFSS STATUS,Z GOTO BTN_RST_DONE CLRF BTN_RST_PREV GOTO BTN_RST_DONE BTN_RST_FREE CLRF BTN_RST_CNT MOVLW 0xFF MOVWF BTN_RST_PREV BTN_RST_DONE RETURN ; ============================================================ ; UPDATE_FSM: gestisce transizioni di stato ; Legge BTN_SS_PREV e BTN_RST_PREV (0=evento confermato) ; ============================================================ UPDATE_FSM ; --- BTN_RST premuto? → sempre IDLE --- MOVF BTN_RST_PREV,W BTFSS STATUS,Z ; W==0 → evento RST GOTO CHECK_SS ; Reset: torna a IDLE, azzera SECONDS CLRF SYS_STATE CLRF SECONDS MOVLW D'100' MOVWF SEC_CNT RETURN CHECK_SS ; --- BTN_SS premuto? --- MOVF BTN_SS_PREV,W BTFSS STATUS,Z RETURN ; nessun evento SS → esci ; BTN_SS evento: comportamento dipende dallo stato MOVF SYS_STATE,W ; IDLE → RUNNING SUBLW ST_IDLE BTFSS STATUS,Z GOTO SS_NOT_IDLE MOVLW ST_RUNNING MOVWF SYS_STATE RETURN SS_NOT_IDLE MOVF SYS_STATE,W ; RUNNING → PAUSED SUBLW ST_RUNNING BTFSS STATUS,Z GOTO SS_NOT_RUNNING MOVLW ST_PAUSED MOVWF SYS_STATE RETURN SS_NOT_RUNNING ; PAUSED → RUNNING (unica possibilità rimasta) MOVLW ST_RUNNING MOVWF SYS_STATE RETURN ; ============================================================ ; COUNT_SECONDS: incrementa SECONDS ogni 100 cicli (1 secondo) ; ============================================================ COUNT_SECONDS DECFSZ SEC_CNT,F RETURN ; non ancora 1 secondo MOVLW D'100' MOVWF SEC_CNT ; ricarica INCF SECONDS,F MOVF SECONDS,W SUBLW D'60' ; == 60? BTFSS STATUS,Z RETURN CLRF SECONDS ; dopo 59 → 00 RETURN ; ============================================================ ; SPLIT_DIGITS: SECONDS → DIGIT0 (unità) + DIGIT1 (decine) ; Sottrazione ripetuta (identica alla Lezione 13) ; ============================================================ SPLIT_DIGITS MOVF SECONDS,W MOVWF TEMP CLRF DIGIT1 SD_LOOP MOVLW D'10' SUBWF TEMP,F BTFSS STATUS,C GOTO SD_DONE INCF DIGIT1,F GOTO SD_LOOP SD_DONE MOVLW D'10' ADDWF TEMP,F MOVF TEMP,W MOVWF DIGIT0 RETURN ; ============================================================ ; SEG_TABLE — codifica cifre 0-9, Common Cathode ; ============================================================ SEG_TABLE ADDWF PCL,F RETLW 0x3F RETLW 0x06 RETLW 0x5B RETLW 0x4F RETLW 0x66 RETLW 0x6D RETLW 0x7D RETLW 0x07 RETLW 0x7F RETLW 0x6F ; ============================================================ ; DELAY_5MS ≈ 5ms @ 4 MHz (7 × 256 × 3 cicli) ; ============================================================ DELAY_5MS MOVLW D'7' MOVWF TEMP D5O CLRF D_LO D5I DECFSZ D_LO,F GOTO D5I DECFSZ TEMP,F GOTO D5O RETURN END
5 · Riepilogo Modulo 2
Lez. 11–12 — Pulsanti
TRIS/PORT, shadow register, read-modify-write, debounce attesa fissa, macchina a 3 stati pulsante.
Lez. 13–14 — Display
Lookup table RETLW, multiplexing 7 segmenti, framebuffer in RAM, FSR/INDF indirizzamento indiretto.
Lez. 15 — Integrazione
FSM a stati espliciti, time-slicing senza RTOS, debounce non-bloccante, struttura firmware reale.
⚠ Bug comuni: dimenticare di azzerare BTN_SS_CNT su rilascio · invertire BTFSS/BTFSC sul pin · non azzerare SEC_CNT durante il Reset · SEG_TABLE che attraversa boundary di pagina.
🧪 Prova tu
- Traccia FSM: metti un breakpoint su
MOVWF SYS_STATEe verifica che i valori scritti corrispondano al diagramma (0→1→2→1→0). - Misura 1 secondo: conta quante volte viene chiamata SCAN_DISPLAY prima che SECONDS incrementi. Deve essere ~100. Perché potrebbe non essere esatto?
- Sfida — Conto alla rovescia: modifica COUNT_SECONDS per contare da 59 a 00. Usa
DECF SECONDS,Fe azzera a 59 quando SECONDS va in underflow a 0xFF. - Sfida avanzata — Lampeggio in PAUSED: ogni 50 cicli inverti un flag e, se il flag è 1, manda 0x00 a PORTB prima di SCAN_DISPLAY per spegnere il display brevemente.
❓ Quiz rapido
1. Perché il debounce non-bloccante usa un contatore invece di un delay?
Mostra risposta
Un delay fisso blocca il PIC per 20ms: il display smette di aggiornarsi e altri pulsanti non vengono letti. Con il contatore, READ_BUTTONS dura solo ~10 istruzioni per ciclo, e il debounce viene distribuito nel tempo sfruttando il loop da 10ms come “orologio” naturale.
2. Cosa succederebbe se SYS_STATE assumesse un valore illegale (es. 3)?
Mostra risposta
In UPDATE_FSM, dopo aver escluso ST_IDLE e ST_RUNNING, il codice cade in SS_NOT_RUNNING che assegna ST_RUNNING senza verificare che lo stato fosse davvero ST_PAUSED. Con STATE=3, BTN_SS farebbe passare a RUNNING — non è un crash ma un comportamento inatteso. Per robustezza si dovrebbe aggiungere un controllo esplicito o un salto a INIT per stati illegali (defensive programming).
3. Il sistema può perdere una pressione di pulsante durante DELAY_5MS?
Mostra risposta
Sì, in teoria: se il pulsante viene premuto e rilasciato interamente durante i 5ms del delay, l’evento non viene rilevato. In pratica il debounce richiede 20ms di stabilità, quindi un impulso più breve di 20ms non sarebbe considerato valido comunque. Il vero problema sarebbe se il bounce durasse più di 5ms — raro con contatti di qualità. Per sistemi critici si usano interrupt (Modulo 4).
4. Come si estenderebbe per contare anche i minuti (MM:SS)?
Mostra risposta
Aggiungere una variabile MINUTES (0–59) e altri due pin di abilitazione per due display aggiuntivi. SCAN_DISPLAY diventa 4 cifre × 2.5ms = 10ms (stesso periodo). COUNT_SECONDS, quando SECONDS raggiunge 60, azzera SECONDS e incrementa MINUTES. SPLIT_DIGITS viene chiamata per entrambe le coppie. La FSM rimane invariata. Unica complicazione hardware: servono 4 pin di abilitazione ma RA4 è open-drain — con un transistor NPN su RA4 si può usare.
Prossima lezione → Modulo 3
Lezione 16 — Il Timer0
Il PIC ha un timer hardware che conta da solo. Come configurarlo con OPTION_REG, calcolare la precarica e rilevare l’overflow con T0IF — senza loop di delay.
© Prof. D. Bertolino · PIC16 From Zero to Hero · WebPicSimulator · Lezione 15