AVR-Tutorial: ADC

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Was macht der ADC?

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

Die ADC-Versorgungsspannung (AVCC) darf maximal um ±0,3 V von der Versorgung des Digitalteils (VCC) abweichen, jedoch nicht 5,5 V überschreiten. Die externe Referenzspannung VREF darf nicht kleiner als die im Datenblatt unter ADC Characteristics als VREFmin angegebene Spannung (z. B. ATmega8: 2 V, ATmega644P: 1 V) 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,5 V, folgt AVCCmax = VREFmax = VINmax = 5,5 V.

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 sogenannte 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

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 anderen 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 1 kΩ bis 1 MΩ 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 groß sein sollte wie der Widerstand des Sensors in der Mitte des Messbereichs.

Beispiel: Wenn ein Temperatursensor seinen Widerstand im Bereich 0…100 °C von 2 kΩ auf 5 kΩ ändert, sollte der zweite Widerstand eine Größe von etwa (2+5)/2 kΩ = 3,5 kΩ 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 ATmega8 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

Beschaltung von AREF

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

  • interne Referenzspannung
  • externe Referenzspannung

Bei der Umstellung der Referenzspannung sind Wartezeiten zu beachten, bis die ADC-Hardware einsatzfähig ist (siehe Datenblatt (PDF; 6,3 MB) und diesen Forumsthread).

Interne Referenzspannung

Mittels Konfigurationsregister können beim ATmega8 verschiedene Referenzspannungen eingestellt werden. Dies umfasst die Versorgungsspannung AVcc sowie eine vom AVR bereitgestellte Spannung von 2,56 V (bzw. bei den neueren AVRs 1,1 V, wie z. B. beim ATtiny13, ATmega48, -88, -168, …). In beiden Fällen wird an den AREF-Pin des Prozessors ein Kondensator von 100 nF 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,9 V, ATmega324P: 2,33…2,79 V bzw. 1,0…1,2 V, „Values are guidelines only.“). Die typische Abweichung der internen Referenzspannung vom Sollwert bei einigen AVR-Controllern wird in dieser Testschaltung exemplarisch untersucht.

Externe Referenzspannung

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 5 V bzw. 2,56 V 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

Der im AVR eingebaute ADC ist ein 10-Bit-ADC, d. h. er liefert Messwerte im Bereich 0 bis 1023. Liegt am Eingangskanal 0 V 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 zwei 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 Wandlerergebnisse 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. Vielmehr ist diese Spannung nur ein Mittel zum Zweck, 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 Controller gelieferte Spannung benutzt und vom Spannungsteiler bearbeitet wieder an den Controller 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 10 kΩ besitzen. Werte von 100 kΩ oder höher sind anzustreben. Verwendet man AVCC anstatt AREF und schaltet auch die Referenzspannung auf AVCC um, ist die Belastung durch das 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 5 V:

     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 0 V bis 1 V 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 4 V bis 5 V) bewegt.

Umrechnung des ADC-Wertes in eine Spannung

Die Größe eines „Bereiches“ bestimmt sich also zu

[math]\displaystyle{ \text{Bereichsbreite} = \frac{\text{Referenzspannung}}{\text{Maximalwert}+1} }[/math].

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

[math]\displaystyle{ \text{Spannung} = \text{ADCwert} \cdot \frac{\text{Referenzspannung}}{\text{Maximalwert}} }[/math]

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

[math]\displaystyle{ \text{Bereichsbreite} = \frac{5\,\text{V}}{1024} = 0{,}004883\,\text{V} = 4{,}883\,\text{mV} }[/math]

[math]\displaystyle{ \text{Spannung} = \text{ADCwert} \cdot 4{,}883\,\text{mV} }[/math]

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.

Umgekehrte Weltsicht: Eigene Betriebsspannung messen

Bei vielen AVRs ist es möglich, die Referenzspannung (bspw. 1,1 V) bezüglich der Betriebsspannung zu messen. Dadurch ist es möglich, auf die Höhe der Betriebsspannung zu schließen:

[math]\displaystyle{ \text{Betriebsspannung} = \text{Maximalwert} \cdot \frac{\text{Referenzspannung}}{\text{ADCwert}} }[/math]

Leider geht das nicht mit einer Referenzspannung am AREF-Pin (wenn vorhanden) sondern nur mit der internen Referenzspannung. Folgende AVRs können das (X = Flash-Größe in KByte):

  • Tinys: ATtinyX61, ATtinyX0Y
  • Megas ohne USB: ATmega8, ATmegaX8, ATmega32, ATmega2560, ...
  • Megas mit USB: ATmegaXU4, ATmegaXU6

