AVR-Tutorial: ADC

Wechseln zu: Navigation, Suche

Was macht der ADC?[Bearbeiten]

Wenn es darum geht, Spannungen zu messen, wird der Analog-/Digital-Wandler (kurz: A/D-Wandler) oder englisch Analog Digital Converter (ADC) benutzt. Er konvertiert eine elektrische Spannung in eine Digitalzahl. Prinzipiell wird dabei die Messspannung mit einer Referenzspannung verglichen. Die Zahl drückt daher das Verhältnis der Messspannung zu dieser Referenzspannung aus. Sie kann in gewohnter Weise von einem Mikrocontroller weiterverarbeitet werden.

Elektronische Grundlagen[Bearbeiten]

Die ADC-Versorgungsspannung (AVCC) darf maximal um +/-0,3V von der Versorgung des Digitalteils (VCC) abweichen, jedoch nicht 5,5V überschreiten. Die externe Referenzspannung VREF darf nicht kleiner als die im Datenblatt unter ADC Characteristics als VREFmin angegebene Spannung (z. B. ATmega8: 2V, ATmega644P: 1V) und nicht größer als AVCC sein. Die Spannungen an den Wandlereingängen müssen im Intervall GND ≤ VIN ≤ VREF liegen.

Im Extremfall bedeutet dies: Sei VCC = 5,5V, folgt AVCC_max = VREF_max = VIN_max = 5,5V.

Der Eingangswiderstand des ADC liegt in der Größenordnung von einigen Megaohm, so dass der ADC die Signalquelle praktisch nicht belastet. Desweiteren enthält der Mikrocontroller eine sog. Sample&Hold Schaltung. Dies ist wichtig, wenn sich während des Wandlungsvorgangs die Eingangsspannung verändert, da die AD-Wandlung eine bestimmte Zeit dauert. Die Sample&Hold-Stufe speichert zum Beginn der Wandlung die anliegende Spannung und hält sie während des Wandlungsvorgangs konstant.

Beschaltung des ADC-Eingangs[Bearbeiten]

Um den ADC im Folgenden zu testen wird eine einfache Schaltung an den PC0-Pin des ATmega8 angeschlossen. Dies ist der ADC-Kanal 0. Bei anderen AVR-Typen liegt der entsprechende Eingang auf einem andern Pin, hier ist ein Blick ins Datenblatt angesagt.

Testschaltung

Der Wert des Potentiometers ist Dank des hohen Eingangswiderstandes des ADC ziemlich unkritisch. Es kann jedes Potentiometer von 1kΩ bis 1MΩ benutzt werden.

Wenn andere Messgrößen gemessen werden sollen, so bedient man sich oft und gern des Prinzips des Spannungsteilers. Der Sensor ist ein veränderlicher Widerstand. Zusammen mit einem zweiten, konstanten Widerstand bekannter Größe wird ein Spannungsteiler aufgebaut. Aus der Variation der durch den variablen Spannungsteiler entstehenden Spannung kann auf den Messwert zurückgerechnet werden.

     Vcc ----------+                Vcc ---------+
                   |                             |
                  ---                         Sensor,
                  | |                     der seinen Widerstand
                  | |                     in Abhängigkeit der
                  ---                     Messgröße ändert
                   |                             |
                   +------- PC0                  +-------- PC0
                   |                             |
               Sensor,                          ---
          der seinen Widerstand                 | |
          in Abhängigkeit der                   | |
          Messgröße ändert                      ---
                   |                             |
      GND ---------+                 GND --------+

Die Größe des zweiten Widerstandes im Spannungsteiler richtet sich nach dem Wertebereich, in welchem der Sensor seinen Wert ändert. Als Daumenregel kann man sagen, dass der Widerstand so gross sein sollte wie der Widerstand des Sensors in der Mitte des Messbereichs.

Beispiel: Wenn ein Temperatursensor seinen Widerstand von 0..100 Grad von 2kΩ auf 5kΩ ändert, sollte der zweite Widerstand eine Grösse von etwa (2+5)/2 = 3,5kΩ haben.

Aber egal wie immer man das auch macht, der entscheidende Punkt besteht darin, dass man seine Messgröße in eine veränderliche Spannung 'übersetzt' und mit dem ADC des Mega8 die Höhe dieser Spannung misst. Aus der Höhe der Spannung kann dann wieder in der Umkehrung auf die Messgröße zurückgerechnet werden.

Referenzspannung AREF[Bearbeiten]

Beschaltung von AREF

Der ADC benötigt für seine Arbeit eine Referenzspannung. Dabei gibt es 2 Möglichkeiten:

  • interne Referenzspannung
  • externe Referenzspannung

Bei der Umstellung der Referenzspannung sind Wartezeiten zu beachten, bis die ADC-Hardware einsatzfähig ist (Datenblatt und [1]).

Interne Referenzspannung[Bearbeiten]

Mittels Konfigurationsregister können beim ATmega8 verschiedene Referenzspannungen eingestellt werden. Dies umfasst die Versorgungsspannung AVcc sowie eine vom AVR bereitgestellte Spannung von 2,56V (bzw. bei den neueren AVRs 1,1V, wie z. B. beim ATtiny13, ATmega48, 88, 168, ...). In beiden Fällen wird an den AREF-Pin des Prozessors ein Kondensator von 100nF als Minimalbeschaltung nach Masse angeschlossen, um die Spannung zu puffern/glätten. Es ist jedoch zu beachten, dass die interne Referenzspannung ca. +/-10% vom Nominalwert abweichen kann, vgl. dazu das Datenblatt Abschnitt ADC Characteristics VINT (z. B. ATmega8: 2,3-2,9V, ATmega324P: 2,33-2,79V bzw. 1,0-1,2V "Values are guidelines only."). Die typische Abweichung der internen Referenzspannung vom Sollwert bei einigen AVR-Controllern wird in dieser Testschaltung exemplarisch untersucht.

