Lezione 16 di 43

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.

IDLE STATE=0 display fermo RUNNING STATE=1 conta secondi PAUSED STATE=2 display fermo BTN_SS ↓ BTN_SS ↓ BTN_SS ↓ BTN_RST ↓ = BTN_RST (da qualsiasi stato)

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.

1. Scan display DIG0 5ms DIG1 5ms 2. Leggi pulsanti debounce edge detect (non-blocking) 3. Aggiorna FSM transizioni di stato 4. Conta secondi solo se RUNNING 100×10ms=1s 5. Aggiorna display SPLIT_DIGITS → D0, D1 ciclo ≈ 10ms

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

  1. Traccia FSM: metti un breakpoint su MOVWF SYS_STATE e verifica che i valori scritti corrispondano al diagramma (0→1→2→1→0).
  2. Misura 1 secondo: conta quante volte viene chiamata SCAN_DISPLAY prima che SECONDS incrementi. Deve essere ~100. Perché potrebbe non essere esatto?
  3. Sfida — Conto alla rovescia: modifica COUNT_SECONDS per contare da 59 a 00. Usa DECF SECONDS,F e azzera a 59 quando SECONDS va in underflow a 0xFF.
  4. 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