Forum: Projekte & Code ADC und Fixed-Point Arithmetik


von Bernd N (Gast)


Angehängte Dateien:

Lesenswert?

Genaues Messen mit dem ADC und "Fixed-Point Arithmetik" sind immer 
wieder ein Thema. Häufig werden hier die Grundlagen nicht verstanden und 
man sieht die abenteuerlichsten "Floating point" Berechnungen. Trotz des 
erhöhten Aufwands werden dennoch keine perfekten Ergebnisse 
zurückgeliefert weil Offset und Gain Error falsch ermittelt werden. Das 
folgende Beispiel wurde für das AVR NET IO Board, bestückt mit einem 
ATMEGA 644p, geschrieben.

Bei Verwendung des ADC müssen folgende Fehlerquellen in Betracht gezogen 
werden.

- Gain Error, Verstärkungsfehler des ADC / Eingangsverstärker.
- Offset Fehler des ADC, Idealerweise 0.
- INL / DNL Error, nichtlineare Verstärker, AD Wandler Fehler.
- VREF Fehler, Range 2,4 - 2,9 Volt.

In der AppNote AVR 120 werden alle diese Fehler in der Theorie 
besprochen. Der hier vorliegende C Code setzt, im wesentlichen, diese 
AppNote um. In der AppNote AVR 121 wird zu dem das Prinzip des 
Oversampling beschrieben. Auch dieses Verfahren ist im Code enthalten. 
Es werden 512 Messwerte gemittelt und auf 12 BIT skaliert. Bei einer 
4-Stelligen Anzeige beträgt die Auflösung 1 mVolt. Die letzte Ziffer ist 
somit frei von "Sprüngen".

Alle Berechnungen erfolgen mittels "Fixed-point Arithmetik". Die Ausgabe 
des Meßwert erfolgt über die USART.

Die Referenzierung wird über 2 Stützpunktmessungen realisiert. Hierbei 
wird der "rohe AD Wert" zugrunde gelegt.
1
return (AvgSum >> 7);                               // ADC raw Value

Alle linearen Fehler sind somit kompensiert. Die Korrektur der 
nichtlinearen Fehler wird hierbei, wie auch in der AppNote, nicht 
berücksichtigt.

Die tatsächliche Größe der internen Referenzspannung muß nicht bekannt 
sein da die Referenzierung über die Stützpunkte erfolgt. Man muß 
lediglich 2 Testmessungen ausführen und den tatsächlichen AD Wert in das 
adc.h file eintragen.
1
#define AdcRawH 3206                                // Stützpunkt 2.00 Volt
2
#define AdcRawL  605                                // Stützpunkt 0.40 Volt

Da alle Zahlen zur Basis 2 erweitert wurden kann die Berechnung des ADC 
Wertes mit simplen shift Operationen ausgeführt werden.
1
return ((((AvgSum >> 7) * Factor) + Offset) >> 16);

Der Code benötigt gerade einmal 600 Byte und ist ein schönes Beispiel 
für die Effizienz von Fixed-point Arithmetik.

Viel Spaß,

Bernd

von Tobias (Gast)


Lesenswert?

Hallo Bernd,

sehr elegant gelöst. ich kannte zwar die atmel dokumente konnte diese 
aber nicht umsetzen. wenn man dann sieht wie es geht dann ist es 
nachvollziehbar.

ich bin c anfänger und würde gerne eine automatische abgleich routine 
machen wollen. wenn ich so rechne wie in deinem .h file angegeben dann 
kommt wieder float ins spiel. wie bekomme ich das anders hin ?

von Bernd N (Gast)


Lesenswert?

