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.
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.
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.
; ============================================================ ; 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
- Carica il programma con pattern fisso e modifica ROW0–ROW3 nell’INIT per disegnare una X sulla matrice. Quali valori hex usi?
- Osserva FSR nel pannello registri durante la scansione: vedi il valore cambiare da ROW0 a ROW3 ad ogni riga?
- Sfida: aggiungi una quinta “riga” alla tabella di animazione che sia tutta spenta (0x00) per creare un effetto lampeggio tra i frame.
- 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