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.
2 · Layout memoria e display
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'è 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
- 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).
- 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.
- 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.
- 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