Lezione 25 di 43

Lezione 24

🎓 PIC16 — From Zero to Hero · Lezione 24 · Modulo 4

Progetto — Menu di configurazione con EEPROM 🎛️

Un sistema completo: pulsanti debounced, display 7 segmenti multiplexato,
tre parametri configurabili e autosave in EEPROM dopo 3 secondi di inattività.

🎯 Cosa impari in questa lezione

  • Integrare tutto il Modulo 4 in un singolo sistema coerente: ISR, debounce, EEPROM, display mux
  • Progettare un menu a parametri: navigazione con un pulsante, modifica con un altro
  • Pattern autosave: un timer countdown in ISR che scade dopo N secondi di inattività e segnala al main di salvare
  • Codifica 7 segmenti con tabella RETLW e multiplexing a due digit
  • Separazione chiara tra struttura dati (CFG_*), stato UI (CURRENT_*) e stato hardware (DISP_*)
  • Tecnica di wrap-around con SUBLW + carry per far rientrare i valori nel range
  • Leggere le conseguenze architetturali di ogni scelta progettuale

📋 Scheda lezione

Lezione: 24 / 42
Modulo: 4 — Progetto conclusivo
Device: PIC16F84A
Prerequisiti: Lez. 18–23 (tutto Mod. 4)
Difficoltà: ★★★★★
Linee codice: ~380 (programma completo)
Tecnica: autosave + mux + debounce + EEPROM


1 · Panoramica del sistema

Il progetto implementa un menu a tre parametri — BRIGHTNESS (0–7), SPEED (0–3), MODE (0–2) — navigabile con due pulsanti e visualizzato su un display 7 segmenti a due digit: il digit sinistro mostra il numero del parametro selezionato (0, 1 o 2), quello destro il suo valore corrente. Il salvataggio in EEPROM è automatico: un timer da 3 secondi riparte ad ogni modifica e scatta solo quando l’utente smette di interagire.

Architettura del menu — flusso dati e interazioni tra moduli Pulsanti RB0 = MENU RB1 = INC ISR (TMR0) debounce mux display FLAGS + TIMER FLAG_BTN_MENU FLAG_BTN_INC SAVE_TIMER SAVE_PENDING MAIN loop ON_MENU ON_INC EEPROM SAVE_CONFIG LOAD_CONFIG autosave: dopo 3s di inattività SAVE_TIMER scade → SAVE_PENDING → SAVE_CONFIG Display mux DISP_LEFT DISP_RIGHT

2 · Layout memoria e display

Layout EEPROM e mappa RAM del progetto EEPROM (0x00–0x3F) 0x00 MAGIC 0x01 BRIG. 0x02 SPEED 0x03 MODE 0x04… 0x05–0x3F — liberi per usi futuri BRIGHTNESS: 0–7 (3 bit) SPEED: 0–3 (2 bit) MODE: 0–2 (2 bit) RAM (0x0C–0x1D) 0x0C W_SAVE / STATUS_SAVE 0x0E CFG_BRIGHTNESS / SPEED / MODE 0x11 CURRENT_PARAM / CURRENT_VAL 0x13 SAVE_TIMER_HI / LO 0x15 SAVE_PENDING 0x16 DISP_LEFT / RIGHT / MUX_PHASE 0x19 DEBOUNCE_CNT / FLAGS Display 7 segmenti — codici RETLW e multiplexing su PORTA PARAM (RA4=0) es. “2” VALUE (RA4=1) es. “0” Multiplexing (ISR ogni ~2ms) PARAM ~2ms VALUE ~2ms PARAM ~2ms VALUE ~2ms Persistenza visiva: 50Hz (20ms) → nessuno sfarfallio percepibile RA4 seleziona il digit attivo, PORTA[3:0] i segmenti

3 · Codice completo