Diese können das nicht:

  • ATtiny5, ATtiny10, ATtiny13, ATtinyX4, ATtinyX5

Bei allen AVRs mit ADC lässt sich die Betriebsspannung ermitteln, wenn man eine Referenzspannung(squelle) an einen Analogeingang legt.

Kalibrierung

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

ADMUX

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 Auswirkung
0 Das Ergebnis wird in den Registern ADCH/ADCL rechtsbündig ausgerichtet. Die 8 niederwertigsten Bits des Ergebnisses werden in ADCL abgelegt. Die verbleibenden 2 höchstwertigen Bits des Ergebnisses werden im Register ADCH in den Bits 0 und 1 abgelegt.
1 Das 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,23 V, Vbg
1 1 1 1 0 V, GND

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

ADCSRA

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 50 kHz und 200 kHz 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.000 Hz / 200.000 Hz = 40
Da mit 200 kHz gerechnet wurde (maximale Frequenz), nimmt man den nächsthöheren Wert, also 64.
8.000.000 Hz / 64 = 125.000 Hz = 125 kHz
So erhält man bei 8 MHz einen Prescaler von 64 und eine Frequenz von 125 kHz.

Die Ergebnisregister ADCL und ADCH

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 zwei 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

Ausgabe als ADC-Wert

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 und 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 127, 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 à la 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 des negativen Werts ist. Also „addiere +1“ ist gleich „subtrahiere −1“. Dafür hat der AVR zwei Befehle, subi (Subtract Immediate, Subtrahiere Konstante) und sbci (Subtract 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 (siehe diesen Forumsthread und die Application Note AVR094: Replacing ATmega8 by ATmega88 (PDF; 74 KB)).

.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 32 -> ADC-Frequenz = 125 kHz @ F_OSC = 4 MHz,
;   bei anderen Taktfrequenzen die ADPS-Werte ggf. anpassen!

    ldi     temp1, (1<<REFS0)                   ; Kanal 0, interne Referenzspannung 5V
    out     ADMUX, temp1
    ldi     temp1, (1<<ADEN) | (1<<ADPS2) | (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 das 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, kann das 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   ; Zehntausenderstelle ausgeben
    rcall   transmit
    mov     zeichen, tausend    ; Tausenderstelle ausgeben
    rcall   transmit
    mov     zeichen, hundert    ; Hunderterstelle ausgeben
    rcall   transmit
    mov     zeichen, zehner     ; Zehnerstelle ausgeben
    rcall   transmit
    mov     zeichen, adlow      ; Einerstelle ausgeben
    rcall   transmit
    ldi     zeichen, 13         ; CR, Carriage 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

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 : 5 V
 alte Auflösung   : 5 V / 1024 = 4,8828125 mV
 neue Auflösung   : 1 mV

→ Faktor = 4,8828125 mV / 1 mV = 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

[math]\displaystyle{ F_r = \frac{4883}{4882{,}8125}-1 = 0{,}00384\,\% = \frac{1}{26042} }[/math].

Dieser Fehler ist absolut vernachlässigbar. Nach der Multiplikation des ADC-Wertes mit 4883 liegt die gemessene Spannung in der Einheit Mikrovolt (µ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 einfache 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 Umwandlung 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. Außerdem wird der String im RAM gespeichert und nicht mehr in CPU-Registern. Die Berechnung der einzelnen Ziffern erfolgt über eine 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: Kanal 0, int. Ref. AVCC, Single Conversion,
;   Vorteiler 32 -> ADC-Frequenz = 125 kHz @ F_OSC = 4 MHz,
;   bei anderen Taktfrequenzen die ADPS-Werte ggf. anpassen!

    ldi     temp1, (1<<REFS0)            ; ADC0, interne Ref. Vcc
    out     ADMUX, temp1
    ldi     temp1, (1<<ADEN) | (1<<ADPS2) | (1<<ADPS0) ; f/32
    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, 13         ; Carriage Return (Steuerzeichen)
    rcall   sende_einzelzeichen

    ldi     zeichen, 10         ; New Line (Steuerzeichen)
    rcall   sende_einzelzeichen ; CR+NL bewirkt Zeilenumbruch

    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. Außerdem 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 notwendigen 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

Literatur