Skalieren wir mal folgendermaßen und nutzen den Zahlenraum von uint32_t 
aus.
1
#define AdcRawHigh 3012                                           // Stützpunkt 2.00 Volt
2
#define AdcRawLow   598                                           // Stützpunkt 0.40 Volt
3
4
// Skalierung der Stützpunkte Fixed Point
5
#define FacScale 1600000000UL                                     // Factor skalieren 2V-0.4V = 1.6V
6
#define OffScale  400000000UL                                     // Offset skalieren 0.4V
7
#define Scale 100000UL                                            // ADC Wert skalieren
8
9
// Berechnung der ADC Korrekturwerte mittels Referenzpunkte
10
#define FACTOR (FacScale / (AdcRawHigh - AdcRawLow))              // 2.0-0.4 / 3012-598 = 1.6/2414 ADC Steigung
11
#define OFFSET (OffScale - (AdcRawLow * FACTOR))                  // 4-(598*0,006628) = 3,6456 mV (Offset)
12
13
// AREF ermitteln (2,7184 Volt skaliert = 27184)
14
#define AREF (((0.006628 * 4096) + 0.036456) * 1000)              // Berechnung von AREF just for fun :-)
15
16
// Grenzwerte für die Kalibration
17
#define RawMaxHighH 3090                                          // oberer Grenzwert Stützpunkt  2.05 V
18
#define RawMaxLowH  2938                                          // unterer Grenzwert Stützpunkt 1.95 V
19
#define RawMaxHighL 673                                           // oberer Grenzwert Stützpunkt  0.45 V
20
#define RawMaxLowL  523                                           // unterer Grenzwert Stützpunkt 0.35 V
21
22
// Boolsche Variablen zur ADC Ausgabe
23
#define DispVal 1                                                 // ADC Ausgabe in Volt
24
#define RawVal 0                                                  // ADC Raw Wert für die Kalibrierung
Der ADC Code sieht dann so aus:
1
uint16_t getAdcValue (uint8_t AdcValType)
2
{
3
    uint8_t AvgCount = 0;
4
    uint32_t AvgSum = 0;
5
6
    for (AvgCount = 0; AvgCount < 128; AvgCount ++) {
7
        ADCSR |= (1 << ADSC);                                     // AD Wandler starten
8
        while (ADCSR & (1 << ADSC));                              // AD Wandlung fertig ?
9
        AvgSum += ADC;                                            // Mittelwert über 128 Messungen
10
    }
11
    if (AdcValType) {
12
        /*
13
          ---- Oversampling 12 BIT sowie Gain, AREF und Offset Fehler Kompensation ---- 
14
        */
15
        return ((((AvgSum >> 5) * Factor) + Offset) / Scale);     // Ausgabe -> in Volt
16
    } else {
17
        /*
18
          Der ADC raw Wert wird für die Autokalibrierung benötigt.
19
        */
20
        return (AvgSum >> 5);
21
    }
22
}

Mittels Flag (AdcValType) bekommt man entweder den skalierten oder den 
"rohen" AD Wert.

Für die Autokalibrierung dann:
1
void FactorOffsetCalHigh (void)
2
{
3
    DispStr (pCAL1);
4
5
    for (;;) {
6
        if (KeyStateDown) {                                       // Taste gedrückt ? => ADC Raw Wert
7
            AdcRawValHigh = getAdcValue (RawVal);                 // für Stützpunkt 2.00V erfassen
8
            /*
9
              Stützpunkt 2.00 Volt auf plausiblen Wert prüfen.
10
            */
11
            if (AdcRawValHigh < RawMaxLowH || AdcRawValHigh > RawMaxHighH) {
12
13
                DispStr (pErr1);                                  // Stützpunkt 2.00 Volt adjust failed
14
            } else {
15
                SaveCalData ();
16
                break;
17
            }
18
        } else {
19
            intTo7Segment (getAdcValue (DispVal));
20
        }
21
    }
22
}
23
24
void FactorOffsetCalLow (void)
25
{
26
    DispStr (pCAL2);
27
28
    for (;;) {
29
        if (KeyStateDown) {                                       // Taste gedrückt ? => ADC Raw Wert
30
            AdcRawValLow = getAdcValue (RawVal);                  // für Stützpunkt 0.40V erfassen
31
            /*
32
              Stützpunkt 0.40 Volt auf plausiblen Wert prüfen.
33
            */
34
            if (AdcRawValLow < RawMaxLowL || AdcRawValLow > RawMaxHighL) {
35
36
                DispStr (pErr1);                                  // Stützpunkt 0.40 Volt adjust failed
37
            } else {
38
                SaveCalData ();
39
                break;
40
            }
41
        } else {
42
            intTo7Segment (getAdcValue (DispVal));
43
        }
44
    }
45
}

Den Code habe ich aus einem anderen Projekt kopiert und Du mußt ihn nur 
noch entsprechend anpassen.
Das Ganze Projekt kann ich hier nicht hereinstellen da es auf einer ganz 
anderen Hardware läuft.

Berechnung zur Laufzeit dann:
1
    Factor = (FacScale / (AdcRawValHigh - AdcRawValLow));
2
    Offset = (OffScale - (AdcRawValLow * Factor));

Das sollte es sein.

von Dennis B. (Firma: Home) (deboman)


Lesenswert?

Ich habe mir die Fixed-Point-Arithmetik angeschaut von dir. Danach bin 
ich nämlich auf der Suche.
Mein ADC 10Bit (Eva Board mit Atmega128) gibt nämlich 2.56V aus, obwohl 
nur 2.51V anliegen. Ein Unterschied von 50mV. Jetzt habe ich versucht 
deinen Quelltext an mein Board anzupassen.
Ich habe mir zwei Stückpunkte ausgepickt für die Ref Spg von 5V.
4V = 845
0.8V = 169
Dann habe ich noch die RefA und RefB angepasst. Jedoch komme ich nicht 
auf korrekte Werte. Wenn ich z.B. 2.5V an den ADC anliege, dann bekomme 
ich irgendwas mit 34582 raus...

Muss ich noch etwas an den Funktionen anpassen? Liegt es daran, dass du 
einen ADC 12Bit und ich einen ADC 10Bit verwende? Wenn ja, wo muss ich 
die Funktion verändern?

Danke

von Bernd N (Gast)


Lesenswert?

Nein, der 644er hat auch nur einen 10 BIT Wandler und der Code 
funktioniert 100%ig. Das ist ja der Sinn des Ganzen... von 10 auf 12 BIT 
zu kommen. Hast du den Code nachvollziehen können ? wenn auch kurz aber 
nicht ganz trivial :-)

Schalte deine Referenz mal auf die interne 2,5V und lass den Code mal 
original. Dann machst du den 2ten Schritt und verwendest VCC als REF. So 
kannst du dich dem ganzen annähern.

Ohne deinen Code kann ich auch nichts dazu sagen. Vielleicht läufst du 
aus einer Grenze... mach dir mal nen Excelsheet und rechne es durch.

von Dennis B. (Firma: Home) (deboman)


Lesenswert?

Bernd N schrieb:
> Die tatsächliche Größe der internen Referenzspannung muß nicht bekannt
> sein da die Referenzierung über die Stützpunkte erfolgt. Man muß
> lediglich 2 Testmessungen ausführen und den tatsächlichen AD Wert in das
> adc.h file eintragen.
>
>
1
> #define AdcRawH 3206                                // Stützpunkt 2.00
2
> Volt
3
> #define AdcRawL  605                                // Stützpunkt 0.40
4
> Volt
5
>
>
> Da alle Zahlen zur Basis 2 erweitert wurden kann die Berechnung des ADC
> Wertes mit simplen shift Operationen ausgeführt werden.
>
>
1
> return ((((AvgSum >> 7) * Factor) + Offset) >> 16);
2
>

Deinen Code habe ich soweit verstanden bis auf den return Wert.
AvgSum wurde doch mit 512 durchgeführt. Wieso dann 7Bits verschieben und 
nicht 8? Verschiebung um >>16 machst du, weil du von 32 auf 16bit willst 
richtig? Der Rest ist ja gut in der Atmel Doku erklärt.

Noch eine Frage zu den Raw-Werten oben im Zitat. Du hast einmal 2V und 
0.4V am ADC erzeugt und den Wert eingelesen und notiert. Nur wie kommst 
du auf die Werte 3206 und 605 bei einem 10Bit ADC? Der maximale Wert 
liegt doch bei 1024 bzw. 1023.

Übers Wochenende werde ich meinen Quelltext etwas sortieren. Sonst macht 
das Posten des Quelltextes keinen Sinn. Größtenteils habe ich deinen 
(bis auf die Outputfunktion) kopiert und angewandt.

Danke, dass du geantwortet hast.

von Bernd N (Gast)


Lesenswert?

Wie oft mußt du zurückschieben wenn du wieder auf 10 BIT kommen willst ?
1
    for (AvgCount = 0; AvgCount < 128; AvgCount ++) {
2
        ADCSR |= (1 << ADSC);                                     // AD Wandler starten
3
        while (ADCSR & (1 << ADSC));                              // AD Wandlung fertig ?
4
        AvgSum += ADC;                                            // Mittelwert über 128 Messungen
5
    }