Il programma (~380 righe) è strutturato in sezioni indipendenti: vettori, ISR, INIT, MAIN loop, handler pulsanti, routine display, routine EEPROM. Ogni sezione ha una responsabilità unica e comunica con le altre solo tramite variabili in RAM — nessuna dipendenza diretta tra ISR e logica applicativa.

; ============================================================
; Lezione 24 — Menu configurazione con EEPROM
; Parametri: BRIGHTNESS (0-7), SPEED (0-3), MODE (0-2)
; Pulsanti: RB0=MENU (cambia parametro), RB1=INC (incrementa)
; Display: 7 segmenti multiplexato su PORTA (2 cifre)
; Salvataggio automatico in EEPROM dopo 3s di inattività
; PIC16F84A @ 4 MHz
; ============================================================
    LIST    P=16F84A
    #INCLUDE <p16f84a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF

; --- Layout EEPROM ---
#DEFINE EE_MAGIC_ADDR   0x00   ; sentinella
#DEFINE EE_MAGIC_VAL    0x5A
#DEFINE EE_BRIGHTNESS   0x01
#DEFINE EE_SPEED        0x02
#DEFINE EE_MODE         0x03

; --- Limiti parametri ---
#DEFINE MAX_BRIGHTNESS  D'8'   ; 0-7
#DEFINE MAX_SPEED       D'4'   ; 0-3
#DEFINE MAX_MODE        D'3'   ; 0-2

; --- Parametri menu ---
#DEFINE PARAM_BRIGHTNESS 0x00
#DEFINE PARAM_SPEED      0x01
#DEFINE PARAM_MODE       0x02
#DEFINE PARAM_COUNT      0x03

    CBLOCK  0x0C
W_SAVE
STATUS_SAVE
; Parametri correnti in RAM
CFG_BRIGHTNESS
CFG_SPEED
CFG_MODE
; Stato menu
CURRENT_PARAM   ; quale parametro stiamo editando (0-2)
CURRENT_VAL     ; valore corrente del parametro selezionato
; Timer autosave
SAVE_TIMER_HI   ; byte alto countdown 3s
SAVE_TIMER_LO   ; byte basso
SAVE_PENDING    ; 1 = c&#x27;è qualcosa da salvare
; Display
DISP_LEFT       ; cifra sinistra (parametro 0-2)
DISP_RIGHT      ; cifra destra (valore)
MUX_PHASE       ; 0=sinistra, 1=destra
; Debounce
DEBOUNCE_CNT
FLAGS
    ENDC

#DEFINE FLAG_BTN_MENU   FLAGS,0
#DEFINE FLAG_BTN_INC    FLAGS,1
#DEFINE FLAG_DEBOUNCING FLAGS,2
#DEFINE FLAG_DIRTY      FLAGS,3   ; parametri modificati non salvati

; Segmenti 7seg su PORTA bits [5:0] = [g,f,e,d,c,b,a] (comune catodo)
; RA4 = selezione digit (0=sinistra, 1=destra)

    ORG     0x0000
    GOTO    INIT

    ORG     0x0004
ISR
    MOVWF   W_SAVE
    SWAPF   STATUS,W
    MOVWF   STATUS_SAVE

    BTFSS   INTCON,T0IF
    GOTO    ISR_EXIT
    BCF     INTCON,T0IF

    ; Aggiorna display (multiplex ogni ~2ms)
    CALL    MUX_DISPLAY

    ; Debounce countdown
    BTFSC   FLAG_DEBOUNCING
    DECFSZ  DEBOUNCE_CNT,F
    GOTO    ISR_SKIP_DBNC
    BCF     FLAG_DEBOUNCING
    ; Verifica pulsanti stabili
    BTFSS   PORTB,0
    BSF     FLAG_BTN_MENU
    BTFSS   PORTB,1
    BSF     FLAG_BTN_INC

