Lezione 21 di 43

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.

Architettura orologio — connessioni PIC16F84A PIC16F84A RA0 RA1 RA2 RA3 RB0/INT RB1 RB2 RB3 RB4 RB5 RB6 RB7 Display 4× 7-seg HH:MM Common Cathode col0 col1 col2 col3 segmenti a-g Segmenti a–g RB2=a RB3=b RB4=c RB5=d RB6=e RB7=f RB2=g (semplificato) SET_H (RB0) SET_M (RB1) Timer0 prescaler ÷256 → overflow ogni ~65.5ms → 15 overflow ≈ 983ms ≈ 1s

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.

Struttura del main loop — time-slicing senza RTOS MAIN (loop infinito) BTFSC TICK_1S → ON_SECOND BTFSC BTN_H_FLAG → ON_SET_H BTFSS PORTB,1 → ON_SET_M ON_SECOND SECONDS++ → 60? INC_MINUTE MINUTES++ → 60? INC_HOUR HOURS++ → 24? CLRF HOURS ON_SET_H / ON_SET_M INC_HOUR / INC_MINUTE → SPLIT_TIME → display

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.

Multiplexing display — round-robin su 4 cifre COL_0 ON PORTB = SEG7[H10] T0 overflow ≈65ms COL_1 ON PORTB = SEG7[H01] COL_2 ON PORTB = SEG7[M10] COL_3 ON PORTB = SEG7[M01] Periodo totale = 4 × 65ms = ~260ms → frequenza refresh ≈ 3.8 Hz (visibile flickering) Per refresh fluido (≥50Hz) → prescaler ÷2, overhead ISR basso, o Timer0 precaricato

⚠ 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 &lt; 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

  1. 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.
  2. Premi RB0 (SET_H) nel pannello I/O: osserva HOURS incrementare e DIGIT_H10/H01 aggiornarsi. Verifica che dopo HOURS=23 torni a 0.
  3. 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.
  4. 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