Lezione 30 di 43

Lezione 29

🎓 PIC16 — From Zero to Hero · Lezione 29 · Modulo 5

SPI — Comunicazione ad alta velocità ⚡

Il modulo MSSP del PIC16F877A in modalità SPI Master. Lettura da ADC esterno
MCP3204 a 12 bit e generazione segnali analogici con DAC MCP4921.

🎯 Cosa impari in questa lezione

  • Differenze tra SPI e I²C: velocità, pin, full-duplex, modalità clock
  • I 4 modi SPI: CPOL (polarità clock) × CPHA (fase campionamento)
  • Configurazione MSSP SPI Master: SSPCON, SSPSTAT, baud rate (Fosc/4, /16, /64)
  • Temporizzazione SPI mode 0,0: dati stabili su fronte di salita SCK
  • Lettura ADC esterno MCP3204 a 12 bit: sequenza 3 byte, ricostruzione risultato
  • Scrittura su DAC MCP4921 a 12 bit: pacchetto 16 bit con header di controllo
  • Segnale LDAC per sincronizzare l’aggiornamento dell’uscita analogica

📋 Scheda lezione

Lezione: 29 / 42
Modulo: 5 — Periferiche avanzate
Device: PIC16F877A
Prerequisiti: Lez. 28 (I²C/MSSP)
Difficoltà: ★★★★☆
Periferica: MSSP SPI Master
Slave: MCP3204 ADC, MCP4921 DAC


1 · SPI vs I²C — quando usare quale

SPI è più veloce di I²C (da 10× a 100× in base all’hardware) ed è full-duplex — master e slave trasmettono contemporaneamente sulle linee MOSI e MISO. Il prezzo è un filo in più per ogni slave aggiuntivo (CS). I²C è preferibile quando i pin sono scarsi e la velocità è secondaria. Sul PIC16F877A entrambi usano lo stesso modulo MSSP (RC3/RC4/RC5), ma non possono essere attivi simultaneamente.

SPI vs I²C — confronto caratteristiche Caratteristica SPI I²C Fili 4 (SCK, MOSI, MISO, CS×N) 2 (SCL, SDA) Velocità tipica 1–50 MHz 100kHz / 400kHz / 3.4MHz Multi-slave CS separato per ogni slave Indirizzo 7 bit (127 slave) Full duplex ✓ Sì (MOSI e MISO simultanei) ✗ Half-duplex (SDA bidirezionale) Hardware PIC16F877A MSSP in SPI mode (RC3/4/5) MSSP in I²C mode (RC3/4) Overhead protocollo Minimo (nessun ACK) 9° bit ACK ogni byte Modi clock (CPOL/CPHA) 4 modi (0,0 / 0,1 / 1,0 / 1,1) 1 modo fisso Uso tipico embedded Flash, DAC/ADC, display OLED Sensori, RTC, expander I/O

2 · Temporizzazione SPI mode 0,0 e configurazione MSSP

Temporizzazione SPI mode 0,0 (CPOL=0, CPHA=0) — 8 bit CS SCK MOSI MISO CS attivo basso — 8 periodi clock 765 432 10 TX: 1 0 1 0 0 1 0 1 = 0xA5 (MOSI = SDO del PIC) RX: 0 1 0 1 1 0 1 0 = 0x5A (MISO = SDI del PIC) Dati campionati sul fronte di SALITA di SCK (CPHA=0). Cambiano sul fronte di discesa.

🔑 SSPSTAT per mode 0,0: impostare SMP=0 (campiona al centro del bit, obbligatorio in Master mode) e CKE=1 (i dati cambiano sul fronte attivo di SCK). Con CKP=0 in SSPCON (clock idle basso) si ottiene esattamente CPOL=0, CPHA=0. La combinazione SSPSTAT = 0x40 (CKE=1, SMP=0) è quella più comune per ADC e DAC SPI standard.


3 · Codice — configurazione MSSP SPI e trasferimento base

; ============================================================
; Lezione 29 — SPI Master: configurazione MSSP e invio byte
; PIC16F877A @ 4 MHz, SPI mode 0,0 (CPOL=0, CPHA=0)
; Fbaud = Fosc/4 = 1 MHz (SSPM=0000)
; RC5=SDO, RC4=SDI, RC3=SCK, RB2=CS (chip select software)
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