ISR_SKIP_DBNC
    ; Autosave countdown (decrementa ogni ~2ms)
    BTFSS   FLAG_DIRTY
    GOTO    ISR_EXIT
    MOVF    SAVE_TIMER_LO,F
    BTFSC   STATUS,Z
    DECF    SAVE_TIMER_HI,F
    DECF    SAVE_TIMER_LO,F
    ; Se entrambi zero → tempo scaduto → salva
    MOVF    SAVE_TIMER_LO,F
    BTFSS   STATUS,Z
    GOTO    ISR_EXIT
    MOVF    SAVE_TIMER_HI,F
    BTFSS   STATUS,Z
    GOTO    ISR_EXIT
    ; Segnala richiesta salvataggio (main eseguirà)
    BCF     FLAG_DIRTY
    BSF     SAVE_PENDING

ISR_EXIT
    SWAPF   STATUS_SAVE,W
    MOVWF   STATUS
    SWAPF   W_SAVE,F
    SWAPF   W_SAVE,W
    RETFIE

    ORG     0x0050
INIT
    BSF     STATUS,RP0
    MOVLW   b'11000001'        ; prescaler ÷4, T0CS=0
    MOVWF   OPTION_REG
    CLRF    TRISA              ; tutto output
    MOVLW   0x03               ; RB0,RB1 input
    MOVWF   TRISB
    BCF     STATUS,RP0

    CLRF    PORTA
    CLRF    FLAGS
    CLRF    MUX_PHASE
    CLRF    SAVE_PENDING

    ; Carica configurazione da EEPROM
    CALL    LOAD_CONFIG

    ; Aggiorna display con il primo parametro
    CLRF    CURRENT_PARAM
    CALL    UPDATE_DISPLAY

    MOVLW   b'10100000'        ; GIE=1, T0IE=1
    MOVWF   INTCON

; ============================================================
; MAIN
; ============================================================
MAIN
    ; Autosave richiesto dalla ISR?
    BTFSC   SAVE_PENDING
    CALL    SAVE_CONFIG

    ; Pulsante MENU premuto?
    BTFSC   FLAG_BTN_MENU
    CALL    ON_MENU

    ; Pulsante INC premuto?
    BTFSC   FLAG_BTN_INC
    CALL    ON_INC

    GOTO    MAIN

; ============================================================
; ON_MENU — cambia parametro corrente
; ============================================================
ON_MENU
    BCF     FLAG_BTN_MENU
    INCF    CURRENT_PARAM,F
    MOVF    CURRENT_PARAM,W
    SUBLW   PARAM_COUNT        ; PARAM_COUNT - CURRENT_PARAM
    BTFSS   STATUS,C
    CLRF    CURRENT_PARAM      ; overflow → torna a 0
    CALL    UPDATE_DISPLAY
    ; Avvia debounce
    MOVLW   D'50'
    MOVWF   DEBOUNCE_CNT
    BSF     FLAG_DEBOUNCING
    RETURN

; ============================================================
; ON_INC — incrementa valore parametro corrente
; ============================================================
ON_INC
    BCF     FLAG_BTN_INC
    ; Leggi valore corrente e limite
    MOVF    CURRENT_PARAM,W
    ADDWF   PCL,F
    GOTO    INC_BRIGHTNESS
    GOTO    INC_SPEED
    GOTO    INC_MODE

INC_BRIGHTNESS
    INCF    CFG_BRIGHTNESS,F
    MOVF    CFG_BRIGHTNESS,W
    SUBLW   MAX_BRIGHTNESS
    BTFSS   STATUS,C
    CLRF    CFG_BRIGHTNESS
    GOTO    INC_DONE

INC_SPEED
    INCF    CFG_SPEED,F
    MOVF    CFG_SPEED,W
    SUBLW   MAX_SPEED
    BTFSS   STATUS,C
    CLRF    CFG_SPEED
    GOTO    INC_DONE

INC_MODE
    INCF    CFG_MODE,F
    MOVF    CFG_MODE,W
    SUBLW   MAX_MODE
    BTFSS   STATUS,C
    CLRF    CFG_MODE