Externe Referenzspannung[Bearbeiten]

Wird eine externe Referenz verwendet, so wird diese an AREF angeschlossen. Aber aufgepasst! Wenn eine Referenz in Höhe der Versorgungsspannung benutzt werden soll, so ist es besser, dies über die interne Referenz zu tun. Außer bei anderen Spannungen als 5V bzw. 2,56V gibt es eigentlich keinen Grund, an AREF eine Spannungsquelle anzuschließen. In Standardanwendungen fährt man immer besser, wenn die interne Referenzspannung mit einem Kondensator an AREF benutzt wird. Die 10µH-Spule L1 kann man meist auch durch einen 47Ω-Widerstand ersetzen.

Ein paar ADC-Grundlagen[Bearbeiten]

Der ADC ist ein 10-Bit ADC, d.h. er liefert Messwerte im Bereich 0 bis 1023. Liegt am Eingangskanal 0V an, so liefert der ADC einen Wert von 0. Hat die Spannung am Eingangskanal die Referenzspannung erreicht (stimmt nicht ganz), so liefert der ADC einen Wert von 1023. Unterschreitet oder überschreitet die zu messende Spannung diese Grenzen, so liefert der ADC 0 bzw. 1023. Wird die Auflösung von 10 Bit nicht benötigt, so ist es möglich die Ausgabe durch ein Konfigurationsregister so einzuschränken, dass ein leichter Zugriff auf die 8 höchstwertigen Bits möglich ist.

Wie bei vielen analogen Schaltungen, unterliegt auch der ADC einem Rauschen. Das bedeutet, dass man nicht davon ausgehen sollte, dass der ADC bei konstanter Eingangsspannung auch immer denselben konstanten Wert ausgibt. Ein "Zittern" der niederwertigsten 2 Bits ist durchaus nicht ungewöhnlich. Besonders hervorgehoben werden soll an dieser Stelle nochmals die Qualität der Referenzspannung. Diese Qualität geht in erheblichem Maße in die Qualität der Wandlergebnisse ein. Die Beschaltung von AREF mit einem Kondensator ist die absolut notwendige Mindestbeschaltung, um eine einigermaßen akzeptable Referenzspannung zu erhalten. Reicht dies nicht aus, so kann die Qualität einer Messung durch Oversampling erhöht werden. Dazu werden mehrere Messungen gemacht und deren Mittelwert gebildet.

Tut ADC 03.gif

Oft interessiert auch der absolute Spannungspegel nicht. Im Beschaltungsbeispiel oben ist man normalerweise nicht direkt an der am Poti entstehenden Spannung interessiert. Viel mehr ist diese Spannung nur ein notwendiges Übel, um die Stellung des Potis zu bestimmen. In solchen Fällen kann die Poti-Beschaltung wie folgt abgewandelt werden:

Hier wird AREF (bei interner Referenz) als vom µC gelieferte Spannung benutzt und vom Spannungsteiler bearbeitet wieder an den µC zur Messung zurückgegeben. Dies hat den Vorteil, dass der Spannungsteiler automatisch Spannungen bis zur Höhe der Referenzspannung ausgibt, ohne dass eine externe Spannung mit AREF abgeglichen werden müsste. Selbst Schwankungen in AREF wirken sich hier nicht mehr aus, da ja das Verhältnis der Spannungsteilerspannung zu AREF immer konstant bleibt (ratiometrische Messung). Und im Grunde bestimmt der ADC ja nur dieses Verhältnis. Wird diese Variante gewählt, so muss berücksichtigt werden, dass die Ausgangsspannung an AREF nicht allzusehr belastet wird. Der Spannungsteiler muss einen Gesamtwiderstand von deutlich über 10kΩ besitzen. Werte von 100kΩ oder höher sind anzustreben. Verwendet man anstatt AREF AVCC und schaltet auch die Referenzspannung auf AVCC um, ist die Belastung durch den Poti unkritisch, weil hier die Stromversorgung direkt zur Speisung verwendet wird.

Ist hingegen die absolute Spannung von Interesse, so muss man darauf achten, dass ein ADC in digitalen Bereichen arbeitet (Quantisierung). An einem einfacheren Beispiel soll demonstriert werden was damit gemeint ist.

Angenommen der ADC würde nur 5 Stufen auflösen können und AREF sei 5V:

     Volt    Wert vom ADC
      0 -+
         |         0
      1 -+
         |         1
      2 -+
         |         2
      3 -+
         |         3
      4 -+
         |         4
      5 -+

Ein ADC Wert von 0 bedeutet also keineswegs, dass die zu messende Spannung exakt den Wert 0 hat. Es bedeutet lediglich, dass die Messspannung irgendwo im Bereich von 0V bis 1V liegt. Sinngemäß bedeutet daher auch das Auftreten des Maximalwertes nicht, dass die Spannung exakt AREF beträgt, sondern lediglich, dass die Messspannung sich irgendwo im Bereich der letzten Stufe (also von 4V bis 5V) bewegt.

Umrechnung des ADC Wertes in eine Spannung[Bearbeiten]

Die Größe eines "Bereiches" bestimmt sich also zu

Bereichsbreite={\frac  {Referenzspannung}{Maximalwert+1}}

Der Messwert vom ADC rechnet sich dann wie folgt in eine Spannung um:

Spannung=ADCwert\cdot {\frac  {Referenzspannung}{Maximalwert+1}}

