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
ADCON0eADCON1: 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.
2 · Configurazione pin e giustificazione risultato
⚠️ 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 < 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
- 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.
- 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.
- 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é?)
- 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