Lezione 27 di 43

Lezione 26

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

ADC — Convertitore Analogico-Digitale 🌡️

Il modulo ADC a 10 bit del PIC16F877A. Lettura di tensioni analogiche,
scansione multi-canale e termometro digitale con output UART.

🎯 Cosa impari in questa lezione

  • Come funziona un ADC SAR (Successive Approximation Register) a 10 bit
  • Registri ADCON0 e ADCON1: selezione canale, clock ADC, configurazione pin
  • Tabella PCFG: quanti e quali pin sono analogici o digitali
  • Parametro ADFM: giustificazione destra/sinistra del risultato a 10 bit in 16 bit
  • Tempo di acquisizione Tacq ≥ 20µs e clock ADC Tad ≥ 1.6µs
  • Scansione sequenziale di più canali con modifica di CHS in ADCON0
  • Conversione ADC → temperatura e trasmissione via UART come stringa “T=023C”

📋 Scheda lezione

Lezione: 26 / 42
Modulo: 5 — Periferiche avanzate
Device: PIC16F877A (nuovo!)
Prerequisiti: Lez. 25 (UART)
Difficoltà: ★★★★☆
Periferica: ADC 10 bit, 8 canali
Applicazione: termometro + UART


1 · Come funziona l’ADC SAR

Il PIC16F877A include un ADC a 10 bit con 8 canali multiplexati (AN0–AN7) e architettura SAR (Successive Approximation Register). Il SAR funziona per tentativi binari successivi: prima prova se il segnale è nella metà alta o bassa del range, poi divide ulteriormente la metà selezionata, e così via per 10 passi — come cercare un numero tra 0 e 1023 chiedendo sempre “è maggiore di X?”. Ogni passo richiede un periodo di clock ADC (Tad), che deve essere almeno 1.6µs. Con Fosc=4MHz e ADCS=01 si ottiene Tad=2µs, la conversione completa dura 10×2µs=20µs.

Pipeline ADC PIC16F877A — dal segnale analogico al valore digitale Segnale AN0–AN7 0–Vref Sample & Hold Tacq ≥ 20µs carica Chold SAR ADC 10 cicli × Tad Tad = Fosc/8 = 2µs ADRESH:ADRESL 10 bit: 0–1023 GO/DONE → 0 CPU legge ADRESH (Bank 0) ADRESL (Bank 1) ADCON0 (Bank 0) ADCS1 ADCS0 CHS2 CHS1 CHS0 GO — ADON 0 1 0 0 0 0 1 ADCS=01→Fosc/8 CHS=000→AN0 ADON=1→ADC acceso ADCON1 (Bank 1) ADFM — — — PCFG3 PCFG2 PCFG1 PCFG0 1 1 1 1 0 ADFM=1→destra PCFG=1110→solo AN0 analogico

2 · Configurazione pin e giustificazione risultato

PCFG — tabella configurazione pin analogici/digitali (selezione) PCFG Pin analogici Vref+ Vref− 0000 AN0–AN7 (tutti) Vdd Vss 0101 AN0–AN2 Vdd Vss 1110 AN0 (solo uno) Vdd Vss 0001 AN0–AN7, Vref+ su AN3 AN3 (esterno) Vss 1111 nessuno (tutti digitali) — — Riga verde = usata nei programmi di questa lezione. Tabella completa nel datasheet PIC16F877A §11. Conversione ADC → temperatura — giustificazione e formula ADFM=1 — Giustificato a destra (consigliato) 0 0 0 0 0 0 9 8 ADRESH 7 0 ADRESL (Bank 1) Formula di conversione Risoluzione = Vref / 1023 = 5V / 1023 = 4.887 mV/step V(AN0) = ADC × 4.887 mV T(°C) = ADC × 100 / 1023 (modello lineare 0V=0°C, 5V=100°C)

⚠️ Errore comune: dimenticare di configurare ADCON1 e lasciare PCFG=0000 (tutti gli 8 pin analogici). Se poi si tenta di usare PORTB o PORTC come digitali, i bit corrispondenti si comportano come analogici e si leggono sempre 0. Regola: sempre configurare ADCON1 esplicitamente anche se si usano solo pin digitali — impostare PCFG=1111 per disabilitare completamente l’ADC.