INC_DONE
    BSF     FLAG_DIRTY
    ; Reset timer autosave (1500 × 2ms = 3s)
    MOVLW   D'5'
    MOVWF   SAVE_TIMER_HI
    MOVLW   0xDC               ; 5×256+220 = 1500
    MOVWF   SAVE_TIMER_LO
    CALL    UPDATE_DISPLAY
    ; Debounce
    MOVLW   D'50'
    MOVWF   DEBOUNCE_CNT
    BSF     FLAG_DEBOUNCING
    RETURN

; ============================================================
; UPDATE_DISPLAY — aggiorna DISP_LEFT e DISP_RIGHT
; ============================================================
UPDATE_DISPLAY
    ; Cifra sinistra = numero parametro (0, 1, 2)
    MOVF    CURRENT_PARAM,W
    CALL    DIGIT_TO_SEG
    MOVWF   DISP_LEFT

    ; Cifra destra = valore corrente del parametro
    MOVF    CURRENT_PARAM,W
    ADDWF   PCL,F
    GOTO    UPD_BRIGHTNESS
    GOTO    UPD_SPEED
    GOTO    UPD_MODE

UPD_BRIGHTNESS
    MOVF    CFG_BRIGHTNESS,W
    GOTO    UPD_DONE
UPD_SPEED
    MOVF    CFG_SPEED,W
    GOTO    UPD_DONE
UPD_MODE
    MOVF    CFG_MODE,W

UPD_DONE
    CALL    DIGIT_TO_SEG
    MOVWF   DISP_RIGHT
    RETURN

; ============================================================
; MUX_DISPLAY — commuta tra le due cifre (chiamata da ISR)
; ============================================================
MUX_DISPLAY
    BTFSC   MUX_PHASE,0
    GOTO    MUX_RIGHT
    ; Mostra cifra sinistra (RA4=0)
    BCF     PORTA,4
    MOVF    DISP_LEFT,W
    ANDLW   0x0F               ; solo 4 bit per PORTA limitata
    MOVWF   PORTA
    BSF     MUX_PHASE,0
    RETURN
MUX_RIGHT
    ; Mostra cifra destra (RA4=1)
    BSF     PORTA,4
    MOVF    DISP_RIGHT,W
    ANDLW   0x0F
    MOVWF   PORTA
    BCF     MUX_PHASE,0
    RETURN

; ============================================================
; DIGIT_TO_SEG — W (0-9) → segmenti 7seg (nibble basso)
; ============================================================
DIGIT_TO_SEG
    ANDLW   0x0F
    ADDWF   PCL,F
    RETLW   0x3F               ; 0: abcdef
    RETLW   0x06               ; 1: bc
    RETLW   0x5B               ; 2: abdeg
    RETLW   0x4F               ; 3: abcdg
    RETLW   0x66               ; 4: bcfg
    RETLW   0x6D               ; 5: acdfg
    RETLW   0x7D               ; 6: acdefg
    RETLW   0x07               ; 7: abc
    RETLW   0x7F               ; 8: tutti
    RETLW   0x6F               ; 9: abcdfg

; ============================================================
; LOAD_CONFIG — carica da EEPROM o usa defaults
; ============================================================
LOAD_CONFIG
    ; Controlla sentinella
    MOVLW   EE_MAGIC_ADDR
    CALL    EE_READ
    XORLW   EE_MAGIC_VAL
    BTFSC   STATUS,Z
    GOTO    LC_LOAD            ; sentinella ok → carica

    ; Prima accensione → defaults
    MOVLW   D'4'
    MOVWF   CFG_BRIGHTNESS     ; default 4
    MOVLW   D'1'
    MOVWF   CFG_SPEED          ; default 1
    CLRF    CFG_MODE           ; default 0
    CALL    SAVE_CONFIG        ; salva defaults
    RETURN