; Registri MSSP SPI:
; SSPCON  (0x14): WCOL SSPOV SSPEN CKP SSPM3-0
;   SSPM=0000 → SPI Master Fosc/4
;   SSPM=0001 → SPI Master Fosc/16
;   SSPM=0010 → SPI Master Fosc/64
;   CKP=0 → clock idle LOW (CPOL=0)
; SSPSTAT (0x94 Bank1): SMP CKE ...
;   SMP=0 → campiona a metà (Master)
;   CKE=1 → trasmette su fronte discendente (CPHA=0 con CKP=0)
; SSPBUF  (0x13): dato TX/RX (stesso registro)

#DEFINE CS_PIN   PORTB,2       ; Chip Select attivo basso
#DEFINE CS_TRIS  TRISB,2

    CBLOCK  0x20
SPI_TX          ; byte da trasmettere
SPI_RX          ; byte ricevuto
    ENDC

    ORG     0x0000
    GOTO    INIT

    ORG     0x0005
INIT
    BSF     STATUS,RP0         ; Bank 1
    ; RC3=SCK output, RC4=SDI input, RC5=SDO output
    BCF     TRISC,3            ; SCK output
    BSF     TRISC,4            ; SDI input
    BCF     TRISC,5            ; SDO output
    BCF     CS_TRIS            ; CS output
    ; SSPSTAT: SMP=0, CKE=1 (mode 0,0)
    MOVLW   b'01000000'        ; CKE=1
    MOVWF   SSPSTAT
    BCF     STATUS,RP0         ; Bank 0

    ; CS alto (inattivo) all'avvio
    BSF     CS_PIN

    ; SSPCON: SSPEN=1, CKP=0, SSPM=0000 (Fosc/4=1MHz)
    MOVLW   b'00100000'        ; SSPEN=1, SSPM=0000
    MOVWF   SSPCON

; ============================================================
; MAIN: invia byte 0xA5 e legge risposta
; ============================================================
MAIN
    MOVLW   0xA5
    CALL    SPI_TRANSFER       ; TX=0xA5, RX salvato in SPI_RX
    MOVLW   0x3C
    CALL    SPI_TRANSFER
    GOTO    MAIN

; ============================================================
; SPI_TRANSFER — invia W, restituisce byte ricevuto in SPI_RX
; CS non gestito qui: chiamare CS_LOW/CS_HIGH intorno
; ============================================================
SPI_TRANSFER
    MOVWF   SSPBUF             ; avvia trasmissione
SPI_WAIT
    BSF     STATUS,RP0
    BTFSS   SSPSTAT,BF         ; BF=1 → trasmissione completata
    GOTO    SPI_WAIT
    BCF     STATUS,RP0
    MOVF    SSPBUF,W           ; leggi byte ricevuto (azzera BF)
    MOVWF   SPI_RX
    RETURN

CS_LOW
    BCF     CS_PIN
    RETURN

CS_HIGH
    BSF     CS_PIN
    RETURN

    END

4 · Codice — lettura ADC esterno MCP3204 (12 bit)

Il MCP3204 è un ADC SPI a 12 bit con 4 canali. La comunicazione richiede 3 byte: il primo trasmette il bit di START (bit0=1), il secondo seleziona la modalità (single-ended o differenziale) e il canale, il terzo è dummy per ricevere gli ultimi 8 bit. Il risultato a 12 bit si ottiene combinando i 4 bit meno significativi del byte 2 con i 8 bit del byte 3.

; ============================================================
; Lezione 29 — SPI: lettura ADC esterno MCP3204 a 12 bit
; MCP3204: 4 canali, 12 bit, SPI mode 0,0
; Sequenza: CS↓ → start(1) → SGL(1) → D2(0) → D1(0) → D0(ch) → 0 → 12 bit
; Risposta: null bit + B11..B0
; PIC16F877A @ 4 MHz
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

#DEFINE CS_MCP   PORTB,2
#DEFINE CS_TRIS  TRISB,2

    CBLOCK  0x20
ADC_HI          ; bit11:8 del risultato
ADC_LO          ; bit7:0 del risultato
SPI_RX
TX_BYTE
    ENDC

    ORG     0x0000
    GOTO    INIT

    ORG     0x0005
