(→Normaler Modus (Normal Mode)) |
(→Normaler Modus (Normal Mode)) |
||
(24 dazwischenliegende Versionen von 6 Benutzern werden nicht angezeigt) | |||
Zeile 2: | Zeile 2: | ||
Die Konfigurationsmöglichkeiten sind von Timer zu Timer unterschiedlich. | Die Konfigurationsmöglichkeiten sind von Timer zu Timer unterschiedlich. | ||
− | '''Hinweis:''' Die folgenden Code-Beispiele sind in C programmiert und wurden für einen [[ATMega32|ATmega32]] entwickelt. Sie lassen sich also ohne große Änderungen auch auf anderen Mikrocontrollern der AVR-Familie einsetzen. | + | '''Hinweis:''' Die folgenden Code-Beispiele sind in C programmiert und wurden für einen [[ATMega32|ATmega32]] entwickelt. Sie lassen sich also ohne große Änderungen auch auf anderen Mikrocontrollern der AVR-Familie einsetzen. Allerdings hat Atmel bei den neueren µCs (etwa Mega88, Mega324 und fast alle der aktuellen Tiny) die Namen für die Register vielfach geändert. |
== Allgemeine Funktionsweise == | == 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. | 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 | + | Angenommen, der Timer arbeitet im einfachsten Betriebsmodus, dem [[Timer/Counter (Avr)#Normaler Modus (Normal Mode)|Normalen Modus]]. Die Zählrichtung des Timers ist aufsteigend gerichtet. Je nach Auflösung, also 8-Bit oder 16-Bit, folgt auf den maximalen Zählerstand wieder die Null. Wenn z.B. bei einem 8-Bit Timer der Wert 255 inkrementiert wird folgt die Null (siehe Grafik). |
[[Bild:AbstrakterZaehlvorgang.png]] | [[Bild:AbstrakterZaehlvorgang.png]] | ||
== Der Prescaler == | == Der Prescaler == | ||
− | Der Prescaler (eng. = Vorteiler) kann | + | Der Prescaler (eng. = Vorteiler) kann der Takt für den Timer herunter geteilt werden. Oft hat man Faktoren von 1, 8, 64 ,256 oder 1024 zur Auswahl. Über das selbe Register kann der Timer auch ganz angehalten werden oder ein externer Takt ausgewählt werden. Ein externer Takt darf dabei höchstens halb so hoch wie der Prozezessortakt sein. |
+ | Hier eine Grafik die den Prescaler veranschaulicht: | ||
[[Bild:Prescaler.png]] | [[Bild:Prescaler.png]] | ||
− | Das obere Diagramm zeigt den Betrieb ohne Prescaler, das untere mit Prescaler. Die gestrichelte Linie zeigt, wann der Timer weiterzählt. | + | Das obere Diagramm zeigt den Betrieb ohne Prescaler, das untere mit Prescaler (:2). Die gestrichelte Linie zeigt, wann der Timer weiterzählt. |
Im Teil [[Timer/Counter (Avr)#Die Betriebsmodi|Die Betriebsmodi]] wird weiter auf die praktische Verwendung des Prescalers eingegangen. | Im Teil [[Timer/Counter (Avr)#Die Betriebsmodi|Die Betriebsmodi]] wird weiter auf die praktische Verwendung des Prescalers eingegangen. | ||
Zeile 27: | Zeile 28: | ||
=== Normaler Modus (Normal Mode) === | === Normaler Modus (Normal Mode) === | ||
− | Der einfachste Betriebsmodus ist der normale Modus. Er funktioniert wie im Abschnitt "[[Timer/Counter (Avr)#Allgemeine Funktionsweise|Allgemeine Funktionsweise]]" beschrieben. Die Zählrichtung des Timers ist immer aufsteigend, | + | Der einfachste Betriebsmodus ist der normale Modus. Er funktioniert wie im Abschnitt "[[Timer/Counter (Avr)#Allgemeine Funktionsweise|Allgemeine Funktionsweise]]" beschrieben. Die Zählrichtung des Timers ist immer aufsteigend, bis zum Überlauf - da fängt der Zähler wieder bei 0 an. Der Überlauf kann einen Interrupt (Timer-Overflow) auslösen. Im einfachsten Fall kann dieser Modus im folgendem Diagramm dargestellt werden: |
[[Bild:NormalerModus_1.png]] | [[Bild:NormalerModus_1.png]] | ||
Zeile 47: | Zeile 48: | ||
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. | 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. | ||
− | Zwischen dem Timer Overflow und dem tatsächlichen Aufrufen der ISR mit dem Nachladen des Timers ergibt sich eine kleine Verzögerung, die nicht einmal immer gleich ist. | + | Zwischen dem Timer Overflow und dem tatsächlichen Aufrufen der ISR mit dem Nachladen des Timers ergibt sich eine kleine Verzögerung, die nicht einmal immer gleich ist. Bei einem genügend großen Prescaler (z.B. 64) kommt durch die Verzögerung kein zusätzlicher Timerschritt zustande, und auch die Methode mit dem Nachladen liefert exakte Ergebnisse. Bei kleinen Prescalern kommt es durch die Verzögerung zu längeren und nicht immer gleichen Zeitabständen. Wenn möglich wird für die Erzeugung einer konstanten Interruptrate deshalb besser der CTC Moduls benutzt. |
− | Zusammenfassend ein | + | Zusammenfassend ein Code-Beispiel (kein vollständiges Programm): |
<pre> | <pre> | ||
/* Es wird der Timer2 (8-Bit) eines ATmega32 verwendet, der mit einem Quarz mit 7,3728 MHz | /* Es wird der Timer2 (8-Bit) eines ATmega32 verwendet, der mit einem Quarz mit 7,3728 MHz | ||
− | betrieben wird. Im Abstand von 0, | + | betrieben wird. Im Abstand von etwa 0,1 ms erzeugt der Timer einen Interrupt, also eine |
− | Frequenz von | + | Frequenz von 10000 Hz. Der Timer wird auf einen Prescaler von 64 und |
− | einem Preloader von | + | einem Preloader von 244 konfiguriert.*/ |
volatile uint8_t countTimer2; // Speichert den aktuellen Zählerwert | volatile uint8_t countTimer2; // Speichert den aktuellen Zählerwert | ||
// ISR zum auffangen der Interrupts: | // ISR zum auffangen der Interrupts: | ||
− | SIGNAL(SIG_SIG_OVERFLOW2) | + | SIGNAL(SIG_SIG_OVERFLOW2) // alter Form, für neuere GCC Versionen: ISR(TIMER2_OVF_vect) |
{ | { | ||
+ | TCNT2 = 244; // Nachladen | ||
countTimer2++; | countTimer2++; | ||
− | |||
} | } | ||
// Initialisierung: | // Initialisierung: | ||
− | TCCR2 = (1<<CS22); // Prescaler von | + | TCCR2 = (1<<CS22); // Prescaler von 64 und damit Timer starten |
− | TCNT2 = | + | TCNT2 = 244; // Vorladen |
− | TIMSK |= (1<<TOIE2); // Interrupts aktivieren | + | TIMSK |= (1<<TOIE2); // Interrupts aktivieren |
sei(); | sei(); | ||
// Funktionen zum benutzen der Timer: | // Funktionen zum benutzen der Timer: | ||
/** Diese Funktion nicht aufrufen. Wird von sleep_millisec aufgerufen. | /** Diese Funktion nicht aufrufen. Wird von sleep_millisec aufgerufen. | ||
− | Bei t= | + | Bei t=10 schläft die Funktion 1 ms. */ |
inline void sleep (uint8_t t) | inline void sleep (uint8_t t) | ||
{ | { | ||
// countTimer2 wird in der ISR oben inkrementiert | // countTimer2 wird in der ISR oben inkrementiert | ||
− | + | countTimer2 = 0; // 1 Byte Typ, daher kein cli()... sei() nötig | |
while (countTimer2 < t); | while (countTimer2 < t); | ||
} | } | ||
Zeile 87: | Zeile 88: | ||
uint16_t i; | uint16_t i; | ||
for(i=0; i<msec; i++) { | for(i=0; i<msec; i++) { | ||
− | sleep( | + | sleep(10); |
} | } | ||
} | } | ||
</pre> | </pre> | ||
− | + | Dieses Beispiel zeigt nicht unbedingt eine vorbildliche Nutzung des Timers. Eine ISR einfach nur zum schnellen Hochzählen der Zeit verbraucht recht viel Rechenzeit und sollte sonst eher vermieden werden. Das Hochzählen der "Zeit" ist eigentlich genau das, was der Timer in Hardware macht - nur halt nicht immer in geraden Zeitschritte, wie 0,1 ms, sondern halt in den Schritten, die der Prescaler vorgibt (z.B. 256 Takte). Die Umrechnung kann man aber gut auch bei der Wartezeit vorher, oder bei einer gemessenen Zeit nachher machen. | |
==== Input Capture ==== | ==== Input Capture ==== | ||
− | Die 16 Bit Timer | + | Die 16 Bit Timer haben eine "Input Capture" Funktion. Dieser Hardwareteil dient zur genauen Zeitmessung. Die typische Anwendung ist die Messung von kurze Zeiten, wie z.B. die Zeit für eine Motorumdrehung. Außer in einigen PWM Betriebsarten, wo das ICP Register als TOP-wert für den Timer benutzt wird, ist die ICP-Funktion immer aktiv. Wenn am ICP Pin die über das Bit "ICESx" eingestellte Flanke auftritt, wird der aktuelle Zählerstand in das ICP Register kopiert. Außerdem kann ein Interrupt ausgelöst werden. Der Interrupt wird, wie die anderen Timer Interrupts, in den Registern TIMSK und TIFR an- oder abgestellt. Man kann zwar die ICP-Funktion selber nicht ohne weiteres abschalten, aber natürlich den dazugehörigen Interrupt. Als eine spezielle Funktion ("Noise Cancler") gibt es die Möglichkeit sehr kurze Pulse (unter 4 Zyklen) zu unterdrücken. In der Regel kann man diese Funktion angestellt lassen, denn so schnell kann man die Daten ohnehin nicht verarbeiten. |
− | + | Solange die 16 Bit des Timers ausreichen ist die Benutzung ganz einfach: Der Timer wird mit dem gewünschten Vorteiler im normalen Modus gestartet. Im ICP-Interrupt wird die Differenz aus zwei aufeinanderfolgenden Zeiten (Werte in ICP-Register) berechnet. Dazu wird jeweils die vorherige Zeit im RAM zwischengespeichert. Wenn man bei der Rechnung (vorzeichenlose 16 Bit Zahlen) eventuelle Überläufe ignoriert, bekommt man die richtige Zeitdifferenz, auch wenn der Timer während der Messzeit einen Überlauf hatte. Das funktioniert so einfach, denn wenn noch weitere (höherwertige) Bytes vorhanden wären, damit es keinen Überlauf gibt, würde man genau so die unteren Bits berechnen. | |
− | Etwas komplizierter wird es, wenn die 16 Bit Auflösung nicht mehr ausreicht. Dann kann der Timer-Überlauf benutzt werden, um auch längere Zeiten mit voller Auflösung zu messen. Die wesentliche Schwierigkeit ist es, den Fall zu berücksichtigen, dass ein Überlauf Interrupt und der ICP Interrupt fast gleichzeitig ausgelöst werden. Es kann passieren, dass der ICP-Interrupt aufgerufen wird, obwohl eigentlich erst der Overflow Interrupt dran gewesen wäre. Dieser seltene Fall | + | Etwas komplizierter wird es, wenn die 16 Bit Auflösung nicht mehr ausreicht. Dann kann der Timer-Überlauf benutzt werden, um auch längere Zeiten mit voller Auflösung zu messen. Die wesentliche Schwierigkeit ist es, den Fall zu berücksichtigen, dass ein Überlauf Interrupt und der ICP Interrupt fast gleichzeitig ausgelöst werden. Es kann passieren, dass der ICP-Interrupt aufgerufen wird, obwohl eigentlich erst der Overflow Interrupt dran gewesen wäre. Dieser seltene Fall lässt sich daran erkennen, dass das Overflow-Interrupt Flag gesetzt ist und der Wert im ICP Register klein ist (high Byte < 128, meistens 0). |
Beispielpropgramm (für GCC): | Beispielpropgramm (für GCC): | ||
Zeile 111: | Zeile 112: | ||
// mit leichten Anpassungen auch für Tiny2313, Mega16, Mega32,... | // mit leichten Anpassungen auch für Tiny2313, Mega16, Mega32,... | ||
− | #include <stdlib.h> | + | #include <stdlib.h> // für utoa |
#include <avr/io.h> | #include <avr/io.h> | ||
#include <avr/interrupt.h> | #include <avr/interrupt.h> | ||
− | #include <avr/sleep.h> | + | #include <avr/sleep.h> // Unterstützung für sleep mode |
− | + | #define F_CPU 1000000UL // Definition der Frequenz, ist ggf. im makefile | |
− | // Definition | + | |
#define BAUD 19200UL | #define BAUD 19200UL | ||
#define UBRR_BAUD ((F_CPU/(16UL*BAUD))-1) | #define UBRR_BAUD ((F_CPU/(16UL*BAUD))-1) | ||
− | typedef union { // union erlaubt einen effektiven, | + | typedef union { // union erlaubt einen effektiven, separaten Zugriff auf Teile der Variable |
unsigned long i32; | unsigned long i32; | ||
− | struct {uint8_t i8l; | + | struct {uint8_t i8l; // low |
− | uint8_t i8m; | + | uint8_t i8m; // mid |
− | unsigned int high; // high, soft timer | + | unsigned int high; // high, soft timer |
}; | }; | ||
} convert32to8; | } convert32to8; | ||
− | volatile unsigned long timestamp; // volatile wegen Zugriff im | + | volatile unsigned long timestamp; // volatile wegen Zugriff im Interrupt |
volatile unsigned int softtimer; | volatile unsigned int softtimer; | ||
volatile unsigned long zeitdifferenz; | volatile unsigned long zeitdifferenz; | ||
unsigned long zeit; | unsigned long zeit; | ||
− | char puffer[12]; | + | char puffer[12]; // Puffer für Ausgabe als Ascii |
− | ISR(TIMER1_OVF_vect) | + | ISR(TIMER1_OVF_vect) // Timer1 Überlauf |
{ | { | ||
− | ++softtimer; | + | ++softtimer; // zählen der Überläufe |
} | } | ||
− | ISR(TIMER1_CAPT_vect) | + | ISR(TIMER1_CAPT_vect) // Flanke an ICP pin |
{ | { | ||
− | convert32to8 cap; | + | convert32to8 cap; // Variablendeklaration |
− | cap.i8l = ICR1L; | + | cap.i8l = ICR1L; // low Byte zuerst, high Byte wird gepuffert |
cap.i8m = ICR1H; | cap.i8m = ICR1H; | ||
// overflow verpasst, wenn ICR1H klein und wartender Overflow Interrupt | // overflow verpasst, wenn ICR1H klein und wartender Overflow Interrupt | ||
Zeile 150: | Zeile 150: | ||
{ // wartenden timer overflow Interrupt vorziehen | { // wartenden timer overflow Interrupt vorziehen | ||
++softtimer; | ++softtimer; | ||
− | TIFR1 = (1<<TOV1); | + | TIFR1 = (1<<TOV1); // timer overflow int. löschen, da schon hier ausgeführt |
} | } | ||
− | cap.high = softtimer; | + | cap.high = softtimer; // obere 16 Bit aus Software Zähler |
zeitdifferenz = cap.i32 - timestamp; | zeitdifferenz = cap.i32 - timestamp; | ||
− | timestamp = cap.i32; | + | timestamp = cap.i32; // Zeit merken |
} | } | ||
− | void uart_init(void) | + | void uart_init(void) // USART initialisieren (Mega48 etc.) |
{ | { | ||
// Baudrate einstellen (Normaler Modus) | // Baudrate einstellen (Normaler Modus) | ||
// kann bei älteren AVR Typen etwas anders sein (kein UBRR0H, dafür prescaler) | // kann bei älteren AVR Typen etwas anders sein (kein UBRR0H, dafür prescaler) | ||
− | UBRR0H = (uint8_t) (UBRR_BAUD>>8); | + | UBRR0H = (uint8_t) (UBRR_BAUD>>8); // bei Mega32 anders ! |
− | UBRR0L = (uint8_t) (UBRR_BAUD & 0x0ff); | + | UBRR0L = (uint8_t) (UBRR_BAUD & 0x0ff); // bei Mega32 UBRRL |
− | UCSR0B = (1<<TXEN0); // Aktivieren des Senders | + | UCSR0B = (1<<TXEN0); // Aktivieren des Senders, bei Mega32 UCSRB |
− | UCSR0C = (1<<UCSZ01)|(1<<UCSZ00)|(1<<USBS0); | + | UCSR0C = (1<<UCSZ01)|(1<<UCSZ00)|(1<<USBS0); // bei Mega32 UCSRC |
− | // Einstellen des Datenformats: 8 Datenbits, 2 | + | // Einstellen des Datenformats: 8 Datenbits, 2 Stopbit: |
} | } | ||
Zeile 178: | Zeile 178: | ||
unsigned char i; | unsigned char i; | ||
// Datenrichtungen: | // Datenrichtungen: | ||
− | DDRB = 0; | + | DDRB = 0; // Alles Eingänge, PB0 ist ICP |
− | PORTB = 0xFF - (1<<PB0); | + | PORTB = 0xFF - (1<<PB0); // Pullups an Eingängen außer ICP |
− | DDRC = 0; | + | DDRC = 0; // Eingänge |
− | PORTC = 0xFF; | + | PORTC = 0xFF; // Pullups an Eingängen |
− | DDRD = (1<<PD1); | + | DDRD = (1<<PD1); // Eingänge, außer PD1 = Tx (UART) |
− | + | PORTD = 0xFF- (1<<PD1); // Pullups an alle Eingängen (außer TX) | |
// Timer1 initialisieren: | // Timer1 initialisieren: | ||
− | TCCR1A = 0; | + | TCCR1A = 0; // normal mode, keine PWM Ausgänge |
TCCR1B = (1<< ICNC1) + (1<<CS10) // start Timer mit Systemtakt | TCCR1B = (1<< ICNC1) + (1<<CS10) // start Timer mit Systemtakt | ||
+ (1 << ICES1); // steigende Flanke auswählen | + (1 << ICES1); // steigende Flanke auswählen | ||
− | TIMSK1 = (1<<TOIE1) + (1<<ICIE1); // Input-capture | + | TIMSK1 = (1<<TOIE1) + (1<<ICIE1); // overflow und Input-capture aktivieren, Mega32: TIMSK |
− | TIFR1 = (1<<TOIE1) + (1<<ICIE1); // Schon aktive | + | TIFR1 = (1<<TOIE1) + (1<<ICIE1); // Schon aktive Interrupts löschen, Mega32: TIFR |
// UART initialisieren: | // UART initialisieren: | ||
− | uart_init(); | + | uart_init(); |
zeitdifferenz = 0; | zeitdifferenz = 0; | ||
− | softtimer = 0; | + | softtimer = 0; // wird für Zeitdifferenzmessung nicht mal gebraucht, |
− | + | // denn Differenz geht auch über Überlauf bei Softtimer | |
set_sleep_mode (SLEEP_MODE_IDLE); // idle Mode: timer läuft weiter, int zum aufwachen | set_sleep_mode (SLEEP_MODE_IDLE); // idle Mode: timer läuft weiter, int zum aufwachen | ||
− | sei(); | + | sei(); // Interrupts erlauben: Messung startet |
while (1) | while (1) | ||
{ | { | ||
− | sleep_enable (); // | + | sleep_enable (); // Sleep Befehl freigeben |
sei(); | sei(); | ||
sleep_cpu(); // wartet auf irgendeinen Interrupt, z.B. ICP, timer_ovr,... | sleep_cpu(); // wartet auf irgendeinen Interrupt, z.B. ICP, timer_ovr,... | ||
− | sleep_disable(); // | + | sleep_disable(); // Sleep Befehl sperren |
− | cli(); // Interrupt sperren wegen Zugriff auf volatile | + | cli(); // Interrupt sperren wegen Zugriff auf volatile Variable |
zeit = zeitdifferenz; | zeit = zeitdifferenz; | ||
zeitdifferenz = 0; // als Markierung für ungültigen Wert | zeitdifferenz = 0; // als Markierung für ungültigen Wert | ||
Zeile 211: | Zeile 211: | ||
if (zeit > 0) | if (zeit > 0) | ||
{ | { | ||
− | + | ultoa(zeit,puffer,10); // nach ASCII umwandeln | |
i = 0; | i = 0; | ||
while (puffer[i]) | while (puffer[i]) | ||
Zeile 217: | Zeile 217: | ||
putser(puffer[i++]); // Ausgabe | putser(puffer[i++]); // Ausgabe | ||
} | } | ||
− | putser(13); | + | putser(13); putser(10); // Zeilenumbruch senden |
− | + | ||
} | } | ||
} // Ende von While-schleife | } // Ende von While-schleife | ||
Zeile 225: | Zeile 224: | ||
=== CTC Modus (Clear Timer on Compare Match mode) === | === CTC Modus (Clear Timer on Compare Match mode) === | ||
− | Der CTC Modus ist eine Erweiterung des "Output-Compare"-Funktion. Der CTC Modus eignet sich besonders, um einen mit konstanter Frequenz wiederkehrenden Interrupt zu erzeugen. Wie im normalen Modus zählt der Timer hoch. Wenn der Wert im OCRx Register erreicht wird, wird zusätzlich zum möglichen Interrupt der Zähler wieder auf 0 gesetzt. Es kann also die maximalen Zählergrenze selber definiert werden. | + | Viele Timer haben "Output-Compare" Register: OCRx oder OCRAx,OCRBx. Wenn der Zähler den darin eingestellten Wert erreicht hat, kann ein Interrupts ausgelöst werden. Der CTC Modus ist eine Erweiterung des "Output-Compare"-Funktion. Der CTC Modus eignet sich besonders, um einen mit konstanter Frequenz wiederkehrenden Interrupt zu erzeugen. Wie im normalen Modus zählt der Timer hoch. Wenn der Wert im OCRx Register erreicht wird, wird zusätzlich zum möglichen Interrupt der Zähler wieder auf 0 gesetzt. Es kann also die maximalen Zählergrenze selber definiert werden. |
Dieses Diagramm veranschaulicht den CTC Modus. | Dieses Diagramm veranschaulicht den CTC Modus. | ||
Zeile 233: | Zeile 232: | ||
<pre> | <pre> | ||
− | /* Es wird der Timer2 (8-Bit) eines ATmega32 verwendet, der mit einem Quarz mit 7,3728 MHz | + | /* Es wird der Timer2 (8-Bit) eines ATmega32 verwendet, der mit einem Quarz |
− | 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.*/ | + | 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 | volatile uint8_t countTimer2; // Speichert den aktuellen Zählerwert | ||
Zeile 245: | Zeile 246: | ||
// Initialisierung: | // Initialisierung: | ||
− | TCCR2 = (1<< | + | TCCR2 = (1<<CS20) | (1<<WGM21); // Prescaler von 1 | CTC-Modus (siehe unten für Beschreibung) |
OCR2 = 73; // Vergleichswert | OCR2 = 73; // Vergleichswert | ||
TIMSK |= (1<<OCIE2); // Interrupts aktivieren und damit Timer starten | TIMSK |= (1<<OCIE2); // Interrupts aktivieren und damit Timer starten | ||
Zeile 277: | Zeile 278: | ||
==== nutzbare Pins am AVR ==== | ==== nutzbare Pins am AVR ==== | ||
− | Die Hardware-PWM-Funktion steht nur an bestimmten Pins zur Verfügung. In der Pinbelegungsübersicht im Datenblatt ist erkenntbar, dass als Sonderfunktion in Klammern "OC..." angegeben ist. Beim Mega32 sind dies zb. OC0 an | + | Die Hardware-PWM-Funktion steht nur an bestimmten Pins zur Verfügung. In der Pinbelegungsübersicht im Datenblatt ist erkenntbar, dass als Sonderfunktion in Klammern "OC..." angegeben ist. Beim Mega32 sind dies zb. OC0 an PB3, 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!''' | '''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!''' | ||
Zeile 293: | Zeile 294: | ||
<pre> | <pre> | ||
#include <avr/io.h> | #include <avr/io.h> | ||
− | #include <util/delay.h> //Warteschleife für die Demo | + | #include <util/delay.h> // Warteschleife für die Demo. Für die eigentliche PWM nicht benötigt! |
/* PWM-Beispiel für Mega16/32 (beiden haben den gleichen Timer) | /* PWM-Beispiel für Mega16/32 (beiden haben den gleichen Timer) | ||
Zeile 304: | Zeile 305: | ||
// 2. Den Timer in den Fast PWM Mode, 8 Bit schalten | // 2. Den Timer in den Fast PWM Mode, 8 Bit schalten | ||
− | //ACHTUNG: Die WGM-Bits sind auf beide Konfigurationsregister | + | // ACHTUNG: Die WGM-Bits sind auf beide Konfigurationsregister verteilt! |
TCCR1A |= (1<<WGM10); | TCCR1A |= (1<<WGM10); | ||
TCCR1B |= (1<<WGM12); | TCCR1B |= (1<<WGM12); | ||
− | // 3. Compare Output mode einstellen: Pin geht auf high bei Compare match, auf low bei Überlauf. Ergibt nichtinvertierte PWM. | + | // 3. Compare Output mode einstellen: Pin geht auf high bei Compare match, auf low bei Überlauf. |
+ | // Ergibt nichtinvertierte PWM. | ||
TCCR1A |= (1<<COM1A1) | (1<<COM1B1) ; | TCCR1A |= (1<<COM1A1) | (1<<COM1B1) ; | ||
Zeile 315: | Zeile 317: | ||
OCR1B = 0; | OCR1B = 0; | ||
− | // 4. | + | // 4. Die Pins als Ausgänge konfigurieren. Erst jetzt liegt das PWM-Signal an den Pins an! |
DDRD |= (1<<PD4) | (1<< PD5); | DDRD |= (1<<PD4) | (1<< PD5); | ||
− | /*Nun ist der PWM-Modus aktiv! Der Ausgangswert kann nun über die Register OCR1A und OCR1B vorgegeben werden. | + | /*Nun ist der PWM-Modus aktiv! Der Ausgangswert kann nun über die Register OCR1A und OCR1B |
− | Man könnte ihnen per define noch einen Zweitnamen verpassen, zb */ | + | vorgegeben werden. Man könnte ihnen per define noch einen Zweitnamen verpassen, zb */ |
#define MotorLinks OCR1A | #define MotorLinks OCR1A | ||
#define MotorRechts OCR1B | #define MotorRechts OCR1B | ||
Zeile 326: | Zeile 328: | ||
MotorRechts = 127; | MotorRechts = 127; | ||
//seinen Roboter mit halber Kraft vorwärts fahren lassen. | //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. | /*PWM-Demo: Die PWM-Werte werden erst bis zum Maximalwert erhöht und dann wieder verringert. | ||
− | Ein angeschlossener Motor wird beschleunigen und dann | + | Ein angeschlossener Motor wird beschleunigen und dann wieder abbremsen. */ |
uint8_t wert; | uint8_t wert; | ||
while(1) | while(1) | ||
Zeile 384: | Zeile 385: | ||
* [[Atmel]] | * [[Atmel]] | ||
* [[HEX Beispiel-Dateien für AVR]] | * [[HEX Beispiel-Dateien für AVR]] | ||
− | + | * [[Bascom_und_Timer]] | |
== Weblinks == | == Weblinks == | ||
* [http://www.atmel.com/dyn/products/devices.asp?family_id=607 Die Datenblätter zu Atmel Controllern] | * [http://www.atmel.com/dyn/products/devices.asp?family_id=607 Die Datenblätter zu Atmel Controllern] | ||
− | * [ | + | * [http://frank.circleofcurrent.com/cache/avrtimercalc.htm Javascript-Toll zur Timerberechnung] |
* [http://www.roboternetz.de/phpBB2/dload.php?action=file&file_id=169 AvrTimer Windows Berechnungstool (für Bascom, nur nach Anmeldung)] | * [http://www.roboternetz.de/phpBB2/dload.php?action=file&file_id=169 AvrTimer Windows Berechnungstool (für Bascom, nur nach Anmeldung)] | ||
Aktuelle Version vom 8. November 2014, 12:35 Uhr
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 ein 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. Allerdings hat Atmel bei den neueren µCs (etwa Mega88, Mega324 und fast alle der aktuellen Tiny) die Namen für die Register vielfach geändert.
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. Die Zählrichtung des Timers ist aufsteigend gerichtet. Je nach Auflösung, also 8-Bit oder 16-Bit, folgt auf den maximalen Zählerstand wieder die Null. Wenn z.B. bei einem 8-Bit Timer der Wert 255 inkrementiert wird folgt die Null (siehe Grafik).
Der Prescaler
Der Prescaler (eng. = Vorteiler) kann der Takt für den Timer herunter geteilt werden. Oft hat man Faktoren von 1, 8, 64 ,256 oder 1024 zur Auswahl. Über das selbe Register kann der Timer auch ganz angehalten werden oder ein externer Takt ausgewählt werden. Ein externer Takt darf dabei höchstens halb so hoch wie der Prozezessortakt sein. Hier eine Grafik die den Prescaler veranschaulicht:
Das obere Diagramm zeigt den Betrieb ohne Prescaler, das untere mit Prescaler (:2). Die gestrichelte Linie zeigt, wann der Timer weiterzählt.
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, bis zum Überlauf - da fängt der Zähler wieder bei 0 an. Der Überlauf kann einen Interrupt (Timer-Overflow) auslösen. Im einfachsten Fall kann dieser Modus im folgendem Diagramm dargestellt werden:
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 zugegriffen werden und ihn vorladen, bevor dieser wieder vom eigentlichen Timer hochgezählt wird. Dies veranschaulicht folgendes Diagramm:
Dadurch kann eingestellt werden, wie lange es dauert, bis ein Overflow auftritt. Um zu berechnen, welchen Wert wir vorladen müssen, kann auch ein Java-Applet genutzt werden, siehe unter Weblinks Java Applet.
Natürlich kann das auch "von Hand" berechnet werden. 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 wird der errechneten Wert vom maximalen Zählerwert subtrahiert([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.
Zwischen dem Timer Overflow und dem tatsächlichen Aufrufen der ISR mit dem Nachladen des Timers ergibt sich eine kleine Verzögerung, die nicht einmal immer gleich ist. Bei einem genügend großen Prescaler (z.B. 64) kommt durch die Verzögerung kein zusätzlicher Timerschritt zustande, und auch die Methode mit dem Nachladen liefert exakte Ergebnisse. Bei kleinen Prescalern kommt es durch die Verzögerung zu längeren und nicht immer gleichen Zeitabständen. Wenn möglich wird für die Erzeugung einer konstanten Interruptrate deshalb besser der CTC Moduls benutzt.
Zusammenfassend ein Code-Beispiel (kein vollständiges Programm):
/* Es wird der Timer2 (8-Bit) eines ATmega32 verwendet, der mit einem Quarz mit 7,3728 MHz betrieben wird. Im Abstand von etwa 0,1 ms erzeugt der Timer einen Interrupt, also eine Frequenz von 10000 Hz. Der Timer wird auf einen Prescaler von 64 und einem Preloader von 244 konfiguriert.*/ volatile uint8_t countTimer2; // Speichert den aktuellen Zählerwert // ISR zum auffangen der Interrupts: SIGNAL(SIG_SIG_OVERFLOW2) // alter Form, für neuere GCC Versionen: ISR(TIMER2_OVF_vect) { TCNT2 = 244; // Nachladen countTimer2++; } // Initialisierung: TCCR2 = (1<<CS22); // Prescaler von 64 und damit Timer starten TCNT2 = 244; // Vorladen TIMSK |= (1<<TOIE2); // Interrupts aktivieren sei(); // Funktionen zum benutzen der Timer: /** Diese Funktion nicht aufrufen. Wird von sleep_millisec aufgerufen. Bei t=10 schläft die Funktion 1 ms. */ inline void sleep (uint8_t t) { // countTimer2 wird in der ISR oben inkrementiert countTimer2 = 0; // 1 Byte Typ, daher kein cli()... sei() nötig while (countTimer2 < t); } /** Schläft x-Millisekunden. */ inline void sleep_millisec(uint16_t msec) { uint16_t i; for(i=0; i<msec; i++) { sleep(10); } }
Dieses Beispiel zeigt nicht unbedingt eine vorbildliche Nutzung des Timers. Eine ISR einfach nur zum schnellen Hochzählen der Zeit verbraucht recht viel Rechenzeit und sollte sonst eher vermieden werden. Das Hochzählen der "Zeit" ist eigentlich genau das, was der Timer in Hardware macht - nur halt nicht immer in geraden Zeitschritte, wie 0,1 ms, sondern halt in den Schritten, die der Prescaler vorgibt (z.B. 256 Takte). Die Umrechnung kann man aber gut auch bei der Wartezeit vorher, oder bei einer gemessenen Zeit nachher machen.
Input Capture
Die 16 Bit Timer haben eine "Input Capture" Funktion. Dieser Hardwareteil dient zur genauen Zeitmessung. Die typische Anwendung ist die Messung von kurze Zeiten, wie z.B. die Zeit für eine Motorumdrehung. Außer in einigen PWM Betriebsarten, wo das ICP Register als TOP-wert für den Timer benutzt wird, ist die ICP-Funktion immer aktiv. Wenn am ICP Pin die über das Bit "ICESx" eingestellte Flanke auftritt, wird der aktuelle Zählerstand in das ICP Register kopiert. Außerdem kann ein Interrupt ausgelöst werden. Der Interrupt wird, wie die anderen Timer Interrupts, in den Registern TIMSK und TIFR an- oder abgestellt. Man kann zwar die ICP-Funktion selber nicht ohne weiteres abschalten, aber natürlich den dazugehörigen Interrupt. Als eine spezielle Funktion ("Noise Cancler") gibt es die Möglichkeit sehr kurze Pulse (unter 4 Zyklen) zu unterdrücken. In der Regel kann man diese Funktion angestellt lassen, denn so schnell kann man die Daten ohnehin nicht verarbeiten.
Solange die 16 Bit des Timers ausreichen ist die Benutzung ganz einfach: Der Timer wird mit dem gewünschten Vorteiler im normalen Modus gestartet. Im ICP-Interrupt wird die Differenz aus zwei aufeinanderfolgenden Zeiten (Werte in ICP-Register) berechnet. Dazu wird jeweils die vorherige Zeit im RAM zwischengespeichert. Wenn man bei der Rechnung (vorzeichenlose 16 Bit Zahlen) eventuelle Überläufe ignoriert, bekommt man die richtige Zeitdifferenz, auch wenn der Timer während der Messzeit einen Überlauf hatte. Das funktioniert so einfach, denn wenn noch weitere (höherwertige) Bytes vorhanden wären, damit es keinen Überlauf gibt, würde man genau so die unteren Bits berechnen.
Etwas komplizierter wird es, wenn die 16 Bit Auflösung nicht mehr ausreicht. Dann kann der Timer-Überlauf benutzt werden, um auch längere Zeiten mit voller Auflösung zu messen. Die wesentliche Schwierigkeit ist es, den Fall zu berücksichtigen, dass ein Überlauf Interrupt und der ICP Interrupt fast gleichzeitig ausgelöst werden. Es kann passieren, dass der ICP-Interrupt aufgerufen wird, obwohl eigentlich erst der Overflow Interrupt dran gewesen wäre. Dieser seltene Fall lässt sich daran erkennen, dass das Overflow-Interrupt Flag gesetzt ist und der Wert im ICP Register klein ist (high Byte < 128, meistens 0).
Beispielpropgramm (für GCC): (Bisher nur im Simulator getestet)
// Beispielprogramm für Zeitmessung mit ICP-Funktion // Erweiterung des Timers auf 32 Bit durch Software // Es wird die Periodendauer am ICP-Eingang gemessen und als ASCII via UART ausgegeben // Code für Mega48 / Mega88 / Mega 168 / ... // mit leichten Anpassungen auch für Tiny2313, Mega16, Mega32,... #include <stdlib.h> // für utoa #include <avr/io.h> #include <avr/interrupt.h> #include <avr/sleep.h> // Unterstützung für sleep mode #define F_CPU 1000000UL // Definition der Frequenz, ist ggf. im makefile #define BAUD 19200UL #define UBRR_BAUD ((F_CPU/(16UL*BAUD))-1) typedef union { // union erlaubt einen effektiven, separaten Zugriff auf Teile der Variable unsigned long i32; struct {uint8_t i8l; // low uint8_t i8m; // mid unsigned int high; // high, soft timer }; } convert32to8; volatile unsigned long timestamp; // volatile wegen Zugriff im Interrupt volatile unsigned int softtimer; volatile unsigned long zeitdifferenz; unsigned long zeit; char puffer[12]; // Puffer für Ausgabe als Ascii ISR(TIMER1_OVF_vect) // Timer1 Überlauf { ++softtimer; // zählen der Überläufe } ISR(TIMER1_CAPT_vect) // Flanke an ICP pin { convert32to8 cap; // Variablendeklaration cap.i8l = ICR1L; // low Byte zuerst, high Byte wird gepuffert cap.i8m = ICR1H; // overflow verpasst, wenn ICR1H klein und wartender Overflow Interrupt if ((cap.i8m < 128) && (TIFR1 & (1<<TOV1))) { // wartenden timer overflow Interrupt vorziehen ++softtimer; TIFR1 = (1<<TOV1); // timer overflow int. löschen, da schon hier ausgeführt } cap.high = softtimer; // obere 16 Bit aus Software Zähler zeitdifferenz = cap.i32 - timestamp; timestamp = cap.i32; // Zeit merken } void uart_init(void) // USART initialisieren (Mega48 etc.) { // Baudrate einstellen (Normaler Modus) // kann bei älteren AVR Typen etwas anders sein (kein UBRR0H, dafür prescaler) UBRR0H = (uint8_t) (UBRR_BAUD>>8); // bei Mega32 anders ! UBRR0L = (uint8_t) (UBRR_BAUD & 0x0ff); // bei Mega32 UBRRL UCSR0B = (1<<TXEN0); // Aktivieren des Senders, bei Mega32 UCSRB UCSR0C = (1<<UCSZ01)|(1<<UCSZ00)|(1<<USBS0); // bei Mega32 UCSRC // Einstellen des Datenformats: 8 Datenbits, 2 Stopbit: } void putser(char c) // sende ein Byte via UART { while ( !( UCSR0A & (1<<UDRE0)) ) ; // Warten bis der Sendepuffer frei ist UDR0 = c; } void main(void) { unsigned char i; // Datenrichtungen: DDRB = 0; // Alles Eingänge, PB0 ist ICP PORTB = 0xFF - (1<<PB0); // Pullups an Eingängen außer ICP DDRC = 0; // Eingänge PORTC = 0xFF; // Pullups an Eingängen DDRD = (1<<PD1); // Eingänge, außer PD1 = Tx (UART) PORTD = 0xFF- (1<<PD1); // Pullups an alle Eingängen (außer TX) // Timer1 initialisieren: TCCR1A = 0; // normal mode, keine PWM Ausgänge TCCR1B = (1<< ICNC1) + (1<<CS10) // start Timer mit Systemtakt + (1 << ICES1); // steigende Flanke auswählen TIMSK1 = (1<<TOIE1) + (1<<ICIE1); // overflow und Input-capture aktivieren, Mega32: TIMSK TIFR1 = (1<<TOIE1) + (1<<ICIE1); // Schon aktive Interrupts löschen, Mega32: TIFR // UART initialisieren: uart_init(); zeitdifferenz = 0; softtimer = 0; // wird für Zeitdifferenzmessung nicht mal gebraucht, // denn Differenz geht auch über Überlauf bei Softtimer set_sleep_mode (SLEEP_MODE_IDLE); // idle Mode: timer läuft weiter, int zum aufwachen sei(); // Interrupts erlauben: Messung startet while (1) { sleep_enable (); // Sleep Befehl freigeben sei(); sleep_cpu(); // wartet auf irgendeinen Interrupt, z.B. ICP, timer_ovr,... sleep_disable(); // Sleep Befehl sperren cli(); // Interrupt sperren wegen Zugriff auf volatile Variable zeit = zeitdifferenz; zeitdifferenz = 0; // als Markierung für ungültigen Wert sei(); if (zeit > 0) { ultoa(zeit,puffer,10); // nach ASCII umwandeln i = 0; while (puffer[i]) { putser(puffer[i++]); // Ausgabe } putser(13); putser(10); // Zeilenumbruch senden } } // Ende von While-schleife }
CTC Modus (Clear Timer on Compare Match mode)
Viele Timer haben "Output-Compare" Register: OCRx oder OCRAx,OCRBx. Wenn der Zähler den darin eingestellten Wert erreicht hat, kann ein Interrupts ausgelöst werden. Der CTC Modus ist eine Erweiterung des "Output-Compare"-Funktion. Der CTC Modus eignet sich besonders, um einen mit konstanter Frequenz wiederkehrenden Interrupt zu erzeugen. Wie im normalen Modus zählt der Timer hoch. Wenn der Wert im OCRx Register erreicht wird, wird zusätzlich zum möglichen Interrupt der Zähler wieder auf 0 gesetzt. Es kann also die maximalen Zählergrenze selber definiert werden. Dieses Diagramm veranschaulicht den CTC Modus.
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<<CS20) | (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); } }
PWM
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. Es muss nur die PWM aktiviert werden 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 ist erkenntbar, dass als Sonderfunktion in Klammern "OC..." angegeben ist. Beim Mega32 sind dies zb. OC0 an PB3, 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 dem 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, regeln 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 PWM-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. 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 */ // 1. Den Prescaler einstellen, der die Frequenz festlegt TCCR1B |= (1<<CS12); //Prescaler 256 // 2. Den Timer in den Fast PWM Mode, 8 Bit schalten // ACHTUNG: Die WGM-Bits sind auf beide Konfigurationsregister verteilt! TCCR1A |= (1<<WGM10); TCCR1B |= (1<<WGM12); // 3. 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; // 4. 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); } }
Es sind also vier Einstellungen zu treffen:
- Takt anlegen per Prescaler
- PWM-Modus wählen
- Ausgangs-Aktion festlegen
- Pins als Ausgänge schalten
Diese Schritte müssen bei jedem AVR-PWM-Kanal ausgeführt werden. Die genauen Registernamen und Werte können sich jedoch je nach Timer-Ausführung etwas unterscheiden.
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.
| ||||||||||||||||||
|