[Risolto(finalmente)] Problema ADC con interrupt

Salve a tutti!
Ultimamente sono sceso un po' sotto il cofano dell'Arduino e ho cominciato a esplorare le grandi possibilità(e grandi fregnacce) dell'amega328. In particolare sto ho implementato una lettura di due canali ADC con oversampling che avviene automaticamente ogni 8µs e che poi richiama la funzione che gestisce i dati ricavati:

ISR(ADC_vect) {
        uint8_t i = 0;
        uint8_t adlow;
        int16_t currentadc;
        static uint8_t chpos = 0;
        static int16_t raw_vO[4], raw_vR[4];  
        int16_t last_vO = 0, last_vR = 0;

        ADMUX ^= (1 << MUX1);				//alterna ADMUX tra i canali 1 e 3
        adlow = ADCL;					//"salva" ADCL(è l'unico modo per leggerlo)
        currentadc = (ADCH << 8) | adlow;		//valore completo

        if(ADMUX & (1 << MUX1)) {			//se ADMUX è invertito
                chpos = (chpos + 1) % 4;		//avanza l'indice per l'oversampling
                raw_vO[chpos] = currentadc;		//calcola oversampling per il canale 1
                while(i < 4) {
                        last_vO += raw_vO[i];
                        i ++;
                }
                last_vO >>= 1;
                vO = (last_vO + vO) / 2;
        }
        else {						//altrimenti calcola oversampling per il canale 3
                raw_vR[chpos] = currentadc;     //(l'indice viene gestito solo nell'altro canale)
                while(i < 4) {
                        last_vR += raw_vR[i];
                        i ++;
                }
                last_vR >>= 1;
                vR = (last_vR + vR) / 2;
                }
}

Ecco come inizializzo l'ADC

	sei();//abilita interrupt globali
	ADMUX = (1 << REFS0) | (1 << MUX0); //5V riferimento, canale 1
	ADCSRB = 0;
	ADCSRA = (1 << ADEN) | (1 << ADATE) | (1 << ADIF) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
	ADCSRA |= (1 << ADSC); //attiva ADC, non mi ricordo, richiama ciclicamente, abilita interrupt alla fine della conversione, divide  il clock per 128, poi avvia la conversione

in pratica l'ISR ogni volta che viene chiamato fa girare il contatore "chpos" dell'oversampling e alterna il valore del registro ADMUX per leggere alternativamente i due canali.
Fin qui tutto bene, la lettura è veloce, precisa... tutto ok!

Adesso voglio aggiungere un sensore di temperatura(senza oversampling, sul canale 0), quindi il meccanismo deve diventare a tre stati...
come prima cosa lo faccio nel modo più stupido possibile: con uno switch case:

ISR(ADC_vect) {
        uint8_t i = 0;
        uint8_t adlow;
        int16_t currentADC;
        static uint8_t chpos = 0;
        static int16_t raw_vO[4], raw_vR[4];
        int16_t last_vO = 0, last_vR = 0;

        adlow = ADCL;
        currentADC = (ADCH << 8) | adlow;

        switch(ADMUX) {
                case (1 << REFS0) | (1 << MUX0) | (1 << MUX1): 
                        ADMUX = (1 << REFS0) | (1 << MUX0); 
                        temp = currentADC;                        
                        break;
                case (1 << REFS0) | (1 << MUX0): 
                        ADMUX = (1 << REFS0); 
                        chpos = (chpos + 1) % 4;
                        raw_vO[chpos] = currentADC;
                        while(i < 4) {
                                last_vO += raw_vO[i];
                                i ++;
                        }
                        last_vO >>= 1;
                        vO = (last_vO + vO) / 2;
                        break;
//                case (1 << REFS0):
                  default:
                        ADMUX = (1 << REFS0) | (1 << MUX0) | (1 << MUX1);
                        raw_vR[chpos] = currentADC;
                        while(i < 4) {
                                last_vR += raw_vR[i];
                                i ++;
                        }
                        last_vR >>= 1;
                        vR = (last_vR + vR) / 2;
        }
}

Stesso discorso di prima, la lettura di un canale prepara quella del successivo...
Sembrava tutto ok anche qui... invece no!
Ogni tanto nel quando leggo vR(ch 3) compaiono senza motivo apparente valori intorno al 700 quando invece dovrebbe stare molto più giù.

Riprovo ad implementare la lettura in modo più raffinato:
definisco un array di puntatori a funzione letto usando come indice proprio ADMUX, in questo modo l'accesso al codice che deve gestire i dati è velocissimo:

int16_t currentADC;
uint8_t chpos = 0;
int16_t raw_vO[4], raw_vR[4];
int16_t last_vO = 0, last_vR = 0;

void readVO() {
        ADMUX = (1 << REFS0);
        
        chpos = (chpos + 1) % 4;
        raw_vO[chpos] = currentADC;
        for(uint8_t i = 0; i < 4; i++) last_vO += raw_vO[i];
        last_vO >>= 1;
        vO = (last_vO + vO) / 2;
        
}