INIT
    BSF     STATUS,RP0
    BCF     TRISC,3            ; SCK
    BSF     TRISC,4            ; SDI
    BCF     TRISC,5            ; SDO
    BCF     CS_TRIS
    MOVLW   b'01000000'        ; CKE=1, SMP=0
    MOVWF   SSPSTAT
    BCF     STATUS,RP0
    BSF     CS_MCP             ; CS alto
    MOVLW   b'00100000'        ; SSPEN=1, SSPM=0000 (Fosc/4)
    MOVWF   SSPCON

MAIN
    ; Leggi canale 0 (CH0, single-ended)
    MOVLW   0
    CALL    MCP3204_READ
    ; ADC_HI:ADC_LO contiene il valore 12 bit (0-4095)
    MOVF    ADC_LO,W
    MOVWF   PORTB              ; mostra byte basso su PORTB (debug)
    CALL    DELAY_10MS
    GOTO    MAIN

; ============================================================
; MCP3204_READ — legge canale W (0-3) dal MCP3204
; Risultato: ADC_HI (bit11:8), ADC_LO (bit7:0)
; Protocollo SPI MCP3204:
;   Byte 1 TX: 0000 0001  (leading zeros + start bit)
;   Byte 2 TX: 1000 dcba  (SGL=1, D2=0, D1=D0=canale)
;   Byte 3 TX: 0000 0000  (clocking out)
;   Risposta sul byte 2 (bit3:0) e byte 3 (bit7:0) = 12 bit
; ============================================================
MCP3204_READ
    ; Prepara comando canale: SGL=1, ch selezionato
    ; Byte 2: 1000 00ch → per ch=0: 0x80, ch=1: 0x90, ch=2: 0xA0, ch=3: 0xB0
    MOVWF   TX_BYTE            ; salva numero canale (0-3)
    BCF     STATUS,C
    RLF     TX_BYTE,F          ; × 2
    RLF     TX_BYTE,F          ; × 4
    RLF     TX_BYTE,F          ; × 8
    RLF     TX_BYTE,F          ; × 16 → canale in posizione bit4
    MOVF    TX_BYTE,W
    IORLW   b'10000000'        ; SGL=1 (bit7)
    MOVWF   TX_BYTE

    ; Avvia transazione SPI
    BCF     CS_MCP             ; CS basso

    ; Byte 1: 0x01 (start bit in posizione bit0)
    MOVLW   0x01
    CALL    SPI_TX

    ; Byte 2: comando canale
    MOVF    TX_BYTE,W
    CALL    SPI_TX
    ; bit3:0 di SPI_RX contengono bit11:8 del risultato
    MOVF    SPI_RX,W
    ANDLW   0x0F
    MOVWF   ADC_HI

    ; Byte 3: dummy 0x00 per ricevere bit7:0
    MOVLW   0x00
    CALL    SPI_TX
    MOVF    SPI_RX,W
    MOVWF   ADC_LO

    BSF     CS_MCP             ; CS alto
    RETURN

; ============================================================
; SPI_TX — trasmette W, risposta in SPI_RX
; ============================================================
SPI_TX
    MOVWF   SSPBUF
STW  BSF     STATUS,RP0
    BTFSS   SSPSTAT,BF
    GOTO    STW
    BCF     STATUS,RP0
    MOVF    SSPBUF,W
    MOVWF   SPI_RX
    RETURN

DELAY_10MS
    MOVLW   D'10'
    MOVWF   0x70
DLA MOVLW   D'250'
    MOVWF   0x71
DLB DECFSZ  0x71,F
    GOTO    DLB
    DECFSZ  0x70,F
    GOTO    DLA
    RETURN

    END

5 · DAC MCP4921 — pacchetto 16 bit e segnale LDAC

Pacchetto 16 bit MCP4921 — formato e header A/B 15 BUF 14 GA 13 SHDN 12 D11 11 D10 D9 D8 D7 7 D6 D5 D4 D3 D2 D1 D0 0 Byte alto (TX prima) Byte basso (TX secondo) Valori header per operazione normale (canale A, output ON, gain ×1) A/B=0 canale A BUF=0 no buffer Vref GA=1 gain ×1 (0=×2) SHDN=1 output attivo → byte alto = 0011 D11D10D9D8 IORLW b'00110000' sul nibble alto del valore Vout = Vref × (D / 4096) × gain Vref=2.048V, D=2048 → Vout=1.024V

