CONVERTITORE A/D - EasyPic4 - 16F887

Il PIC16F887 dispone di 14 canali analogici. Il convertitore A/V consente di misurare la tensione su questi canali e di convertirla in un valore numerico.
Il convertitore A/D traduce l'input analogico in una rappresentazione a 10 bit del segnale che viene salvata nei due registri dell'ADC ADRESL e ADRESH.
In base al voltaggio di riferimento Vref- e Vref+ è possibile poi effettuare una corretta conversione in Volts o milliVolts a seconda delle necessità. Durante la conversione verrà associato il valore 0 alla tensione di riferimento negativa Vref- mentre 1023 a quella positiva Vref+. Quindi la tensione misurata sarà calcolata, partendo dal numero letto nei registri dell'ADC, mediante la formula:

Van=(Vref-)+{ [(Vref+)-(Vref-)]/1023 }*Numero10BitLetto

La differenza  [(Vref+)-(Vref-)] è detta tensione di riferimento Vref.

Il funzionamento del modulo A/D è basato su 4 registri

- ADRESH - Contiene i bit alti del risultato della conversione
- ADRESL - Contiene i bit bassi del risultato della conversione
- ADCON0 - Registro di controllo 0
- ADCON1 - Registro di controllo 1

Per misurare il voltaggio su uno dei pin di input con l'A/D converter seguiamo questi 3 step:

Step 1 - Configurazione delle porte

Impostiamo i pin di una porta come input attribuendo il valore 1 ai bit del registro TRIS abbinato alla porta che vogliamo configurare;

esempio:
    TRISA = 0x00;       // Tutti i pin di PORTA sono di output
    TRISA = 0xFF;       // Tutti i pin di PORTA sono di input
    TRISA = 0b00000100  // Il pin 2 (RA2) della porta A e di input
    TRISA = 0x04;       // Il pin 2 (RA2) della porta A e di input

Poniamo ad 1 il bit corrispondente nei registri ANSEL e ANSELH dei pin che vogliamo configurare come analogici;

I registri ANSEL e ANSELH sono utilizzati per impostare i pin di I/O in analogico o digitale.

 

La selezione del canale avviene settando a 1 il bit corrispondente nei registri ANSEL e ANSELH. Lo schema da seguire è il seguente:
Per la porta A solo 5 pin possono essere settati come analogici:
- il bit ANS0 {ANSEL Register} determina se il pin PORTA.0 è analogico - CH0
- il bit ANS1 {ANSEL Register} determina se il pin PORTA.1 è analogico - CH1
- il bit ANS2 {ANSEL Register} determina se il pin PORTA.2 è analogico - CH2
- il bit ANS3 {ANSEL Register} determina se il pin PORTA.3 è analogico - CH3
- il bit ANS4 {ANSEL Register} determina se il pin PORTA.4 è analogico - CH4
Per la porta E solo 3 pin possono essere settati come analogici:
- il bit ANS5 {ANSEL Register} determina se il pin PORTE.0 è analogico - CH5
- il bit ANS6 {ANSEL Register} determina se il pin PORTE.1 è analogico - CH6
- il bit ANS7 {ANSEL Register} determina se il pin PORTE.2 è analogico - CH7
Per la porta B solo 6 pin possono essere settati come analogici:
- il bit ANS8 {ANSELH Register} determina se il pin PORTB.2 è analogico - CH8
- il bit ANS9 {ANSELH Register} determina se il pin PORTB.3 è analogico - CH9
- il bit ANS10 {ANSELH Register} determina se il pin PORTB.1 è analogico - CH10
- il bit ANS11 {ANSELH Register} determina se il pin PORTB.4 è analogico - CH11
- il bit ANS12 {ANSELH Register} determina se il pin PORTB.0 è analogico - CH12
- il bit ANS13 {ANSELH Register} determina se PORTB.5 è analogico - CH13

esempio:
    ANSEL = 0xFF;       // Tutti i pin configurabili abbinati a ANSEL sono analogici
    ANSEL = 0b00000001; // Solo il pin 1 della portA è analogico
    ANSELH = 0;         // I pin della porta B configurabili sono digitali

Step 2 - Configurazione del modulo ADC:

Configurare il voltaggio di riferimento tramite il registro ADCON1;

