Sensibelchen

Mikrocontroller-Programmierung: Timer, Sensoren und Drehgeber

Praxis & Tipps | Praxis

Der c't-Mikrocontroller-im-LAN kann verschiedenste Sensoren abfragen, um Temperaturen, Lichtintensitäten und andere physikalische Größen zu messen. Wenige Zeilen Programmcode fragen dazu Sensoren ab oder ermöglichen es, den Controller mit einem inkrementellen Drehgeber bequem zu bedienen.

Aufmacher

Das c't-Projekt Mikrocontroller-im-LAN aus den Artikeln [1, 2] soll nun praktische Aufgaben übernehmen und Temperaturen und andere physikalische Größen messen. Die nötigen Messschaltungen für eine Vielzahl von Sensoren befinden sich schon auf dem B-Modul, wurden aber noch nicht detailliert besprochen. Bislang konnte der Controller (ATMega 8535) schon testweise Daten an PCs im Netzwerk übermitteln oder diese auf dem Display anzeigen. Die Benutzerführung auf dem LCD gestaltete sich mit den bislang verwendeten Komponenten etwas schwierig. Hier sorgt ein inkrementeller Drehgeber, wie man ihn aus vielen modernen Geräten kennt, für Abhilfe.

Bevor sich eine physikalische Größe mit einem Mikrocontroller messen lässt, muss ein geeigneter Sensor die Ursprungsgröße in eine elektrisch messbare verwandeln - zum Beispiel eine Temperatur in einen Widerstandswert. Im zweiten Schritt fällt über diesem Widerstand eine Spannung ab, die dann schließlich ein Analog-Digital-Umsetzer (ADU) in einen digitalen Wert verwandelt. Andere ebenfalls nutzbare Zwischengrößen sind Kapazitäts- (Kondensator) und Induktivitätsänderungen (Spule), die man beispielsweise in einen Schwingkreis einbauen kann. Gemessen wird dann die veränderte Frequenz des Schwingers.

Zur Temperaturmessung bieten sich Messwiderstände wie der PT100 an. Diese sind in internationalen (IEC 751 und EN 60751) und einer deutschen Norm (DIN 60751) genau spezifiziert. Als Material kommt Platin (Pt) zum Einsatz, das in weiten Bereichen seinen Widerstand annähernd linear mit der Temperatur verändert. Die Zahl hinter dem Elementzeichen gibt den Widerstandswert (100 [OMEGA]) bei 0 °C an. Da die Widerstandsänderung jedoch nicht über den gesamten Temperaturbereich konstant ist, muss man für eine korrekte Umrechnung der gemessenen Ohm-Werte in Temperaturen zwei Abschnitte getrennt betrachten. Zwischen 0 °C und 180 °C gilt folgender Zusammenhang:

R(T) = R0(1 + A·T + B·T2)
A=3,9083·10-3·°C-1; B=-5,775·10-7 °C-2

im Bereich von -200 °C bis 0 °C kommt noch ein Term dritter Potenz hinzu:

R(T) = R0(1 + A·T + B·T2 + C·T3)
C=-4,183·10-12·°C-3

Das B-Modul besitzt zwei komplette analoge Messschaltungen für Temperatursensoren, die versuchen, möglichst viele Störeinflüsse zu vermeiden. Dazu betreibt es den Sensor an einer Konstantstromquelle, die ungefähr 1 mA durch den Sensor fließen lässt. Diese Aufgabe übernimmt etwas zweckentfremdet der einstellbare Spannungsregler LM317L (IC6). Er regelt seine Ausgangsspannung so, dass zwischen dem Justiereingang ADJ und dem Ausgang VO immer 1,25 V anliegen. Da der Widerstand R1 mit 1,3 k[OMEGA] deutlich größer als der PT100 ist, fließt durch beide näherungsweise konstant 1 mA. R38 dient nur dazu, den Spannungsregler ein wenig zu belasten, damit die Regelung einwandfrei arbeitet.

