Kampis Elektroecke

XMega – ADC

XMega Blockdiagramm

Ein ADC ermöglicht es dem Mikrocontroller analoge Spannungen einzulesen. Der XMega besitzt zwei ADCs mit jeweils 8 Eingängen und einer Auflösung von 12 Bit. Zusätzlich verfügt jeder ADC über 4 Messkanäle. Dadurch wird es dem ADC ermöglicht bis zu vier Wandlungen durchführen zu können, ohne das die CPU des Mikrocontrollers den ADC unterbrechen muss.

ADC konfigurieren:

In diesem Tutorial verwende ich den ADC vom Port A. Das Ergebnis soll per USART an einem Computer geschickt und dort auf einem Terminal ausgegeben. Durch das Löschen des CTRLB-Registers

ADCA.CTRLB = 0x00;

wird die Auflösung auf 12 Bit gesetzt, das Messergebnis rechtseitig ausgerichtet und der unsigned-Modus ausgewählt.

Im REFCTRL-Register des ADC wird die Referenzspannung zum Messen eingestellt.

Die Referenzspannung soll in diesem Beispiel 1 V betragen. Dem entsprechend muss auch dieses Register gelöscht werden.

ADCA.REFCTRL = ADC_REFSEL_INT1V_gc;

Als nächstes muss noch der Prescaler eingestellt werden. Der Wert des Prescalers muss an die verwendete Taktfrequenz angepasst werden, da der ADC laut Datenblatt nicht mehr als 2 MS/s schafft. In diesem Beispiel stelle ich den Prescaler auf 32.

ADCA.PRESCALER = ADC_PRESCALER_DIV32_gc;

Nun wird der ADC noch aktiviert:

ADCA.CTRLA = ADC_ENABLE_bm;

Messen einer Spannung:

In diesem Beispiel soll die Spannung des Lichtsensors auf dem Board gemessen werden, wecher sich an Pin A.0. befindet. Dieser muss zuerst als Eingang konfiguriert werden:

PORTA.DIR &= ~(0x01 << 0x00)

Für die Messung soll der Kanal 0 des ADCs verwendet werden. Dieser muss ebenfalls konfiguriert werden. Dazu wird als erstes der Modus und die Verstärkung eingestellt. Dies erfolgt über das CTRL-Register des Kanals. In diesem Beispiel soll eine Verstärkung von 1 und der Single-Ended Modus (die Spannung soll gegen Masse gemessen werden) verwendet werden.

ADCA.CH0.CTRL = ADC_CH_INPUTMODE0_bm;

Bei der Konfiguration des Eingangsmodus muss der Modus des ADCs berücksichtigt werden. Falls der signed-Modus verwendet, ergeben sich andere Konfigurationsmöglichkeiten für den Eingangsmodus des Kanals.

Über das MUXCTRL-Register wird nun der Eingangspin für den Kanal festgelegt. Auch hier ist die Funktion wieder vom gewählten Modus des ADCs und vom Eingangsmodus des Kanals abhängig (siehe Datenblatt).

ADCA.CH0.MUXCTRL = ADC_CH_MUXNEG_PIN0_gc;

Wenn alles eingestellt worden ist, kann eine Messung gestartet werden. Dazu wird im CTRL-Register das START-Bit gesetzt.

ADCA.CH0.CTRL |= ADC_CH_START_bm;

Der Abschluss einer Messung wird im INTFLAGS-Register angezeigt. Solange das IF-Bit nicht gesetzt ist, ist eine Messung aktiv.

while(!(ADCA.CH0.INTFLAGS & ADC_CH_CHIF_bm));

Sobald die Messung beendet worden ist, muss das Flag durch ein erneutes Setzen gelöscht werden. Dann kann das Messergebnis aus dem RES-Register ausgelesen werden.

ADCA.CH0.INTFLAGS = ADC_CH_CHIF_bm;
uint16_t Result = ADCA.CH0.RES;

Zum Schluss wird das Ergebnis in einen String umgewandelt und per USART an einen Computer gesendet, wo das Ergebnis mittels Terminal ausgegeben werden kann. Der passende Codeschnipsel kann aus dem USART-Tutorial kopiert und angepasst werden.

PORTC.DIRSET = (0x01 << 0x03);
USARTC0.BAUDCTRLA = 0xB0 & 0xFF;
USARTC0.BAUDCTRLB = ((0xB0 & 0xF00) >> 0x08);
USARTC0.BAUDCTRLB |= ((-5 & 0x0F) << 0x04);
USARTC0.CTRLB = USART_TXEN_bm;
USARTC0.CTRLC = USART_CHSIZE_8BIT_gc;
USARTC0.CTRLC &= ~(USART_PMODE0_bm | USART_PMODE1_bm | USART_SBMODE_bm);

