Lezione 15 di 43

Lezione 14

🎓 PIC16 — From Zero to Hero · Lezione 14 · Modulo 2

Multiplexing LED e Matrice 4×4 💡

Come controllare 16 LED con soli 8 pin: scansione per riga, framebuffer in RAM
e primo uso dell’indirizzamento indiretto con FSR e INDF.

🎯 Cosa impari in questa lezione

  • Come funziona una matrice LED 4×4: righe come anodi, colonne come catodi
  • Scansione per riga: accendere una riga alla volta con la maschera colonne
  • Conversione logica/catodo con XORLW 0x0F
  • Framebuffer in RAM: 4 byte che rappresentano lo stato della matrice
  • Indirizzamento indiretto: FSR come puntatore, INDF come dereferenziazione
  • Animazione a scorrimento verticale con tabella RETLW ciclica
  • Calcolo del duty cycle e frequenza di refresh (100 Hz, 25%)

📋 Scheda lezione

Lezione: 14 / 42
Modulo: 2 — I/O Digitale
Device: PIC16F84A
Prerequisiti: Lez. 13
Difficoltà: ★★★★☆
Tecnica nuova: FSR/INDF (indirizzamento indiretto)
Pattern chiave: framebuffer, scansione righe


1 · Struttura della matrice 4×4

Una matrice LED 4×4 ha 4 righe (anodi condivisi) e 4 colonne (catodi condivisi): in totale 16 LED controllati con soli 8 pin. Per accendere il LED in posizione (riga R, colonna C): portare la riga a HIGH e la colonna a LOW. Un solo LED alla volta può essere acceso direttamente; per visualizzare pattern arbitrari si usa la scansione per riga.

C0 C1 C2 C3 R0 R1 R2 R3 Schema cablaggio 4×4 rosso = LED acceso RA0-RA3 → righe R0-R3 RB0-RB3 → col C0-C3
FRAMEBUFFER (RAM) ROW0: 0b0000_0101 ROW1: 0b0000_1010 ROW2: 0b0000_1111 ← scan ROW3: 0b0000_0110 1 byte per riga bit0=C0 … bit3=C3 SCANSIONE RIGA 2 1. Spegni righe CLRF PORTA 2. Carica colonne MOVF ROW2,W XORLW 0x0F ; ~col MOVWF PORTB 3. Accendi riga 2 BSF PORTA,2 MATRICE (vista) ← ON istantanea: solo riga 2

2 · Framebuffer e indirizzamento indiretto FSR/INDF

Il framebuffer è un array di 4 byte in RAM (ROW0–ROW3): ogni byte rappresenta quali colonne accendere per quella riga. Invece di accedere alle variabili per nome, usiamo FSR (File Select Register, 0x04) come puntatore e INDF (0x00) come dereferenziazione: MOVF INDF,W legge il byte puntato da FSR. È l’equivalente assembly di *ptr in C.

Inizio 0000_0001 RA0 = R0 → dopo RLF 0000_0010 RA1 = R1 → 0000_0100 RA2 = R2 → 0000_1000 RA3 = R3

3 · Programma — Pattern fisso e scansione riga

; ============================================================
; Lezione 14 — Matrice LED 4×4, pattern statico
; Pattern "X": riga per riga con scansione continua
; PIC16F84A @ 4 MHz
; RA0-RA3 = righe R0-R3 (anodi, HIGH=ON)
; RB0-RB3 = colonne C0-C3 (catodi, LOW=ON)
; ============================================================
    LIST    P=16F84A
    #INCLUDE <p16f84a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF

    CBLOCK  0x0C
ROW_IDX                 ; indice riga corrente (0-3)
ROW_MASK                ; maschera bit riga su PORTA
D_LO                    ; delay interno
TEMP                    ; uso temporaneo
    ENDC

; ============================================================
; Pattern "X" su matrice 4×4 (bit0=C0, bit3=C3)
;   C3 C2 C1 C0
; R0: 1  0  0  1  → 0b0000_1001 = 0x09
; R1: 0  1  1  0  → 0b0000_0110 = 0x06
; R2: 0  1  1  0  → 0b0000_0110 = 0x06
; R3: 1  0  0  1  → 0b0000_1001 = 0x09
; ============================================================

    ORG     0x0000
    GOTO    INIT

    ORG     0x0005