Nach dem Ohmschen Gesetz(U = R·I) fällt bei einem Messstrom von 1 mA am PT100 bei 0 °C 100 mV Spannung ab, bei 100 °C entsprechend 137,6 mV. Diese relativ kleinen Spannungswerte sind empfindlich gegenüber Störungen oder Belastungen, wie sie eine direkte Analog-nach-Digital-Umsetung mit sich bringen würde. Daher entkoppelt zunächst ein Verstärker mit dem Verstärkungsfaktor eins (IC4A) den Eingang von der folgenden Schaltung.

Diese subtrahiert zunächst den Offset von 100 mV, damit der Messbereich bei 0 °C beginnt. Dazu erzeugt der Spannungsteiler R2/R3 eine Spannung von ebenfalls 100 mV, die IC4C wieder entkoppelt und IC4B als Subtraktor vom Messsignal abzieht. Zuletzt verstärkt IC4D das Signal so weit, dass es den Messbereich des A/D-Umsetzers voll ausnutzt. Die Verstärkung kann man mit dem Spindelpotentiometer TR1 relativ genau einstellen.

Möchte man den Messbereich verschieben, um beispielsweise Temperaturen unter 0 Grad zu messen, muss man die Offset-Spannung (Spannungsteiler R2/R3) anpassen. Die Schaltung ist ziemlich universell - die Subtraktionsstufe (R2, R3, IC4C) verschiebt die Eingangsspannung um beliebige Offsets und legt damit das untere Ende des Messbereiches fest, der nachgeschaltete Verstärker (TR1, IC4D) das obere Ende. So lassen sich auch alle anderen Sensoren anschließen, die eine Ausgangsspannung oder eine Widerstandsänderung erzeugen.

Bevor der Mikrocontroller den Messwert weiter verarbeiten kann, muss er das analoge Signal in digitaler Form erhalten. Dazu besitzt der ATMega 8535 acht Eingänge (PA0 bis PA7), die über einen Multiplexer an einem einzigen 10-Bit-D/A-Umsetzer hängen. Dieser teilt den Eingangsbereich durch sukzessive Approximation in 1024 (210) gleich große Stufen ein. Als Referenzspannung und Obergrenze des Messbereichs dient entweder die analoge Versorgungsspannung (im vorgestellten Modul AVCC mit 5 Volt), der Eingang AREF oder eine interne Referenzspannung von 2,56 Volt.

Welche der drei Varianten zum Einsatz kommt, legt die Firmware über die obersten beiden Bits im ADMUX-Register fest. Um die beiden PT100-Sensoren auszuwerten, nutzt unser Beispiel-code die interne Referenz und richtet die zehn Daten-Bits rechtsbündig im 16-Bit-Ergebnis aus (ADLAR=0):

#include <avr/io.h> 
...
ADMUX=(1<<REFS1)+(1<<REFS0)+(0<<ADLAR);

Die ADMUX-Bits null bis zwei wählen den Eingang aus, der mit dem Umsetzer verbunden werden soll. Die übrigen Bits (drei und vier) sind nur in speziellen Betriebsarten nötig und aktivieren unter anderem die differenziellen Eingänge und den internen Verstärker. Genaue Informationen dazu liefert das Datenblatt (Soft-Link). Beim Eintragen der gewünschten Kanalnummern in die zuständigen Bits müssen alle anderen unverändert bleiben, was man durch das logische Oder (|-Operator) und die Maskierung (&-Operator) erreicht:

ADMUX = ADMUX | (channel &; 0x07); 

Die Verknüpfung einzelner Bits kann man in C auch kürzer schreiben, wie die folgenden Zeilen mit dem logischen Und-Gleich (&=-Operator) zeigen. Sie schalten den ausgewählten Kanal als Input-Pin und deaktivieren den internen Pullup-Widerstand, der nur für die Nutzung als digitaler Eingang benötigt wird:

DDRA &= ~(1<<(channel & 0x07));
PORTA &= ~(1<<(channel & 0x07));

Für die Approximation eines Signals benötigt der Umsetzer ein Taktsignal, das ungefähr zehn- bis fünfzehnmal höher liegt als die erwünschte Abtastrate. Diese sollte, sofern die maximale Auflösung erwünscht ist, 15 kHz nicht überschreiten. Daraus ergibt sich ein sinnvoller Umsetzertakt von maximal 200 kHz, den ein Teiler aus dem Chiptakt (14,7456 MHz) erzeugt. Wir haben uns für ein Teilerverhältnis von 128 (die drei untersten Bits im Register ADCSRA) beziehungsweise eine Frequenz von 115 kHz entschieden.