itoa(RawData, Result, 10);
for(uint8_t i = 0x00; i < 0x0A; i++)
{
	while(!(USARTC0.STATUS & USART_DREIF_bm));
	USARTC0.DATA = Result[i];
}

ADC kalibrieren:

Die Genauigkeit des ADC kann durch die Verwendung der abgelegten Kalibrationsdaten erhöht werden. Die Werte können mittels NVM-Controller aus den abgelegten Produktionsdaten ausgelesen und dann in die Kalibrationsregister kopiert werden.

#include <avr/pgmspace.h>
#include <stddef.h>

uint8_t CalibrationByteL;
uint8_t CalibrationByteH;
NVM_CMD = NVM_CMD_READ_CALIB_ROW_gc;
CalibrationByteL = pgm_read_byte(offsetof(NVM_PROD_SIGNATURES_t, ADCACAL0));
CalibrationByteH = pgm_read_byte(offsetof(NVM_PROD_SIGNATURES_t, ADCACAL1));
NVM_CMD = NVM_CMD_NO_OPERATION_gc;
ADCA.CALL = CalibrationByteL;
ADCA.CALH = CalibrationByteH;

Verwenden der Interrupts:

Als letztes möchte ich noch zeigen, wie der ADC im Interruptmodus betrieben werden kann. In diesem Beispiel soll wieder mit dem ADCA eine analoge Spannung gemessen werden. Nur dieses mal soll das Ergebnis automatisch per USART übertragen werden, sobald die Messung abgeschlossen ist.

Zuerst wird wieder der ADC konfiguriert. Da sich an der Konfiguration des ADCs, im Vergleich zum ersten Beispiel, nur eine Einstellung ändert, kann der Codeabschnitt wiederverwendet werden.

ADCA.REFCTRL = ADC_REFSEL_INT1V_gc;
ADCA.PRESCALER = ADC_PRESCALER_DIV32_gc;
ADCA.CTRLA = ADC_ENABLE_bm;

In diesem Beispiel soll der ADC allerdings im Free Run Modus arbeiten, damit dieser die ganze Zeit Messwerte aufnimmt. Dazu wird vor der Aktivierung des ADCs das FREERUN-Bit im CTRLB-Register des ADCs gesetzt.

ADCA.CTRLB = ADC_FREERUN_bm;

Jetzt muss noch der ADC Kanal konfiguriert werden. Auch hier entspricht die Konfiguration weitestgehend der Konfiguration aus dem ersten Beispiel, außer das im INTCTRL-Register das COMPLETE-Bit gesetzt wird um die Interrupts beim Abschluss einer Messung zu aktivieren. Zudem wird die Interruptpriorität auf Niedrig eingestellt.

PORTA.DIR &= ~(0x01 << 0x00);
ADCA.CH0.CTRL = ADC_CH_INPUTMODE0_bm;
ADCA.CH0.MUXCTRL = ADC_CH_MUXNEG_PIN0_gc;
ADCA.CH0.INTCTRL = ADC_CH_INTMODE_COMPLETE_gc | ADC_CH_INTLVL_LO_gc;

Abschließend müssen noch die Interrupts aktiviert und die ISR des ADC-Kanals programmiert werden.

PMIC.CTRL = PMIC_LOLVLEN_bm;
sei();

ISR(ADCA_CH0_vect)
{
	itoa(ADCA.CH0.RES, Result, 10);
	for(uint8_t i = 0x00; i < 0x0A; i++)
	{
		while(!(USARTC0.STATUS & USART_DREIF_bm));
		USARTC0.DATA = Result[i];
	}
}

In der ISR wird das Ergebnis der Messung ausgelesen und über die itoa-Funktion in ein Array umgewandelt und dann per USART versendet, sodass das Ergebnis z. B. per PC eingelesen werden kann.

Das komplette Beispielprojekt steht in meinem GitHub-Repository zum Download bereit.