Il MCP4921 riceve un pacchetto da 16 bit (2 byte SPI) contenente 4 bit di controllo e 12 bit di dato. Il segnale LDAC (Load DAC) permette di caricare il valore nel registro interno senza ancora aggiornare l’uscita analogica: tenendo LDAC alto si può preparare il dato, poi abbassare LDAC su più DAC contemporaneamente per un aggiornamento sincronizzato — utile ad esempio per generare forme d’onda multi-canale in fase.

; ============================================================
; Lezione 29 — SPI: scrittura su DAC MCP4921 a 12 bit
; MCP4921: 1 canale, 12 bit, SPI mode 0,0
; Pacchetto 16 bit: A/B | BUF | GA | SHDN | D11..D0
; A/B=0 (canale A), BUF=0, GA=1 (×1), SHDN=1 (output ON)
; Byte alto: 0011 D11 D10 D9 D8
; Byte basso: D7 D6 D5 D4 D3 D2 D1 D0
; Genera rampa 0→4095 in loop (onda dente di sega)
; PIC16F877A @ 4 MHz
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

#DEFINE CS_DAC   PORTB,3
#DEFINE LDAC_PIN PORTB,4       ; LDAC: latch output (attivo basso)

    CBLOCK  0x20
DAC_HI          ; byte alto del pacchetto 16 bit
DAC_LO          ; byte basso
RAMP_HI         ; valore corrente rampa (bit11:8)
RAMP_LO         ; valore corrente rampa (bit7:0)
SPI_RX
    ENDC

    ORG     0x0000
    GOTO    INIT

    ORG     0x0005
INIT
    BSF     STATUS,RP0
    BCF     TRISC,3
    BSF     TRISC,4
    BCF     TRISC,5
    BCF     TRISB,3            ; CS_DAC output
    BCF     TRISB,4            ; LDAC output
    MOVLW   b'01000000'
    MOVWF   SSPSTAT
    BCF     STATUS,RP0
    BSF     CS_DAC
    BSF     LDAC_PIN           ; LDAC alto (inattivo)
    MOVLW   b'00100000'
    MOVWF   SSPCON

    CLRF    RAMP_HI
    CLRF    RAMP_LO

; ============================================================
; MAIN: rampa 0→4095, poi ricomincia
; ============================================================
MAIN
    ; Costruisci pacchetto MCP4921
    ; Byte alto: 0011 + bit11:8 di DAC value
    ; Header: A/B=0, BUF=0, GA=1, SHDN=1 → 0011
    MOVF    RAMP_HI,W
    ANDLW   0x0F               ; solo bit3:0 (= bit11:8 del valore)
    IORLW   b'00110000'        ; header 0011
    MOVWF   DAC_HI

    MOVF    RAMP_LO,W
    MOVWF   DAC_LO

    ; Invia al MCP4921
    CALL    MCP4921_WRITE

    ; Incrementa valore rampa (16 bit)
    INCF    RAMP_LO,F
    BTFSC   STATUS,Z           ; overflow byte basso?
    INCF    RAMP_HI,F
    ; Limita a 12 bit (max 0x0FFF)
    MOVF    RAMP_HI,W
    ANDLW   0x0F
    MOVWF   RAMP_HI

    GOTO    MAIN

; ============================================================
; MCP4921_WRITE — invia DAC_HI:DAC_LO al DAC
; ============================================================
MCP4921_WRITE
    BCF     CS_DAC             ; CS basso
    MOVF    DAC_HI,W
    CALL    SPI_TX
    MOVF    DAC_LO,W
    CALL    SPI_TX
    BSF     CS_DAC             ; CS alto

    ; Strobe LDAC per aggiornare output
    BCF     LDAC_PIN           ; LDAC basso → latch dati
    NOP
    NOP
    BSF     LDAC_PIN           ; LDAC alto
    RETURN

; ============================================================
; SPI_TX — invia W via SPI, risultato ignorato (DAC = write only)
; ============================================================
SPI_TX
    MOVWF   SSPBUF