void readVR() {
        ADMUX = (1 << REFS0) | (1 << MUX0);

        raw_vR[chpos] = currentADC;
        for(uint8_t i = 0; i < 4; i ++) last_vR += raw_vR[i];
        last_vR >>= 1;
        vR = (last_vR + vR) / 2;

}

void readTemp() {
        ADMUX = (1 << REFS0) | (1 << MUX0) | (1 << MUX1);

        temp = currentADC;

}

void (*ADCReadFunction[])() = {
        readTemp, readVO, NULL, readVR};

ISR(ADC_vect) {
        uint8_t adlow = ADCL;
        currentADC = last_vO = last_vR = 0;
        currentADC = (ADCH << 8) | adlow;

        ADCReadFunction[ADMUX & ~240]();
}

Bene, a chiacchiere funziona alla grande... ma in pratica non funziona minimamente!!!
Per riuscire a leggere i valori giusti dopo mille combinazioni ho dovuto scambiare tra loro il corpo di read_vR e readTemp... ed è veramente insensata questa cosa =(, senza contare che una volta invertiti i codici si ripresenta il problema dell'implementazione con switch-case... valori a ca*** che compaiono ogni tanto...
Dove sbaglio? Qualcuno vede l'errore?
Grazie, ciao

Ciao,

da quel che vedo nel codice hai cambiato la posizione del cambio di ADMUX.

Nel primo esempio, nella funzione ISR, prima fai il cambio di ADMUX e poi leggi il valore,

ADMUX ^= (1 << MUX1);				//alterna ADMUX tra i canali 1 e 3
adlow = ADCL;					//"salva" ADCL(è l'unico modo per leggerlo)
currentadc = (ADCH << 8) | adlow;		//valore completo

mentre nel secondo lo fai dopo

adlow = ADCL;
currentADC = (ADCH << 8) | adlow;

switch(ADMUX) {
...

Per cui non stai leggendo il valore convertito in precedenza?
Inoltre tieni conto che ci vuole tempo prima che avvenga il cambiamento di pin.

Guarda questi link per maggiori informazioni

NerdKits - ADC HELP!!! (Microcontroller Programming) (vedi in fondo esempio codice)

Se poi hai necessita' di cambiare il voltaggio di riferimento, allora devi scartare la prima misura.

Spero questo possa esserti d'aiuto.

Ciao,
Marco.

Ciao Marco, grazie per la risposta

Penso che il momento in cui cambio l'ADMUX non dovrebbe fare differenza perché il valore che imposto non sceglie il canale che leggo quando l'interrupt viene sollevato, ma quello che leggerò con l'interrupt successivo...

A quanto ho capito il ciclo funziona così:

  • ADMUX vale 3
  • inizio la conversione sul canale 3
  • a conversione completa viene chiamato l'interrupt che legge il valore convertito del canale 3 e sposta ADMUX a 1
  • al trigger del prescaler parte la conversione sul canale 1
  • a conversione completa viene chiamato l'interrupt che legge il canale 1 e sposta ADMUX a 0
  • al trigger del prescaler parte la conversione sul canale 0
  • a conversione completa viene chiamato l'interrupt che legge il canale 0 e sposta ADMUX a 3
  • a capo...

...ma non sono sicuro al 100% che le cose vadano così, quindi stasera proverò a seguire il tuo suggerimento...

Grazie ancora
Ciao

Ciao,

guarda anche questo link
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=printview&t=101325&start=0

Una nota

sei();//abilita interrupt globali
ADMUX = (1 << REFS0) | (1 << MUX0); //5V riferimento, canale 1
ADCSRB = 0;
ADCSRA = (1 << ADEN) | (1 << ADATE) | (1 << ADIF) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
ADCSRA |= (1 << ADSC); //start della conversione

L'abilitazione degli interrupt (sei()) la sposterei dopo la configurazione

ADMUX = (1 << REFS0) | (1 << MUX0); //5V riferimento, canale 1
ADCSRB = 0;
ADCSRA = (1 << ADATE) | (1 << ADIF) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
sei();//abilita interrupt globali
ADCSRA |= (1 << ADEN) ;
ADCSRA |= (1 << ADSC); //start della conversione

Altra considerazione che devi tener conto e' la stabilita' del voltaggio di riferimento se utilizzi i 5V provenienti dalla sezione di alimentazione dell'Arduino.

Ciao,
Marco.

MGuruDC:
Salve a tutti!
Ultimamente sono sceso un po' sotto il cofano dell'Arduino e ho cominciato a esplorare le grandi possibilità(e grandi fregnacce) dell'amega328. In particolare sto ho implementato una lettura di due canali ADC con oversampling che avviene automaticamente ogni 8µs e che poi richiama la funzione che gestisce i dati ricavati:

Senza andare avanti a leggere ti dico subito che parti col piede sbagliato, gli ADC dell'ATmega, anche impostando la minima risoluzione possibile, al massimo lavorano a poche decine di kHz, 8us sono 125 kHz quindi sei totalmente fuori dalle specifiche dell'ADC.

Ciao,

per la verita' sta utilizzando un prescaler per l'ADC clock di 128 ((1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0)).
Per cui 16MHz / 128 = 125 kHz che e' nel range di 50 - 200 KHz richiesto dalle specifiche di clock per l'ADC del ATmega328.

By default, the successive approximation circuitry requires an input clock frequency between 50
kHz and 200 kHz to get maximum resolution. If a lower resolution than 10 bits is needed, the
input clock frequency to the ADC can be higher than 200 kHz to get a higher sample rate.

Poi fa una media dei valori (se non sbaglio su 4 valori misurati).

Penso i suoi problemi siano dovuti al fatto che lavora in "free running mode", per cui vi e' interferenza fra interrupt e termine conversione.

Dato che ADCSRB e' posto = 0, in cui i bit da 0 a 2 sono ADTS2:0: ADC Auto Trigger Source.

ADTS2 ADTS1 ADTS0 Trigger Source
0 0 0 Free Running mode

Ciao,
Marco.

Un conto è il clock per l'ADC, e per fare una conversione completa occorrono più cicli di clock, e un conto è il sample rate dell'ADC,
data sheet, pagina 250 nelle caratteristiche tecniche generali c'è anche il massimo sample rate:

Up to 76.9 kSPS (Up to 15 kSPS at Maximum Resolution)
13 - 260 ?s Conversion Time

e poco più avanti:

A normal conversion takes 13 ADC clock cycles

Ovvero fino a 76.9 ksps con risoluzione ridotta a 7 bit e 15 ksps con risoluzione a 10 bit,ovviamente nel caso di uso di un singolo canale perché se sono più di uno il sample rate va diviso per il numero dei canali più il delay per la commutazione, ora dimmi come è possibile pretendere un oversampling dell'ADC ogni 8 us ?

Ciao Astrobeed,

mica detto che quello che hai scritto e' sbagliato, ma spenso che semplicemente ha utilizzato dei termini non corretti per descrivere quel che intendeva fare (come dal codice che ha inserito nel posto).

Ciao,
Marco.

Le incongruenze che riscontro sono macroscopiche (leggo valori più di 10 volte superiori a quelli che mi aspetto), quindi la stabilità della Vref per ora non è un problema.
Anche io penso che ci sia una probabilità molto alta che durante una lettura parta una nuova conversione ma su questo punto sono molto perlplesso: se posso fare al massimo 15k conversioni perché il prescaler arriva solo a 128? dovrei avere a disposizione valori molto più alti... o forse durante la conversione il free-running viene spento...
Più tardi mi rimetto a lavoro.
Grazie
Ciao

dimmi come è possibile pretendere un oversampling dell'ADC ogni 8 us ?

Hai ragione, su questo non ero abbastanza documentato... è esattamente come hai detto tu.

Sempre documentandomi meglio ho trovato che in free running la nuova conversione riparte solo alla fine dell'interrupt(ho anche provato empiricamente), quindi non ci sono problemi, senza contare che i registri ADCL-H vengono bloccati finché non vengono letti almeno una volta.
Adesso provo a spostare lo switch dell'ADMUX
A più tardi i risultati...
Ciao

Oooooops mi ero dimenticato di questo topic...
Risolto!

void startADC() {
        DIDR0 = (1 << ADC3D) | (1 << ADC1D) | (1 << ADC0D);
        ADMUX = REF | (1 << MUX0);
        ADCSRB = 0;
        ADCSRA = (1 << ADEN) | (1 << ADATE) | (1 << ADIF) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
        ADCSRA |= (1 << ADSC);
        sei();
}

uint8_t chpos = 0;
int16_t raw_vO[4], raw_vR[4];  
uint8_t adlow;

void read_vO() {
        ADMUX = REF | (1 << MUX0);
        adlow = ADCL;
        chpos = (chpos + 1) % 4;
        raw_vO[chpos] = (ADCH << 8) | adlow;
        vO = (((raw_vO[0] + raw_vO[1] + raw_vO[2] + raw_vO[3]) >> 1) + vO) / 2;
}

void read_T() {
        ADMUX = REF | (1 << MUX1) | (1 << MUX0);
        adlow = ADCL;
        temp = (ADCH << 8) | adlow;
}

void read_vR() {
        ADMUX = REF;
        adlow = ADCL;
        raw_vR[chpos] = (ADCH << 8) | adlow;
        vR = (((raw_vR[0] + raw_vR[1] +raw_vR[2] +raw_vR[3]) >> 1) + vR) / 2;
}

void (*readFunctions[])() = {
        read_vO, read_T, NULL, read_vR
};

ISR(ADC_vect) {
        readFunctions[ADMUX & ADMUX_SLICE]();
        if(vR > (I_MAX_RATIO + vO)) emergencyOff();
        if((target.mode == CV_MODE && vR > (raw_lim + vO)) || (target.mode == CC_MODE && vO > raw_lim)) (*offP)();
}

Per quanto ci potessi ragionare... niente, alla fine era questione di trovare la giusta combinazione...
Ciao