22 Kommentare

  1. Hallo, ich habe fast eins zu eins deinen code kopiert aber leider bin ich trotzdem nicht im Stande das es funktioniert. Kannst du mir vielleicht helfen?

    1. Hallo,

      ich habe deinen Code mal entfernt, da der Kommentar sonst zu lang wird. Sowas bitte demnächst per Mail schicken.
      Auf den ersten Blick ist dein Code ziemlich durcheinander. Ich schaue mal ob ich ihn vernünftig zusammengebastelt bekomme.
      Du hast auch kein richtiges Hauptprogramm und alles.
      Wenn du mir deine E-Mailadresse gibst, kann ich dir den Code gerne zuschicken.
      Laut AVR Studio ist er in Ordnun….was die Syntax angeht. Ob er dann bei dir funktioniert ist ein anderes Thema :)

  2. s.reps@gmx.at

    Sorry das es so ein starkes durcheinander ist, normalerweiße programmier ich nicht so, nur normalerwieße benutz ich einen pic und da ist für mich alles einfacher :)
    Ich hab in dem programm schon einiges probiert und deswegen dann nichts mehr sortiert und auch nicht mehr alles herausgelöscht. :S

    Ich hoffe du kannst mir helfen bei einer einfachen ausgabe auf die uart vom adc.
    Danke

  3. Tolle, wirklich tolle Seite!!!
    Habe sie leider erst viel zu spät entdeckt :) nachdem ich mich schon durch gefühlte hundert Atmel User Guides durch gelesen habe.

    Weiter so!!!

  4. Hallo zusammen,
    kann nur meinen Vorgängern zustimmen, wirklich eine tolle Seite die ihr hier habt.

    Ich habe bisher immer mit dem ATmega programmiert und bin soweit klar gekommen. Seit kurzem bin ich auf den Xmega umgestiegen und komme bei dem ADC Initialisierung nicht weiter.

    Bitte deshalb um hilfe
    Könn

    1. Hey,

      da ich jetzt endlich ein neues XMega Board habe, kann ich auch endlich weiter machen :)
      Schick mir das Programm mal bitte per E-Mail zu.
      Ich gucke dann die Tage / Wochenende drüber (bin erst wieder Freitag zu Hause).

      Gruß
      Daniel

  5. Habe inzwischen rausgefunden woran es wirklich lag.
    In der Funktion ADCA_Conversion() fehlt das Ruecksetzen der INTFLAGS.
    Es sollte also noch folgende Zeile eingefuegt werden:
    Channel->INTFLAGS = ADC_CH_CHIF_bm; //INTFLAGS loeschen

    Ausserdem ist noch die Zeile „int Result = 0x00;“ ueberfluessig.
    (Ergibt eine entsprechende Compiler-Warnung)

    Nach dem Umrechnen der gelesenen Werte in Millivolts bekomme
    ich immer einen signifikanten Offset. Wobei es keine Rolle spielt ob
    ADCA_Cal() benutzt oder nicht.
    Der Offset ist auch noch leicht unterschiedlich ob PA1 oder PA2 benutzt.
    Ich habe mir mit diesem Unterprogramm fuer die Umrechnung beholfen:

    uint adc_millivolt(uint adwert,int offset)
    {
    long mvlong = (adwert<=offset) ? 0 : (long)REFERENZSPANNUNG*(adwert-offset);
    return (mvlong+2048)/4096;
    }

    Aufgerufen wird es dann jeweils so:
    sprintf(text,"%4d mV %4d mV ",adc_millivolt(adwert1,156),adc_millivolt(adwert2,166));

    Statt mit dem Uart gebe ich es jeweils auf ein LCD-Modul aus.

  6. Hallo Kampi,

    danke für deinen Code. Hab den ADC vom Xmega mal ausprobiert. Jetzt ist mir aufgefallen: Der Offset beträgt bei mir 129, ist das nicht ein bisschen arg viel? Hab jetzt schon zwei Controller ausprobiert, beide ziemlich identisch. Wie viel beträgt der Offset bei dir?

    Lg Andreas

    1. Hey Andreas,

      der ADC hat 12-Bit Auflösung. Das macht bei einer Referenzspannung von Vcc, also 3,3V, etwa 0,0008056640625V. Das mit 129 multipliziert ergibt 0,1039306640625V. Du siehst, das es nicht ganz so viel ist. Der Fehler kann schon durch eine fehlende Kalibration auftreten oder wenn deine Referenzspannung (hier exemplarisch dein Vcc) schwankt.
      Gerade wenn du mehrere Controller getestet hast, würde ich es mit der Versorgungsspannung / fehlender Kalibration in Verbindung bringen.

      Gruß
      Daniel

      1. Also das Kalibrationsregister hab ich sogar ausgelesen, dachte auch dass es daran liegt. Dadurch hat sich aber nix geändert.
        Ja wahrscheinlich liegts an der Stromversorgung. Ich stabilisiere die Eingangsspannung mit einem LM317 auf 3,3V, vielleicht sollt ich mal die Pufferkondensatoren vergrößern.

  7. Hallo,
    super Seite erst mal.

    Ich habe dein Beispiel versucht und es funktioniert Grundsetzlich. Allerdings habe ich folgendes Problem. Wenn mein ADC auf GNG liegt zeigt es immer noch LSB von 11 an.
    ich habe den ADC auf 8Bit laufen, Single Ended.Habe auch schon mit den Referenzspg. gespielt. Meine Ref erzeuge ich über einen Spannungsteiler der an einem OP als Buffer geschaltet hängt. Mein µC ist ein XMEGA 32D3.
    Vielleicht kennst du das Problem.

    1. Hallo Matze,

      hast du den ADC mal kalibriert?
      Eventuell ist es eine fehlende Kalibration / Rauschen im ADC?
      Eine andere Idee hätte ich jetzt auch nicht.

      Gruß
      Daniel

  8. Hey Daniel,

    erstmal vielen Dank für deine ausführliche und gute Beschreibung.
    Wenn ich jedoch deinen Code verwende, funktioniert bei mir leider nichts. Das Programm bleibt immer in der while(!…..)-Schleife hängen. Rolf hatte dazu ja schon einmal eine Anmerkung gemacht.
    Welches Board hast du denn verwendent? Oben ist das XMEGA-A3BU Xplained verlinkt, hast du das auch verwendet?

    Liebe Grüße,
    Dominik

    1. Hallo Dominik,

      ich glaube, dass ich für die Überarbeitung des Tutorials einen anderen Mikrocontroller (XMega384C3) verwendet habe. Das sollte aber nicht schlimm sein, da der Peripherieaufbau so gewählt ist, dass die Register und Adressen bei allen XMegas identisch sind. Nichts desto trotz schaue ich mir das Beispiel Montag noch einmal an und korrigiere es ggf. (habe aktuell keinen Debugger zur Hand). Bei welcher while-Schleife bleibt das Programm den hängen? Beim USART oder beim ADC?
      So lange bitte noch Geduld haben :)

      Gruß
      Daniel

    2. Hallo Dominik,

      ich habe meinen Code gerade noch einmal auf meinem XMEGA-A3BU Xplained getestet und er läuft wunderbar. Ggf. war noch ein falscher Code im Repository. Ich aktualisiere auch den mal.

      Gruß
      Daniel

  9. Guten Morgen Daniel,

    vielen Dank für deine Hilfe. Ich werde es später nochmal ausprobieren. Ich habe es schrittweise von dieser Seite übernommen und einmal aus dem Repository kopiert. Beides hat nicht funktioniert. Er hat sich immer an der Zeile
    while(!(ADCA.CH0.INTFLAGS & ADC_CH_CHIF_bm));
    festgefahren. Aus dieser Schleife geht er leider nicht mehr raus… :(

    Liebe Grüße,
    Dominik

  10. Hey Daniel,

    ich bin immer noch an dem ADC dran….. :D
    Welches Terminalprogramm hast du verwendet?
    Mit welcher Peripherie versendest du die Daten des ADCs an den PC?
    Mein Problem liegt eventuell irgendwo am USART. Ich verwende den „USB UART Click“ für die Übertragung.

    Gruß,
    Dominik

    1. Hallo Dominik,

      ich verwende PuTTY als Terminalprogramm. Ich habe vorhin aber noch einmal das komplette Programm (und nicht nur den ADC) getestet und gesehen, dass ich in dem Beispiel vergessen habe den Tx-Pin als Ausgang zu setzen, wodurch der USART zwar funktioniert, aber nichts sendet. Ich habe meinen Beispielcode und die Anleitung entsprechend aktualisiert und bei Git hochgeladen. Hoffentlich funktioniert es nun :)

      Gruß
      Daniel

  11. Guten Morgen Daniel,

    vielen Dank für deine Hilfe und Zeit, die du dafür investiert hast! Nun funktioniert es endlich!
    Deine Seite kann ich nur empfehlen!! Sie hat mir bis jetzt sehr viel geholfen und den Einstieg in die Programmierung von Mikrocontrollern erleichtert.
    Ich hoffe, ich darf dich weiterhin mit Fragen nerven…. :P

    Schöne Grüße,
    Dominik

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert