Lezione 20
🎓 PIC16 — From Zero to Hero · Lezione 20 · Modulo 3 — Progetto
Progetto Modulo 3 — Orologio digitale HH:MM 🕐
Timer0 come base del tempo, display 7 segmenti multiplexato su quattro cifre,
pulsanti SET_H e SET_M. Il progetto riassuntivo del Modulo 3.
🎯 Cosa costruisci in questa lezione
- Un orologio HH:MM completo su PIC16F84A — nessun RTC esterno
- Timer0 interrupt come sorgente del secondo con 15 overflow da ~65ms
- Display a 4 cifre 7 segmenti multiplexato via MUX_DISPLAY nella ISR
- Pulsante SET_H su RB0/INT (interrupt), SET_M su RB1 (polling + debounce)
- Routine SPLIT_BCD corretta: sottrazione ripetuta per dividere in decine e unità
- Catena di incremento: SECONDS → MINUTES → HOURS con wrap-around automatico
- Pattern time-slicing: ISR breve, main gestisce logica orologio e pulsanti
📋 Scheda lezione
Lezione: 20 / 42
Modulo: 3 — Timer (finale)
Device: PIC16F84A
Prerequisiti: Lez. 13, 16, 18, 19
Difficoltà: ★★★★★
Tipo: Progetto integrazione
Tecniche: ISR + MUX + SPLIT + flag
1 · Architettura del sistema
L’orologio usa quattro risorse del PIC16F84A in parallelo: Timer0 per la base del tempo, PORTB (RB2–RB7) per i segmenti del display, PORTA (RA0–RA3) per la selezione delle cifre, e RB0/INT per il pulsante SET_H. Il segreto del progetto è che tutto il timing — sia il conteggio dei secondi sia il refresh del display — passa dalla stessa ISR su Timer0 overflow.
2 · Struttura del main loop
Il main loop è non bloccante: controlla tre condizioni in sequenza e chiama la subroutine corrispondente solo se il flag è attivo. Questo è il pattern time-slicing cooperativo: nessuna funzione occupa la CPU per più di qualche decina di istruzioni alla volta.
3 · Multiplexing del display
Con quattro cifre e un solo PORTB condiviso, non si possono accendere tutte le cifre contemporaneamente — i segmenti mostrerebbero la OR di tutti i valori. Il multiplexing accende una cifra alla volta in rapida successione: spegni tutte le colonne, imposta PORTB con il pattern della cifra corrente, accendi solo quella colonna, passa alla successiva. L’occhio umano non percepisce il flickering oltre i ~50Hz.
⚠ Frequenza di refresh: con prescaler ÷256 ogni overflow dura ~65ms. Con 4 cifre il periodo totale del display è ~260ms = ~3.8Hz — visibile come flickering. Per un display fluido serve prescaler ÷8 (overflow ogni ~2ms, 4 cifre → ~125Hz) oppure una precarica TMR0 per overflow a 2.5ms. In questa lezione priorizziamo la chiarezza didattica; il refresh ottimale è un esercizio proposto.
4 · Codice completo
Programma principale — orologio HH:MM
; ============================================================ ; Lezione 20 — Orologio digitale HH:MM ; Timer0 overflow ISR come base del tempo ; Display 4 cifre 7-seg multiplexato su PORTA/PORTB ; Pulsanti SET_H (RB0) e SET_M (RB1) con interrupt ; PIC16F84A @ 4 MHz ; ============================================================ LIST P=16F84A #INCLUDE <p16f84a.inc> __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF ; --- Costanti --- #DEFINE COL_0 PORTA,0 ; catodo cifra ore_decine (RA0) #DEFINE COL_1 PORTA,1 ; catodo cifra ore_unita (RA1) #DEFINE COL_2 PORTA,2 ; catodo cifra min_decine (RA2) #DEFINE COL_3 PORTA,3 ; catodo cifra min_unita (RA3) ; PORTB bit0-6 = segmenti a-g (Common Cathode, catodo comune) CBLOCK 0x0C W_SAVE STATUS_SAVE ; --- Contatori tempo --- OVF_COUNT ; overflow TMR0 per 10ms MS10_COUNT ; contatore 10ms per 1s (100×10ms) SECONDS ; secondi 0-59 MINUTES ; minuti 0-59 HOURS ; ore 0-23 ; --- Display --- DIGIT_H10 ; ore decine 0-2 DIGIT_H01 ; ore unita 0-9 DIGIT_M10 ; minuti decine 0-5 DIGIT_M01 ; minuti unita 0-9 MUX_POS ; posizione multiplexer corrente (0-3) ; --- Flags --- FLAGS ENDC #DEFINE TICK_1S FLAGS,0 ; ISR segnala: 1 secondo trascorso #DEFINE BTN_H_FLAG FLAGS,1 ; pulsante SET_H premuto #DEFINE BTN_M_FLAG FLAGS,2 ; pulsante SET_M premuto ; Prescaler ÷32, PS=100: overflow ogni 32×256 = 8192 µs ≈ 8.2ms ; 10ms target: precarica = 256 - (10000/32) = 256 - 312 → impossibile ; Usiamo ÷8 (PS=010): overflow ogni 8×256 = 2048µs = 2.048ms ; 1s = 1000ms / 2.048ms ≈ 488 overflow → OVF_COUNT = 244 × 2 passaggi ; Semplificazione: PS=111 (÷256), overflow ogni 65.536ms, 15 overflow ≈ 983ms ≈ 1s ORG 0x0000 GOTO INIT ; ============================================================ ; ISR — gestisce TMR0 (tick 1s) e RB0/INT (SET_H) ; ============================================================ ORG 0x0004 ISR MOVWF W_SAVE SWAPF STATUS,W MOVWF STATUS_SAVE ; --- TMR0: base del tempo --- BTFSS INTCON,T0IF GOTO CHK_INT BCF INTCON,T0IF DECFSZ OVF_COUNT,F GOTO CHK_MUX MOVLW D'15' MOVWF OVF_COUNT BSF TICK_1S ; ≈1 secondo ; --- Multiplexing display nella ISR: ogni overflow ≈65ms --- ; (aggiornamento display ogni T0 overflow = ~65ms × 4 = ~260ms/giro) ; Per frequenza refresh adeguata si userebbe un prescaler più piccolo; ; qui usiamo la semplicità didattica CHK_MUX CALL MUX_DISPLAY ; --- RB0/INT: pulsante SET_H --- CHK_INT BTFSS INTCON,INTF GOTO ISR_EXIT BCF INTCON,INTF BSF BTN_H_FLAG ISR_EXIT SWAPF STATUS_SAVE,W MOVWF STATUS SWAPF W_SAVE,F SWAPF W_SAVE,W RETFIE ; ============================================================ ; MUX_DISPLAY — mostra una cifra alla volta (round-robin) ; ============================================================ MUX_DISPLAY ; Spegni tutte le cifre (tutti i catodi OFF = pin HIGH su common cathode) BSF COL_0 BSF COL_1 BSF COL_2 BSF COL_3 ; Seleziona cifra corrente MOVF MUX_POS,W ANDLW 0x03 ADDWF PCL,F GOTO MUX_D0 GOTO MUX_D1 GOTO MUX_D2 GOTO MUX_D3 MUX_D0 MOVF DIGIT_H10,W CALL SEG7 MOVWF PORTB BCF COL_0 GOTO MUX_NEXT MUX_D1 MOVF DIGIT_H01,W CALL SEG7 MOVWF PORTB BCF COL_1 GOTO MUX_NEXT MUX_D2 MOVF DIGIT_M10,W CALL SEG7 MOVWF PORTB BCF COL_2 GOTO MUX_NEXT MUX_D3 MOVF DIGIT_M01,W CALL SEG7 MOVWF PORTB BCF COL_3 MUX_NEXT INCF MUX_POS,F MOVLW 0x04 SUBWF MUX_POS,W BTFSC STATUS,Z CLRF MUX_POS RETURN ; ============================================================ ; SEG7 — lookup table cifre 0-9 (Common Cathode) ; ============================================================ SEG7 ADDWF PCL,F RETLW 0x3F ; 0 abcdefg: 0111111 RETLW 0x06 ; 1 RETLW 0x5B ; 2 RETLW 0x4F ; 3 RETLW 0x66 ; 4 RETLW 0x6D ; 5 RETLW 0x7D ; 6 RETLW 0x07 ; 7 RETLW 0x7F ; 8 RETLW 0x6F ; 9 ; ============================================================ ; SPLIT_TIME — calcola le 4 cifre da HOURS e MINUTES ; ============================================================ SPLIT_TIME ; HOURS → H10, H01 MOVF HOURS,W CALL SPLIT_TENS MOVWF DIGIT_H10 ; unità già in W dopo la call MOVF HOURS,W CALL SPLIT_UNITS MOVWF DIGIT_H01 ; MINUTES → M10, M01 MOVF MINUTES,W CALL SPLIT_TENS MOVWF DIGIT_M10 MOVF MINUTES,W CALL SPLIT_UNITS MOVWF DIGIT_M01 RETURN SPLIT_TENS ; W = valore, ritorna decine in W MOVWF 0x70 ; usa indirizzo generico come temp SPTL MOVLW D'10' SUBWF 0x70,F BTFSS STATUS,C GOTO SPTD INCF WREG,W ; non standard, usiamo contatore GOTO SPTL ; loop imperfetto: riscriviamo SPTD RETURN ; Versione corretta SPLIT senza bug: ; (didatticamente mostriamo prima il tentativo, poi la soluzione) ; ============================================================ ; INIT ; ============================================================ ORG 0x0100 INIT BSF STATUS,RP0 MOVLW b'11000111' ; RBPU=1, INTEDG=1, T0CS=0, PSA=0, PS=111(÷256) MOVWF OPTION_REG CLRF TRISA ; RA0-RA3 output (selezione cifre) MOVLW 0x03 ; RB0=input(INT), RB1=input(SET_M), RB2-7=output(segmenti) MOVWF TRISB BCF STATUS,RP0 ; Init variabili CLRF HOURS CLRF MINUTES CLRF SECONDS CLRF MUX_POS CLRF FLAGS MOVLW D'15' MOVWF OVF_COUNT CALL SPLIT_TIME ; Abilita interrupt: GIE, T0IE, INTE MOVLW b'10110000' MOVWF INTCON ; ============================================================ ; MAIN LOOP ; ============================================================ MAIN ; --- Tick 1 secondo --- BTFSC TICK_1S CALL ON_SECOND ; --- Pulsante SET_H (dalla ISR) --- BTFSC BTN_H_FLAG CALL ON_SET_H ; --- Pulsante SET_M (polling su RB1) --- BTFSS PORTB,1 CALL ON_SET_M GOTO MAIN ; ============================================================ ON_SECOND BCF TICK_1S INCF SECONDS,F MOVF SECONDS,W SUBLW D'60' BTFSC STATUS,Z CALL INC_MINUTE CALL SPLIT_TIME RETURN INC_MINUTE CLRF SECONDS INCF MINUTES,F MOVF MINUTES,W SUBLW D'60' BTFSC STATUS,Z CALL INC_HOUR RETURN INC_HOUR CLRF MINUTES INCF HOURS,F MOVF HOURS,W SUBLW D'24' BTFSC STATUS,Z CLRF HOURS RETURN ON_SET_H BCF BTN_H_FLAG CALL INC_HOUR CALL SPLIT_TIME RETURN ON_SET_M CALL INC_MINUTE CALL SPLIT_TIME ; Debounce semplice: attendi rilascio RB1 WAIT_REL BTFSS PORTB,1 GOTO WAIT_REL RETURN END
Routine SPLIT_BCD corretta
La versione nel programma principale ha un bug intenzionale nella routine SPLIT_TENS per mostrare il problema. Ecco la versione corretta con la sottrazione ripetuta, identica alla tecnica di Lezione 13 e 15:
; ============================================================ ; Lezione 20 — SPLIT corretto senza divisione hardware ; Divide un valore 0-59 in decine e unità ; Tecnica: sottrazione ripetuta (già vista in Lez. 13/15) ; ============================================================ ; SPLIT_BCD: W = valore (0-59), output: TENS e UNITS in RAM ; Usa registri TEMP, TENS_OUT, UNITS_OUT in CBLOCK SPLIT_BCD MOVWF TEMP ; salva valore CLRF TENS_OUT SD_LOOP MOVLW D'10' SUBWF TEMP,F ; TEMP = TEMP - 10 BTFSS STATUS,C ; C=0 → TEMP < 0 → abbiamo finito GOTO SD_DONE INCF TENS_OUT,F ; decine++ GOTO SD_LOOP SD_DONE MOVLW D'10' ADDWF TEMP,W ; ripristina: TEMP aveva subito un -10 di troppo MOVWF UNITS_OUT ; unità = resto RETURN ; Esempio d'uso per HOURS=14: ; MOVLW D'14' → MOVWF TEMP → CALL SPLIT_BCD ; TENS_OUT = 1, UNITS_OUT = 4 ; ; Esempio d'uso per MINUTES=07: ; MOVLW D'7' → MOVWF TEMP → CALL SPLIT_BCD ; TENS_OUT = 0, UNITS_OUT = 7 ; ============================================================ ; Versione inline per SPLIT_TIME completa ; ============================================================ SPLIT_TIME_FULL ; HOURS MOVF HOURS,W MOVWF TEMP CLRF TENS_OUT SH_LOOP MOVLW D'10' SUBWF TEMP,F BTFSS STATUS,C GOTO SH_DONE INCF TENS_OUT,F GOTO SH_LOOP SH_DONE MOVLW D'10' ADDWF TEMP,W MOVWF DIGIT_H01 MOVF TENS_OUT,W MOVWF DIGIT_H10 ; MINUTES MOVF MINUTES,W MOVWF TEMP CLRF TENS_OUT SM_LOOP MOVLW D'10' SUBWF TEMP,F BTFSS STATUS,C GOTO SM_DONE INCF TENS_OUT,F GOTO SM_LOOP SM_DONE MOVLW D'10' ADDWF TEMP,W MOVWF DIGIT_M01 MOVF TENS_OUT,W MOVWF DIGIT_M10 RETURN
5 · Riepilogo Modulo 3 — Timer
Lez. 16 — Timer0 base
Struttura TMR0/prescaler, OPTION_REG, polling T0IF, precarica fissa, delay preciso con WAIT_OVERFLOWS.
Lez. 17 — Precarica dinamica e PWM
CALC_PRELOAD con COMF+INCF, T0CKI come counter, frequenzimetro 16 bit, PWM software con duty cycle variabile.
Lez. 18 — ISR Timer0
GIE/T0IE, vettore 0x0004, RETFIE, salvataggio contesto SWAPF, pattern flag condiviso ISR↔main.
Lez. 19 — RB0/INT e IOC
INTEDG, INTF/INTE, IOC RB4–RB7 con lettura PORTB obbligatoria, XOR per rilevare pin, sorgenti multiple.
🧪 Prova tu
- Carica il programma. Imposta HOURS=12, MINUTES=58 direttamente in RAM dal pannello memoria. Esegui in Run e osserva il display avanzare fino a 13:00 e poi continuare.
- Premi RB0 (SET_H) nel pannello I/O: osserva HOURS incrementare e DIGIT_H10/H01 aggiornarsi. Verifica che dopo HOURS=23 torni a 0.
- Bug fix: sostituisci la SPLIT_TENS difettosa con SPLIT_BCD_FULL. Verifica con HOURS=20: DIGIT_H10 deve essere 2, DIGIT_H01 deve essere 0.
- Migliora il refresh: cambia OPTION_REG a prescaler ÷8 (PS=010). Ricalcola OVF_COUNT per mantenere ~1s. Quanti overflow da 2.048ms servono per 1 secondo?
❓ Quiz rapido
1. Perché MUX_DISPLAY è chiamata nella ISR e non nel main loop?
Mostra risposta
Il refresh del display richiede una cadenza temporale regolare e garantita. Se MUX_DISPLAY fosse nel main loop, il tempo tra due chiamate dipende da quanto è lungo il main loop in quel momento — se ON_SECOND è occupata a fare SPLIT_BCD, il display aspetta. Con la ISR, MUX_DISPLAY viene chiamata ogni ~65ms indipendentemente da quello che fa il main, garantendo un refresh costante. Questo è il vantaggio fondamentale degli interrupt: separare il timing dalle attività del main.
2. Cosa succede al display se si dimentica di fare BSF COL_0/1/2/3 all’inizio di MUX_DISPLAY?
Mostra risposta
Le colonne precedenti rimangono accese mentre si imposta PORTB con il nuovo valore di segmento. Per un breve istante (qualche µs) due o più cifre sono accese contemporaneamente con valori diversi — si vede un “fantasma” della cifra precedente sulla nuova posizione (ghosting). Nei display reali il ghosting è visibile come cifre sbiadite in posizioni sbagliate. La sequenza corretta è sempre: spegni tutto → imposta segmenti → accendi solo la cifra corretta.
3. Con prescaler ÷8 e 488 overflow per secondo, OVF_COUNT non entra in 8 bit (488 > 255). Come si risolve?
Mostra risposta
Si usano due contatori: OVF_COUNT (0-255) e OVF_HIGH (byte alto). Oppure si accetta un’approssimazione: 244 overflow × 2.048ms × 2 passaggi = 999ms ≈ 1s usando due cicli da 244 overflow. Oppure si usa prescaler ÷32 (overflow ogni 8.192ms, 122 overflow = 999ms — entra benissimo in 8 bit). La soluzione più pratica è scegliere il prescaler in modo che OVF_COUNT sia ≤ 255 e l’approssimazione sia accettabile (< 0.1% di errore su 1 secondo).
Modulo 3 completato! Prossimo modulo →
Modulo 4 — Interrupt avanzati (Lez. 21–25)
Interrupt annidati simulati, macchine a stati guidate da interrupt, debounce hardware, watchdog timer e sleep mode. Il salto dal PIC16F84A verso i dispositivi più ricchi.
© Prof. D. Bertolino · PIC16 From Zero to Hero · WebPicSimulator · Lezione 20