mikrocontroller.net

Forum: Projekte & Code ADC und Fixed-Point Arithmetik


Autor: Bernd N (Gast)
Datum:
Angehängte Dateien:

Bewertung
1 lesenswert
nicht 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.
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.
#define AdcRawH 3206                                // Stützpunkt 2.00 Volt
#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.
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

Autor: Tobias (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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 ?

Autor: Bernd N (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Skalieren wir mal folgendermaßen und nutzen den Zahlenraum von uint32_t 
aus.
#define AdcRawHigh 3012                                           // Stützpunkt 2.00 Volt
#define AdcRawLow   598                                           // Stützpunkt 0.40 Volt

// Skalierung der Stützpunkte Fixed Point
#define FacScale 1600000000UL                                     // Factor skalieren 2V-0.4V = 1.6V
#define OffScale  400000000UL                                     // Offset skalieren 0.4V
#define Scale 100000UL                                            // ADC Wert skalieren

// Berechnung der ADC Korrekturwerte mittels Referenzpunkte
#define FACTOR (FacScale / (AdcRawHigh - AdcRawLow))              // 2.0-0.4 / 3012-598 = 1.6/2414 ADC Steigung
#define OFFSET (OffScale - (AdcRawLow * FACTOR))                  // 4-(598*0,006628) = 3,6456 mV (Offset)

// AREF ermitteln (2,7184 Volt skaliert = 27184)
#define AREF (((0.006628 * 4096) + 0.036456) * 1000)              // Berechnung von AREF just for fun :-)

// Grenzwerte für die Kalibration
#define RawMaxHighH 3090                                          // oberer Grenzwert Stützpunkt  2.05 V
#define RawMaxLowH  2938                                          // unterer Grenzwert Stützpunkt 1.95 V
#define RawMaxHighL 673                                           // oberer Grenzwert Stützpunkt  0.45 V
#define RawMaxLowL  523                                           // unterer Grenzwert Stützpunkt 0.35 V

// Boolsche Variablen zur ADC Ausgabe
#define DispVal 1                                                 // ADC Ausgabe in Volt
#define RawVal 0                                                  // ADC Raw Wert für die Kalibrierung
Der ADC Code sieht dann so aus:
uint16_t getAdcValue (uint8_t AdcValType)
{
    uint8_t AvgCount = 0;
    uint32_t AvgSum = 0;

    for (AvgCount = 0; AvgCount < 128; AvgCount ++) {
        ADCSR |= (1 << ADSC);                                     // AD Wandler starten
        while (ADCSR & (1 << ADSC));                              // AD Wandlung fertig ?
        AvgSum += ADC;                                            // Mittelwert über 128 Messungen
    }
    if (AdcValType) {
        /*
          ---- Oversampling 12 BIT sowie Gain, AREF und Offset Fehler Kompensation ---- 
        */
        return ((((AvgSum >> 5) * Factor) + Offset) / Scale);     // Ausgabe -> in Volt
    } else {
        /*
          Der ADC raw Wert wird für die Autokalibrierung benötigt.
        */
        return (AvgSum >> 5);
    }
}

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

Für die Autokalibrierung dann:
void FactorOffsetCalHigh (void)
{
    DispStr (pCAL1);

    for (;;) {
        if (KeyStateDown) {                                       // Taste gedrückt ? => ADC Raw Wert
            AdcRawValHigh = getAdcValue (RawVal);                 // für Stützpunkt 2.00V erfassen
            /*
              Stützpunkt 2.00 Volt auf plausiblen Wert prüfen.
            */
            if (AdcRawValHigh < RawMaxLowH || AdcRawValHigh > RawMaxHighH) {

                DispStr (pErr1);                                  // Stützpunkt 2.00 Volt adjust failed
            } else {
                SaveCalData ();
                break;
            }
        } else {
            intTo7Segment (getAdcValue (DispVal));
        }
    }
}

void FactorOffsetCalLow (void)
{
    DispStr (pCAL2);

    for (;;) {
        if (KeyStateDown) {                                       // Taste gedrückt ? => ADC Raw Wert
            AdcRawValLow = getAdcValue (RawVal);                  // für Stützpunkt 0.40V erfassen
            /*
              Stützpunkt 0.40 Volt auf plausiblen Wert prüfen.
            */
            if (AdcRawValLow < RawMaxLowL || AdcRawValLow > RawMaxHighL) {

                DispStr (pErr1);                                  // Stützpunkt 0.40 Volt adjust failed
            } else {
                SaveCalData ();
                break;
            }
        } else {
            intTo7Segment (getAdcValue (DispVal));
        }
    }
}

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:
    Factor = (FacScale / (AdcRawValHigh - AdcRawValLow));
    Offset = (OffScale - (AdcRawValLow * Factor));

Das sollte es sein.

Autor: Dennis B. (Firma: Home) (deboman)
Datum:

Bewertung
0 lesenswert
nicht 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

Autor: Bernd N (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Dennis B. (Firma: Home) (deboman)
Datum:

Bewertung
0 lesenswert
nicht 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.
>
>
> #define AdcRawH 3206                                // Stützpunkt 2.00
> Volt
> #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.
>
>
> return ((((AvgSum >> 7) * Factor) + Offset) >> 16);
> 

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.

Autor: Bernd N (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Wie oft mußt du zurückschieben wenn du wieder auf 10 BIT kommen willst ?

    for (AvgCount = 0; AvgCount < 128; AvgCount ++) {
        ADCSR |= (1 << ADSC);                                     // AD Wandler starten
        while (ADCSR & (1 << ADSC));                              // AD Wandlung fertig ?
        AvgSum += ADC;                                            // Mittelwert über 128 Messungen
    }


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.
return (AvgSum >> 5);

Und den trägst du dann ein.

Autor: Bernd N (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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 ?

Autor: Dennis B. (Firma: Home) (deboman)
Datum:

Bewertung
0 lesenswert
nicht 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?

Autor: Bernd N (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Dennis B. (Firma: Home) (deboman)
Datum:

Bewertung
0 lesenswert
nicht 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?

Autor: Dennis B. (Firma: Home) (deboman)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Dennis B. (Firma: Home) (deboman)
Datum:

Bewertung
0 lesenswert
nicht 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

Autor: Bernd N (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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 :-)

Autor: BioSniper (Gast)
Datum:

Bewertung
-1 lesenswert
nicht 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.

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.
Hinweis: der ursprüngliche Beitrag ist mehr als 6 Monate alt.
Bitte hier nur auf die ursprüngliche Frage antworten,
für neue Fragen einen neuen Beitrag erstellen.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.