GREETBoard – ATMega128 Extension – Analoge Werte erfassen

Der ATMega128 besitzt acht Eingänge, die analoge Signale auswerten können. Bereits beim allerersten Board (damals noch mit den ATMega32) habe ich vermisst, diese Funktion testen zu können. Beim aktuellen GREETBoard – ATMega128 Extension habe ich ein Potentiometer hinzugefügt und dieses an einem der analogen Eingänge angeschlossen. Jetzt steht dem Test des internen ADC (Analog-Digital-Converter) nichts mehr im Weg.

Mein Ziel ist es, einen Spannungswert zu erfassen. Im Anschluss soll dieser Wert in in eine Kommazahl verwandelt werden um das Ergebnis schlussendlich auf dem GREETBoard – I2CLCD auszugeben. Auf dem Weg zum Ziel werde ich die Schaltung erklären und die einzelnen Register beleuchten. Im Code wird ein wichtiger neuer Befehl auftauschen, mit dem das Ausleseergebnis des ADC in eine darstellbare Fließkommazahl umgewandelt wird.

Die Schaltung

Wie oben beschrieben befindet sich auf dem GREETBoard – ATMega128 Extension ein Potentiometer, das als Spannungsteiler fungiert. Am Pin 1 liegt VCC und am Pin 3 GND an, sodass die gesamte Spannung von 0-5V am Pin 2 anliegen kann, je nach Einstellung des Potis. Dieser Pin ist über den Jumper JP3 and den Pin PA7(ADC7) gelegt und kann dort vom Mikrocontroller abgefragt werden. Wird diese Funktion nicht benötigt kann der Jumper das Poti vom Pin trennen und PA7 kann als normaler Ein-/Ausgang genutzt werden.

Analoge Werte erfassen - Schaltung am ADC7

Analoge Werte erfassen – Schaltung am ADC7

Auf der Hardwareseite ist nichts weiteres nötig um den Analog-Digital-Converter zu testen. Daher widme ich mich nun den notwendigen Registern zu.

Die ADC Register

Der Analog-Digital-Converter führt eine fortlaufende Berechnung von analogen Signalen an den Pins des Port A durch. Alle acht Pins haben ihren Minimal-Referenzpunkt bei GND (0V). Die Berechnung liefert einen 10-Bit Wert, dessen Maximum-Referenzwert bei bis zu VCC liegen kann. Angenommen der obere Wert der Referenzspannung liegt bei 5V, würde ein 5V Signal am analogen Pin einen Wert von 1024 (10 Bit) erzeugen. Wird die Eingangsspannung auf 2,5V reduziert, lautet das Ergebnis der ADC-Berechnung 512.

ADC Multiplexer Selection Register

Die Referenzspannung auswählen

Wie zuvor erwähnt, ist die obere Grenze der Referenzspannung, bis zu einem Wert von VCC, frei wählbar. Das ADC Multiplexer Selection Register (ADMUX) ist u.a. zuständig um die gewünschte Referenzspannung auszuwählen. Dies ist die Spannung, gegen die das analoge Eingangssignal gemessen wird.

 Der ATMega1284P bietet vier Möglichkeiten eine Referenzspannung auszuwählen:

  1. Interne 1,1V
  2. Interne 2,56V
  3. VCC (am AVCC-Pin angelegt)
  4. Spannung VREF (am VREF-Pin angelegt)

Die Möglichkeiten 1 und 2 sind wohl selbsterklärend. Punkt 3 ermöglicht eine Messung von 0V (GND) bis zur Versorgungsspannung VCC (beim GREETBoard-ATMega128 sind dies 5V). Dafür soll der Eingang VREF mit einem Kondensator gegen GND geschaltet werden. Für einen individuellen Referenzwert wird der Pin VREF herangezogen. Die hier angelegte Spannung ist der Maximalwert, bis zu dem eine Spannung gemessen werden kann. Wird also nur ein Wert bis 3,5V gemessen, kann auf VREF eine Spannung von 3,5V angelegt werden.

Das ADMUX Register besitzt die beiden Bits REFS0 und REFS1, die die Quelle der Referenzspannung definieren.

Analoge Werte erfassen - ADMUX Register

Analoge Werte erfassen – ADMUX Register

In der weiter oben gezeigten Schaltung liegt das Poti zwischen 5V (VCC) und 0V (GND). Der Eingang AVCC am GREETBoard – ATMega128 ist bereits direkt auf VCC gelegt und somit wird die rot umrandete Referenzspannungsquelle AVCC ausgewählt um die vollen 5V messen zu können.

Analoge Werte erfassen - Tabelle Referenzspannungen

Analoge Werte erfassen – Tabelle Referenzspannungen

Den Kanal auswählen

Die Bits 0 bis 4 dienen der Auswahl des zu messenden Kanals. Im folgenden Beispiel möchte ich den ADC7, also Kanal 7, verwenden. Wie schon zuvor erwähnt soll auch nur ein Wert zwischen 0 und 5V gemessen werden, was in der Tabelle als „Single Ended Input“ bezeichnet wird. Mit den beiden Informationen ist es nicht schwer zu erkennen, dass MUX0-2 auf 1 und MUX3-4 auf 0 gesetzt werden müssen.

Analoge Werte erfassen - Tabelle MUXn Bits

Analoge Werte erfassen – Tabelle MUXn Bits

ADC Control and Status Register

 

Analoge Werte erfassen - ADCSRA Register

Analoge Werte erfassen – ADCSRA Register

Vorteiler (ADPS – ADC Prescaler Select Bits)

Nachdem die Referenzspannung eingestellt wurde, erfolgt noch eine Einstellung im ADCSRA, bevor die erste Messung gestartet werden kann. Bereits zu Beginn habe ich geschrieben, dass die Messung fortlaufend ist um das Ergebnis (denn 10 Bit Wert) an den gemessenen Wert anzunähern. Dafür benötigt der ADC einen Timer mit einer Frequenz von 50-200kHz. Diese Frequenz wird vom Mikrocontroller-Takt abgeleitet und muss durch einen Vorteiler verringert werden um von 16MHz auf die benötigte Frequenz zu kommen.

In diesem Fall würde das Ganze so aussehen: 16.000000Hz : 128 = 125000Hz = 125kHz

Alle anderen Vorteiler schaffen es nicht den Wert unter die 200kHz-Grenze zu bringen. Somit worden alle drei ADPS-Bits auf High gesetzt.

Analoge Werte erfassen - Vorteiler auswählen

Analoge Werte erfassen – Vorteiler auswählen

ADC aktivieren (ADEN – ADC Enable)

Analoge Messungen werden nur dann durchgeführt, wenn der ADC zuvor aktiviert wurde. Daher muss ADEN auf 1 gesetzt werden um diese Funktion nutzen zu können.

Messung durchführen (ADSC – ADC Start Conversion)

Wird dieses Bit auf 1 gesetzt startet eine Messung und das Ergebnis landet im ADC-Register. Danach setzt sich dieses Bit automatisch auf 0 zurück. Da eine Messung üblicherweise 13 ADC-Zyklen (50-200kHz) dauert, kann der Abschluss der Messung über dieses Bit erfasst werden.

Bei der ersten Messung nach der Aktivierung des ADC werden jedoch 25 Zyklen benötigt, was eine Initial-Messung nötig macht. Nach dem Start der Messung wird also so lange gewartet bis ADSC wieder 0 ist um diese erste Messung sauber abzuschließen.

Das Programm in kleinen Schritten

Für dieses Beispiel setze ich das GREETBoard – I²CLCD ein, um die Werte als Text anzeigen zu können. Das Programm startet mit den üblichen Verdächtigen – dem Takt und der notwendigen Header-Dateien.

#ifndef F_CPU   //Definiere CPU-Takt
  #define F_CPU 16000000
#endif

#include <avr/signal.h>
#include "i2clcd.h"
#include "i2cmaster.h"

Es werden noch ein paar Variablen benötigt. Puffer dient der Umwandlung in einen anzeigbaren Wert, adcwert2 wird für die Löschung des LCD-„Bildschirms“ verwendet und in volt wird das Ergebnis der ADC-Umrechnung in eine Spannung als Kommazahl (double), abgelegt.

char puffer[20];
uint16_t adcwert2;
double volt;

Die erste Unterroutine ist für die Initialisierung des ADC zuständig. Hier werden die oben beschriebenen Bits gesetzt und die Variable ergebnis deklariert.

// ADC initialisieren
void ADC_Init(void) {
  //Ergebnis-Variable deklarieren
  //16-Bit da Ergebnis größer als 8-Bit ist
  uint16_t ergebnis;

  //die Versorgungsspannung AVCC als Referenz wählen:
  ADMUX = (1<<REFS0);    

  //Vorteiler auf 128 setzen
  ADCSRA = (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);

  //ADC aktivieren
  ADCSRA |= (1<<ADEN);

Nach der Aktivierung des ADC muss eine Initial-Messung durchgeführt werden. Dafür wird ADSC auf 1 gesetzt um die Messung zu starten. Das Programm bleibt solange in der leeren While-Schleife, wie das Bit ADSC gesetzt ist. Erst wenn es wieder 0 ist, wird das Register ADC (das ist das Ergebnis der Messung als 10-Bit Wert) in der Variable ergebnis abgelegt. Dieses Auslesen muss nach der Initial-Messung erfolgen um weitere Messungen zu ermöglichen.

  // nach Aktivieren des ADC wird ein Initial-Messung durchgeführt
  ADCSRA |= (1<<ADSC);                  // eine ADC-Wandlung 
  while (ADCSRA & (1<<ADSC)) {         // auf Abschluss der Konvertierung warten
  }
  /* ADC muss einmal gelesen werden, sonst wird Ergebnis der nächsten
  Wandlung nicht übernommen. */
  ergebnis = ADC;
}