INIT
    BSF     STATUS,RP0
    MOVLW   0xF0
    MOVWF   TRISB              ; RB0-RB3 output, RB4-RB7 input
    MOVLW   0x00
    MOVWF   TRISA              ; RA0-RA3 output (righe)
    BCF     STATUS,RP0
    CLRF    PORTA              ; tutte righe spente
    MOVLW   0x0F
    MOVWF   PORTB              ; tutte colonne OFF (catodi HIGH)

; --- Loop di scansione ---
SCAN_LOOP
    CLRF    ROW_IDX
    MOVLW   0x01
    MOVWF   ROW_MASK           ; inizia da RA0 (riga 0)

NEXT_ROW
    ; 1. Spegni riga corrente (evita ghosting)
    CLRF    PORTA
    MOVLW   0x0F
    MOVWF   PORTB              ; colonne OFF (tutti HIGH)

    ; 2. Leggi pattern della riga corrente dalla tabella
    MOVF    ROW_IDX,W
    CALL    PATTERN_TABLE      ; W = byte riga (1=acceso)

    ; 3. Converti logica positiva → catodi (inveteri i 4 bit bassi)
    XORLW   0x0F               ; 1→0 (col ON), 0→1 (col OFF)
    MOVWF   PORTB

    ; 4. Attiva riga (ROW_MASK ha il bit giusto)
    MOVF    ROW_MASK,W
    MOVWF   PORTA

    ; 5. Aspetta ~2.5ms
    CALL    DELAY_2_5MS

    ; 6. Avanza alla riga successiva
    INCF    ROW_IDX,F
    BCF     STATUS,C           ; pulisci carry prima di shift
    RLF     ROW_MASK,F         ; shift left: 01→02→04→08

    ; 7. Tutte e 4 le righe fatte?
    MOVF    ROW_IDX,W
    SUBLW   D'4'
    BTFSS   STATUS,Z
    GOTO    NEXT_ROW
    GOTO    SCAN_LOOP          ; ricomincia

; ============================================================
; PATTERN_TABLE — pattern "X" 4×4
; W in: riga (0-3)   W out: byte riga (bit0=C0, bit3=C3, 1=ON)
; ============================================================
PATTERN_TABLE
    ADDWF   PCL,F
    RETLW   0x09               ; R0: 1001 → C0,C3 ON
    RETLW   0x06               ; R1: 0110 → C1,C2 ON
    RETLW   0x06               ; R2: 0110 → C1,C2 ON
    RETLW   0x09               ; R3: 1001 → C0,C3 ON

; ============================================================
; DELAY_2_5MS ≈ 2.5ms @ 4 MHz
; 3 × 256 × 3 = 2304 + overhead ≈ 2.5ms
; ============================================================
DELAY_2_5MS
    MOVLW   D'3'
    MOVWF   TEMP
D25_OUTER
    CLRF    D_LO
D25_INNER
    DECFSZ  D_LO,F
    GOTO    D25_INNER
    DECFSZ  TEMP,F
    GOTO    D25_OUTER
    RETURN

    END

4 · Animazione — Scorrimento verticale

L’animazione aggiorna il framebuffer ogni N cicli di scansione. Una tabella RETLW ciclica fornisce il pattern di ogni riga ad ogni frame: quando l’indice supera la lunghezza della tabella, riparte da 0 creando il loop continuo.

PRIMA ROW0: 0x09 ROW1: 0x06 ROW2: 0x06 ROW3: 0x09 NEXT: 0x0F shift down +load new DOPO ROW0: 0x0F ← nuova ROW1: 0x09 ← era R0 ROW2: 0x06 ← era R1 ROW3: 0x06 ← era R2 NEXT: prossimo MATRICE PERCEPITA ← nuova FSR (0x04) 0x0E ← ROW2 puntatore RAM Bank 0 0x0C: ROW0 = 0x09 0x0D: ROW1 = 0x06 0x0E: ROW2 = 0x06 ← FSR punta qui 0x0F: ROW3 = 0x09 INDF (0x00) legge: 0x06 MOVF INDF,W → W=0x06 W 0x06
; ============================================================
; Lezione 14 — Matrice LED 4×4, scorrimento verticale
; Un frame scorre dall'alto verso il basso ogni 200ms
; Il framebuffer è in RAM (ROW0-ROW3), i dati in una tabella
; ============================================================
    LIST    P=16F84A
    #INCLUDE <p16f84a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF

    CBLOCK  0x0C
ROW0                    ; framebuffer riga 0 (top)
ROW1                    ; framebuffer riga 1
ROW2                    ; framebuffer riga 2
ROW3                    ; framebuffer riga 3 (bottom)
ANIM_IDX               ; indice nella tabella animazione
ANIM_MAX               ; numero totale frame (lunghezza tabella)
REFRESH_CNT            ; cicli di refresh prima di avanzare frame
ROW_IDX                ; riga scansione corrente
ROW_MASK               ; maschera bit riga
D_LO
TEMP
    ENDC

    ORG     0x0000
    GOTO    INIT

    ORG     0x0005
INIT
    BSF     STATUS,RP0
    MOVLW   0xF0
    MOVWF   TRISB
    CLRF    TRISA
    BCF     STATUS,RP0
    CLRF    PORTA
    MOVLW   0x0F
    MOVWF   PORTB
    ; Precarica framebuffer con prime 4 righe dell'animazione
    CLRF    ANIM_IDX
    MOVLW   D'8'
    MOVWF   ANIM_MAX           ; 8 righe nella tabella animazione
    CALL    LOAD_FRAME
    CALL    LOAD_FRAME
    CALL    LOAD_FRAME
    CALL    LOAD_FRAME         ; riempie ROW0-ROW3
    MOVLW   D'20'
    MOVWF   REFRESH_CNT        ; 20×10ms = 200ms per riga

; ============================================================
; LOOP PRINCIPALE
; Ogni ciclo: 1 refresh completo (4 righe × 2.5ms = 10ms)
; Ogni 20 cicli: avanza animazione (shift framebuffer + nuova riga)
; ============================================================
MAIN_LOOP
    ; --- Scansione completa delle 4 righe ---
    CLRF    ROW_IDX
    MOVLW   0x01
    MOVWF   ROW_MASK
SCAN_ROW
    ; Spegni
    CLRF    PORTA
    MOVLW   0x0F
    MOVWF   PORTB
    ; Leggi riga dal framebuffer usando FSR (indirect addressing)
    MOVLW   ROW0               ; indirizzo base framebuffer
    ADDWF   ROW_IDX,W         ; + offset riga
    MOVWF   FSR                ; punta alla riga corrente
    MOVF    INDF,W             ; leggi tramite indirezione
    XORLW   0x0F               ; converti logica → catodi
    MOVWF   PORTB
    MOVF    ROW_MASK,W
    MOVWF   PORTA
    CALL    DELAY_2_5MS
    ; Avanza riga
    INCF    ROW_IDX,F
    BCF     STATUS,C
    RLF     ROW_MASK,F
    MOVF    ROW_IDX,W
    SUBLW   D'4'
    BTFSS   STATUS,Z
    GOTO    SCAN_ROW

    ; --- Aggiorna animazione ogni 20 cicli ---
    DECFSZ  REFRESH_CNT,F
    GOTO    MAIN_LOOP
    MOVLW   D'20'
    MOVWF   REFRESH_CNT

    ; Shift framebuffer: ROW3←ROW2←ROW1←ROW0←nuova
    MOVF    ROW2,W
    MOVWF   ROW3
    MOVF    ROW1,W
    MOVWF   ROW2
    MOVF    ROW0,W
    MOVWF   ROW1
    CALL    LOAD_FRAME         ; carica nuova riga in ROW0
    GOTO    MAIN_LOOP

; ============================================================
; LOAD_FRAME: carica la prossima riga da ANIM_TABLE in ROW0
; Avanza ANIM_IDX (ciclico con ANIM_MAX)
; ============================================================
LOAD_FRAME
    MOVF    ANIM_IDX,W
    CALL    ANIM_TABLE
    MOVWF   ROW0
    INCF    ANIM_IDX,F
    MOVF    ANIM_IDX,W
    SUBWF   ANIM_MAX,W         ; W = MAX - IDX
    BTFSS   STATUS,Z           ; == 0 → siamo alla fine
    RETURN
    CLRF    ANIM_IDX           ; cicla
    RETURN

; ============================================================
; ANIM_TABLE — sequenza di righe da scorrere
; 8 righe = 2 frame "X" (pattern si ripete)
; ============================================================
ANIM_TABLE
    ADDWF   PCL,F
    RETLW   0x09               ; riga 0: 1001
    RETLW   0x06               ; riga 1: 0110
    RETLW   0x06               ; riga 2: 0110
    RETLW   0x09               ; riga 3: 1001
    RETLW   0x0F               ; riga 4: 1111 (separatore pieno)
    RETLW   0x00               ; riga 5: 0000 (vuota)
    RETLW   0x00               ; riga 6: 0000
    RETLW   0x00               ; riga 7: 0000 (spazio)