Wird der ADC also mit 10 Bit an 5 V betrieben, so lauten die Umrechnungen:

Bereichsbreite={\frac  {5~{\text{V}}}{1024}}=0,004883~{\text{V}}=4,883~{\text{mV}}

Spannung=ADCwert\cdot 4,883~{\text{mV}}

Wenn man genau hinsieht stellt man fest, dass sowohl die Referenzspannung als auch der Maximalwert Konstanten sind. D.h. der Quotient aus Referenzspannung und Maximalwert ist konstant. Somit muss nicht immer eine Addition und Division ausgeführt werden, sondern nur eine Multiplikation! Das spart viel Aufwand und Rechenzeit! Dabei kann sinnvollerweise Festkommaarithmetik zum Einsatz kommen.

Kalibrierung[Bearbeiten]

Hat man eine externe, genaue Referenzspannung zur Hand, dann kann ein Korrekturfaktor berechnet werden, mit dem die Werte des ADCs im Nachhinein korrigiert werden können. Dies geschieht normalerweise über eine sogenannte gain offset Korrektur an einer Geraden oder einer Parabel. In erster Näherung kann man auch die interne Referenzspannung um das Inverse des ermittelten Korrekturwertes verstellen, um einen genaueren bereits digitalisierten Wert zu bekommen.

Die Steuerregister des ADC[Bearbeiten]

ADMUX[Bearbeiten]

ADMUX
REFS1 REFS0 ADLAR MUX3 MUX2 MUX1 MUX0


  • Referenzspannung REFS1, REFS0
REFS1 REFS0 Referenz
0 0 externe Referenz
0 1 interne Referenz: Avcc
1 0 wird beim Mega8 nicht benutzt
1 1 interne Referenz: 2.56 Volt
  • Ausrichtung ADLAR
ADLAR
0Das Ergebnis wird in den Registern ADCH/ADCL rechtsbündig ausgerichtet. Die 8 niederwertigsten Bits des Ergebnisses werden in ADCL abgelegt. Die verbleibenden 2 Bits des Ergebnisses werden im Register ADCH in den Bits 0 und 1 abgelegt.
1Das Ergebnis wird in den Registern ADCH/ADCL linksbündig ausgerichtet. Die 8 höchstwertigen Bits des Ergebnisses werden in ADCH abgelegt. Die verbleibenden 2 niederwertigen Bits werden im Register ADCL in den Bits 6 und 7 abgelegt.


  • Kanalwahl MUX3, MUX2, MUX1, MUX0
MUX3 MUX2 MUX1 MUX0 Kanal
0 0 0 0 Kanal 0, Pin PC0
0 0 0 1 Kanal 1, Pin PC1
0 0 1 0 Kanal 2, Pin PC2
0 0 1 1 Kanal 3, Pin PC3
0 1 0 0 Kanal 4, Pin PC4
0 1 0 1 Kanal 5, Pin PC5
0 1 1 0 Kanal 6 (*)
0 1 1 1 Kanal 7 (*)
1 1 1 0 1.23V, Vbg
1 1 1 1 0V, GND

(*) Bei Atmega8 nur in der Gehäusebauform TQFP und MLF verfügbar, nicht in PDIP

ADCSRA[Bearbeiten]

ADCSRA
ADEN ADSC ADFR ADIF ADIE ADPS2 ADPS1 ADPS0


ADEN
"ADC Enable": Mittels ADEN wird der ADC ein und ausgeschaltet. Eine 1 an dieser Bitposition schaltet den ADC ein.
ADSC
"ADC Start Conversion": Wird eine 1 an diese Bitposition geschrieben, so beginnt der ADC mit der Wandlung. Das Bit bleibt auf 1, solange die Wandlung im Gange ist. Wenn die Wandlung beendet ist, wird dieses Bit von der ADC Hardware wieder auf 0 gesetzt.
ADFR
"ADC Free Running": Wird eine 1 an ADFR geschrieben, so wird der ADC im Free Running Modus betrieben. Dabei startet der ADC nach dem Abschluss einer Messung automatisch die nächste Messung. Die erste Messung wird ganz normal über das Setzen des ADSC-Bits gestartet.
ADIF
"ADC Interrupt Flag": Wenn eine Messung abgeschlossen ist, wird das ADIF Bit gesetzt. Ist zusätzlich noch das ADIE Bit gesetzt, so wird ein Interrupt ausgelöst und der entsprechende Interrupt Handler angesprungen.
ADIE
"ADC Interrupt Enable": Wird eine 1 an ADIE geschrieben, so löst der ADC nach Beendigung einer Messung einen Interrupt aus.
ADPS2, ADPS1, ADPS0
"ADC Prescaler": Mit dem Prescaler kann die ADC-Frequenz gewählt werden. Laut Datenblatt sollte diese für die optimale Auflösung zwischen 50kHz und 200kHz liegen. Ist die Wandlerfrequenz langsamer eingestellt, kann es passieren dass die eingebaute Sample & Hold Schaltung die Eingangsspannung nicht lange genug konstant halten kann. Ist die Frequenz aber zu schnell eingestellt, dann kann es passieren dass sich die Sample & Hold Schaltung nicht schnell genug an die Eingangsspannung anpassen kann.
ADPS2 ADPS1 ADPS0 Vorteiler
0 0 0 2
0 0 1 2
0 1 0 4
0 1 1 8
1 0 0 16
1 0 1 32
1 1 0 64
1 1 1 128
Beispiel
8 MHz Prozessortakt: 8.000.000Hz / 200.000Hz = 40

Da mit 200kHz gerechnet wurde(Maximale Frequenz), nimmt man den nächst höheren Wert, also 64.

8.000.000 Hz / 64 = 125.000Hz = 125kHz So erhält man bei 8 MHz einen Prescaler von 64 und eine Frequenz von 125kHz.

Die Ergebnisregister ADCL und ADCH[Bearbeiten]

Da das Ergebnis des ADC ein 10 Bit Wert ist, passt dieser Wert naturgemäß nicht in ein einzelnes Register, das ja bekanntlich nur 8 Bit breit ist. Daher wird das Ergebnis in 2 Register ADCL und ADCH abgelegt. Standardmäßig (d.h. ADLAR = 0) werden von den 10 Ergebnisbits die niederwertigsten 8 im Register ADCL abgelegt und die noch fehlenden 2 Bits im Register ADCH an den niederwertigsten Bitpositionen gespeichert.

             ADCH                                   ADCL
  +---+---+---+---+---+---+---+---+   +---+---+---+---+---+---+---+---+
  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
  +---+---+---+---+---+---+---+---+   +---+---+---+---+---+---+---+---+
                            9   8       7   6   5   4   3   2   1   0

Ist keine 10-bit Genauigkeit gefragt, kann diese Zuordnung aber auch geändert werden: Durch Setzen des ADLAR Bits im ADMUX Register wird die Ausgabe geändert zu:

             ADCH                                   ADCL
  +---+---+---+---+---+---+---+---+   +---+---+---+---+---+---+---+---+
  |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
  +---+---+---+---+---+---+---+---+   +---+---+---+---+---+---+---+---+
    9   8   7   6   5   4   3   2       1   0

Auf diese Weise kann das ADC Ergebnis direkt als 8 Bit Zahl weiterverarbeitet werden: Die 8 höchstwertigen Bits stehen bereits verarbeitungsfertig im Register ADCH zur Verfügung.

Beim Auslesen der ADC-Register ist zu beachten: Immer zuerst ADCL und erst dann ADCH auslesen. Beim Zugriff auf ADCL wird das ADCH Register gegenüber Veränderungen vom ADC gesperrt. Erst beim nächsten Auslesen des ADCH-Registers wird diese Sperre wieder aufgehoben. Dadurch ist sichergestellt, daß die Inhalte von ADCL und ADCH immer aus demselben Wandlungsergebnis stammen, selbst wenn der ADC im Hintergrund selbsttätig weiterwandelt. Das ADCH Register muss ausgelesen werden!

Beispiele[Bearbeiten]

Ausgabe als ADC-Wert[Bearbeiten]

Das folgende Programm liest in einer Schleife ständig den ADC aus und verschickt das Ergebnis im Klartext (ASCII) über die UART. Zur Verringerung des unvermeidlichen Rauschens werden 256 Messwerte herangezogen und deren Mittelwert als endgültiges Messergebnis gewertet. Dazu werden die einzelnen Messungen in den Registern temp2, temp3, temp4 als 24 Bit Zahl aufaddiert. Die Division durch 256 erfolgt dann ganz einfach dadurch, dass das Register temp2 verworfen wird und die Register temp3 und temp4 als 16 Bit Zahl aufgefasst werden. Eine Besonderheit ist noch, dass je nach dem Wert in temp2 die 16 Bit Zahl in temp3 und temp4 noch aufgerundet wird: Enthält temp2 einen Wert größer als 128, dann wird zur 16 Bit Zahl in temp3/temp4 noch 1 dazu addiert.

In diesem Programm findet man oft die Konstruktion

 
    subi    temp3, low(-1)      ; addieren von 1
    sbci    temp4, high(-1)     ; addieren des Carry

Dabei handelt es sich um einen kleinen Trick. Um eine Konstante zu einem Register direkt addieren zu können bräuchte man einen Befehl ala addi (Add Immediate, Addiere Konstante), den der AVR aber nicht hat. Ebenso gibt es kein adci (Add with carry Immediate, Addiere Konstante mit Carry Flag). Man müsste also erst eine Konstante in ein Register laden und addieren. Das kostet aber Programmspeicher, Rechenzeit und man muss ein Register zusätzlich frei haben.

 
; 16 Bit Addition mit Konstante, ohne Cleverness
    ldi     temp5, low(1)
    add     temp3, temp5        ; addieren von 1
    ldi     temp5, high(1)
    adc     temp3, temp5        ; addieren des Carry

Hier greift man einfach zu dem Trick, dass eine Addition gleich der Subtraktion der negativen Werts ist. Also "addiere +1" ist gleich "subtrahiere -1". Dafür hat der AVR zwei Befehle, subi (Substract Immediate, Subtrahiere Konstante) und sbci (Substract Immediate with carry, Subtrahiere Konstante mit Carry Flag).

Das folgende Programm ist für den ATmega8 geschrieben. Für moderne Nachfolgetypen wie den ATmega88 muss der Code angepasst werden ([2], AVR094: Replacing ATmega8 by ATmega88 (PDF)).

 
.include "m8def.inc"
 
.def temp1     = r16         ; allgemeines temp Register, zur kurzfristigen Verwendung
.def temp2     = r17         ; Register für 24 Bit Addition, Lowest Byte
.def temp3     = r18         ; Register für 24 Bit Addition, Middle Byte
.def temp4     = r19         ; Register für 24 Bit Addition, Highest Byte
.def adlow     = r20         ; Ergebnis vom ADC / Mittelwert der 256 Messungen
.def adhigh    = r21         ; Ergebnis vom ADC / Mittelwert der 256 Messungen
.def messungen = r22         ; Schleifenzähler für die Messungen
.def ztausend  = r23         ; Zehntausenderstelle des ADC Wertes
.def tausend   = r24         ; Tausenderstelle des ADC Wertes
.def hundert   = r25         ; Hunderterstelle des ADC Wertes
.def zehner    = r26         ; Zehnerstelle des ADC Wertes
.def zeichen   = r27         ; Zeichen zur Ausgabe auf den UART
 
.equ F_CPU = 4000000                            ; Systemtakt in Hz
.equ BAUD  = 9600                               ; Baudrate
 
; Berechnungen
.equ UBRR_VAL   = ((F_CPU+BAUD*8)/(BAUD*16)-1)  ; clever runden
.equ BAUD_REAL  = (F_CPU/(16*(UBRR_VAL+1)))     ; Reale Baudrate
.equ BAUD_ERROR = ((BAUD_REAL*1000)/BAUD-1000)  ; Fehler in Promille
 
.if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))       ; max. +/-10 Promille Fehler
  .error "Systematischer Fehler der Baudrate grösser 1 Prozent und damit zu hoch!"
.endif
 
; hier geht das Programm los
 
    ldi     temp1, LOW(RAMEND)                  ; Stackpointer initialisieren
    out     SPL, temp1
    ldi     temp1, HIGH(RAMEND)
    out     SPH, temp1
 
;UART Initalisierung
 
    ldi     temp1, LOW(UBRR_VAL)                    ; Baudrate einstellen
    out     UBRRL, temp1
    ldi     temp1, HIGH(UBRR_VAL)
    out     UBRRH, temp1
 
    sbi     UCSRB, TXEN                         ; TX einschalten
 
; ADC initialisieren: ADC0, Vcc als Referenz, Single Conversion, Vorteiler 128
 
    ldi     temp1, (1<<REFS0)                   ; Kanal 0, interne Referenzspannung 5V
    out     ADMUX, temp1
    ldi     temp1, (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0)
    out     ADCSRA, temp1
 
Main:
    clr     temp1
    clr     temp2
    clr     temp3
    clr     temp4
 
    ldi     messungen, 0        ; 256 Schleifendurchläufe
 
; neuen ADC-Wert lesen  (Schleife - 256 mal)
 
sample_adc:
    sbi     ADCSRA, ADSC        ; den ADC starten
 
wait_adc:
    sbic    ADCSRA, ADSC        ; wenn der ADC fertig ist, wird dieses Bit gelöscht
    rjmp    wait_adc
 
; ADC einlesen:
 
    in      adlow, ADCL         ; immer zuerst LOW Byte lesen
    in      adhigh, ADCH        ; danach das mittlerweile gesperrte High Byte
 
; alle 256 ADC-Werte addieren
; dazu wird mit den Registern temp4, temp3 und temp2 ein
; 24-Bit breites Akkumulationsregister gebildet, in dem
; die 10 Bit Werte aus adhigh, adlow aufsummiert werden
 
    add     temp2, adlow        ; addieren
    adc     temp3, adhigh       ; addieren über Carry
    adc     temp4, temp1        ; addieren über Carry, temp1 enthält 0
    dec     messungen           ; Schleifenzähler MINUS 1
    brne    sample_adc          ; wenn noch keine 256 ADC Werte -> nächsten Wert einlesen
 
; Aus den 256 Werten den Mittelwert berechnen
; Mathematisch eine Division durch 256
; Da aber 2^8 = 256 ist ist da einfach durch das weglassen des niederwertigsten Bytes
; erreicht werden
;
; allerdings wird der Wert noch gerundet
 
    cpi     temp2,128           ; "Kommastelle" kleiner als 128 ?
    brlo    no_round            ; ist kleiner ==> Sprung
 
; Aufrunden
    subi    temp3, low(-1)      ; addieren von 1
    sbci    temp4, high(-1)     ; addieren des Carry
 
no_round:
 
;   Ergebnis nach adlow und adhigh kopieren
;   damit die temp Register frei werden
 
    mov     adlow, temp3
    mov     adhigh, temp4
 
;in ASCII umwandeln
; Division durch mehrfache Subtraktion
 
    ldi     ztausend, '0'-1     ; Ziffernzähler direkt als ASCII Code
    ; bzgl. '0'-1 siehe http://www.mikrocontroller.net/topic/198681
Z_ztausend:
    inc     ztausend
    subi    adlow, low(10000)   ; -10,000
    sbci    adhigh, high(10000) ; 16 Bit
    brcc    Z_ztausend
 
    subi    adlow, low(-10000)  ; nach Unterlauf wieder einmal addieren
    sbci    adhigh, high(-10000); +10,000
 
    ldi     tausend, '0'-1      ; Ziffernzähler direkt als ASCII Code
Z_tausend:
    inc     tausend
    subi    adlow, low(1000)    ; -1,000
    sbci    adhigh, high(1000)  ; 16 Bit
    brcc    Z_tausend
 
    subi    adlow, low(-1000)   ; nach Unterlauf wieder einmal addieren
    sbci    adhigh, high(-1000) ; +1,000
 
    ldi     hundert, '0'-1      ; Ziffernzähler direkt als ASCII Code
Z_hundert:
    inc     hundert
    subi    adlow, low(100)     ; -100
    sbci    adhigh, high(100)   ; 16 Bit
    brcc    Z_hundert
 
    subi    adlow, low(-100)    ; nach Unterlauf wieder einmal addieren
    sbci    adhigh, high(-100)  ; +100
 
    ldi     zehner, '0'-1       ; Ziffernzähler direkt als ASCII Code
Z_zehner:
    inc     zehner
    subi    adlow, low(10)      ; -10
    sbci    adhigh, high(10)    ; 16 Bit
    brcc    Z_zehner
 
    subi    adlow, low(-10)     ; nach Unterlauf wieder einmal addieren
    sbci    adhigh, high(-10)   ; +10
 
    subi    adlow, -'0'         ; adlow enthält die Einer, Umwandlung in ASCII
 
;an UART Senden
 
    mov     zeichen, ztausend   ; Zehntausender Stelle
    rcall   transmit
    mov     zeichen, tausend    ; Tausender Stelle ausgeben
    rcall   transmit    
    mov     zeichen, hundert    ; Hunderter Stelle ausgeben
    rcall   transmit
    mov     zeichen, zehner     ; Zehner Stelle ausgeben
    rcall   transmit
    mov     zeichen, adlow      ; Einer Stelle ausgeben
    rcall   transmit
    ldi     zeichen, 13         ; CR, Carrige Return (Wagenrücklauf)
    rcall   transmit
    ldi     zeichen, 10         ; LF, Line Feed (Neue Zeile)
    rcall   transmit
 
    rjmp    Main
 
transmit:
    sbis    UCSRA,UDRE          ; Warten, bis UDR bereit ist ...
    rjmp    transmit
    out     UDR, zeichen        ; und Zeichen ausgeben
    ret

Ausgabe als Spannungswert[Bearbeiten]

Das zweite Beispiel ist schon um einiges größer. Hier wird der gemittelte ADC-Wert in eine Spannung umgerechnet. Dazu wird Festkommaarithmetik verwendet. Die Daten sind in diesem Fall

  • Referenzspannung : 5V
  • alte Auflösung  : 5V / 1024 = 4,8828125mV
  • neue Auflösung  : 1mV

-> Faktor = 4,8828125mV / 1mV = 4,8828125

Der Faktor wird dreimal mit 10 multipliziert und das Ergebnis auf 4883 gerundet. Die neue Auflösung wird dreimal durch 10 dividiert und beträgt 1μV. Der relative Fehler beträgt

F_{r}={\frac  {4883}{4882,8125}}-1=0,00384\%={\frac  {1}{26042}}

Dieser Fehler ist absolut vernachlässigbar. Nach der Multiplikation des ADC-Wertes mit 4883 liegt die gemessene Spannung in der Einheit μV vor. Vorsicht! Das ist nicht die reale Auflösung und Genauigkeit, nur rein mathematisch bedingt. Für maximale Genauigkeit sollte man die Versorgungsspannung AVCC, welche hier gleichzeitig als Referenzspannung dient, exakt messen, die Rechnung nachvollziehen und den Wert im Quelltext eintragen. Damit führt man eine einfach Einpunktkalibrierung durch.

Da das Programm schon um einiges größer und komplexer ist, wurde es im Vergleich zur Vorgängerversion geändert. Die Multiplikation sowie die Umwandung der Zahl in einen ASCII-String sind als Unterprogramme geschrieben, dadurch erhält man wesentlich mehr Überblick im Hauptprogramm und die Wiederverwendung in anderen Programmen vereinfacht sich. Ausserdem wird der String im RAM gespeichert und nicht mehr in CPU-Registern. Die Berechung der einzelnen Ziffern erfolgt über ein Schleife, das ist kompakter und übersichtlicher.

 
.include "m8def.inc"
 
.def z0        = r1          ; Zahl für Integer -> ASCII Umwandlung
.def z1        = r2
.def z2        = r3
.def z3        = r4
.def temp1     = r16         ; allgemeines Register, zur kurzfristigen Verwendung
.def temp2     = r17         ; Register für 24 Bit Addition, niederwertigstes Byte (LSB)
.def temp3     = r18         ; Register für 24 Bit Addition, mittlerers Byte
.def temp4     = r19         ; Register für 24 Bit Addition, höchstwertigstes Byte (MSB)
.def adlow     = r20         ; Ergebnis vom ADC-Mittelwert der 256 Messungen
.def adhigh    = r21         ; Ergebnis vom ADC-Mittelwert der 256 Messungen
.def messungen = r22         ; Schleifenzähler für die Messungen
.def zeichen   = r23         ; Zeichen zur Ausgabe auf den UART
.def temp5     = r24
.def temp6     = r25
 
; Faktor für Umrechung des ADC-Wertes in Spannung
; = (Referenzspannung / 1024 ) * 100000
; = 5V / 1024 * 1.000.000
.equ Faktor = 4883
 
.equ F_CPU = 4000000                            ; Systemtakt in Hz
.equ BAUD  = 9600                               ; Baudrate
 
; Berechnungen
.equ UBRR_VAL   = ((F_CPU+BAUD*8)/(BAUD*16)-1)  ; clever runden
.equ BAUD_REAL  = (F_CPU/(16*(UBRR_VAL+1)))     ; Reale Baudrate
.equ BAUD_ERROR = ((BAUD_REAL*1000)/BAUD-1000)  ; Fehler in Promille
 
.if ((BAUD_ERROR>10) || (BAUD_ERROR<-10))       ; max. +/-10 Promille Fehler
  .error "Systematischer Fehler der Baudrate grösser 1 Prozent und damit zu hoch!"
.endif
 
; RAM
.dseg
.org 0x60
Puffer: .byte 10
 
; hier geht das Programm los
.cseg
.org 0
 
    ldi     temp1, LOW(RAMEND)                  ; Stackpointer initialisieren
    out     SPL, temp1
    ldi     temp1, HIGH(RAMEND)
    out     SPH, temp1
 
;UART Initalisierung
 
    ldi     temp1, LOW(UBRR_VAL)                ; Baudrate einstellen
    out     UBRRL, temp1
    ldi     temp1, HIGH(UBRR_VAL)
    out     UBRRH, temp1
 
    sbi     UCSRB, TXEN                         ; TX einschalten
 