Die nächste Funktion regelt die regulären Messungen nach der Initialisierung. Wenn diese Funktion aufgerufen wird, benötigt sie die Angabe der Variable kanal. Dies ist die Zahl des ADC-Pins, an dem eine Messung stattfinden soll. Das das Poti am ADC7 angeschlossen ist, muss er der Kanal 7 sein.

Die etwas kompliziert erscheinende Bitmanipulation in der ersten Funktionszeile ist notwendig um auf verschiedene Kanal-Angaben zu passen. Es wäre ja sehr einfach, wenn immer der Kanal 7 verwendet wird, dann werden MUX0-2 auf 1 gesetzt (siehe oben). Dann würde die Variable kanal aber nicht gebraucht. Also passiert hier folgendes:

(ADMUX & ~(0x1F))

Das Register ADMUX (REFS0 = High) wird mit dem invertierten (~) Hexwert 1F (00011111 => 1110000) UND verknüpft.

  ADMUX 01000000
&  0x1F 11100000
=       01000000

Egal welche MUXn vorher gesetzt waren, mit dieser Manipulation sind sie auf 0 gesetzt und die Angaben bei REFs0, REFS1 und ADLAR sind „gespeichert“.

(kanal & 0x1F)

Der zweite Teil macht eine UND-Verknüpfung mit der Kanalangabe (7 => 00000111) und dem Hexwert 1F (00011111).

  Kanal 00000111
&  0x1F 00011111
=       00000111

Diese Bitmanipulation stellt sicher, dass auch nur die Bits 0-4 für Kanalauswahl verwendet und eventuelle andere Bits (wegen falscher Kanalangabe, z.B. 12) ausgeblendet werden. Ansonsten würde die nächste Operation ggf. REFS0, REFS1 oder ADLAR überschreiben (Bits 5-7)

(ADMUX & ~(0x1F))(kanal & 0x1F)

Zum Schluss werden die beiden Ergebnisse miteinander ODER-Verknüpft. Also:

  01000000
| 00000111
= 01000111

Warum dieser Aufwand? Mit den drei Operationen wird die vorhandene Einstellung bei REFS0, REFS1 und ADLAR gesichert, die Kanalbits resettet und mit dem gewünschten Wert gesetzt und im Anschluss die beiden Informationen verbunden und ins ADMUX Register geschrieben.

/* ADC Einzelmessung */
uint16_t ADC_Lesen(uint8_t kanal)
{
  // Kanal waehlen, ohne andere Bits zu beeinflußen
  ADMUX = (ADMUX & ~(0x1F)) | (kanal & 0x1F);

Nun wird die Messung ausgelöst und solange gewartet, bis die Messung abgeschlossen wurde.

  ADCSRA |= (1<<ADSC);            // eine Wandlung "single conversion"
  while (ADCSRA & (1<<ADSC) ) {   // auf Abschluss der Konvertierung warten
  }

Zum Schluss gibt die Funktion den ausgelesenen zurück.

  return ADC;                    // ADC auslesen und zurückgeben
}

Im Hauptprogramm wird der I²C-Bus und das LCD-Modul gestartet. Nach dem Löschen des LCD-Moduls wird die Variable adcwert definiert und adcwert2 auf 0 gesetzt. Letztlich folgt der Funktionsaufruf für die Initial-Messung.