3 · Codice — lettura singolo canale AN0

; ============================================================
; Lezione 26 — ADC: lettura singolo canale (polling)
; PIC16F877A @ 4 MHz
; Legge AN0 (RA0), converte a 10 bit, risultato in ADRESH:ADRESL
; Tensione: V = (risultato / 1023) × Vref (tipico 5V)
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

; Registri ADC PIC16F877A:
; ADCON0 (0x1F) Bank0: ADCS1 ADCS0 CHS2 CHS1 CHS0 GO/DONE — ADON
; ADCON1 (0x9F) Bank1: ADFM — — — PCFG3 PCFG2 PCFG1 PCFG0
; ADRESH (0x1E) Bank0, ADRESL (0x9E) Bank1
; ADFM=1: risultato giustificato a destra (bit9:0 in ADRESH:ADRESL)
; ADFM=0: giustificato a sinistra (bit9:2 in ADRESH, bit1:0 in ADRESL)

    CBLOCK  0x20
ADC_HI          ; byte alto risultato (bit9:8)
ADC_LO          ; byte basso risultato (bit7:0)
    ENDC

    ORG     0x0000
    GOTO    INIT

    ORG     0x0005
INIT
    ; --- Configura ADC ---
    BSF     STATUS,RP0         ; Bank 1

    ; ADCON1: ADFM=1 (destra), AN0=analogico, AN1-AN7=digitali
    ; PCFG=1110 → solo AN0 analogico, Vref=Vdd
    MOVLW   b'10001110'        ; ADFM=1, PCFG=1110
    MOVWF   ADCON1

    ; RA0 come input (AN0)
    BSF     TRISA,0
    BCF     STATUS,RP0         ; Bank 0

    ; ADCON0: ADCS=01 (Fosc/8 → Tad=2µs), CHS=000 (AN0), ADON=1
    ; Tad minimo richiesto: 1.6µs → Fosc/8 @ 4MHz = 2µs OK
    MOVLW   b'01000001'        ; ADCS=01, CHS=000, ADON=1
    MOVWF   ADCON0

    ; Attendi tempo di acquisizione: min 20µs (11 istruzioni @ 4MHz)
    MOVLW   D'6'
    MOVWF   0x70
ACQ DECFSZ  0x70,F
    GOTO    ACQ

; ============================================================
; MAIN: acquisisce continuamente AN0
; ============================================================
MAIN
    CALL    ADC_READ
    ; ADC_HI:ADC_LO ora contiene il valore 10 bit (0-1023)
    ; Mostra byte basso su PORTB (0-255, risoluzione ridotta)
    MOVF    ADC_LO,W
    MOVWF   PORTB

    ; Piccolo delay prima della prossima conversione
    CALL    DELAY_100US
    GOTO    MAIN

; ============================================================
; ADC_READ — avvia conversione e attende completamento
; Risultato: ADC_HI = bit9:8, ADC_LO = bit7:0
; ============================================================
ADC_READ
    ; Avvia conversione: setta GO/DONE (bit2 ADCON0)
    BSF     ADCON0,GO          ; GO=1 → avvia
ADC_WAIT
    BTFSC   ADCON0,GO          ; GO=0 → conversione completata
    GOTO    ADC_WAIT

    ; Leggi risultato (ADFM=1, giustificato a destra)
    MOVF    ADRESH,W
    MOVWF   ADC_HI             ; bit9:8 (solo bit1:0 significativi)
    BSF     STATUS,RP0
    MOVF    ADRESL,W           ; ADRESL è in Bank 1
    BCF     STATUS,RP0
    MOVWF   ADC_LO             ; bit7:0
    RETURN

DELAY_100US
    MOVLW   D'25'
    MOVWF   0x71
D1  DECFSZ  0x71,F
    GOTO    D1
    RETURN

    END

4 · Codice — scansione multi-canale (AN0, AN1, AN2)