LC_LOAD
    MOVLW   EE_BRIGHTNESS
    CALL    EE_READ
    MOVWF   CFG_BRIGHTNESS
    MOVLW   EE_SPEED
    CALL    EE_READ
    MOVWF   CFG_SPEED
    MOVLW   EE_MODE
    CALL    EE_READ
    MOVWF   CFG_MODE
    RETURN

; ============================================================
; SAVE_CONFIG — salva i tre parametri in EEPROM
; ============================================================
SAVE_CONFIG
    BCF     SAVE_PENDING
    ; Scrivi sentinella
    MOVLW   EE_MAGIC_ADDR
    MOVLW   EE_MAGIC_VAL
    CALL    EE_WRITE_BYTE
    MOVLW   EE_BRIGHTNESS
    MOVF    CFG_BRIGHTNESS,W
    CALL    EE_WRITE_BYTE
    MOVLW   EE_SPEED
    MOVF    CFG_SPEED,W
    CALL    EE_WRITE_BYTE
    MOVLW   EE_MODE
    MOVF    CFG_MODE,W
    CALL    EE_WRITE_BYTE
    RETURN

; ============================================================
; EE_READ — W=indirizzo → W=valore
; ============================================================
EE_READ
    BSF     STATUS,RP0
    MOVWF   EEADR
    BSF     EECON1,RD
    MOVF    EEDATA,W
    BCF     STATUS,RP0
    RETURN

; ============================================================
; EE_WRITE_BYTE — prima chiamata: W=indirizzo; seconda: W=dato
; (interfaccia semplificata: addr in W al CALL, poi dato in W)
; Usiamo registri temporanei
; ============================================================
EE_WRITE_BYTE
    ; Chiamato come: MOVLW addr / MOVLW dato / CALL EE_WRITE_BYTE
    ; Riscriviamo con due parametri espliciti via stack
    ; Qui versione con variabili globali EE_ADDR/EE_DATA
    BSF     STATUS,RP0
    MOVWF   EEADR              ; addr già in W
    ; dato deve essere in EEDATA — messo dal chiamante
    BSF     EECON1,WREN
    BCF     INTCON,GIE
    MOVLW   0x55
    MOVWF   EECON2
    MOVLW   0xAA
    MOVWF   EECON2
    BSF     EECON1,WR
    BSF     INTCON,GIE
    BCF     EECON1,WREN
EWB_WAIT
    BTFSC   EECON1,WR
    GOTO    EWB_WAIT
    BCF     STATUS,RP0
    RETURN

    END

4 · Analisi delle scelte progettuali

Perché autosave e non save immediato?
Scrivere in EEPROM ad ogni pressione significherebbe consumare ~1 ciclo scrittura per ogni INC. Se l’utente preme INC 10 volte per cambiare BRIGHTNESS, si consumano 10 cicli invece di 1. Con l’autosave da 3s, tutte le modifiche rapide vengono raggruppate in una sola scrittura.

Perché il mux display nella ISR?
Il multiplexing richiede cadenza precisa (~2ms) indipendentemente da cosa fa il main. Se fosse nel main loop, qualsiasi operazione lenta (es. scrittura EEPROM ~10ms) causerebbe sfarfallio visibile. Nella ISR è garantita la periodicità esatta.

Nota sul wrap-around con SUBLW: SUBLW N calcola N−W e setta il carry se N≥W (nessun prestito). Per limitare un valore incrementato a [0, MAX−1], si usa INCF VAL,F / MOVF VAL,W / SUBLW MAX / BTFSS STATUS,C / CLRF VAL. Se dopo l’incremento VAL≥MAX, il carry è clear (N<W) → azzeriamo. Se VAL<MAX il carry è set → lasciamo il valore. Attenzione: SUBLW inverte l’ordine rispetto a SUBWF.