int main()
{
	i2c_init();                // Starte I2C Bus
	lcd_init();                
	lcd_command(LCD_CLEAR);
	uint16_t adcwert;
	adcwert2 = 0;
	ADC_Init();

Zum Beginn der While-Schleife erfolgt die Auslesung des ADC-Kanal 7. Der dadurch ermittelte Wert wird in adcwert abgelegt. Die LCD-Anzeige muss nicht in jedem Schleifendurchlauf gelöscht und mit dem adcwert beschrieben werden, da diese sonst stark flackern würde. Die anschließende if-Abfrage prüft, ob adcwert und adcwert2 nicht übereinstimmen, denn nur dann muss die Anzeige gelöscht werden.

	while(1) {
		adcwert = ADC_Lesen(7);  // Kanal 7

		if (adcwert2 != adcwert){
			lcd_command(LCD_CLEAR);
			adcwert2 = adcwert;
		}

Nun erfolgt eine Multiplikation des adcwert mit 0,0048828. Das Messergebnis ist ein 10-Bit-Wert. Somit würde ein Signal von 5V am ADC7 einen adcwert von 1024 ergeben. 5 geteilt durch 1024 ergibt 0,0048828. Die Multiplikation wandelt also den Binärwert in den passenden Dezimalwert um.

		volt = adcwert * 0.0048828;

Normalerweise benutze ich den Befahl itoa um einen numerischen Wert so umzuwandeln, dass er auf dem LCD-Display angezeigt werden kann. Das klappt mit Integerzahlen super, aber die Variable volt ist eine Double. Nach vielem Haareraufen und anschließender Internetrecherche habe ich heraus gefunden, dass der Befehl „dtostrf“ für Double-Zahlen verwendet wird. Mit itoa kommt nämlich nur Murks am Display an 😉

Die beiden letzten Zeilen sind für die Anzeige des umgewandelten Messergebnis da.

		dtostrf(volt, 8, 2, puffer);
		lcd_printlc(1,1,puffer);
		lcd_printlc(1,9," Volt");
	}
}

Abschluss

Analoge Werte am Port A auszulesen war gar nicht so schwer wie ich dachte. Ich stelle immer wieder fest, dass es unglaublich wichtig ist, die ganzen Register zu kennen. Naja, zumindest ist es wichtig zu wissen wo ich nachschauen muss! 😉

Wenn jemanden die Muse küsst und Lust verspürt auch mal ins Datenblatt zu schauen, wird feststellen, dass es noch weitere ADC-Funktionen gibt, die ich hier nicht beschrieben habe. Es gibt also noch Potential das ausgenutzt werden kann.

Ich hoffe, dass dieser Artikel gefallen und vielleicht auch geholfen hat. Wenn dem so ist, dann freue ich mich darauf euer Feedback zu lesen. Wenn euch etwas fehlt oder ihr gar Fehler findet dann schreibt erst recht Kommentare. Also: Kommentiert fleißig und viel!

Alles Gute, Timo

ANHANG: Das ganze Programm

#ifndef F_CPU   //Definiere CPU-Takt
  #define F_CPU 16000000
#endif

#include <avr/signal.h>
#include "i2clcd.h"
#include "i2cmaster.h"

char puffer[20];
uint16_t adcwert2;
double volt;

// ADC initialisieren
void ADC_Init(void) {
  //Ergebnis-Variable deklarieren
  //16-Bit da Ergebnis größer als 8-Bit ist
  uint16_t ergebnis;

  //die Versorgungsspannung AVCC als Referenz wählen:
  ADMUX = (1<<REFS0);    

  //Vorteiler auf 128 setzen
  ADCSRA = (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);

  //ADC aktivieren
  ADCSRA |= (1<<ADEN);

  // nach Aktivieren des ADC wird ein Initial-Messung durchgeführt
  ADCSRA |= (1<<ADSC);                  // eine ADC-Wandlung 
  while (ADCSRA & (1<<ADSC)) {         // auf Abschluss der Konvertierung warten
  }
  /* ADC muss einmal gelesen werden, sonst wird Ergebnis der nächsten
  Wandlung nicht übernommen. */
  ergebnis = ADC;
}

/* ADC Einzelmessung */
uint16_t ADC_Lesen(uint8_t kanal)
{
	// Kanal waehlen, ohne andere Bits zu beeinflußen
	ADMUX = (ADMUX & ~(0x1F)) | (kanal & 0x1F);
	ADCSRA |= (1<<ADSC);            // eine Wandlung "single conversion"
	while (ADCSRA & (1<<ADSC) ) {   // auf Abschluss der Konvertierung warten
	}
	return ADC;                    // ADC auslesen und zurückgeben
}

int main()
{
	i2c_init();                // Starte I2C Bus
	lcd_init();                
	lcd_command(LCD_CLEAR);
	uint16_t adcwert;
	adcwert2 = 0;
	ADC_Init();

	while(1) {
		adcwert = ADC_Lesen(7);  // Kanal 7

		if (adcwert2 != adcwert){
			lcd_command(LCD_CLEAR);
			adcwert2 = adcwert;
		}

		volt = adcwert * 0.0048828;

		dtostrf(volt, 8, 2, puffer);
		lcd_printlc(1,1,puffer);
		lcd_printlc(1,9," Volt");
	}
}

Schreibe einen Kommentar

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