Per leggere più canali in sequenza si cambia il campo CHS2:CHS0 (bit6:4 di ADCON0) tra una lettura e l’altra. Dopo ogni cambio di canale è necessario attendere nuovamente il tempo di acquisizione Tacq ≥ 20µs per permettere al condensatore di campionamento di caricarsi al nuovo livello di tensione.

; ============================================================
; Lezione 26 — ADC: scansione multi-canale (AN0, AN1, AN2)
; PIC16F877A @ 4 MHz
; Acquisisce 3 canali in sequenza, salva in buffer RAM
; PCFG=0101 → AN0,AN1,AN2 analogici, resto digitali
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

    CBLOCK  0x20
CH0_HI          ; AN0 high byte
CH0_LO          ; AN0 low byte
CH1_HI          ; AN1 high byte
CH1_LO          ; AN1 low byte
CH2_HI          ; AN2 high byte
CH2_LO          ; AN2 low byte
ADC_TEMP
    ENDC

; Macro per selezionare canale ADC
; Uso: SELECT_CH 0  oppure  SELECT_CH 1
; Modifica i bit CHS2:CHS0 (bit6:4) di ADCON0
#DEFINE SELECT_CH(ch) \
    MOVF    ADCON0,W  ;\
    ANDLW   b'10001111' ;\
    IORLW   (ch << 4) ;\
    MOVWF   ADCON0

    ORG     0x0000
    GOTO    INIT

    ORG     0x0005
INIT
    BSF     STATUS,RP0
    ; PCFG=0101: AN0,AN1,AN2 analogici; Vref=Vdd
    MOVLW   b'10000101'        ; ADFM=1, PCFG=0101
    MOVWF   ADCON1
    MOVLW   b'00000111'        ; RA0,RA1,RA2 input
    MOVWF   TRISA
    CLRF    TRISB
    BCF     STATUS,RP0

    ; ADCON0: ADCS=01 (Fosc/8), canale 0, ADC ON
    MOVLW   b'01000001'
    MOVWF   ADCON0

    CLRF    PORTB

MAIN
    CALL    SCAN_ALL
    ; Mostra CH0_LO su PORTB (debug rapido)
    MOVF    CH0_LO,W
    MOVWF   PORTB
    GOTO    MAIN

; ============================================================
; SCAN_ALL — acquisisce AN0, AN1, AN2 in sequenza
; ============================================================
SCAN_ALL
    ; --- AN0 ---
    MOVF    ADCON0,W
    ANDLW   b'10001111'        ; azzera CHS
    MOVWF   ADCON0             ; CHS=000 → AN0
    CALL    ACQUIRE_AND_READ
    MOVF    ADRESH,W
    MOVWF   CH0_HI
    BSF     STATUS,RP0
    MOVF    ADRESL,W
    BCF     STATUS,RP0
    MOVWF   CH0_LO

    ; --- AN1 ---
    MOVF    ADCON0,W
    ANDLW   b'10001111'
    IORLW   b'00010000'        ; CHS=001 → AN1
    MOVWF   ADCON0
    CALL    ACQUIRE_AND_READ
    MOVF    ADRESH,W
    MOVWF   CH1_HI
    BSF     STATUS,RP0
    MOVF    ADRESL,W
    BCF     STATUS,RP0
    MOVWF   CH1_LO

    ; --- AN2 ---
    MOVF    ADCON0,W
    ANDLW   b'10001111'
    IORLW   b'00100000'        ; CHS=010 → AN2
    MOVWF   ADCON0
    CALL    ACQUIRE_AND_READ
    MOVF    ADRESH,W
    MOVWF   CH2_HI
    BSF     STATUS,RP0
    MOVF    ADRESL,W
    BCF     STATUS,RP0
    MOVWF   CH2_LO
    RETURN

; ============================================================
; ACQUIRE_AND_READ — attende Tacq, avvia, attende fine
; ============================================================
ACQUIRE_AND_READ
    ; Tacq >= 20µs: 5 cicli × 4µs/ciclo = 20µs
    MOVLW   D'5'
    MOVWF   ADC_TEMP