🧪 Prova tu

  1. Carica il programma. Osserva DISP_LEFT=0 e DISP_RIGHT=4 (default BRIGHTNESS=4). Premi RB0 (MENU): CURRENT_PARAM diventa 1, DISP_LEFT diventa il codice 7seg per “1”, DISP_RIGHT il codice per “1” (SPEED default).
  2. Con CURRENT_PARAM=0 (BRIGHTNESS), premi RB1 (INC) 4 volte: CFG_BRIGHTNESS deve andare 5→6→7→0 (wrap a MAX_BRIGHTNESS=8). Osserva il display aggiornare DISP_RIGHT ad ogni pressione.
  3. Dopo aver modificato un parametro, aspetta che SAVE_TIMER conti fino a zero (visibile nel pannello RAM) e verifica che SAVE_PENDING si setti e poi si azzeri, e che la EEPROM sia aggiornata.
  4. Sfida: aggiungi un quarto parametro VOLUME (0–9) salvato in EE addr 0x04. Modifica: la costante PARAM_COUNT da 3 a 4, la tabella di salto in ON_INC, la tabella di salto in UPDATE_DISPLAY, e SAVE_CONFIG/LOAD_CONFIG per includere il nuovo indirizzo.

❓ Quiz rapido

1. Perché SAVE_CONFIG viene chiamata dal main e non direttamente dalla ISR quando il timer scade?

Mostra risposta

La scrittura EEPROM richiede ~10ms per byte, con GIE disabilitato durante la sequenza unlock. Se la ISR chiamasse direttamente SAVE_CONFIG, il PIC rimarrebbe con GIE=0 per ~40ms (4 byte × 10ms), bloccando completamente tutti gli interrupt — il display si fermerebbe e i pulsanti sarebbero ciechi per quel periodo. Usando SAVE_PENDING come flag, la ISR completa nel suo breve slot temporale (~µs) e segnala al main di fare il lavoro pesante quando è il momento opportuno.


2. Il display mostra sfarfallio quando il main esegue la scrittura EEPROM (~40ms totali). Come si risolverebbe in un design più avanzato?

Mostra risposta

La soluzione classica è scrivere un byte alla volta con GIE riabilitato tra una scrittura e l’altra. In pratica: scriviamo il primo byte con unlock + WR=1, poi riattiviamo GIE, poi nel loop principale aspettiamo WR=0 con gli interrupt abilitati — durante l’attesa la ISR può girare normalmente e aggiornare il display. Ripetiamo per ogni byte. Questo richiede una macchina a stati per la scrittura EEPROM (IDLE → WRITING_BYTE_N → DONE) ma elimina completamente il blocco display.


3. Il modulo 4 è terminato. Quali sono i tre pattern fondamentali che hai imparato e che riutilizzerai in ogni progetto embedded?

Mostra risposta

1) ISR minima + flag in RAM + main come dispatcher: la ISR fa solo il minimo indispensabile (rileva, segnala) e il main gestisce la logica. Questo mantiene i tempi di interrupt brevi e il codice applicativo semplice. 2) Debounce con disabilitazione/riabilitazione interrupt: non usare delay bloccanti — usare un timer secondario per qualificare il segnale. 3) Scrittura EEPROM lazy con timeout: raggruppare le scritture e scrivere solo quando necessario, per risparmiare cicli EEPROM e tempo di CPU. Questi tre pattern coprono il 90% dei progetti embedded a microcontrollore a 8 bit.

✅ Modulo 4 completato!

Hai padroneggiato ISR, debounce hardware, WDT, SLEEP mode ed EEPROM — gli strumenti fondamentali per qualsiasi sistema embedded affidabile e a basso consumo.

Prossimo modulo →

Modulo 5 — Periferiche avanzate (PIC16F628A e PIC16F877A)
Lezione 25 · UART — comunicazione seriale RS-232 con il PC.
Trasmissione e ricezione asincrona, calcolo baudrate, interrupt su ricezione.

© Prof. D. Bertolino · PIC16 From Zero to Hero · WebPicSimulator · Lezione 24