Il bit VCFG0 seleziona la tensione di riferimento positiva:
- se è posto a 1 allora viene utilizzato come riferimento positivo la tensione al pin Vref+
- se è posto a 0 utilizza la tensione di alimentazione Vdd (tensione massima detta di funzionamento) come tensione di riferimento positiva
Il bit VCFG1 seleziona quale è la tensione di riferimento negativa utilizzata dal convertitore AD:
- se è posto a 1 la tensione di riferimento negativa è quella del pin Vref-
- se è posto a 0 utilizza la tensione di alimentazione Vss (tensione minima che corrisponde solitamente allo zero della massa comune) come tensione di riferimento negativa.

La possibilità di utilizzare anche una tensione di riferimento esterna (prodotta da un opportuno circuito) consente di implementare tensione di riferimento adeguata alla nostra  applicazione.

Caso 1: con VCFG1 a 0 e VCFG0 a 0 verrà utilizzata come tensione di riferimento la tensione di alimentazione (Vdd -Vss). Quindi se il microcontroller è alimentato a 5V questa sarà la tensione di riferimento.

Caso 2: con VCFG1 a 0 e VCFG0 a 1 verrà utilizzata come riferimento positivo la tensione ad un certo pin dedicato a ricevere la Vref+ di una fonte esterna mentre il riferimento negativo sarà quello della massa (Vss)

Caso 1: con VCFG1 a 1 e VCFG0 a 1 entrambe i pin Vref+ e Vref- verranno utilizzati. Vref+ accetterà la la tensione esterna maggiore (ma che dovrà essere comunque uguale o minore della Vdd) mentre Vref- quella minore (che potrà essere maggiore di Vss). Questa scelta consente di ottenere uno "spazio di misura" quanto più possibile adeguato alle nostre esigenze.

Riassumendo:

Tensione di riferimento Vmax Vmin
1 Tensione di alimentazione Vdd Vss
2 Massa e Vref+ esterna Vref+ Vss
3 Tensione Esterna Vref+ Vref-

esempio:
    ADCON1 = 0b..00....;       // Vref => Vdd e Vss
    ADCON1 = 0b..01....;       // Vref => Vref+ e Vss
    ADCON1 = 0b..11....;       // Vref => Vref+ e Vref-
 

Selezionare il clock dell'ADC Converter (ClockADC) con il registro ADCON0;

All'interno del microprocessore esistono diverse configurazioni che permettono di selezionare l’oscillatore che fornisce il clock al PIC (Fosc):

LP Con cristallo a bassa frequenza da 5 a 200kHz
XT Con quarzo a media frequenza da 0,1 a 4MHz
HS Con quarzo ad alta frequenza da 4 a 25MHz
RC Circuito resistore condensatore da 0 a 4MHz
EC Clock esterno da 0 a 40MHz

Il tempo totale per acquisire un dato analogico dipende da due tempi fondamentali:

- T1: tempo di acquisizione  (circa 20μS nel caso peggiore - microsecondi 10-6 - dipende dall'impedenza del circuito {sorgente di tensione} e corrisponde al tempo necessario per caricare il condensatore)
- T2: tempo di conversione (periodo che inizia quando il bit GO/DONE viene posto a 1 e termina quando questo ritorna a 0)

Il T1 dipende da fattori come la resistenza di ingresso (generatore),  la temperatura etc. Il tempo di acquisizione T1 è ottenuto come somma dei contributi:

T1 = Tamp + Tc + Tcoff

dove:

Tamp = Amplifier Settling time
Tc = Hold capacitor Charging time
Tcoff = Coefficiente di temperatura

Tali valori (Tamp, Tc, Tcoff ) sono desumibili dal datasheet. Il tempo di acquisizione T1 è invariabile, a meno di non intervenire sui parametri hardware del sistema.  

T2 invece dipende dal tempo per completare una conversione di un singolo bit (chiamato TAD). Il  TAD corrisponde al periodo di convertitore ADC per cui  TAD=1/ClockADC. Il  TAD viene desunto dal datasheet e deve essere almeno di 1.6μS (10-6 secondi). Quindi una conversione completa a 10 bit richiede un tempo pari o superiore a 10  TAD. Come si desume nella figura sottostante e di circa 11.5  TAD poichè occorre aggiungere anche il tempo perso per disconnettere/connettere il condensatore.

Il  TAD dipende dal clock di riferimento per cui dobbiamo tarare la sorgente di clock dell'ADC in modo che venga garantito il superamento del valore minimo che è almeno di 1.6μS.
E' possibile diminuire il tempo di conversione agendo via software a patto di accettare una diminuzione della precisione del valore convertito.

Ad esempio se la frequenza di clock è 4Mhz ed impostiamo come sorgente di clock del convertitore Fosc/8 avremo:

 TAD=1/ClockADC = 1/(Fosc/8)=1/(4MHz/8) = 500kHz = 2μS che è >1.6μS

Nella tabella seguente sono evidenziati i valori del  TAD in base alla frequenza di riferimento utilizzata

Clock di riferimento
per l'ADC
 converter
ADCS1 ADCS0 Device Frequency (Fosc)
20 Mhz 8 Mhz 4 Mhz 1 Mhz
Fosc/2 0 0 100 nS 250 nS 500 nS 2 uS
Fosc/8 0 1 400 nS 1 uS 2 uS 8 uS
Fosc/32 1 0 1.6 uS 4 uS 8 uS 32 uS
Frc 1 1

2 - 6 uS

Frc è il clock generato da un oscillatore che si trova all'interno del modulo ADC. Tale sorgente se selezionata consente il funzionamento dell'ADC anche quando la CPU è in stato di sleep.

Selezionare uno dei canali di input (CH0-CH13) tramite il registro ADCON0;

per selezionare il canale occorre utilizzare questa tabella

CHS3 CHS2 CHS1 CHS0 Canale Pin
0 0 0 0 0 RA0/AN0
0 0 0 1 1 RA1/AN1
0 0 1 0 2 RA2/AN2
0 0 1 1 3 RA3/AN3
0 1 0 0 4 RA5/AN4
0 1 0 1 5 RE0/AN5
0 1 1 0 6 RE1/AN6
0 1 1 1 7 RE2/AN7
1 0 0 0 8 RB2/AN8
1 0 0 1 9 RB3/AN9
1 0 1 0 10 RB1/AN10
1 0 1 1 11 RB4/AN11
1 1 0 0 12 RB0/AN12
1 1 0 1 13 RB5/AN13
1 1 1 0 CVref
1 1 1 1 Vref = 0.6V

esempio:
    ADCON0 = 0b..0000..;       // Abilito il canale 0
    ADCON0 = 0b..0010..;       // Abilito il canale 2

Selezionare il formato di output dei registri ADRESL e ADRESH tramite il bit ADFM del registro ADCON1

il primo bit (ADFM) del registro ADCON1

consente di definire se il valore convertito è giustificato a destra o sinistra.
Se vale ADFM vale 1 allora il risultato è giustificato a destra (per cui i 6 bit più significativi del registro ADRESLH non vengono usati)




Se vale ADFM vale 1 allora il risultato è giustificato a sinistra (per cui i 6 bit meno significativi del registro ADRESL non verranno usati).



esempio:

    ADCON1 = 0b1.......;       // Abilito il canale 0
    ADCON1 = 0b0.......;       // Abilito il canale 2

Abilitare il convertitore A/D ponendo il bit ADON ad 1 nel registro ADCON0

Il bit ADON del registro ADCON0 abilita o disabilita il convertitore A/D

Step 3 - Questi step sono inclusi nell'istruzione ADC_Read(Channel)

Aspettare che il tempo di acquisizione richiesto passi (circa 20uS - microsecondi 10-6)

Affinchè l'ADC raggiunga il suo specifico livello di precisione è necessario fornire un certo ritardo tra la selezione del canale analogico di input e la sua misurazione. Questo periodo è detto "tempo di acquisizione" e dipende dall'impedenza (resistenza al passaggio di una corrente variabile) della sorgente. Esistono equazioni che permettono di calcolarlo accuratamente ma possiamo utilizzare come riferimento il caso peggiore che è approssimativamente 20uS.

Iniziare la conversione ponendo il bit GO/DONE a 1 nel registro ADCON0.



Aspettare che la conversione sia completata (il bit GO/DONE ritorna a 0). Sarà quindi necessario un loop di attesa. Esiste la possibilità di attendere un A/D interrupt ma occorre abilitarlo.

Leggere i risultati nei registri ADRESH and ADRESL.

ESEMPIO APPLICAZIONE 1:

In questo esempio visualizzeremo su un display LCD il valore letto direttamente dal convertitore A/D ed espresso in Volts. Il progetto è stato sviluppato utilizzando la Development Board EasyPic4 con micro PIC16F887.

Per variare i valori agiremo direttamente sui potenziometri:

Il programma sottostante è stato scritto con il compilatore MikroC for PIC vers. 7.0.0.3. Perché funzioni è necessario connettere il potenziometro P1 sulla porta RA1 (vedi posizione ponticello giallo) mentre la tensione di riferimento (RA3) deve essere connessa a P2 (vedi 2° ponticello) nell'immagine

unsigned char ch;                    // Dichiarazione Variabili
long tlong;                          //
unsigned int adc_rd;                 //
void main()
{
    // --------------------------------------------------------------------
    // Step 1 - Configurazione delle porte
    // --------------------------------------------------------------------
    TRISA = 0x02;    // Il pin 1 (RA1) della porta A e' di input
    ANSEL = 0x02;    // Solo il pin 1 (RA1) e' posto come input analogico
    ANSELH = 0;      // tutti i pin della porta E sono digitali
    PORTA = 0;       // Azzero i bit di della porta A
    
    // --------------------------------------------------------------------
    // Step 2 - Configurazione modulo ADC     /*
    // --------------------------------------------------------------------
    ADCON1  = 0b00000000;  // Azzero i registri di controllo
    ADCON0  = 0b00000000;
    ADCON1 |= 0b00100000;  // VCFG1 => tensione di riferimento inferiore Vref-
    ADCON1 |= 0b00010000;  // VCFG0 => tensione di riferimento superiore Vref+ (RA3)
    ADCON0 |= 0b11000000;  // ADCS1 e ADCS0 => setto il clock di riferimento a quello interno
    ADCON0 |= 0b00000100;  // CHS3 ... CHS0 => setto il canale 1
    ADCON1 |= 0b00000000;  // bit ADFM => scrittura giustificata a sinistra
    ADCON0 |= 0b00000001;  // ADON => attiva A/D
    // Oppure
    //    ADCON1=0b00110000;
    //    ADCON0=0b11000101;

    // --------------------------------------------------------------------
    // GESTIONE LCD
    // --------------------------------------------------------------------
    LCD_init(&PORTD);                // Inizializzazione Display LCD
    Lcd_cmd(LCD_CURSOR_OFF);         // Comando LCD (cursor OFF)
    Lcd_cmd(LCD_CLEAR);              // Comando LCD (clear LCD)
    Lcd_Out(1,1," A/D  CONVERTER "); // Scrivo il primo messaggio sulla prima riga
    Lcd_Out(2,1,"Tensione: ");       // Scrivo la 2° Linea
    while (1)
    {
        // Il risultato a 10bit varia da 0..1023
        // deve essere convertito nel range 0-5000mV
        // 00000.00000 ==> 0mV
        // 00000.00001 ==> 4,887 mV (5000/1023)
        // 00000.00010 ==> 9,774 mV
        // ...
        // 11111.11111 ==> 4999,401 mV
        adc_rd = ADC_Read(1);           // Leggo il canale 1.
        tlong = (long)adc_rd * 5000;    // Converto il risultato in mV
        tlong = tlong / 1023;

        // Il numero che rappresenta i Volts viene scritto estraendo ogni
        // singolo digit (cifra) e trasformato nel corrispondente simbolo
        // ascii per la stampa con LCD_Chr
        ch = tlong / 1000;           // Estraggo le migliaia di mV
        Lcd_Chr(2,11,48+ch);         // Scrivo il corrispondente ASCII del numero
        Lcd_Chr_CP(',');             // Scrivo la virgola decimale
        ch = (tlong / 100) % 10;     // Estraggo le centinaia di mV
        Lcd_Chr_CP(48+ch);           // Scrivo il corrispondente ASCII del numero
        ch = (tlong / 10) % 10;      // Estraggo le decine di mV
        Lcd_Chr_CP(48+ch);           // Scrivo il corrispondente ASCII del numero
        ch = tlong % 10;             // Estraggo i mV
        Lcd_Chr_CP(48+ch);           // Scrivo il corrispondente ASCII del numero
        Lcd_Chr_CP('V');
        // Oppure
        //    char txt[10];            // Variabile buffer per il valore della tensione
        //    IntToStr(tlong, txt);
        //    Lcd_Out(2,11,txt);       // Scrivo la 2° Linea
    }
}

ESEMPIO APPLICAZIONE 2:

In questo esempio simuleremo l'impostazione del volume mediante il potenziometro  P1 disponibile sulla Development Board EasyPic4 con micro PIC16F887. Visualizzeremo su un display LCD il volume letto direttamente dal convertitore A/D e mostreremo una sequenza di "tacche" fino ad un massimo di 10. Il funzionamento finale è mostrato nella figura sottostante:

Per variare il volume agiremo sul potenziometro P1 (quello a sinistra). Il programma sottostante è stato scritto con il compilatore MikroC for PIC vers. 7.0.0.3. Perchè funzioni è necessario impostare il potenziometro P1 sulla porta RA1 (vedi posizione ponticello giallo a sinistra) mentre la tensione di riferimento (RA3) deve essere connessa a P2 (vedi 2° ponticello a destra) dell'immagine sovrastante.

unsigned char ch;                    // Dichiarazione Variabili
unsigned int adc_rd;                 //
int nrtacche, i;
char volumestr[11];
void main()
{
    // --------------------------------------------------------------------
    // Step 1 - Configurazione delle porte
    // --------------------------------------------------------------------
    TRISA = 0x02;    // Il pin 1 (RA1) della porta A e' di input
    ANSEL = 0x02;    // Solo il pin 1 (RA1) e' posto come input analogico
    ANSELH = 0;      //
    PORTA = 0;

    // --------------------------------------------------------------------
    // Step 2 - Configurazione modulo ADC     /*
    // --------------------------------------------------------------------
    ADCON1  = 0b00000000;  // Azzero i registri
    ADCON0  = 0b00000000;
    ADCON1 |= 0b00100000;  // VCFG1 => tensione di riferimento inferiore Vref-
    ADCON1 |= 0b00010000;  // VCFG0 => tensione di riferimento superiore Vref+
    ADCON0 |= 0b11000000;  // ADCS1 e ADCS0 => setto il clock di riferimento a quello interno
    ADCON0 |= 0b00000100;  // CHS3 ... CHS0 => setto il canale 1
    ADCON1 |= 0b00000000;  // bit ADFM => scrittura giustificata a sinistra
    ADCON0 |= 0b00000001;  // ADON => attiva A/D
    // Oppure
    //    ADCON1=0b00110000;
    //    ADCON0=0b11000101;

    // --------------------------------------------------------------------
    // GESTIONE LCD
    // --------------------------------------------------------------------
    LCD_init(&PORTD);                // Inizializzazione Display LCD
    Lcd_cmd(LCD_CURSOR_OFF);         // Comando LCD (cursor OFF)
    Lcd_cmd(LCD_CLEAR);              // Comando LCD (clear LCD)
    Lcd_Out(1,1,"Volume: ");         // Scrivo il primo messaggio sulla prima riga
    while (1)
    {
        // ---------------------------------------------------------------
        // Il risultato della conversione a 10bit varia da 0..1023
        // devo convertire tale valore in un range 0 .. 10  - Quindi
        // ad ogni intervallo (1024/10) devo variare di una tacca
        // ---------------------------------------------------------------
        adc_rd = ADC_Read(1);           // Leggo il canale 1.
        nrtacche =(int)(adc_rd/(1024/10));

        // ---------------------------------------------------------------
        // Stampa Tacche corrispondenti al volume corrente
        // ---------------------------------------------------------------
        for ( i=0 ; i<nrtacche ; i++ )
           volumestr[i]='>';
        for ( i=nrtacche ; i<10 ; i++ )
           volumestr[i]=' ';
        volumestr[i]='\0';
        Lcd_Out(2,1,volumestr);        // Scrivo le tacche sulla 2° riga

        // ---------------------------------------------------------------
        // Scrivo il volume (nr di tacche accanto alla dicitura volume)
        // ---------------------------------------------------------------
        ch = nrtacche / 10;                 //
        Lcd_Chr(1,9,48+ch);                 //  48 codice ascii di 0
        ch = (nrtacche % 10);            
        Lcd_Chr_CP(48+ch);               
    }
}