ACQ DECFSZ  ADC_TEMP,F
    GOTO    ACQ
    ; Avvia e attendi
    BSF     ADCON0,GO
ACW BTFSC   ADCON0,GO
    GOTO    ACW
    RETURN

    END

5 · Codice — termometro digitale ADC + UART

Integrazione di ADC e UART in un’applicazione completa: il PIC legge un sensore di temperatura simulato su AN0, converte il valore ADC in gradi centigradi con una formula lineare, e trasmette il risultato ogni 500ms come stringa ASCII formattata T=023C . Include la moltiplicazione per 100 tramite loop di somme e la divisione a 8 bit per sottrazioni ripetute — algoritmi elementari ma esplicitamente visibili.

; ============================================================
; Lezione 26 — ADC + UART: termometro digitale simulato
; Legge AN0 (tensore NTC simulato), converte in temperatura
; Trasmette via UART: "T=023C\r\n"
; PIC16F877A @ 4 MHz, 9600 baud
; Modello lineare: T(°C) = (ADC × 100) / 1023
; (0V=0°C, 5V=100°C — sensore lineare ideale)
; ============================================================
    LIST    P=16F877A
    #INCLUDE <p16f877a.inc>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF & _BOREN_OFF & _LVP_OFF

    CBLOCK  0x20
ADC_HI
ADC_LO
TEMP_C          ; temperatura in gradi (0-100)
TEMP_DEC        ; decimali (0-9, non usato qui — arrotondamento)
TX_CHAR
; Variabili calcolo divisione
DIV_NUM_HI      ; dividendo high
DIV_NUM_LO      ; dividendo low
DIV_DEN         ; divisore
DIV_QUOT        ; quoziente
DIV_REM         ; resto
DEC_100
DEC_10
DEC_1
SCRATCH
    ENDC

    ORG     0x0000
    GOTO    INIT

    ORG     0x0005
INIT
    BSF     STATUS,RP0
    MOVLW   b'10001110'        ; ADFM=1, PCFG=1110 (solo AN0)
    MOVWF   ADCON1
    BSF     TRISA,0            ; RA0 input analogico
    CLRF    TRISB
    CLRF    TRISC
    MOVLW   D'25'
    MOVWF   SPBRG              ; 9600 baud @ 4MHz
    MOVLW   b'00100100'
    MOVWF   TXSTA              ; TXEN=1, BRGH=1
    BCF     STATUS,RP0
    MOVLW   b'01000001'
    MOVWF   ADCON0             ; ADCS=01, AN0, ADON
    MOVLW   b'10000000'
    MOVWF   RCSTA              ; SPEN=1

MAIN
    ; 1. Leggi ADC
    CALL    ADC_READ

    ; 2. Calcola temperatura: T = (ADC_LO × 100) / 1023
    ;    Approssimazione: T ≈ ADC_LO × 100 / 255 (solo 8 bit)
    ;    Errore max ~25% ma sufficiente per l'esempio didattico
    ;    Per 10 bit completi servirebbe moltiplicazione 16 bit
    MOVF    ADC_LO,W
    MOVWF   DIV_NUM_LO         ; usa solo gli 8 bit bassi
    ; Moltiplica × 100 (shift + somme)
    CALL    MUL_100
    ; Dividi per 255
    MOVLW   D'255'
    MOVWF   DIV_DEN
    CALL    DIV8
    MOVF    DIV_QUOT,W
    MOVWF   TEMP_C

    ; 3. Trasmetti "T="
    MOVLW   'T'
    CALL    UART_TX
    MOVLW   '='
    CALL    UART_TX

    ; 4. Trasmetti valore decimale su 3 cifre
    MOVF    TEMP_C,W
    MOVWF   SCRATCH
    CALL    SEND_DEC3

    ; 5. Trasmetti "C\r\n"
    MOVLW   'C'
    CALL    UART_TX
    MOVLW   0x0D
    CALL    UART_TX
    MOVLW   0x0A
    CALL    UART_TX

    CALL    DELAY_500MS
    GOTO    MAIN

; ============================================================
; MUL_100 — DIV_NUM_LO × 100 → DIV_NUM_HI:DIV_NUM_LO
; Implementato come: N×64 + N×32 + N×4
; ============================================================
MUL_100
    MOVF    DIV_NUM_LO,W
    MOVWF   SCRATCH
    ; N × 4
    BCF     STATUS,C
    RLF     SCRATCH,F          ; ×2
    BCF     STATUS,C
    RLF     SCRATCH,F          ; ×4
    ; Salva N×4
    MOVF    SCRATCH,W
    MOVWF   DIV_NUM_LO
    CLRF    DIV_NUM_HI
    ; Aggiungi N×32 e N×64 (semplificato: uso diretta N×100)
    ; Alternativa semplice: loop di somme
    ; Reimplementazione: moltiplicazione per sottrazione ripetuta
    ; Per semplicità didattica usiamo loop: somma DIV_NUM_LO 100 volte
    ; (costoso ma chiaro)
    MOVF    ADC_LO,W
    MOVWF   SCRATCH
    CLRF    DIV_NUM_LO
    CLRF    DIV_NUM_HI
    MOVLW   D'100'
    MOVWF   DEC_100
MUL_LOOP
    MOVF    SCRATCH,W
    ADDWF   DIV_NUM_LO,F
    BTFSC   STATUS,C
    INCF    DIV_NUM_HI,F
    DECFSZ  DEC_100,F
    GOTO    MUL_LOOP
    RETURN

; ============================================================
; DIV8 — DIV_NUM_LO / DIV_DEN → DIV_QUOT, resto in DIV_REM
; (divisione senza segno 8 bit)
; ============================================================
DIV8
    CLRF    DIV_QUOT
    MOVF    DIV_NUM_LO,W
    MOVWF   DIV_REM
DIV_LOOP
    MOVF    DIV_DEN,W
    SUBWF   DIV_REM,W
    BTFSS   STATUS,C           ; C=0 → resto &lt; divisore
    GOTO    DIV_DONE
    MOVWF   DIV_REM
    INCF    DIV_QUOT,F
    GOTO    DIV_LOOP
DIV_DONE
    RETURN

; ============================================================
; SEND_DEC3 — trasmette SCRATCH come 3 cifre decimali
; ============================================================
SEND_DEC3
    ; Centinaia
    CLRF    DEC_100
SD3_100
    MOVF    SCRATCH,W
    SUBLW   D'100'
    BTFSS   STATUS,C
    GOTO    SD3_10
    MOVLW   D'100'
    SUBWF   SCRATCH,F
    INCF    DEC_100,F
    GOTO    SD3_100
SD3_10
    CLRF    DEC_10
SD3_10L
    MOVF    SCRATCH,W
    SUBLW   D'10'
    BTFSS   STATUS,C
    GOTO    SD3_1
    MOVLW   D'10'
    SUBWF   SCRATCH,F
    INCF    DEC_10,F
    GOTO    SD3_10L
SD3_1
    MOVF    SCRATCH,W
    MOVWF   DEC_1
    ; Trasmetti
    MOVF    DEC_100,W
    ADDLW   '0'
    CALL    UART_TX
    MOVF    DEC_10,W
    ADDLW   '0'
    CALL    UART_TX
    MOVF    DEC_1,W
    ADDLW   '0'
    CALL    UART_TX
    RETURN

ADC_READ
    BSF     ADCON0,GO
ADR BTFSC   ADCON0,GO
    GOTO    ADR
    MOVF    ADRESH,W
    MOVWF   ADC_HI
    BSF     STATUS,RP0
    MOVF    ADRESL,W
    BCF     STATUS,RP0
    MOVWF   ADC_LO
    RETURN

UART_TX
    MOVWF   TX_CHAR
    BSF     STATUS,RP0
UTW BTFSS   TXSTA,TRMT
    GOTO    UTW
    BCF     STATUS,RP0
    MOVF    TX_CHAR,W
    MOVWF   TXREG
    RETURN

DELAY_500MS
    MOVLW   D'200'
    MOVWF   DEC_100
D5A MOVLW   D'250'
    MOVWF   DEC_10
D5B MOVLW   D'10'
    MOVWF   DEC_1
D5C DECFSZ  DEC_1,F
    GOTO    D5C
    DECFSZ  DEC_10,F
    GOTO    D5B
    DECFSZ  DEC_100,F
    GOTO    D5A
    RETURN

    END

🧪 Prova tu

  1. Carica il programma singolo canale. Nel pannello ADC del simulatore, imposta AN0 a 512 (metà scala). Esegui ADC_READ: ADC_HI deve essere 1 (bit9:8 = 0b10) e ADC_LO deve essere 0x00 (512 = 0b10_00000000). Verifica PORTB = 0x00.
  2. Imposta AN0 a 1023 (fondo scala, 5V). Esegui: ADC_HI = 0x03, ADC_LO = 0xFF. PORTB = 0xFF. Poi imposta AN0 a 0: ADC_HI = 0x00, ADC_LO = 0x00.
  3. Carica il termometro. Imposta AN0 a 512. Calcola manualmente: T = 512 × 100 / 255 ≈ 200. Esegui e verifica che UART trasmetta “T=200C “. (Nota: usiamo 255 come denominatore invece di 1023 — perché?)
  4. Sfida: modifica il termometro per usare il risultato a 10 bit completo (ADC_HI:ADC_LO) con la formula corretta T = ADC10 × 100 / 1023. Richiede moltiplicazione a 16 bit: implementa MUL16 che calcola ADC_LO × 100 con propagazione del carry in ADC_HI.

❓ Quiz rapido

1. Perché bisogna attendere Tacq ≥ 20µs dopo aver cambiato canale, prima di avviare la conversione?

Mostra risposta

L’ADC SAR usa un condensatore interno (tipicamente 120pF) che si carica al livello di tensione del pin selezionato tramite una resistenza di campionamento (tipicamente 7kΩ). La costante di tempo RC di questo circuito è ~840ns. Per una precisione a 10 bit il condensatore deve caricarsi con un errore inferiore a 0.5 LSB, il che richiede almeno ln(2^11)×RC ≈ 7.6×840ns ≈ 6.4µs solo per la carica. Aggiungendo le resistenze di sorgente esterne e i margini di temperatura, Microchip specifica Tacq ≥ 20µs come valore sicuro. Se si avvia la conversione troppo presto, il condensatore non è ancora a regime e il risultato è errato.


2. ADFM=0 (giustificato a sinistra) è utile in quale scenario specifico?

Mostra risposta

Quando si vuole usare l’ADC come se fosse a 8 bit anziché 10 bit. Con ADFM=0 i bit9:2 del risultato sono in ADRESH (bit7:0) e i bit1:0 in ADRESL. Leggendo solo ADRESH si ottengono gli 8 bit più significativi del risultato — equivalente a una conversione a 8 bit senza dover fare shift. Questo è conveniente quando la precisione da 10 bit non è necessaria (es. display semplice, controllo PWM grossolano) e si vuole evitare di gestire un valore a 16 bit.


3. Nel termometro, la formula usa 255 come denominatore invece di 1023. Qual è l’errore massimo in °C introdotto da questa approssimazione?

Mostra risposta

Con 1023 come denominatore corretto: T_vero = ADC×100/1023. Con 255: T_approx = ADC×100/255. Il rapporto è 1023/255 ≈ 4.012, quindi T_approx ≈ T_vero × (1023/255) ≈ T_vero × 4.012/4 ≈ T_vero × 1.003. L’errore relativo è circa 0.3%. Su un range 0–100°C l’errore massimo è ~0.3°C — accettabile per molti usi. La vera limitazione è che si usano solo gli 8 bit bassi dell’ADC (ADC_LO), scartando i 2 bit alti e riducendo la risoluzione effettiva da 10 a 8 bit.

Prossima lezione →

Lezione 27 — PWM hardware con CCP (PIC16F877A)
Il modulo CCP1 in modalità PWM: frequenza, duty cycle, registro CCPR1L. Controllo di motori DC, LED dimmer e servo con segnale PWM preciso.

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