SPI_W
    BSF     STATUS,RP0
    BTFSS   SSPSTAT,BF
    GOTO    SPI_W
    BCF     STATUS,RP0
    MOVF    SSPBUF,W           ; svuota buffer (necessario)
    MOVWF   SPI_RX
    RETURN

    END

🧪 Prova tu

  1. Carica il programma base. Nel pannello SPI del simulatore, collega uno slave loopback (MISO=MOSI). Esegui SPI_TRANSFER con W=0xA5: SPI_RX deve contenere 0xA5. Verifica nel pannello che SCK ha generato 8 impulsi durante il trasferimento.
  2. Nel programma MCP3204, calcola manualmente il valore atteso per CH0 con tensione di ingresso = 2.5V e Vref=5V. Formula: D = 2.5/5 × 4096 = 2048 = 0x800. Verifica che ADC_HI=0x08 e ADC_LO=0x00 nel simulatore con CH0=2048.
  3. Carica il DAC. Calcola il pacchetto per Vout=1.0V con Vref=2.048V e gain×1: D = 1.0/2.048 × 4096 = 2000 = 0x7D0. Byte alto = 0x37, byte basso = 0xD0. Verifica che il simulatore invii esattamente questi due byte e che LDAC faccia un impulso basso.
  4. Sfida: combina MCP3204 e MCP4921 in un convertitore analogico passante: leggi CH0 dal MCP3204 (12 bit), converti da 12 a 12 bit (scala 1:1), scrivi sul MCP4921. Il risultato: Vout segue Vin in tempo reale. Attenzione ai CS separati (RB2 per ADC, RB3 per DAC).

❓ Quiz rapido

1. Perché dopo aver scritto in SSPBUF bisogna leggere SSPBUF prima del prossimo trasferimento, anche se il dato ricevuto non interessa?

Mostra risposta

Il flag BF (Buffer Full) in SSPSTAT viene settato quando un byte è stato completamente ricevuto in SSPBUF. L’unico modo per azzerare BF e permettere un nuovo trasferimento è leggere SSPBUF. Se non si legge, BF rimane alto e il prossimo tentativo di scrittura in SSPBUF verrà ignorato (oppure il flag WCOL di SSPCON viene settato, segnalando una collisione scrittura). Ignorare la lettura di SSPBUF causa quindi il blocco silenzioso del modulo SPI dopo il primo byte.


2. Il MCP3204 supporta anche la modalità differenziale (non solo single-ended). Come cambia il byte di comando per leggere CH0+ vs CH0-?

Mostra risposta

Il bit SGL/DIFF (bit7 del secondo byte) seleziona la modalità: SGL=1 → single-ended (misura vs GND), SGL=0 → differenziale. In modalità differenziale i canali sono accoppiati: CH0/CH1 formano una coppia differenziale, dove CH0 è IN+ e CH1 è IN−. Il byte di comando differenziale per CH0+/CH1− è 0x00 (SGL=0, D2=D1=D0=0), contro 0x80 per single-ended CH0. Il risultato può essere positivo o negativo — il MCP3204 restituisce un valore in complemento a due se IN+ < IN−.


3. Con SSPM=0000 (Fosc/4) a 4MHz la velocità SPI è 1MHz. Quanto tempo impiega un trasferimento completo di 3 byte (MCP3204)?

Mostra risposta

A 1MHz ogni bit richiede 1µs, quindi 8 bit = 8µs per byte. Tre byte = 24µs di clock SPI. A questo vanno aggiunti: il tempo di abbassamento e rialzamento del CS (2 istruzioni × 1µs = ~2µs), il tempo di propagazione tra un byte e il successivo (lettura SSPBUF + scrittura nuovo valore = ~2-4 istruzioni = ~2-4µs). Totale stimato: ~30µs per una lettura completa. A confronto, la stessa lettura via I²C a 100kHz richiederebbe circa 240µs (3 byte × 9 bit × 10µs/bit) — 8 volte più lento. Con Fosc/4 e 20MHz di oscillatore si scende a 7.5µs totali.

Prossima lezione →

Lezione 30 — Comparatori e tensione di riferimento interna
I moduli comparatori C1/C2 del PIC16F628A: CMCON, VRCON, tensione di riferimento programmabile (0–Vdd in 32 passi), interrupt su cambio comparatore.

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