L'ADC del PIC18

Attenzione! Questa pagina contiene materiale obsoleto - Vai al tutorial relativo all'ambiente MPLAB X e XC8 Attenzione!

In questo breve tutorial verrà descritto come usare il convertitore analogico digitale (ADC, Analog to Digital Converter) interno al PIC18F14. Il codice mostrato non utilizza le librerie Microchip incluse con il compilatore perché, da quanto ho letto in rete, sono poco efficienti... ma soprattutto perché dopo averci perso un paio di ore non sono riuscito ad utilizzarle!

I LED sulla scheda demo

Il software di esempio, presentato in duplice veste (più un paio di varianti), si comporta sempre allo stesso modo: ruotando il trimmer visibile in secondo piano si accendono i quattro led, mostrando in binario la posizione del cursore:

Cosa è un ADC

Il convertitore interno al PIC18 è un ADC ad approssimazioni successive (SAR) il cui scopo è convertire una tensione in un numero intero di N bit (N è la risoluzione del convertitore). Oltre all'ADC il PIC include sia il multiplexer analogico (AMUX) che un circuito di campionamento (S&H). Per un approfondimento può essere d'aiuto la wikipedia (inglese) o un qualunque testo di elettronica ma, in breve:

Il convertitore interno a questo PIC è caratterizzato da una risoluzione di 10 bit, cioè il risultato è presentato come un numero compreso tra 0 e 1023. Purtroppo questo non significa che la precisione è dello 0,1%: le correzioni ai fogli tecnici parlano di un errore di offset (o di INL, su questo sono pochi chiari, confondendo i due termini) pari a ±7 bit. In pratica si comporta come un modesto 8 bit.

L'ingresso dell'ADC può essere collegato, uno alla volta, ad uno dei 9 pin delle porte con funzioni analogiche, utilizzabili ovviamente dopo aver disattivato le rispettive funzioni digitali .

Il tempo di conversione teorico dell'ADC del PIC18 può essere calcolato come prodotto del numero di cicli di clock necessari alla conversione (12) per la durata minima degli stessi (pari a 0.7 us). A questo occorre aggiungere il tempo di acquisizione che in condizioni ideali è di circa 1.5 us. In teoria si potrebbe quindi arrivare a 100 kHz.

Il circuito utilizzato negli esempi

Il circuito è quello della demo-board inclusa Low Pin Count USB Development Kit. Chi possiede tale scheda non deve aggiungere nulla. Non ci sono comunque problemi nel realizzare il circuito su breadboard.

L'ingresso analogico è costituito da un potenziometro con il cursore collegato al pin 13 del PIC (AN10 oppure RB4) e i due estremi connessi a massa e alla tensione di alimentazione. Ruotando il cursore viene generata sul pin AN10 una tensione compresa tra 0 e 3,3 V circa. Ovviamente è possibile utilizzare un altro pin, con ovvie e semplici modifiche al software. Occorre notare che i progettisti della demo-board non potevano trovare posizione peggiore per mettere il trimmer, proprio al di sotto del PICKIT2!

Il resto dell'hardware è costituito da quattro LED collegati a RC0-RC3, usati per visualizzare 4 dei 10 bit del risultato; il modo migliore per vedere il risultato della conversione è però l'inserimento di un opportuno breakpoint o l'esecuzione passo-passo.

Il codice: polling

Tutto le varie fasi della conversione sono effettuate sequenzialmente dal software. Qui il codice C completo, qui presentato in tre versioni che differiscono tra loro per aspetti marginali, in particolare nella scelta del clock e del numero di bit da utilizzare.

Il primo esempio è eseguito con un clock a 16 MHz, nota importante perché alcune scelte sono legate a questo valore. Il risultato sarà di 10 bit, pari alla risoluzione "piena" dell'ADC.

La prima operazione da effettuare è quella di indicare quali sono i pin di ingresso dell'AMUX, disattivando le funzioni digitali ad essi associati: in questo caso è utilizzato solo RB4 ma in genere potrebbero essere più di uno.

TRISBbits.TRISB4 = 1; // Set RB4 (pin 13, aka AN10) to input
ANSELHbits.ANS10 = 1; // Set AN10 (pin 13, aka RB4) to analog