ADCSRA=(1<<ADPS2)+(1<<ADPS1)+(1<<ADPS0);

Das Setzen zweier weiterer Bits leitet die Konvertierung schließlich ein:

ADCSRA|=(1<<ADEN)+(1<<ADSC); 

Sobald das Ergebnis bereit- liegt, steht das ADSC-Flag im ADCSRA-Register wieder auf null. Darauf wartet die Firmware in einer Warteschleife und setzt danach das Ergebnis aus den zwei 8-Bit-Registern in einer 16-Bit-Variablen (adc) zusammen:

while ((ADCSRA&(1<<ADSC))!=0){} 
adc = ADCL + (ADCH<<8);

Um aus dem ADC-Wert eine Temperatur zu berechnen, müsste man eigentlich die weiter oben gennanten Formeln genau auflösen. Da die Faktoren B und C aber relativ klein sind und im Temperaturbereich zwischen 0 und 100 °C daher nur wenig ins Gewicht fallen, darf man sie im Regelfall vernachlässigen. Es ergibt sich dann eine einfache Geradengleichung: T = d·adc + e, deren Koeffizienten d und e sich anhand von zwei Stützstellen bestimmen lassen. Dazu misst man zwei möglichst weit auseinander liegende Temperaturen (zum Beispiel Zimmer- und Heizungstemperatur) jeweils mit dem PT100 (adc) und mit einem normalen Thermometer (T). Die Faktoren errechnen sich dann wie folgt: d = (T1-T2)/(adc1-adc2) und e = T1-d·adc. Im Beispiel-Quelltext (Soft-Link) kann man am Anfang der Datei adc.c die zwei Messpaare eintragen und die Temperaturen mit der Funktion pt100_read(kanal) in Hundertstel-Grad auslesen.

Genau wie die Temperaturfühler kann man auch alle anderen Sensoren, über denen eine Spannung zwischen 0 und 2,56 V abfällt, auslesen. Ein Licht-Sensor (LDR) ändert seinen Widerstandswert zwischen 100 [OMEGA] und 20 k[OMEGA] und so reicht ein einfacher Spannungsteiler (R19) aus, um ihn direkt an den ADU anzuschließen (P9 auf dem B-Modul). Für empfindlichere Sensoren bietet sich eine Anpassung der oben beschriebenen PT100-Schaltung an.

KMZ10B
KMZ10B
Der Magnetfeldsensor KMZ10B besitzt eine interne Messbrücke und kann direkt an einen der analogen Differenzeingänge des ATMega angeschlossen werden.

Wer Sensoren mit integrierter Messbrücke wie einen Magnetfeldsensor (zum Beispiel KMZ10B von Philips) verwenden will, kann einen der Differenz-Eingänge mit internem Verstärker des ATMega nutzen.

Für regelmäßige Messungen lassen sich die Sensoren in einer Endlosschleife ständig abfragen, aber damit blockiert man den Mikrocontroller völlig. Viel eleganter ist es, einen der eingebauten Timer zu benutzen. Diese generieren in einstellbaren Intervallen Interrupt-Signale. Der Prozessor unterbricht daraufhin seine aktuelle Arbeit und verzweigt in die zugehörige Interrupt-Service-Routine (ISR). Diese liest die Sensoren aus, speichert das Ergebnis ab und kehrt dann zum eigentlichen Code zurück.

Plan 2
Aus dem Chiptakt von 14 MHz generiert der Teiler vier verschiedene Takte, die für die Timer als Referenz zur Verfügung stehen.

Der ATMega8535 bringt gleich drei solcher Timer mit (zwei mit 8- und einen mit 16-Bit-Zähler), von denen jeder mehrere Interrupts auslösen kann. Wir betrachten hier einen der 8-Bit-Timer, da er für das periodische Auslesen der Sensoren ausreicht. Der Timer zählt ein 8-Bit-Register (TCNT0) mit jedem Clock-Signal um eins hoch, vergleicht es dann mit dem Compare-Register (OCR0) und löst wenn nötig einen Interrupt aus (OCF0 bei OCR0 gleich TCNT0, TOV0 bei Zählerüberlauf). Wie schnell dies geschieht, entscheidet der verwendete Zählertakt.

Ein Vorteiler stellt verschiedene Frequenzen zur Verfügung, die er durch Teilen aus dem Systemtakt (14,7456 MHz) erzeugt. Im Timer-Control-Register (TCCR0) legen drei Bits (CS00 bis CS01) fest, welchen Takt der Zähler verwendet (Tabelle im Datenblatt). Temperaturen ändern sich vergleichsweise langsam - eine Messung alle 3 ms reicht für viele Zwecke aus. Der Acht-Bit-Timer kann nur bis 255 zählen und so fällt die Wahl auf den höchstmöglichen Teiler (1024) und somit einen Takt von 14,4 kHz. Der Vergleichswert ergibt sich dann zu (14,400 kHz x 3 ms -1 = 42):

#define XTAL 14745600 
#define clock 333 // 3 ms --> 333 Hz
...
TCCR0 = (1<<CS00) | (1<<CS02) | (1<<WGM01);
OCR0 = (XTAL/1024/clock)-1;

Das WGM01-Bit sorgt dafür, dass der Zähler nach jedem Erreichen des Vergleichswertes wieder bei null anfängt zu zählen. Den Anfangswert schreibt man direkt in das Timer-Register und aktiviert zuletzt die Interrupts:

TCNT0 = 0; 
TIMSK |= (1<<OCIE0);
sei();

Das OCIE0-Bit im TIMSK-Register schaltet den Vergleichs-Interrupt ein und das Makro sei(), das in avr/interrupt.h definiert ist, aktiviert systemweit alle Interrupts.

Die Interrupt-Service-Routine unterscheidet sich dank weiterer Makros aus avr/signal.h kaum von einer gewöhnlichen C-Funktion. Sie beginnt mit dem Schlüsselwort SIGNAL, gefolgt vom Namen des zu behandelnden Interrupts:

SIGNAL (SIG_OUTPUT_COMPARE0){ 
Temp[0]=read_adc(0);
Temp[1]=read_adc(1);
}

Während der Ausführung der Funktion sind alle anderen Interrupts blockiert. Wer das nicht will, nutzt statt SIGNAL das Makro INTERRUPT, muss dann aber selbst dafür sorgen, dass sich Aufrufe nicht gegenseitig überlappen. Grundsätzlich gilt, dass ein Programm möglichst wenig Zeit in ISRs verbringen soll. Aufwendige Operationen gehören dort nicht hin, daher rechnet auch erst das Hauptprogramm die Sensorwerte in Grad Celsius um.

In der regelmäßig aufgerufenen ISR kann man neben Sensoren auch Eingabegeräte wie einen Drehimpulsgeber, kurz Drehgeber, auslesen. Da sich dessen Zustand vergleichsweise langsam ändert, klappt dieses einfache Polling-Verfahren. Bei schnell wechselnden Eingangssignalen bieten sich hingegen die Eingänge an, die bei jeder Pegeländerung einen eigenen Interrupt auslösen. Handelsübliche inkrementelle Drehgeber erzeugen beim Drehen an jedem ihrer zwei Ausgänge ein Rechtecksignal. Beide sind um 90 Grad zueinander phasenverschoben (siehe Grafik). So entsteht ein 2-Bit-Code, bei dem sich immer nur ein Bit auf einmal ändert (Graycode). Die Drehrichtung kann man dann aus einer Zustandstabelle ablesen.

Drehgeber
Ein Drehgeber erzeugt einen Graycode, bei dem zwei aufeinander folgende Werte sich immer in genau einem Bit unterscheiden. Die Drehrichtung liest man dann aus der Zustandstabelle aus.
Plan 3
Das B-Modul besitzt schon einen Anschluss für den Drehgeber. Vertauscht man Pin eins und zwei, dann ändert sich auch die Zählrichtung der Geberauswertung.

Einen Anschlag zur Winkelbegrenzung, wie bei Potentiometern üblich, gibt es nicht. Die aktuelle Position des Gebers kann folglich nur inkrementell durch kontinuierliches Mitrechnen ermittelt werden. Als Zusatzfunktion besitzen viele Drehgeber noch einen Taster, mit dem man beispielsweise in einem Menü die Auswahl bestätigen kann. Dieser wird wie jeder andere Taster auch ausgelesen [2].

Vor der Auswertung des Graycodes werden die drei Ports (2xGeber und 1xTaster) als Eingänge geschaltet und ihre internen Pull-up-Widerstände aktiviert. Eine logische Eins am Port-Pin entspricht damit einem geöffneten, eine Null dem geschlossenen Schalter.

#define DREH_A 0x10 
#define DREH_B 0x20
#define DREH_TAST 0x40
DDRC &= ~ (DREH_A + DREH_B + DREH_TAST);
PORTC |= (DREH_A + DREH_B + DREH_TAST);

Zum Abfragen des Drehgebers muss man der Timer-ISR nur einen Aufruf der im folgenden erklärten Funktion dreh_isr() hinzufügen. Der von uns verwendete Drehgeber erzeugt pro ganzer Drehung 60 Zustandsänderungen (zwei pro Rastung). Für die Abtastfrequenz von 333 Hz bedeutet dies: Alles unter zweieinhalb Umdrehungen pro Sekunde wird garantiert fehlerfrei abgetastet. Wem das nicht reicht, der kann den Wert im OCR0-Register herabsetzen. Die Abfrageroutine liest bei jedem Aufruf den aktuellen Zustand der beiden Geber-Pins ein und speichert sie in derselben Variable (Bits zwei und drei), in der auch die des letzten Aufrufes stehen (Bits null und eins):

void dreh_isr(void){ 
graycode >= 2;
if( (PINC & DREH_A) == DREH_A )
graycode |= 0x04;
if( (PINC & DREH_B) == DREH_B ) graycode |= 0x08;
drehgeber += graytab[graycode];
...
}

Dadurch entsteht ein Code, der für jede der möglichen Zustandsänderungen einen eigenen Wert enthält. Eine einfache Tabelle (graytab[]) mit 16 Werten verbindet jeden Zustand mit einem Wert für die Positionsänderung (+1 für Rechtsdrehung, -1 für eine Linksdrehung).

const int8_t graytab[] = {  
0,-1,1,0,
1,0,0,-1,
-1,0,0,+1,
0,1,-1,0 };

Diesen kumuliert die globale Variable drehgeber, die das Hauptprogramm auslesen kann. Es muss dabei nur beachten, dass je nach Bauform der Geber pro Rastung mehrere Zählerstände durchlaufen kann. Unser Exemplar zählt pro Rastung um zwei weiter, weswegen man seine Funktion nicht vollständig mit einem Digital-Multimeter nachprüfen kann. Die oben beschriebene Funktion eignet sich auch, um den Taster des Gebers mit auszulesen (Soft-Link).

Der Atmega kann nicht nur Signale aufnehmen, sondern sie auch mit seinen I/O-Pins erzeugen. Jeder Port-Pin kann bis zu 20 mA in beide Richtungen (nach High und nach Low) treiben. Allerdings ist der Gesamtstrom pro Port auf rund 100 mA und für den kompletten Chip auf 200 mA begrenzt. Die 20 mA eines Pins sind gut geeignet, um LEDs anzusteuern - für Relais, Motoren oder Glühbirnen reicht das aber nicht aus. Zudem sind die Ausgänge nicht potenzialfrei und können nur Geräte ansteuern, die mit demselben Massepotenzial arbeiten.

Benötigt man nur mehr Strom oder Spannung bei gleichem Bezugspotenzial, so reicht ein Transistor am Ausgang. Der Transistor muss sowohl zum gewünschten Laststrom als auch zur Betriebsspannung des Verbrauchers (V+) passen. Die Sperrspannung muss über der Betriebsspannung liegen. Der maximale Kollektorstrom richtet sich nach dem Innenwiderstand der Last.

Um diese Schaltung für mehrere Ausgänge nicht jeweils einzeln aufbauen zu müssen, gibt es ICs mit Transistor-Arrays (zum Beispiel ULN2803, Ausgangsspannung bis 50 V). Sie enthalten sieben oder acht einzelne Transistoren mit zusammengeschalteten Emittern und integrierten Basiswiderständen. Für induktive Lasten halten sie noch eine Freilaufdiode für jeden Ausgang bereit.

Potenzialfreie Ausgänge erhält man zum Beispiel mit einem kleinen Relais, das ein Transistor treibt. Dabei schützt eine Diode (1N4148) in Gegenrichtung (Kathode an Betriebsspannung) parallel zur Relaisspule den Transistor vor Spannungsspitzen. Diese entstehen in der Spule beim Abschalten der Relaisspannung. Netzspannung sollte man mit dem Relais aber keinesfalls schalten, da für das 230-Volt-Netz zahlreiche Sicherheitsbestimmungen zu beachten sind.

NPN-Transistor
Die Ausgänge des Controllers liefern bis zu 20 mA Strom. Reicht dies nicht aus oder ist eine höhere Schaltspannung nötig, hilft ein NPN-Transistor mit Basiswiderstand weiter.

Deutlich schneller und langlebiger als Relais sind Optokoppler. Sie bestehen aus einer Leuchtdiode und einem Fototransistor in einem lichtdichten Gehäuse. Die Leuchtdiode lässt sich mit einem Vorwiderstand direkt von einem Port-Pin des Atmega ansteuern, der Fototransistor arbeitet als normaler Schalttransistor im Zielsystem.

Um die Messwerte von den einzelnen Sensoren komfortabel auszuwerten und auch die Stellgrößen für die Ausgänge zu verändern, bietet sich der Webserver des A-Moduls an. Codebeispiele dazu gab es bereits im zweiten Teil des Projektes [2].

Das B-Modul bietet genug Raum für eigene Experimente, auch mit Sensoren, die ihre Kapazität oder Induktivität ändern. Sie erfordern aber deutlich mehr Beschaltungsaufwand, da ihre Eigenschaftsänderungen nicht direkt eine Messspannung erzeugen. Für den Austausch von eigenen Ideen steht weiterhin das Leserforum (Soft-Link) zur Verfügung. Dort gibt es schon rege Diskussionen zu möglichen Erweiterungen von Hard- und Software. Programme von Lesern stellen wir auch gerne auf die Projektseite (Soft-Link). (bbe)

[1] Benjamin Benz, Brücken bauen, Umsetzer von Ethernet nach RS-232 im Eigenbau

[2] Benjamin Benz, Fernkontrolle, Mikrocontroller zum Messen und Steuern über das LAN

Soft-Link


Der c't-COM-auf-LAN-Adapter aus dem ersten Teil des Projektes [1] verbindet Geräte, die nur eine RS-232-Schnittstelle besitzen, mit dem LAN. So lässt sich zum Beispiel eine Telefonanlage im Keller fernwarten oder ein Modem aus der Ferne nutzen. Die PCs im LAN können sich auch ein so angeschlossenes Gerät teilen.

Der c't-Mikrocontroller-im-LAN [2] erweitert das Projekt um einen Mikrocontroller, der analoge Signale misst und über digitale Eingänge verfügt. Er kann über zwanzig digitale Ports steuern und mit seriellen Geräten kommunizieren. Aufgesteckt auf das COM-auf-LAN-Modul lässt er sich über das Netzwerk fernsteuern und mit einem beliebigen Webbrowser überwachen.

Die Webseiten zur Abfrage der Sensoren dürfen auch Java-Applets enthalten und können auf dem integrierten Webserver des A-Moduls abgelegt werden. Beispielcode dazu findet sich auf der Projektseite (Soft-Link). Wer lieber mit einer eigenen Software den Controller fernsteuert, dem steht sowohl für Linux als auch für Windows eine virtuelle COM-Schnittstelle zur Verfügung. Hartgesottene können die beiden Module auch direkt über einen TCP/IP-Port ansprechen.