; ============================================================
; DELAY_2_5MS (identico al programma precedente)
; ============================================================
DELAY_2_5MS
    MOVLW   D'3'
    MOVWF   TEMP
D25_O
    CLRF    D_LO
D25_I
    DECFSZ  D_LO,F
    GOTO    D25_I
    DECFSZ  TEMP,F
    GOTO    D25_O
    RETURN

    END

5 · Riepilogo — Punti chiave

✓ Matrice LED
Righe = anodi (HIGH=ON), colonne = catodi (LOW=ON). Una riga attiva per volta, 2.5 ms ciascuna → 100 Hz refresh, 25% duty cycle per LED.

✓ FSR/INDF
FSR = puntatore (indirizzo del dato). INDF = accesso al dato puntato. Incrementare FSR = puntatore al prossimo elemento. Equivalente a *ptr++ in C.

✓ XORLW 0x0F
Inverte i 4 bit bassi: converte il pattern logico (1=LED ON) nel segnale fisico per i catodi (0=LED ON). Un’istruzione sola per tutta la conversione.

🧪 Prova tu

  1. Carica il programma con pattern fisso e modifica ROW0–ROW3 nell’INIT per disegnare una X sulla matrice. Quali valori hex usi?
  2. Osserva FSR nel pannello registri durante la scansione: vedi il valore cambiare da ROW0 a ROW3 ad ogni riga?
  3. Sfida: aggiungi una quinta “riga” alla tabella di animazione che sia tutta spenta (0x00) per creare un effetto lampeggio tra i frame.
  4. Sfida avanzata: implementa lo scorrimento orizzontale invece che verticale. Ogni frame ruota ogni riga di 1 bit con RLF. Cosa cambia nella struttura del framebuffer?

❓ Quiz rapido

1. Perché le righe usano HIGH=ON e le colonne LOW=ON?

Mostra risposta

Nella configurazione fisica della matrice, i LED sono collegati tra righe (anodi) e colonne (catodi). La corrente scorre dall’anodo al catodo: la riga deve essere a potenziale alto (+5V, HIGH) e la colonna a potenziale basso (GND, LOW). Solo quando entrambe le condizioni sono vere contemporaneamente il LED si accende. Se entrambe fossero HIGH o entrambe LOW, nessuna corrente scorre.


2. Cosa succede se si tenta di accedere a INDF con FSR=0x00 (cioè FSR punta a INDF stesso)?

Mostra risposta

INDF è all’indirizzo 0x00. Se FSR=0x00 e si legge INDF, si legge INDF che punta a INDF stesso: loop infinito di dereferenziazione che restituisce sempre 0x00. Il PIC16 risolve questo caso speciale: accedere a INDF con FSR=0x00 legge/scrive sempre 0x00 e non causa loop. Nella pratica, non si dovrebbe mai settare FSR a 0x00 intenzionalmente.


3. Con 4 righe e 2.5ms per riga, qual è la frequenza di refresh e il duty cycle per ogni LED?

Mostra risposta

Periodo di refresh = 4 righe × 2.5ms = 10ms → frequenza = 1/10ms = 100 Hz. Ogni LED è acceso solo durante il suo turno di riga: 2.5ms su 10ms totali = 25% duty cycle. Per mantenere la stessa luminosità apparente del caso continuo (duty 100%), bisognerebbe aumentare la corrente di picco di 4× (con resistori da ¼ del valore normale). Nel simulatore non è rilevante, ma conta per il progetto reale.


4. Perché si usa XORLW 0x0F invece di COMF per invertire la logica delle colonne?

Mostra risposta

COMF invertirebbe tutti e 8 i bit, ma le colonne usano solo i 4 bit bassi (RB0–RB3). I 4 bit alti (RB4–RB7) sono collegati alle righe e devono rimanere controllati separatamente. XORLW 0x0F inverte solo i bit 0–3 (XOR con 1 = inversione), lasciando invariati i bit 4–7. Così colonne e righe possono coesistere sugli stessi 8 bit di PORTB senza interferirsi.

Prossima lezione →

Lezione 15 — Progetto I/O Completo
Mettiamo insieme tutto il Modulo 2: pulsanti con debounce non-bloccante, display 7 segmenti multiplexato e macchina a stati finiti FSM. Il primo sistema embedded reale.

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