Die Mikrocontroller der AVR-Familie besitzen je nach Typ eine unterschiedliche Anzahl an programmierbaren Timern. Bei den aktuellen ATmegas sind das mindestens ein 8-Bit Timer und bei größeren Ausführungen der Serie auch 16-Bit Timer. Die Timer werden immer Timerx benannt, wobei x für die Timernummer steht (also 0, 1, 2, usw.). Die Konfigurationsmöglichkeiten sind von Timer zu Timer unterschiedlich.
Hinweis: Die folgenden Code-Beispiele sind in C programmiert und wurden für einen ATmega32 entwickelt. Sie lassen sich also ohne große Änderungen auch auf anderen Mikrocontrollern der AVR-Familie einsetzen.
Inhaltsverzeichnis
Allgemeine Funktionsweise
Timer funktionieren nach dem allgemeinen Prinzip, dass sie eine Ganzzahl (im weiteren als Zähler bezeichnet) je nach Betriebsmodus auf- oder abwärtszählen, d.h. inkrementieren bzw. dekrementieren.
Angenommen, der Timer arbeitet im einfachsten Betriebsmodus, dem normalen Modus (siehe Normaler Modus (Normal Mode)). Die Zählrichtung des Timers ist aufsteigend gerichtet. Je nach Auflösung, also 8-Bit oder 16-Bit, erreicht der Zähler irgendwann einen bestimmten Zustand. Möglich wäre, dass er überläuft, wenn z.B. bei einem 8-Bit Timer der Wert 255 inkrementiert wird (siehe Grafik).
Der Prescaler
Der Prescaler (eng. = Vorteiler) kann dazu genutzt werden, den Takt, der den Timern zugeführt wird, zu verkleinern. U.a. kann man damit die Timer so konfigurieren, damit diese in den unterschiedlichsten Frequenzen takten. Hier eine Grafik die den Prescaler veranschaulicht:
Das obere Diagramm zeigt den Betrieb ohne Prescaler, das untere mit Prescaler. Die gestrichelte Linie zeigt, wann ein Interrupt eintritt.
Im Teil Die Betriebsmodi wird weiter auf die praktische Verwendung des Prescalers eingegangen.
Die Betriebsmodi
Die AVR-Timer können in unterschiedlichen Betriebsmodi betrieben werden. Diese sind:
- Normaler Modus
- CTC Modus
- PWM
Normaler Modus (Normal Mode)
Der einfachste Betriebsmodus ist der normale Modus. Er funktioniert wie im Abschnitt "Allgemeine Funktionsweise" beschrieben. Die Zählrichtung des Timers ist immer aufsteigend, und irgendwann kommt es zu dem Interrupt Timer-Overflow (welcher in einer passend ISR aufgefangen werden kann). Im einfachsten Fall kann man diesen Modus in folgendem Diagramm darstellen:
Der Zähler des Timers (im Diagramm oben, die aufsteigende und dann wieder zurückgesetzte Linie) ist in dem Register TCNTx gespeichert, wobei x für eine Zahl steht. Soll z.B. auf den Timer0 (siehe Datenblatt des jeweiligen Controllers) des Controllers zugegriffen werden, so ist an TCNT eine 0 anzuhängen, also TCNT0. Wie lange es braucht, bis der Zähler einen Overflow auslöst, ist von der Taktfrequenz des Controllers, dem eingestellten Prescaler-Wert und von der Timerauflösung abhängig. Nun wäre es ja sehr unpraktisch, wenn wir den Zähler nicht anpassen könnten. Denn sonst müssten wir unsere Software die den Timer benutzt evtl. anpassen und viel rechnen um z.B. für 1000 ms zu schlafen. Deswegen kann auf den Zähler zugreifen und ihn vorladen bevor dieser wieder vom eigentlichen Timer hochgezählt wird. Dies veranschaulicht folgendes Diagramm:
Dadurch kann man den Timer beeinflussen, und beeinflussen wie lange es dauert, bis ein Overflow auftritt. Um zu berechnen, welchen Wert wir vorladen müssen, kann man auch ein Java-Applet nutzen, siehe unter Weblinks Java Applet.
Natürlich kann man das auch "von Hand" rechnen. Die Berechnung des Preloader- sowie Prescalerwerts bei Verwendung der Overflow-Interrupts, eines Prescalers von 64 (nicht alle Prescaler können verwendet werden) und eines Quarzes mit der Frequenz von 8 MHz sieht folgendermaßen aus (gesuchte Frequenz beträgt 1000 Hz unter der Verwendung des Timer0 eines ATmega32):
- [math]Prescale = Frequenz * 1000000 [Hz] = 8000000[/math]
- Wir definieren den maximalen Zählerwert. Dieser ist bei einem 8-Bit Timer 256, bei einem 16-Bit Timer 65536. In unserem Fall ist der maximale Zählerwert 256, weil Timer0 verwendet wird.
- Nun wird die Variable Prescale (s.o.) durch den verwendeten Prescaler (64) geteilt ([math]8000000 Hz / 64 = 125000[/math]).
- Als nächstes wird der im dritten Punkt errechnete Wert durch die gesuchte Frequenz geteilt [math]=125000 / 1000Hz = 125[/math].
- Nun wird mathematisch überprüft, ob der errechnete Wert aus dem vierten Punkt kleiner als der maximale Zählerwert ist. Trifft dies zu, so subtrahiert man den errechneten Wert vom maximalen Zählerwert ([math]= 256 - 125 = 131[/math]).
Damit haben wir den Wert errechnet, der bei jedem Interrupt, den der Timer0 auslöst, in TCNTx (in diesem Fall TCNT0) nachgeladen werden muss, damit die Interrupts in dem gewünschten Zeitabstand von einer Millisekunde ausgelöst werden.
Allerdings bleibt zu bemerken, dass bei der Verwendung einer "ungeraden" Quarzfrequenz (z.B. 7,3728 MHz) der Timer mit einer bestimmten Ungenauigkeit arbeitet. Würden wir z.B. den Quarz oben mit einem Quarz mit 7,3728 MHz austauschen, so wäre die Fehlerrate 0,17%. Diese Ungeauigkeit varriert von verwendetem Prescaler zu Prescaler. D.h. wenn wir einen Prescaler von 1024 (und einer Quarzfrequenz von 8 MHz) verwendet hätten, so hätten wir eine inakzeptable Ungeauigkeit von 11,61%. Deswegen sollte sie eines der genannten Programme unter Avr#Weblinks verwenden, denn diese zeigen nur die bestmögliche Konfiguration an.
Die Fehlerrate kann natürlich auch ausgerechnet werden. Hier die Rechenschritte (sie sind erweiternd zu der oberen Berechnung):
- Als erstes wird mathematisch überprüft, ob der Preloaderwert (siehe fünften Schritt oben) größer als 1 ist.
- Trifft dies zu, so wird als nächstes die resultierende Frequenz errechnet. Die geschieht folgendermaßen: Der errechnete Preloaderwert aus der Rechnung oben wird vom maximalen Zählerwert subtrahiert, anschließend mit dem Prescaler multipliziert und dann das Ganze durch die Variable Prescale geteilt
[math](256 - 131) \cdot 64 / 8\,000\,000 \mathrm{Hz} \cdot 1\,000\,000 = 1000[/math].
- Nun wird die gesuchte Frequenz vom errechneten Wert aus dem dritten Punkt subtrahiert und dann wiederum durch diese geteilt [math](1000-1000) / 1000 = 0[/math]. Damit läuft dieser Timer genau mit einer Fehlerrate von 0 %.
Betreibt man den Timer im Overflow-Modus, so muss man, wie bereits erwähnt, nach/bei jedem Overflow-Interrupt den Timer nachladen. Der Interrupt heißt in diesem Fall SIG_OVERFLOWx (x steht für die Nummer des Timers). Dieser muss in einer ISR abgefangen werden.
Zusammenfassend ein Beispielprogramm:
/* Es wird der Timer2 (8-Bit) eines ATmega32 verwendet, der mit einem Quarz mit 7,3728 MHz betrieben wird. Im Abstand von 0,01 ms erzeugt der Timer einen Interrupt, also eine Frequenz von 100000 Hz (oder 100 kHz). Der Timer wird auf einen Prescaler von 1 und einem Preloader von 183 konfiguriert.*/ volatile uint8_t countTimer2; // Speichert den aktuellen Zählerwert // ISR zum auffangen der Interrupts: SIGNAL(SIG_SIG_OVERFLOW2) { countTimer2++; TCNT2 = 183; // Nachladen } // Initialisierung: TCCR2 = (1<<CS22); // Prescaler von 1 TCNT2 = 183; // Vorladen TIMSK |= (1<<TOIE2); // Interrupts aktivieren und damit Timer starten sei(); // Funktionen zum benutzen der Timer: /** Diese Funktion nicht aufrufen. Wird von sleep_millisec aufgerufen. Bei t=100 schläft die Funktion 1 ms. */ inline void sleep (uint8_t t) { // countTimer2 wird in der ISR oben inkrementiert countTimer2 = 0; while (countTimer2 < t); } /** Schläft x-Millisekunden. */ inline void sleep_millisec(uint16_t msec) { uint16_t i; for(i=0; i<msec; i++) { sleep(100); } }
Allerdings wird auf diese leicht veraltete Technik nun nicht weiter eingegangen. Der Artikel wendet sich nun dem neueren "Compare Output"-Betriebsmodus zu.
Beim "Compare Output"-Betriebsmodus wird genauso ein Zähler hochgezählt. Allerdings wird der Zählerwert nach jeder Inkrementierung mit einem vom Benutzer festgelegten Wert verglichen. Entspricht der Zählerwert dem gespeicherten Wert, so kommt es zu einem Interrupt. Dieser Wert wird in das Register OCRx (x steht wieder für die Timernummer) gespeichert. Je nach Auflösung des Timers ist dieses Register 8-Bit oder 16-Bit breit.
Hinweis: Siehe CTC Modus unten. Dieser wird benötigt um den Timer entsprechend im "Compare Output"-Betriebsmodus vernünftig zu betreiben.
Hier ein typisches Diagramm dieses Betriebsmodus (Hinweis: Das Diagramm wurde unter Verwendung des CTC Modus erstellt. Für Begriffserklärung siehe CTC Modus):
Das Ausrechnen des Werts, der in OCRx geschrieben werden muss, damit Frequenz x entsteht, ist nicht sonderlich schwer. Man geht wie bei der Berechnung des Werts für den Overflow-Modus vor (s.o.) nur daß man das resultierende Ergebnis vom maximalen Zählerwert (bei 8-Bit Auflösung ist dieser 256, bei 16-Bit Auflösung 65536) subtrahiert. Das Ergebnis wird dann einmalig in das Register OCRx geschrieben. Mann muss also nicht wie beim Overflow-Modus den Timer nach jedem Interrupt nachladen. Der enstehende Interrupt heißt in diesem Fall SIG_OUTPUT_COMPAREx (x steht für die Nummer des Timers). Dieser muss in einer ISR abgefangen werden.
Wiederum zusammenfassend ein Beispielprogramm:
/* Es wird der Timer2 (8-Bit) eines ATmega32 verwendet, der mit einem Quarz mit 7,3728 MHz betrieben wird. Im Abstand von 0,01 ms erzeugt der Timer einen Interrupt, also eine Frequenz von 100000 Hz (oder 100 kHz). Der Timer wird auf einen Prescaler von 1 und einem OCR2-Wert von 73 konfiguriert.*/ volatile uint8_t countTimer2; // Speichert den aktuellen Zählerwert // ISR zum auffangen der Interrupts: SIGNAL(SIG_OUTPUT_COMPARE2) { countTimer2++; } // Initialisierung: TCCR2 = (1<<CS22) | (1<<WGM21); // Prescaler von 1 | CTC-Modus (siehe unten für Beschreibung) OCR2 = 73; // Vergleichswert TIMSK |= (1<<OCIE2); // Interrupts aktivieren und damit Timer starten sei(); // Funktionen zum benutzen der Timer: /** Diese Funktion nicht aufrufen. Wird von sleep_millisec aufgerufen. Bei t=100 schläft die Funktion 1 ms. */ inline void sleep(uint8_t t) { // countTimer2 wird in der ISR oben inkrementiert countTimer2 = 0; while (countTimer2 < t); } /** Schläft x-Millisekunden. */ inline void sleep_millisec(uint16_t msec) { uint16_t i; for(i=0; i<msec; i++) { sleep(100); } }
CTC Modus (Clear Timer on Compare Match mode)
Der CTC Modus ist eine Erweiterung des "Compare Output"-Betriebsmodus. Der CTC Modus wird z.B. für das obere Beispielprogramm benötigt. Wird der Timer nämlich im normalen Betriebsmodus betrieben, so ist seine Zählergrenze je nach Auflösung 255 oder entsprechend für die 16-Bit Timer. Erst wenn diese Grenze erreicht wurde, wird der Timer zurückgesetzt (also auf 0). Durch den CTC Modus wird der Timer augenblicklich automatisch zurückgesetzt nachdem der Vergleichswert erreicht ist. Man kann also die maximalen Zählergrenze selber definieren. Dieses Diagramm veranschaulicht den CTC Modus.
PWM
An diesem Artikel arbeitet gerade Mitglied uwegw.
Am besten momentan noch keine gravierenden Ergänzungen / Änderungen vornehmen. Dieser Hinweis verschwindet wenn der Autor soweit ist. Sollte dieser Hinweis länger als drei Tage auf einer Seite sein, bitte beim Autor uwegw per PM / Mail oder Forum nachfragen ob er vergessen wurde. |
Eine häufige Aufgabe für Mikrocontroller ist die Erzeugung von PWM-Signalen, zum Beispiel für Motorsteuerungen. Daher sind in den meisten AVRs PWM-Einheiten als Hardware vorhanden. Sie sind direkt mit den Timern verbunden und nutzen diese als Taktquelle. Die Hardware-PWM-Einheiten haben den Vorteil, sehr wenig Rechenzeit in Anspruch zu nehmen. Man muss nur die PWM aktivieren und bei Änderungen den gewünschten Wert in ein Register schreiben. Der Rest läuft automatisch und unabhängig vom restlichen Programm, ohne den AVr ständig zu beschäftigen wie bei einer PWM-Lösung in Software. Allerdings stehen meist nur zwei bis drei solcher PWM-Kanäle zur Verfügung, die außerdem an bestimmte Pins gebunden sind. Für die meisten Roboter mit zwei Antriebsmotoren reicht dies aber für gewöhnlich aus.
nutzbare Pins am AVR
Die Hardware-PWm-Funktion steht nur an bestimmten Pins zur Verfügung. In der Pinbelegungsübersicht im Datenblatt erkennt man sie daran, dass als Sonderfunktion in Klammern "OC..." angegeben ist. Beim Mega32 sind dies zb. OC0 an PB0, OC1A an PD5, OC1B an PD4 und OC2 an PD7. Der Mega32 hat also insgesamt vier Hardware-PWM-Kanäle. Die Zahl hinter dem "OC" gibt an, zu welchem der Timer dieser PWM-Kanal gehört. Wenn noch ein Buchstabe dahinter kommt, dann gehören mehrere PWMs zu diesem Timer. Beim Mega32 sind also OC1A und OC1B demselben Timer, nämlich Timer1, zugeordnet.
Zu beachten ist, dass die für die PWM benutzten Pins zuvor explizit als Ausgang konfiguriert werden müssen! Ansonsten gelangt das PWM-Signal nicht nach draußen!
Funktionsprinzip
Das "OC" in den Pinbezeichnungen steht für "Output Compare Unit", also frei übersetzt Ausgangs-Vergleicher-Einheit. Dies beschreibt die Funktionsweise der PWM-Kanäle: der Zählerstand des Timers wird fortlaufend mit einen einstellbaren Referenzwert verglichen, und wenn beide Werte übereinstimmen, kann ein Ausgangspin des AVRs automatisch geschaltet werden (und ein Interrupt ausgelöst werden, was allerdings für die PWM-Funktion nicht relevant ist). Dies entspricht den Verfahren im Beispiel zur Software-PWM. Es läuft nun allerdings vollautomatisch im Hintergrund, sodass der Controller nicht damit belastet wird.
Die verschiedenen PWM-Modi
Es gibt -je nach AVR und Timer- etliche Betriebsarten, in denen die PWM-Einheit betrieben werden kann. Sie unterscheiden sich vor allem darin, wie schnell und mit welchen Nebeneffekten sich Änderungen des Sollwertes auf das Ausgangssignal auswirken. Für den Anfang sind diese Unterschiede erst einmal nebensächlich, und für eine einfache Motorsteuerung meist auch irrelevant. Daher wird hier zunächst der "Fast PWM Mode" ("Schneller PWM Modus", weil hier die größte Ausgangsfrequenz möglich ist) beschreiben, welcher der einfachste von allen ist.
Hierbei zählt der Timer immer von Null an aufwärts, bis er den Maximalwert (teilweise einstellbar) erreicht hat. Dann läuft er über und fängt von vorne an. Wie schnell dies geschieht, wird, wie im normalen Modus, über den Prescaler eingestellt. Der gewünschte PWM-Ausgangswert wird im "OCRn"-Register abgelegt. Er darf zwischen Null und dem Maximalwert des Timers liegen. Er wird nun mit dem Timer-Wert verglichen. Was dann passiert, reglen die "COM..."-Bits. Sie bestimmen, wie der Ausgang geschaltet wird. Die übliche Konfiguration ist, dass bei erreichen des Sollwertes die Ausgänge auf high geschaltet werden, und beim Überlauf auf low. Damit ergibt sich ein nichtinvertiertes Signal. Schließt man (über einem passenden Motortreiber!) einen Motor an, dreht er sich bei einem Sollwert von 0 gar nicht und beim Maximalwert mit voller Geschwindigkeit.
Beispielcode für den timer1 des Mega16/32 (und vieler anderer AVRs):
#include <avr/io.h> #include <util/delay.h> //Warteschleife für die Demo-Ausgabe. Für die eigentliche PWM nicht benötigt! /* PWM-Beispiel für Mega16/32 (beiden haben den gleichen Timer) Benutzt wird Timer1 im Fast PWM Mode, 8 Bit Auflösung Die PWM-Signale liegen auf PD5/OC1A und PD4/OC1B */ //Den Prescaler einstellen, der die Frequenz festlegt TCCR1B |= (1<<CS12); //Prescaler 256 //Den Timer in den Fast PWM Mode, 8 Bit schalten //ACHTUNG: Die WGM-Bits sind auf beide Konfigurationsregister verteiltt! TCCR1A |= (1<<WGM10); TCCR1B |= (1<<WGM12); //Compare Output mode einstellen: Pin geht auf high bei Compare match, auf low bei Überlauf. Ergibt nichtinvertierte PWM. TCCR1A |= (1<<COM1A1) | (1<<COM1B1) ; // In diesen Registern wird der gwünschte PWM-Wert abgelegt. Erlaubter Bereich: 0 bis 255. OCR1A = 0; OCR1B = 0; //Zuletzt die Pins als Ausgänge konfigurieren. Erst jetzt liegt das PWM-Signal an den Pins an! DDRD |= (1<<PD4) | (1<< PD5); /*Nun ist der PWM-Modus aktiv! Der Ausgangswert kann nun über die Register OCR1A und OCR1B vorgegeben werden. Man könnte ihnen per define noch einen Zweitnamen verpassen, zb */ #define MotorLinks OCR1A #define MotorRechts OCR1B //Und nun kann man per MotorLinks = 127; MotorRechts = 127; //seinen Roboter mit halber Kraft vorwärts fahren lassen. /*PWM-Demo: Die PWM-Werte werden erst bis zum Maximalwert erhöht und dann wieder verringert. Ein angeschlossener Motor wird beschleunigen und dann wieder abbremsen. */ uint8_t wert; while(1) { for (wert=0; wert<255; wert++) { OCR1A = wert; OCR1B = wert; _delay_ms(10); } for (wert=255; wert>0; wert--) { OCR1A = wert; OCR1B = wert; _delay_ms(10); } }
Registerübersicht
Hinweis: Diese Registertabellen wurden für den aktuellen Atmel Controller Mega16 und Mega32 erstellt. Wenn Sie ein anderes Modell verwenden kann es sein, dass ein oder mehrere Register nicht existieren, oder sie eine andere Bezeichnung haben.
TIMSK | ||||||||||||||||||
Mit diesem Register, der von allen Timern verwendet wird, lässt sich die Interruptausführung und Art des jeweiligen Timers bestimmen.
| ||||||||||||||||||
|
Siehe auch