; ADC initialisieren: Single Conversion, Vorteiler 128
; Kanal 0, interne Referenzspannung AVCC
 
    ldi     temp1, (1<<REFS0)                   
    out     ADMUX, temp1
    ldi     temp1, (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0)
    out     ADCSRA, temp1
 
Hauptschleife:
    clr     temp1
    clr     temp2
    clr     temp3
    clr     temp4
 
    ldi     messungen, 0        ; 256 Schleifendurchläufe
 
; neuen ADC-Wert lesen  (Schleife - 256 mal)
 
adc_messung:
    sbi     ADCSRA, ADSC        ; den ADC starten
 
adc_warten:
    sbic    ADCSRA, ADSC        ; wenn der ADC fertig ist, wird dieses Bit gelöscht
    rjmp    adc_warten
 
; ADC einlesen:
 
    in      adlow, ADCL         ; immer zuerst LOW Byte lesen
    in      adhigh, ADCH        ; danach das mittlerweile gesperrte High Byte
 
; alle 256 ADC-Werte addieren
; dazu wird mit den Registern temp4, temp3 und temp2 ein
; 24-Bit breites Akkumulationsregister gebildet, in dem
; die 10 Bit Werte aus adhigh, adlow aufsummiert werden
 
    add     temp2, adlow        ; addieren
    adc     temp3, adhigh       ; addieren über Carry
    adc     temp4, temp1        ; addieren über Carry, temp1 enthält 0
    dec     messungen           ; Schleifenzähler MINUS 1
    brne    adc_messung         ; wenn noch keine 256 ADC Werte -> nächsten Wert einlesen
 
; Aus den 256 Werten den Mittelwert berechnen
; Bei 256 Werten ist das ganz einfach: Das niederwertigste Byte
; (im Register temp2) fällt einfach weg
;
; allerdings wird der Wert noch gerundet
 
    cpi     temp2,128           ; "Kommastelle" kleiner als 128 ?
    brlo    nicht_runden        ; ist kleiner ==> Sprung
 
; Aufrunden
    subi    temp3, low(-1)      ; addieren von 1
    sbci    temp4, high(-1)     ; addieren des Carry
 
nicht_runden:
 
;   Ergebnis nach adlow und adhigh kopieren
;   damit die temp Register frei werden
 
    mov     adlow, temp3
    mov     adhigh, temp4
 
; in Spannung umrechnen
 
    ldi     temp5,low(Faktor)
    ldi     temp6,high(Faktor)
    rcall   mul_16x16
 
; in ASCII umwandeln
 
    ldi     XL, low(Puffer)
    ldi     XH, high(Puffer)
    rcall   Int_to_ASCII
 
;an UART Senden
 
    ldi     ZL, low(Puffer+3)
    ldi     ZH, high(Puffer+3)
    ldi     temp1, 1
    rcall   sende_zeichen       ; eine Vorkommastelle ausgeben
 
    ldi     zeichen, ','        ; Komma ausgeben
    rcall   sende_einzelzeichen
 
    ldi     temp1, 3            ; Drei Nachkommastellen ausgeben
    rcall   sende_zeichen
 
    ldi     zeichen, 'V'        ; Volt Zeichen ausgeben
    rcall   sende_einzelzeichen
 
    ldi     zeichen, 10         ; New Line Steuerzeichen
    rcall   sende_einzelzeichen
 
    ldi     zeichen, 13         ; Carrige Return Steuerzeichen
    rcall   sende_einzelzeichen
 
    rjmp    Hauptschleife
 
; Ende des Hauptprogramms
 
; Unterprogramme
 
 ; ein Zeichen per UART senden
 
sende_einzelzeichen:
    sbis    UCSRA,UDRE          ; Warten, bis UDR bereit ist ...
    rjmp    sende_einzelzeichen
    out     UDR, zeichen        ; und Zeichen ausgeben
    ret
 
; mehrere Zeichen ausgeben, welche durch Z adressiert werden
; Anzahl in temp1
 
sende_zeichen:
    sbis    UCSRA,UDRE          ; Warten, bis UDR bereit ist ...
    rjmp    sende_zeichen
    ld      zeichen, Z+         ; Zeichen laden
    out     UDR, zeichen        ; und Zeichen ausgeben
    dec     temp1
    brne    sende_zeichen
    ret
 
; 32 Bit Zahl in ASCII umwandeln
; Zahl liegt in temp1..4
; Ergebnis ist ein 10stelliger ASCII String, welcher im SRAM abgelegt wird
; Adressierung über X Pointer
; mehrfache Subtraktion wird als Ersatz für eine Division durchgeführt.
 
Int_to_ASCII:
 
    push    ZL                      ; Register sichern
    push    ZH
    push    temp5
    push    temp6
 
    ldi     ZL,low(Tabelle*2)       ; Zeiger auf Tabelle
    ldi     ZH,high(Tabelle*2)
    ldi     temp5, 10               ; Schleifenzähler
 
Int_to_ASCII_schleife:
    ldi     temp6, -1+'0'           ; Ziffernzähler zählt direkt im ASCII Code 
    lpm     z0,Z+                   ; Nächste Zahl laden
    lpm     z1,Z+
    lpm     z2,Z+
    lpm     z3,Z+
 
Int_to_ASCII_ziffer:
    inc     temp6                   ; Ziffer erhöhen
    sub     temp1, z0               ; Zahl subrahieren
    sbc     temp2, z1               ; 32 Bit
    sbc     temp3, z2
    sbc     temp4, z3
    brge    Int_to_ASCII_ziffer     ; noch kein Unterlauf, nochmal
 
    add     temp1, z0               ; Unterlauf, eimal wieder addieren
    adc     temp2, z1               ; 32 Bit
    adc     temp3, z2
    adc     temp4, z3                                            
    st      X+,temp6                ; Ziffer speichern
    dec     temp5
    brne    Int_to_ASCII_schleife   ; noch eine Ziffer?
 
    pop     temp6
    pop     temp5
    pop     ZH
    pop     ZL                      ; Register wieder herstellen
    ret
 