Du addierst 128x auf aber schiebst eben NICHT!! entsprechend zurück. Du 
machst so aus 1024 -> 4096. Das ist Trick No.1

Für die Stützpunkterfassung brauchst du nun den "rohen" AD Wert.
1
return (AvgSum >> 5);

Und den trägst du dann ein.

von Bernd N (Gast)


Lesenswert?

Ich seh dein Problem:

>> Ich habe mir zwei Stückpunkte ausgepickt für die Ref Spg von 5V.
>> 4V = 845
>> 0.8V = 169

Laß mal nur die ADC Routine laufen (128x Abtasten) und dann:
return (AvgSum >> 5);
return (AvgSum >> 6);
return (AvgSum >> 7);

Siehst du es ? Wenn du am Eingang ein Poti anschließt dann solltest du 
einen Range von 0-1023, 0-2047, 0-4095 sehen können, je nach Anzahl der 
Schiebeoperationen.

Alles klar ?

von Dennis B. (Firma: Home) (deboman)


Lesenswert?

Hi!
Danke für die fixe Antwort. Ich weiß jetzt was du mit Rawdata meinst. 
Also nicht den reinen 10Bit Wert zwischen 0-1024 sondern den Wert aus 
der Avg Routine zwischen 0-4095. Ok dein Beispiel werde ich am Montag 
direkt mal ausprobieren. Bin am Wochenende leider viel unterwegs und 
habe dafür wenig Zeit.

Entweder denke ich zu kompliziert oder ich bin noch nicht auf dem 
richtigen Dampfer.
Deine Raw-Daten machst du mit einem Awg von 128 und in der fertigen C 
Routine mit 512. Jedoch rechnest du ja mit den Raw Daten deinen Offset 
etc. aus. Hmm ich hoffe du weißt was ich meine.

KOMMANDO ZURÜCK:
Du skalierst du 12Bit deswegen nur >>7. Deswegen auch die 30XX. Jetzt 
hat es glaube ich klick gemacht :P.

Also wenn ich auf 10Bit skalieren möchte, dann nehme ich eben 128 
Iterationen und den Shiftfaktor 5?

von Bernd N (Gast)


Lesenswert?

>> Danke für die fixe Antwort. Ich weiß jetzt was du mit Rawdata meinst.
>> Also nicht den reinen 10Bit Wert zwischen 0-1024 sondern den Wert aus
>> der Avg Routine zwischen 0-4095.

So ist es.

>> Deine Raw-Daten machst du mit einem Avg von 128 und in der fertigen C
>> Routine mit 512.

Laß dich nicht verwirren, die angehängte original Routine ist NICHT 
identisch mit der diskutierten Variante (Frage von Tobias). Raw Data 
meint den reinen ADC oversampling Wert... da liegst du richtig.

>> Jedoch rechnest du ja mit den Raw Daten deinen Offset etc. aus. Hmm ich
>> hoffe du weißt was ich meine.

So ist es, Offset und Steigung.

>> Also wenn ich auf 10Bit skalieren möchte, dann nehme ich eben 128
>> Iterationen und den Shiftfaktor 5?

So paßt es eher.
return (AvgSum >> 5); = 12 BIT
return (AvgSum >> 6); = 11 BIT
return (AvgSum >> 7); = 10 BIT denn 128x aufaddieren = /128 => >> 7

Ich denke du bist auf dem richtigen Weg.

von Dennis B. (Firma: Home) (deboman)


Lesenswert?

[c]
#include <avr/io.h>
#include <string.h>
#include <kamavr.h>
#include "lcd-routines.h"
#include <stdlib.h>
#include <math.h>
#include <stdio.h>


#define BIT(x) (1<<x)

// Referenzpunkte Abgeglichen mit DMM Fluke 87 III
#define AdcRawH 3387                        // Stützpunkt 4.00 Volt
#define AdcRawL  679                        // Stützpunkt 0.80 Volt

// Skalierung Delta ADC + Referenzpunkte
#define RefA 32000.0                    // Referenzpunkt A 4V-0.8V=3.2V
#define RefB  8000.0                    // Referenzpunkt B 0.8V
#define Scale 65536                     // zur Basis 2^16

// Berechnung der ADC Korrekturwerte mittels Referenzpunkte
#define Slope  (RefA / (AdcRawH - AdcRawL))
#define Offset (uint32_t) ((RefB - (AdcRawL * Slope)) * Scale)
#define Factor (uint32_t) ((RefA / (AdcRawH - AdcRawL)) * Scale)

#define AdcChannel 1

void ADC_Init(void);
uint16_t getAdcValue (void);


int main(void)
{
DDRB = 0XFF;                           // Configure PORTB as output

ADC_Init();

float blub = 0;
uint16_t adcval = 0;
char s[8];
char zahl[20];
while(1)
{
 lcd_init();

 adcval = getAdcValue();

 itoa( adcval, zahl, 10 );
 blub = adcval;

 lcd_string(zahl);
}



return(0);
}

void ADC_Init(void) {

  uint16_t result;

  ADMUX = (0<<REFS1) | (0<<REFS0) | (0<<ADLAR) | (AdcChannel & 0x1F);
  ADCSRA = (1<<ADPS2) | (1<<ADPS1) | (0<<ADPS0);   // Frequenzvorteiler
  ADCSRA |= (1<<ADEN);             // ADC aktivieren

  ADCSRA |= (1<<ADSC);            // eine ADC-Wandlung
  while (ADCSRA & (1<<ADSC) );    // auf Abschluss der Konvertierung 
warten

  result = ADCW;
}


/* ---- ADC loop Tiefpaß fgo ~20 Hz 
------------------------------------------------------------------------ 
--  */

uint16_t getAdcValue (void)
{
    uint16_t AvgCount = 0;
    uint32_t AvgSum = 0;

    for (AvgCount = 0; AvgCount < 128; AvgCount ++) {
        ADCSRA |= (1 << ADSC);               // AD Wandler starten
        while (ADCSRA & (1 << ADSC));        // AD Wandlung fertig ?
        AvgSum += ADC;                // Mittelwert über 512 Messungen
    }
    return ((((AvgSum >> 5) * Factor) + Offset) >> 16); // skaliert auf 
12 BIT
     /*
    for (AvgCount = 0; AvgCount < 512; AvgCount ++) {
        ADCSRA |= (1 << ADSC);               // AD Wandler starten
        while (ADCSRA & (1 << ADSC));        // AD Wandlung fertig ?
        AvgSum += ADC;                  // Mittelwert über 512 Messungen
    }
    return (AvgSum >> 7);  */
}
[\c]

Hi,

ich melde mich noch einmal zurück. Jedoch bin ich nicht wirklich voran 
gekommen. Die RawData heb ich mit 512 Iterationen und einem Shift von 
>>7 erlangt. Die Werte habe ich oben bei #define eingetragen.
Die eigentliche ADC-Wandlung für Messungen etc. habe ich dann auf 128 
und >>5 festgelegt. Jedoch bekomme ich z.B. für 0.8V nicht "679" raus 
sondern liege bei 8035. Was mache ich falsch?

von Dennis B. (Firma: Home) (deboman)


Lesenswert?

Was ich noch vergessen habe zu erwähnen. Ich brauche den Bereich von 
0-5V, also habe ich auch den ADC darauf eingestellt.
Hoffentlich liegt es nicht daran, dass die Werte nicht passen.

von Dennis B. (Firma: Home) (deboman)


Lesenswert?

Ich glaube jetzt habe ich es verstanden. Jetzt habe ich es verstanden. 
Den Dampfer den ich genommen habe ging nicht nach Hamburg sondern nach 
Köln :). Wenn ich die Shiftoperation richtig gesetzt habe bekomme ich 
Werte von z.B. 19261 für ~1.93V raus oder 26694 für ~2.7V. Ich Nase habe 
es natürlich nicht geschnallt, dass das schon das Ergebnis ist... . 
Lediglich das Komma muss gesetzt werden und ich erhalte mein Result. 
Ujuj danke für die Geduld :).

Gruß Dennis

von Bernd N (Gast)


Lesenswert?

>> Den Dampfer den ich genommen habe ging nicht nach Hamburg sondern nach
>> Köln :)

Prima, gut gemacht :-) Köln ist doch schön, da komme ich her :-)

von BioSniper (Gast)


Lesenswert?

Ich würde mich nicht auf die interne Ref-Spannung verlassen, die kann 
nicht besonders stabil ist. Ist nur für Muschiputz-Messungen zu 
gebrauchen. Also, das was ihr derzeit macht.

Nehmt lieber einen LT1021C. Der liefert 5V +/- 0,0025 V.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.