Occorre quindi configurare l'ADC, attraverso tre registri.

Il registro ADCON0 permette di selezionare il canale che si vuole convertire. Permette inoltre di alimentare l'ADC  che al reset è spento.

ADCON0=0b00101001; // Channel AN10 (xx10 10xx), Enable ADC (xxxx xxx1)

Il secondo registro ADCON1 serve per impostare le tensioni di riferimento. Nell'esempio è utilizzata la tensione di alimentazione e la massa, scelta spesso conveniente, ma in genere è possibili scegliere due valori diversi (comunque compresi tra massa e alimentazione)

ADCON1=0b00000000; // VDD and VSS as voltage reference (xxxx 0000)

Il terzo registro ADCON2 imposta i seguenti tre parametri:

ADCON2=0b10001101;
  // Right justify result (1xxx xxx), 2 TAD delay (xx00 1xxx), TAD = 1 us (Fck/16 = 1 Mhz) (xxxx x101)

Queste operazioni vengono fatte una sola volta nell'esempio presentato. In genere l'unica che può essere necessario ripetere ad ogni conversione è la scelta del canale da convertire(modifica di ADCON0).

La conversione vera e propria nell'esempio avviene dentro un ciclo infinito che provvede, con i limiti permessi dalla presenza di soli 4 LED, a visualizzare il risultato della conversione.

La prima operazione è avviare la conversione. L'eventuale tempo di acquisizione e la scelta del canale della AMUX è gestito direttamente dall'hardware, secondo il valore salvato nei registri ADCON0 ADCON2.

ADCON0bits.GO = 1;     // Start conversion

Occorre quindi attendere il termine della conversione, operazione piuttosto lunga, attraverso il test dell'apposito flag

while (ADCON0bits.GO); // Wait conversion done

A questo punto occorre leggere il risultato: nel primo esempio è mostrato il caso di allineamento a destra dove prima vengono letti i due bit più significativi, spostati a sinistra di otto posizioni ed infine letti gli otto bit meno significativi.

voltage = ( (unsigned int)ADRESH << 8) + ADRESL; // Read ADC registers and convert to 0-1023 integer

Infine sono mandati su quattro LED i quattro bit più significativi. L'effetto è l'accensione dei led da 0000 a 1111 a seconda di come è ruotato il potenziometro. Come già detto l'esame completo con la lettura del "risultato" è però possibile solo usando i breakpoint o l'esecuzione passo-passo.

PORTC = (voltage >> 6) & 0x0F; // Display 4 MSB

L'esame delle prestazioni

Per valutare le prestazioni "velocistiche" del convertitore è necessaria l'esecuzione in tempo reale. Abbiamo inserito due istruzioni che segnalano l'inizio (ponendo alto RC4) e la fine della conversione (ponendo basso lo stesso bit). Il codice contiene già tale modifica, come commento. In pratica: quando RC4 è alto è in corso la conversione e il processore sta semplicemente attendendo. Visto all'oscilloscopio il tempo di conversione è in linea con quanto atteso (12 us di conversione + altri 2 di tempo di acquisizione). Guardando il codice assembly si può osservare come la gran parte del tempo di "elaborazione" (anche lui indicativamente di 14 us) è occupato dallo spostamento di 6 posizioni verso destra del risultato della conversione ai fini della visualizzazione, operazione relativamente complessa. La frequenza massima di conversione è di circa 35 kHz.

I tempi necessari per eseguire la conversione - Versione 1

Al fine di migliorare la velocità è possibile:

Si seguito i risultati ottenuti applicando le prime due tecniche descritte:

I tempi necessari per eseguire la conversione - Versione 1 I tempi necessari per eseguire la conversione - Versione 1

Il primo grafico mostra una netta diminuzione nel tempo di elaborazione che passa da 14 us a 2 us, grazie soprattutto alla sostituzione di uno shift verso destra di sei posizioni con uno shift verso destra di quattro posizioni (una lettura del codice assembler chiarisce questo aspetto apparentemente paradossale).

Il secondo grafico mostra come l'aumento del clock a 48 MHz (anziché 16) abbia ridotto di tre volte (ovviamente!) il tempo di esecuzione della parte elaborativa, sceso da 14 us a circa 5 us. Purtroppo non solo in tempo di conversione non è migliorato ma addirittura è aumentato di 3 us a causa di un TAD passato da 1.0 a 1.3 us.

Il codice: interrupt

Negli esempi precedenti si nota un problema evidente: la gran parte del tempo è sprecata dal processore semplicemente nell'attesa del termine della conversione. Usando la tecnica delle interruzioni è possibile utilizzare tale tempo per svolgere altre funzioni più utili che non un ciclo che verifica il valore di un flag. Vediamo come(il codice completo).

La configurazione dell'ADC è praticamente identica agli esempi precedenti. Occorre però attivare le interrupt a fine conversione inserendo il codice seguente, di immediata comprensione per chi ha gi utilizzato le interruzioni con il PIC18.

RCONbits.IPEN = 1;   // Enable priority levels on interrupts
PIE1bits.ADIE = 1;   // Enable interrupt from ADC
IPR1bits.ADIP = 1;   // Set high priority interrupt
PIR1bits.ADIF = 0;   // Clear interrupt status to avoid automatic interrupt ad "boot time"
INTCONbits.GIEH = 1; // Enables high priority interrupts

Occorre poi scrivere la routine di interrupt, anche questa di facile comprensione nella sua struttura per chi conosce le interrupt. Il cuore è la funzione High_Int (void)

void High_Int (void)
 {if (PIR1bits.ADIF )  // Interrupt from ADC?
   {AD_high = ADRESH;  // Get high 2 bits and save in buffer
    AD_low = ADRESL;   // Get low 8 bits and save in buffer
    AD_done_flag = 1;  // Data in buffer
    ADCON0bits.GO = 1; // Start new conversion
    PIR1bits.ADIF = 0; // Clear interrupt status
 }
}

Il codice della funzione di ISR:

La parte principale del codice, dopo le configurazioni, è un ciclo infinito che visualizza quattro bit del risultato della conversione sui LED quando questi sono disponibili:

while (1)
 { while (!AD_done_flag);
   voltage = ( (unsigned int)AD_high << 8) + AD_low;
                                  // Convert data from ADC buffeto 0-1023 integer
   AD_done_flag=0;                // Clear semaphore
   PORTC = (voltage >> 6) & 0x0F; // Display 4 MSB
}

La funzione è molto simile alle precedenti anche se le varie operazioni sono svolte non direttamente sui registri dell'ADC Impegnati in una nuova conversione, ma sulle copie salvate dalla funzione High_Int (void).

Anche in questo caso è presente il blocco di attesa while (!AD_done_flag), per il semplice fatto che l'esempio non prevede altre operazioni da fare: le operazioni di visualizzazione sono infatti eseguite in parallelo alla conversione.

L'esame delle prestazioni

Per valutare i tempi di esecuzione sono stati utilizzati i bit RC4 (posto a 1 durante l'esecuzione della funzione di interrupt) e RC5 (posto a 1 durante l'esecuzione delle operazioni inerenti la visualizzazione).

I tempi necessari per eseguire la conversione - Interrupt

L'immagine mostra i tempi di esecuzione, nel caso di clock a 48 MHz:

L'incremento di prestazioni è decisamente modesto (da 45 a 53 kHz) ma il vantaggio è che i primi tre esempi utilizzano il 100% del tempo del processore, l'ultimo poco più del 30%, lasciandolo "libero" per altre operazioni un intervallo di circa 13 us tra una conversione e l'altra.

Scarica il codice sorgente (polling, 10 bit @ 16 MHz)

Scarica il codice sorgente (polling, 8 bit @ 16 MHz)

Scarica il codice sorgente (polling, 10 bit @ 48 MHz)

Scarica il codice sorgente (interrupt)

Licenza "Creative Commons" - Attribuzione-Condividi allo stesso modo 3.0 Unported


Pagina principaleAccessibilitàNote legaliPosta elettronicaXHTML 1.0 StrictCSS 3

Vai in cima