; Tabelle mit Zahlen für die Berechung der Ziffern
; 1 Milliarde bis 1
Tabelle:
.dd 1000000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1
 
; 16 Bit Wert in Spannung umrechnen
;
; = 16Bitx16Bit=32 Bit Multiplikation
; = vier 8x8 Bit Multiplikationen
;
; adlow/adhigh * temp5/temp6
 
mul_16x16:
    push    zeichen
    clr     temp1                   ; 32 Bit Akku löschen
    clr     temp2
    clr     temp3
    clr     temp4
    clr     zeichen                 ; Null, für Carry-Addition
 
    mul     adlow, temp5            ; erste Multiplikation
    add     temp1, r0               ; und akkumulieren
    adc     temp2, r1
 
    mul     adhigh, temp5           ; zweite Multiplikation
    add     temp2, r0               ; und gewichtet akkumlieren
    adc     temp3, r1
 
    mul     adlow, temp6            ; dritte Multiplikation
    add     temp2, r0               ; und gewichtet akkumlieren
    adc     temp3, r1
    adc     temp4, zeichen          ; carry addieren
 
    mul     adhigh, temp6           ; vierte Multiplikation
    add     temp3, r0               ; und gewichtet akkumlieren
    adc     temp4, r1
 
    pop     zeichen
    ret

Für alle, die es besonders eilig haben gibt es hier eine geschwindigkeitsoptimierte Version der Integer in ASCII Umwandlung. Zunächst wird keine Schleife verwendet sondern alle Stufen der Schleife direkt hingeschrieben. Das braucht zwar mehr Programmspeicher, ist aber schneller. Ausserdem wird abwechselnd subtrahiert und addiert, dadurch entfällt das immer wieder notwendige addieren nach dem Unterlauf. Zu guter Letzt werden die Berechnungen nur mit der minimal notwenigen Wortbreite durchgeführt. Am Anfang mit 32 Bit, dann nur noch mit 16 bzw. 8 Bit.

 
; 32 Bit Zahl in ASCII umwandeln
; geschwindigkeitsoptimierte Version
; Zahl liegt in temp1..4
; Ergebnis ist ein 10stelliger ASCII String, welcher im SRAM abgelegt wird
; Adressierung über X Pointer
 
Int_to_ASCII:
    ldi     temp5, -1 + '0'
_a1ser:
    inc     temp5
    subi    temp1,BYTE1(1000000000) ; - 1.000.000.000
    sbci    temp2,BYTE2(1000000000)
    sbci    temp3,BYTE3(1000000000)
    sbci    temp4,BYTE4(1000000000)
    brcc    _a1ser
 
    st      X+,temp5                ; im Puffer speichern
    ldi     temp5, 10 + '0'
_a2ser:
    dec     temp5
    subi    temp1,BYTE1(-100000000) ; + 100.000.000
    sbci    temp2,BYTE2(-100000000)
    sbci    temp3,BYTE3(-100000000)
    sbci    temp4,BYTE4(-100000000)
    brcs    _a2ser
 
    st      X+,temp5                ; im Puffer speichern
    ldi     temp5, -1 + '0'
_a3ser:
    inc     temp5
    subi    temp1,low(10000000)     ; - 10.000.000
    sbci    temp2,high(10000000)
    sbci    temp3,BYTE3(10000000)
    sbci    temp4,BYTE4(10000000)
    brcc    _a3ser
 
    st      X+,temp5                ; im Puffer speichern
    ldi     temp5, 10 + '0'
_a4ser:
    dec     temp5
    subi    temp1,low(-1000000)     ; + 1.000.000
    sbci    temp2,high(-1000000)
    sbci    temp3,BYTE3(-1000000)
    sbci    temp4,BYTE4(-1000000)
    brcs    _a4ser
 
    st      X+,temp5                ; im Puffer speichern
    ldi     temp5, -1 + '0'
_a5ser:
    inc     temp5
    subi    temp1,low(100000)       ; -100.000
    sbci    temp2,high(100000)
    sbci    temp3,BYTE3(100000)
    brcc    _a5ser
 
    st      X+,temp5                ; im Puffer speichern
    ldi     temp5, 10 + '0'
_a6ser:
    dec     temp5
    subi    temp1,low(-10000)       ; +10,000
    sbci    temp2,high(-10000)
    sbci    temp3,BYTE3(-10000)
    brcs    _a6ser
 
    st      X+,temp5                ; im Puffer speichern 
    ldi     temp5, -1 + '0'
_a7ser:
    inc     temp5
    subi    temp1,low(1000)         ; -1000
    sbci    temp2,high(1000)
    brcc    _a7ser
 
    st      X+,temp5                ; im Puffer speichern
    ldi     temp5, 10 + '0'
_a8ser:
    dec     temp5
    subi    temp1,low(-100)         ; +100
    sbci    temp2,high(-100)
    brcs    _a8ser
 
    st      X+,temp5                ; im Puffer speichern
    ldi     temp5, -1 + '0'
_a9ser:
    inc     temp5
    subi    temp1, 10               ; -10
    brcc    _a9ser
 
    st      X+,temp5                ; im Puffer speichern
    ldi     temp5, 10 + '0'
_a10ser:
    dec     temp5
    subi    temp1, -1               ; +1
    brcs    _a10ser
 
    st      X+,temp5                ; im Puffer speichern
    ret