AVR-GCC-Tutorial/Die Timer und Zähler des AVR

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Die heutigen Mikrocontroller und insbesondere die RISC-AVRs sind für viele Steuerungsaufgaben zu schnell. Wenn wir beispielsweise eine LED oder Lampe blinken lassen wollen, können wir selbstverständlich nicht die CPU-Frequenz verwenden, da ja dann nichts mehr vom Blinken zu bemerken wäre.

Wir brauchen also eine Möglichkeit, Vorgänge in Zeitabständen durchzuführen, die geringer als die Taktfrequenz des Controllers sind. Selbstverständlich sollte die resultierende Frequenz auch noch möglichst genau und stabil sein.

Hier kommen die im AVR vorhandenen Timer/Counter zum Einsatz.

Einleitung

Ein Timer ist ganz einfach ein bestimmtes Register im µC, das völlig ohne Zutun des Programms, also per Hardware, hochgezählt wird. Das alleine wäre noch nicht allzu nützlich, wenn nicht dieses Hardwareregister bei bestimmten Zählerständen einen Interrupt auslösen könnte.

Ein solches Ereignis ist der Overflow (Überlauf): Da die Bitbreite des Registers beschränkt ist, kommt es natürlich auch vor, dass der Zähler so hoch zählt, dass der nächste Zählerstand mit dieser Bitbreite nicht mehr darstellbar ist und der Zähler wieder auf 0 zurückgesetzt wird. Dieses Ereignis nennt man den Overflow und es ist möglich an dieses Ereignis einen Interrupt zu koppeln.

Ein anderes Anwendungsgebiet ist das Zählen von Impulsen, welche über einen I/O-Pin zugeführt werden.

Als Eingangstakt für die Timer/Counter kann entweder die CPU-Taktfrequenz, der Vorteiler-Ausgang oder ein an einen I/O-Pin angelegtes Signal verwendet werden. Wenn ein externes Signal verwendet wird, so darf dessen Frequenz nicht höher sein als die Hälfte des CPU-Taktes.

Die folgenden Ausführungen beziehen sich im Wesentlichen auf den AT90S2313. Für andere Modelltypen müsst ihr euch die allenfalls notwendigen Anpassungen aus den Datenblättern der entsprechenden Controller herauslesen.

Der Vorteiler (Prescaler)

Der Vorteiler dient dazu, den CPU-Takt vorerst um einen einstellbaren Faktor zu reduzieren. Die so geteilte Frequenz wird den Eingängen der Timer zugeführt.

Wenn wir mit einem CPU-Takt von 4 MHz arbeiten und den Vorteiler auf 1024 einstellen, wird der Timer mit einer Frequenz von 4 MHz / 1024, also mit etwas weniger als 4 kHz versorgt. Wenn der Timer läuft, wird das Zählregister (TCNTx) mit dieser Frequenz inkrementiert.

Timer-Bitzahlen verschiedener AVRs

Wir unterscheiden grundsätzlich zwischen 8-Bit Timern, welche eine Auflösung von 256 (2^8) aufweisen und 16-Bit Timern mit einer Auflösung von 65536 (2^16).

Alle AVR-Modelle verfügen über mindestens einen, teilweise sogar zwei, 8-Bit Timer.

AVR-Typ Timer/Counter0 Timer/Counter1 Timer/Counter2
AT90S2313 8 16 -
ATtiny2313 8 16 -
ATtiny25 8 8 -
ATmega8 8 16 8
ATmega88 8 16 8
ATmega16 8 16 8
ATmega32 8 16 8
ATmega644 8 16 8

8-Bit Timer/Counter

Der 8-Bit Timer wird z.B bei AT90S2313 über folgende Register angesprochen (bei anderen Typen weitestgehend analog):

TCCR0 Timer/Counter Control Register

Timer 0

In diesem Register stellen wir ein, wie wir den Timer/Counter verwenden möchten.

Das Register ist wie folgt aufgebaut:

Bit 7 6 5 4 3 2 1 0
Name - - - - - CS02 CS01 CS00
R/W R R R R R R/W R/W R/W
Initialwert 0 0 0 0 0 0 0 0

CS02, CS01, CS00 (Clock Select Bits)

Diese 3 Bits bestimmen die Quelle für den Timer/Counter:
CS02 CS01 CS00 Resultat
0 0 0 Stopp, Der Timer/Counter wird angehalten.
0 0 1 CPU-Takt
0 1 0 CPU-Takt / 8
0 1 1 CPU-Takt / 64
1 0 0 CPU-Takt / 256
1 0 1 CPU-Takt / 1024
1 1 0 Externer Pin TO, fallende Flanke
1 1 1 Externer Pin TO, steigende Flanke
Wenn als Quelle der externe Pin TO verwendet wird, so wird ein Flankenwechsel auch erkannt, wenn der Pin TO als Ausgang geschaltet ist.
TCNT0 Timer/Counter Daten Register Timer 0

Dieses ist als 8-Bit Aufwärtszähler mit Schreib- und Lesezugriff realisiert. Wenn der Zähler den Wert 255 erreicht hat, beginnt er beim nächsten Zyklus wieder bei 0.

Bit 7 6 5 4 3 2 1 0
Name MSB             LSB
R/W R/W R/W R/W R/W R/W R/W R/W R/W
Initialwert 0 0 0 0 0 0 0 0

Um nun also den Timer0 in Betrieb zu setzen und ihn mit einer Frequenz von 1/8-tel des CPU-Taktes zählen zu lassen, schreiben wir die folgende Befehlszeile:

    TCCR0 = (1<<CS01);

Der Zähler zählt nun aufwärts bis 255, um dann wieder bei 0 zu beginnen. Der aktuelle Zählerstand steht in TCNT0. Bei jedem Überlauf von 255 auf 0 wird das Timer Overflow Flag TOV0 im Timer Interrupt Flag TIFR-Register gesetzt und, falls so konfiguriert, ein entsprechender Timer-Overflow-Interrupt ausgelöst und die daran gebundene Interrupt-Service-Routine (ISR) abgearbeitet. Das TOV Flag lässt sich durch das Hineinschreiben einer 1 und nicht wie erwartet einer 0 wieder zurücksetzen.

Overflow Interrupt

Jedesmal wenn der Timer seinen höchsten Wert erreicht hat, erfolgt ein Overflow und der Timer beginnt wieder bei 0 zu zählen. An diesen Overflow kann eine Funktion, die sog. Overflow-ISR, gebunden werden. Damit hat man die Möglichkeit, bestimmte Funktionalitäten in regelmäßigen Zeitabständen ausführen zu lassen. Wie groß diese Zeitabstände sind, wird ausschließlich von der Zählfrequenz des Timers und der Bitbreite des Timers (also dem höchsten Wert, den der Timer erreichen kann) bestimmt. Die Zählfrequenz wiederrum hängt von Taktfrequenz des Controllers und dem eingestellten Vorteiler ab.

/* uC: AT90S2313 */
#include <avr/io.h>
#include <avr/interrupt.h>

int main(void)
{
  // Timer 0 konfigurieren
  TCCR0 = (1<<CS01); // Prescaler 8

  // Overflow Interrupt erlauben
  TIMSK |= (1<<TOIE0);

  // Global Interrupts aktivieren
  sei();

  while(1)
  {
    /* Sonstige Aktionen */
  }
}

/*
Der Overflow Interrupt Handler
wird aufgerufen, wenn TCNT0 von
255 auf 0 wechselt (256 Schritte),
d.h. ca. alle 2 ms
*/
#ifndef TIMER0_OVF_vect
// Für ältere WinAVR Versionen z.B. WinAVR-20071221 
#define TIMER0_OVF_vect TIMER0_OVF0_vect
#endif

ISR (TIMER0_OVF_vect)
{
  /* Interrupt Aktion alle
  (1000000/8)/256 Hz = 488,28125 Hz
  bzw.
  1/488,28125 s = 2,048 ms  
  */
}

Grundsätzlich kann man in einer ISR jeden beliebigen Code ausführen lassen. Allerdings sollte man sich an die Grundregeln der ISR Programmierung halten: Nur das tun was unbedingt notwendig ist. Eine ISR sollte (zeitlich gesehen) so kurz wie möglich aber so lang wie notwendig sein. Komplexe Ausgaben auf LCD oder gar auf die UART gehören nicht in eine ISR. Sie dauern einfach zu lange und blockieren so den Prozessor zu lange. Programme, die mehrere Dinge quasi gleichzeitig machen, werden so zu lange blockiert.

Die Taktfrequenz, mit der eine ISR aufgerufen wird, erscheint nach obigen Ausführungen recht starr, aber sie lässt sich feiner anpassen. Eine Möglichkeit bestünde darin, den Zählerstand des Registers TCNT0 nach einem Overflow auf einen Wert größer als 0 zu setzen. Somit blieben bis zum nächsten Overflow weniger Zähltakte, was die Frequenz erhöhen würde.

Modernere AVRs kennen allerdings einen eleganteren Weg zur Anpassung der Interrupt-Frequenz: den CTC Modus

CTC Clear Timer on Compare Match (Auto Reload)

Im CTC Modus des Timers ist es möglich, anstelle der durch die Hardware bedingten Obergrenze des Timers, einen anderen Wert zu benutzen, an dem der Timer einen Interrupt auslöst und wieder bei 0 zu zählen anfängt. Neben dem Aktivieren des CTC Modus genügt es dazu, einfach den gewünschten Endwert in ein spezielles Register, das OCR0A, zu laden. Und natürlich hat auch die ISR dann einen anderen Namen. Weitere Anmerkungen und Erläuterungen finden sich im CTC-Kapitel der 16-Bit Timer.

/* uC: Attiny2313 */
#include <avr/io.h>
#include <avr/interrupt.h>

//Variablen für die Zeit
volatile unsigned int millisekunden;
volatile unsigned int sekunde;
volatile unsigned int minute;
volatile unsigned int stunde;

int main(void)
{
  // Timer 0 konfigurieren
  TCCR0A = (1<<WGM01); // CTC Modus
  TCCR0B |= (1<<CS01); // Prescaler 8
  // ((1000000/8)/1000) = 125
  OCR0A = 125-1;

  // Compare Interrupt erlauben
  TIMSK |= (1<<OCIE0A);

  // Global Interrupts aktivieren
  sei();

  while(1)
  {
    /*Hier kann die aktuelle Zeit
      ausgeben werden*/
  }
}

/*
Der Compare Interrupt Handler 
wird aufgerufen, wenn 
TCNT0 = OCR0A = 125-1 
ist (125 Schritte), d.h. genau alle 1 ms
*/
ISR (TIMER0_COMPA_vect)
{
  millisekunden++;
  if(millisekunden == 1000)
  {
    sekunde++;
    millisekunden = 0;
    if(sekunde == 60)
    {
      minute++;
      sekunde = 0;
    }
    if(minute == 60)
    {
      stunde++;
      minute = 0;
    }
    if(stunde == 24)
    {
      stunde = 0;
    }
  }
}

Anm.: In diesem Beispiel ist es egal, ob TCCR0B absolut gesetzt wird (TCCR0B = (1<<CS01)) oder der Prescaler-Wert zum alten Inhalt hinzugefügt wird (TCCR0B |= (1<<CS01)). Die anderen Bits in TCCR0B sind nach dem Reset 0 und sollen auch 0 bleiben. Wenn man sicher nur bestimmte Bits ändern will, ist das ODER (bzw. UND mit dem Kehrwert) sicherer. Näheres dazu im Artikel Bitmanipulation.

16-Bit Timer/Counter

Viele AVR-Modelle besitzen außer den 8-Bit Timern auch 16-Bit Timer. Die 16-Bit Timer/Counter sind etwas komplexer aufgebaut als die 8-Bit Timer/Counter, bieten dafür aber auch viel mehr Möglichkeiten, als da sind:

  • Die PWM-Betriebsart zur Erzeugung eines pulsweitenmodulierten Ausgangssignals.
  • Vergleichswert-Überprüfung mit Erzeugung eines Ausgangssignals (Output Compare Match).
  • Einfangen eines Eingangssignals mit Speicherung des aktuellen Zählerwertes (Input Capturing), mit zuschaltbarer Rauschunterdrückung (Noise Filtering).

Folgende Register sind dem Timer/Counter 1 zugeordnet:

TCCR1A Timer/Counter Control Register A Timer 1

In diesem und dem folgenden Register stellen wir ein, wie wir den Timer/Counter verwenden möchten.
Das Register ist wie folgt aufgebaut:

Bit 7 6 5 4 3 2 1 0
Name COM1A1 COM1A0 - - - - PWM11 PWM10
R/W R/W R/W R R R R R/W R/W
Initialwert 0 0 0 0 0 0 0 0

COM1A1, COM1A0 (Compare Match Control Bits)

Diese 2 Bits bestimmen die Aktion, welche am Output-Pin OC1 ausgeführt werden soll, wenn der Wert des Datenregisters des Timer/Counter 1 den Wert des Vergleichsregisters erreicht, also ein so genannter Compare Match auftritt.
Der Pin OC1 (PB3 beim 2313) muss mit dem Datenrichtungsregister als Ausgang konfiguriert werden.
COM1A1 COM1A0 Resultat
0 0 Output-Pin OC1 wird nicht angesteuert.
0 1 Das Signal am Pin OC1 wird invertiert (Toggle).
1 0 Der Output Pin OC1 wird auf 0 gesetzt.
1 1 Der Output Pin OC1 wird auf 1 gesetzt.
In der PWM-Betriebsart haben diese Bits eine andere Funktion.
COM1A1 COM1A0 Resultat
0 0 Output-Pin OC1 wird nicht angesteuert.
0 1 Output-Pin OC1 wird nicht angesteuert.
1 0 Wird beim Hochzählen der Wert im Vergleichsregister erreicht, so wird der Pin OC1 auf 0 gesetzt.

Wird beim Herunterzählen der Wert im Vergleichsregister erreicht, so wird der Pin auf 1 gesetzt.

Man nennt dies nicht invertierende PWM.

1 1 Wird beim Hochzählen der Wert im Vergleichsregister erreicht, so wird der Pin OC1 auf 1 gesetzt.

Wird beim Herunterzählen der Wert im Vergleichsregister erreicht, so wird der Pin auf 0 gesetzt.

Man nennt dies invertierende PWM.

PWM11, PWM10 (PWM Mode Select Bits)

Mit diesen 2 Bits wird die PWM-Betriebsart des Timer/Counter 1 gesteuert.
  • PWM10 PWM11 wurden umbenannt in WGM10 und WGM11
PWM11 PWM10 Resultat
0 0 Die PWM-Betriebsart ist nicht aktiviert. Timer/Counter 1 arbeitet als normaler Timer bzw. Zähler.
0 1 8-Bit PWM Betriebsart aktivieren.
1 0 9-Bit PWM Betriebsart aktivieren.
1 1 10-Bit PWM Betriebsart aktivieren.
TCCR1B Timer/Counter Control Register B Timer 1
Bit 7 6 5 4 3 2 1 0
Name ICNC1 ICES1 - WGM13 WGM12 (CTC1) CS12 CS11 CS10
R/W R/W R/W R R R/W R/W R/W R/W
Initialwert 0 0 0 0 0 0 0 0

ICNC1 (Input Capture Noise Canceler (4 CKs) Timer/Counter 1

oder auf Deutsch Rauschunterdrückung des Eingangssignals.
Wenn dieses Bit gesetzt ist und mit dem Input Capture Signal gearbeitet wird so werden nach der Triggerung des Signals mit der entsprechenden Flanke (steigend oder fallend) am Input Capture Pin ICP jeweils 4 Messungen mit der CPU-Frequenz des Eingangssignals abgefragt. Nur dann, wenn alle 4 Messungen den gleichen Zustand aufweisen gilt das Signal als erkannt.

ICES1 (Input Capture Edge Select Timer/Counter 1)

Mit diesem Bit wird bestimmt, ob die steigende (ICES1=1) oder fallende (ICES1=0) Flanke zur Auswertung des Input Capture Signals an Pin ICP heran gezogen wird.

CTC1 (Clear Timer/Counter on Compare Match Timer/Counter 1)

Wenn dieses Bit gesetzt ist, so wird nach einer Übereinstimmung des Datenregisters TCNT1H/TCNT1L mit dem Vergleichswert in OCR1H/OCR1L das Datenregister TCNT1H/TCNT1L auf 0 gesetzt.
Da die Übereinstimmung im Takt nach dem Vergleich behandelt wird, ergibt sich je nach eingestelltem Vorteiler ein etwas anderes Zählverhalten:
Wenn der Vorteiler auf 1 gestellt, und C der voreingestellte Vergleichswert ist, dann nimmt das Datenregister, im CPU-Takt betrachtet, folgende Werte an:
... | C-2 | C-1 | C | 0 | 1 |...
Wenn der Vorteiler z. B. auf 8 eingestellt ist, dann nimmt das Datenregister folgende Werte an:
... | C-2, C-2, C-2, C-2, C-2, C-2, C-2, C-2 | C-1, C-1, C-1, C-1, C-1, C-1, C-1, C-1 | C, 0, 0, 0, 0, 0, 0, 0 |...
In der PWM-Betriebsart hat dieses Bit keine Funktion.

CS12, CS11, CS10 (Clock Select Bits)

Diese 3 Bits bestimmen die Quelle für den Timer/Counter:
CS12 CS11 CS10 Resultat
0 0 0 Stopp, Der Timer/Counter wird angehalten.
0 0 1 CPU-Takt
0 1 0 CPU-Takt / 8
0 1 1 CPU-Takt / 64
1 0 0 CPU-Takt / 256
1 0 1 CPU-Takt / 1024
1 1 0 Externer Pin T1, fallende Flanke
1 1 1 Externer Pin T1, steigende Flanke
Wenn als Quelle der externe Pin T1 verwendet wird, so wird ein Flankenwechsel auch erkannt, wenn der Pin T1 als Ausgang geschaltet ist.
TCNT1H
TCNT1L
Timer/Counter Daten Register Timer/Counter 1

Dieses ist als 16-Bit Aufwärtszähler mit Schreib- und Lesezugriff realisiert. Wenn der Zähler den Wert 65535 erreicht hat, beginnt er beim nächsten Zyklus wieder bei 0.

Bit 7 6 5 4 3 2 1 0
Name MSB               TCNT1H
Name               LSB TCNT1L
R/W R/W R/W R/W R/W R/W R/W R/W R/W  
Initialwert 0 0 0 0 0 0 0 0  

In der PWM-Betriebsart wird das Register als Auf/Ab-Zähler verwendet, d.h. der Wert steigt zuerst von 0, bis er den Überlauf von 65535 auf 0 erreicht hat. Dann zählt das Register rückwärts wiederum bis 0.

Zum Auslesen des Registers wird von der CPU ein internes TEMP-Register verwendet. Das gleiche Register wird auch verwendet, wenn auf OCR1 oder ICR1 zugegriffen wird.

Deshalb müssen vor dem Zugriff auf eines dieser Register alle Interrupts gesperrt werden, weil sonst die Möglichkeit des gleichzeitigen Zugriffs auf das Temporärregister gegeben ist, was natürlich zu fehlerhaftem Verhalten des Programms führt.. Zudem muss zuerst TCNT1L und erst danach TCNT1H ausgelesen werden.

Wenn in das Register geschrieben werden soll, müssen ebenfalls alle Interrrupts gesperrt werden. Dann muss zuerst das TCNT1H-Register und erst danach das TCNT1L-Register geschrieben werden, also genau die umgekehrte Reihenfolge wie beim Lesen des Registers.

OCR1H
OCR1L
Timer/Counter Output Compare Register Timer/Counter 1
Bit 7 6 5 4 3 2 1 0  
Name MSB               OCR1H
Name               LSB OCR1L
R/W R/W R/W R/W R/W R/W R/W R/W R/W  
Initialwert 0 0 0 0 0 0 0 0  

Der Wert im Output Compare Register wird ständig mit dem aktuellen Wert im Datenregister TCNT1H/TCNT1L verglichen. Stimmen die beiden Werte überein, so wird ein sogenannter Output Compare Match ausgelöst. Die entsprechenden Aktionen werden über die Timer/Counter 1 Control und Status Register eingestellt.

Zum Auslesen des Registers wird von der CPU ein internes TEMP-Register verwendet. Das gleiche Register wird auch verwendet, wenn auf TCNT1 oder ICR1 zugegriffen wird. Deshalb müssen vor dem Zugriff auf eines dieser Register alle Interrupts gesperrt werden, weil sonst die Möglichkeit des gleichzeitigen Zugriffs auf das Temporärregister gegeben ist, was natürlich zu fehlerhaftem Verhalten des Programms führt. Zudem muss zuerst OCR1L und erst danach OCR1H ausgelesen werden.

Wenn in das Register geschrieben werden soll, müssen ebenfalls alle Interrupts gesperrt werden. Dann muss zuerst das OCR1H-Register und erst danach das OCR1L-Register geschrieben werden, also genau die umgekehrte Reihenfolge wie beim Lesen des Registers.

ICR1H
ICR1L
Timer/Counter Input Capture Register Timer/Counter 1
Bit 7 6 5 4 3 2 1 0  
Name MSB               ICR1H
Name               LSB ICR1L
R/W R R R R R R R R  
Initialwert 0 0 0 0 0 0 0 0  

Das Input Capture Register ist ein 16-Bit Register. Bei einigen AVR Modellen kann es als TOP Value für bestimmte PWM Modes verwendet werden.

Wenn am Input Capture Pin ICP die gemäß Einstellungen im TCCR1B definierte Flanke erkannt wird, so wird der aktuelle Inhalt des Datenregisters TCNT1H/TCNT1L sofort in dieses Register kopiert und das Input Capture Flag ICF1 im Timer Interrupt Flag Register TIFR gesetzt.

Wie bereits oben erwähnt, müssen vor dem Zugriff auf dieses Register alle Interrupts gesperrt werden. Zudem müssen Low- und Highbyte des Registers in der richtigen Reihenfolge bearbeitet werden:

Lesen:
ICR1LICR1H

Bei Verwendung des Compilers wird der Zugriff in der korrekten Reihenfolge ausgeführt, wenn man das 16-Bit register ICR1 verwendet anstatt zwei Zugriffe auf 8-Bit Register.

Die PWM-Betriebsart

Wenn der Timer/Counter 1 in der PWM-Betriebsart betrieben wird, so bilden das Datenregister TCNT1H/TCNT1L und das Vergleichsregister OCR1H/OCR1L einen 8-, 9- oder 10-Bit, frei laufenden PWM-Modulator, welcher als PWM-Signal am OC1-Pin (PB3 beim 2313) abgegriffen werden kann. Das Datenregister TCNT1H/TCNT1L wird dabei als Auf-/Ab-Zähler betrieben, welcher von 0 an aufwärts zählt bis zur Obergrenze und danach wieder zurück auf 0. Die Obergrenze ergibt sich daraus, ob 8-, 9- oder 10-Bit PWM verwendet wird, und zwar gemäß folgender Tabelle:

Auflösung Obergrenze Frequenz
8 255 fTC1 / 510
9 511 fTC1 / 1022
10 1023 fTC1 / 2046

Wenn nun der Zählerwert im Datenregister den in OCR1H/OCR1L gespeicherten Wert erreicht, wird der Ausgabepin OC1 gesetzt bzw. gelöscht, je nach Einstellung von COM1A1 und COM1A0 im TCCR1A-Register.

Ich habe versucht, die entsprechenden Signale in der folgenden Grafik zusammenzufassen

PWM Theorie 3.gif PWM Theorie 4.gif

Vergleichswert-Überprüfung (Compare Match)

Hier wird in ein spezielles Vergleichswertregister (OCR1H/OCR1L) ein Wert eingeschrieben, welcher ständig mit dem aktuellen Zählerwert verglichen wird. Erreicht der Zähler den in diesem Register eingetragenen Wert, so kann ein Signal (0 oder 1) am Pin OC1 erzeugt und/oder ein Interrupt ausgelöst werden.

Zu erwähnen ist in dem Zusammenhang, dass das zur Compare-Einheit gehörende Interrupt-Flag erst beim auf die Übereinstimmung der Werte folgenden Timertakt gesetzt wird. Das ist v.a. deshalb wichtig, da es sonst bei OCRnx = 0 einen undefinierten Zustand gäbe.

Möchte man ein Compare-Ereignis 100 Takte nach dem Timerüberlauf auslösen, dann muss in das betreffende Compare-Register eine 99 geschrieben werden.

CTC-Betriebsart (Clear Timer on Compare Match)

Das sogenannte Compare Match-Ereignis kann auch dazu verwendet werden, um den Timer automatisch zurückzusetzen (d.h. das TCNT-Register wird zu Null gesetzt). Diese Betriebsart heißt "Clear Timer on Compare Match", also auf deutsch "Lösche Timer bei Vergleichsübereinstimmung".

Mit dieser Funktionalität ist es möglich, sehr präzise Taktsignale zu erzeugen, ohne dabei programmtechnisch eingreifen zu müssen. Diese Funktion ersetzt das bei anderen Controllern und Timern ohne Compare-Einheit erforderliche Timer Reload (also das Nachladen des Zählregisters mit "Überlaufwert minus gewünschte Taktzahl bis zum Überlauf", v.a. verbreitet bei 8051er-µCs).

Zur Erzeugung eines Taktes per Hardware muss lediglich eine der CTC-Betriebsarten ausgewählt werden und einer der OCnx-Pins so gesetzt werden, dass er bei Auftreten des Compare Match getoggelt wird (über die COM-Bits).

Die Frequenz des Taktes am entsprechenden Ausgang ist dann: [math]\displaystyle{ f_\text{OC} = \frac{f_\text{CPU}}{\text{Prescaler} \cdot \left( \text{OCRnx} + 1 \right)} }[/math]

Umgeformt gilt für OCRnx: [math]\displaystyle{ \text{OCRnx} = \frac{f_\text{CPU}}{\text{Prescaler} \cdot f_\text{OC}} - 1 }[/math]

Diese Betriebsart macht das Timer-Nachladen, das bei AVRs, die ja im Unterschied zu 8051-Derivaten keine Auto-Reload-Funktion haben, immer mit Ungenauigkeiten und programmtechnischen Klimmzügen verbunden ist, überflüssig. Ist das OCRnx einmal gesetzt, dann wird das Signal am Ausgang kontinuierlich ausgegeben, ohne dass die Anwendersoftware eingreifen muss (es sei denn, die Frequenz soll geändert werden).

Beim ATmega8 hat der 8-Bit-Timer 0 keine Compare-Einheit, so dass dort CTC und auch sonstige automatische Vergleichsoperationen nicht möglich sind. Bei Timer 1 und Timer 2 ist das jedoch möglich. Bei den neueren AVRs besitzen i.d.R. alle Timer eine oder mehrere Compare-Einheiten, so dass dort eine größere Flexibilität gegeben ist.

Im Unterschied zu den PWM-Betriebsarten wird die Registeraktualisierung bei CTC nicht automatisch synchronisiert. Schreibt man einen neuen Compare-Wert, dann wird dieser sofort übernommen, was zu Fehlfunktionen führen kann, wenn der neue Compare-Wert höher ist, als der aktuelle Stand von TCNTnx. In den PWM-Betriebsarten wird hingegen der TOP-Wert synchron bei Erreichen von TOP oder BOTTOM aktualisiert.

Einfangen eines Eingangssignals (Input Capturing)

Bei dieser Betriebsart wird an den Input Capturing Pin (ICP) des Controllers eine Signalquelle angeschlossen. Nun kann je nach Konfiguration entweder ein Signalwechsel von 0 nach 1 (steigende Flanke) oder von 1 nach 0 (fallende Flanke) erkannt werden und der zu diesem Zeitpunkt aktuelle Zählerstand in ein spezielles Register abgelegt werden. Gleichzeitig kann auch ein entsprechender Interrupt ausgelöst werden. Wenn die Signalquelle ein starkes Rauschen beinhaltet, kann die Rauschunterdrückung eingeschaltet werden. Dann wird beim Erkennen der konfigurierten Flanke über 4 Taktzyklen das Signal überwacht und nur dann, wenn alle 4 Messungen gleich sind, wird die entsprechende Aktion ausgelöst.

In Verbindung mit dem eingebauten Analogvergleicher, einem externen Operationsverstärker (als Integrator beschaltet) und einem externen Analogsignalschalter lässt sich ein präziser A/D-Wandler nach dem Zweiflankenverfahren, ggf. mit automatischer Nullpunktkorrektur aufbauen.

Timer2 im Asynchron Mode

Bei den (meisten) Atmegas lässt sich der Timer2 asynchron betreiben. Das bedeutet, daß der Timer unabhängig vom CPU-Takt mit einer eigenen Taktquelle betrieben werden kann. In Verbindung mit einem 32,768KHz-Quarz, auch Uhrenquarz genannt, lässt sich so auf einfache Weise eine RTC (Real Time Clock) integrieren, die eine externe RTC überflüssig macht.

Beim ATMega48...328 wird, wie beim ATMega8, der Uhrenquarz alternativ zum "normalen" Quarz an XTAL1 und XTAL2 angeschlossen. Diese Controller müssen dann mit dem internen RC-Oszillator als CPU-Takt betrieben werden. ATMEGA644 & Co. haben einen separaten Anschluss für den Uhrenquarz.

Für den Anschluss des Uhrenquarzes müssen keine Fuses verändert werden!

/* uC: ATMega48PA */
#include <avr/io.h>
#include <avr/interrupt.h>

//Variablen für die Zeit
volatile unsigned char sekunde;
volatile unsigned char minute;
volatile unsigned char stunde;

int main(void)
{
  // Timer 2 konfigurieren
  GTCCR |= (1 << TSM) | (1 << PSRASY);  //Timer anhalten, Prescaler Reset
  ASSR |= (1 << AS2);                   //Asynchron Mode einschalten
  TCCR2A = (1 << WGM21);                //CTC Modus
  TCCR2B |= (1 << CS22) | (1 << CS21);  //Prescaler 256
  // 32768 / 256 / 1 = 128                Intervall = 1s
  OCR2A = 128 - 1;
  TIMSK2 |= (1<<OCIE2A);                //Enable Compare Interrupt
  GTCCR &= ~(1 << TSM);                 //Timer starten
  sei();                                //Enable global Interrupts

  while(1)
  {
    /*Hier kann die aktuelle Zeit
      ausgeben werden*/
  }
}

/*
Der Compare Interrupt Handler 
wird aufgerufen, wenn 
TCNT2 = OCR2A = 128-1 
ist (128 Schritte), d.h. genau alle 1s
*/
ISR (TIMER2_COMPA_vect)
{
    TCCR2B = TCCR2B;              //Wird weiter unten im Text erklärt!
    sekunde++;
    if(sekunde == 60)
    {
      minute++;
      sekunde = 0;
    }
    if(minute == 60)
    {
      stunde++;
      minute = 0;
    }
    if(stunde == 24)
    {
      stunde = 0;
    }
    // Wird weiter unten im Text erklärt!
    while(ASSR & ((1<<TCN2UB) | (1<<OCR2AUB) | (1<<OCR2BUB) |
                  (1<<TCR2AUB) | (1<<TCR2BUB)));
}

Der Timer-Interrupt wird jetzt im Sekundentakt aufgerufen. Dieser Aufruf geschieht jedoch nicht nur im normalen Betrieb des Controllers, sondern auch im Sleep-Mode! Hierzu gibt es extra den Sleep-Mode Power-save. In diesem Mode wird praktisch der gesamte Controller bis auf den Timer2 mit seinem Quarz abgeschaltet.

Das Aufwecken geschieht automatisch mit jedem Interrupt des Timers. Der Stromverbrauch sinkt dabei auf bis zu 0,75µA.

Das "Schlafenlegen" muß dabei in der Mainloop erfolgen, damit der Controller nach der Abarbeitung des Interrupts wieder in den Sleep-Mode geschickt wird. Dies geschieht nicht automatisch!

Die üblichen 32-kHz-Quarze haben eine vergleichsweise lange Anschwingdauer. Je nach Quarz sind Zeiten von mehreren 100 ms bis zu einer Sekunde keine Seltenheit. Entsprechend dauert es nach dem ersten Einschalten des AS2-Bits einige Zeit, bis der Timer losläuft. U.a. aus diesem Grund wird in dem obigen Initialisierungsbeispiel der Timer mit PSRASY im GTCCR angehalten. Nach Löschen des TSM im selben Register wird das PSRASY mit dem nächsten Takt des Timers selbständig gelöscht und der Timer gestartet. Wobei ich es grundsätzlich als sinnvoll ansehe, einen Timer erst nach der Konfiguration definiert loslaufen zu lassen.

Wenn er die Minute in 20s schafft

Alle zum Timer2 gehörigen Register werden weiterhin synchron beschrieben. Allerdings nicht synchron zum CPU-Takt von üblicherweise 1MHz oder 8MHz, sondern synchron zum Takt des Timer2. Diese Taktung des Timers erfolgt jetzt mit 32,768KHz.

Wenn TCNT2 == OCR2A, wird das OCF2A-Flag im TIFR2-Register gesetzt und der Sleep-Mode verlassen.

Der Controller stellt jetzt fest, daß das OCF2A-Flag gesetzt ist und springt auf den COMP2A-Interrupt-Vektor und dann weiter in die ISR. Dabei werden die Rücksprungadresse auf den Stack gelegt und das OCF2A-Flag gelöscht. Das heisst, es wird genau genommen nicht gelöscht, sondern es wird ein internes Bit gesetzt, daß mit dem nächsten Takt in das TIFR2- Register geschrieben wird. Der nächste Takt ist aber der nächste Takt des 32,768KHz Timers. Erst nach diesem Takt wird das Flag tatsächlich gelöscht!

Läuft der Controller mit 8MHz, dauert es vom Auslösen des Interrupt und dem Vorbereiten des Löschens 8MHz / 32768Hz = 244 Takte, abzüglich der paar CPU-Takte, die bis jetzt vergangen sind, bis das Flag gelöscht ist. Schafft der Controller es aber, die ISR in dieser Zeit abzuarbeiten, kehrt er zurück, stellt fest, daß das OCF2A-Flag immer noch gesetzt ist und springt wieder in die ISR, um dieselbe Sekunde nochmal zu zählen. Wenn er das auch noch ein drittes Mal schafft, dauert die Minute nur noch 20 Sekunden.

Beim Betrieb mit 1MHz sind es nur 31 Takte, die werden fast schon durch den Interrupt als solches erreicht. Daher läuft die Uhr meistens mit 1MHz richtig und mit 8MHz zu schnell.

Um jetzt kein Delay in die ISR einzufügen, kann man einen Schreibvorgang auf ein Timer-Register ausführen und im ASSR-Register abfragen, ob dieser Schreibvorgang erfolgt ist. Wenn dies der Fall ist, sind auch alle anderen Timer-Register geschrieben. Vor allen Dingen ist das OCF2A-Flag gelöscht.

Dafür dienen diese beiden Zeilen im Beispiel oben:

   TCCR2B = TCCR2B;              //Dummy Write
   // Warten bis Write erfolgt ist
   while(ASSR & ((1<<TCN2UB) | (1<<OCR2AUB) | (1<<OCR2BUB) |
                 (1<<TCR2AUB) | (1<<TCR2BUB)));

Kalibrieren des internen Oszillators mit Timer2 als Zeitbasis

Bei den Atmega48..328 sowie Atmega8, auf den hier nicht weiter eingegangen wird, wird der Uhrenquarz an Stelle des "normalen" Quarzes eingesetzt.

Für Datenübertragung über UART steht daher nur der Takt des internen Oszillators zur Verfügung. Eine Übertragung bis zu 9600 Bit/s ist mit den Atmega48..328 weitestgehend möglich. Dies ist jedoch nicht ideal und immer mit einem Risiko behaftet. Eine einfache Möglichkeit, Datenübertragung sicher und auch erheblich schneller zu machen, ist die Kalibrierung des internen Oszillators auf eine Baudratenfrequenz. Damit sind auch 115K2 Bit/s problemlos möglich.

Der interne Oszillator der Atmega48..328 wird vom Hersteller bei Vcc = 3V und einer Temperatur von 25°C auf 8MHz kalibriert. Dazu wird ein entsprechender Wert in das OSCCAL-Register des Controllers geschrieben. Dieser Wert lässt sich u.a. mit dem AVRISPMKII auslesen. Die im Datenblatt angegebene Toleranz (+-10%) bezieht sich auf den gesamten zulässigen Temperatur- und Spannungsbereich des Controllers. Bei 3V/25° ist die Abweichung vom Sollwert <1%. Dieser Wert wird auch mit der hier vorgestellten Kalibrierung erreicht.

Zur Kalibrierung gibt der Timer2 ein Zeitintervall vor, in dem der Timer1 zählt. Bei richtiger Frequenz muß der Wert des TCNT1 annähernd dem errechneten Sollwert entsprechen. Ist der Wert grösser, läuft der Timer zu schnell, d.h. die Frequenz des Oszillators ist zu hoch, ist der Wert kleiner, ist die Oszillatorfrequenz zu niedrig. Entsprechend wird der Inhalt des OSCCAL-Registers dann inkrementiert oder dekrementiert.

Im Projekt ist die Sollfrequenz als F_CPU einzustellen. Bei gesetzter CKDIV8-Fuse sind dies 921600Hz, ohne CKDIV8 7372800Hz. In beiden Fällen wird der Oszillator tatsächlich auf 7,3728MHz kalibriert, da die niedrige Frequenz über einen Teiler generiert wird. Gemäss Datenblatt wird als Frequenzbereich für die Kalibrierung 7,3 - 8,1 Mhz empfohlen. Laut Atmel ist nur in diesem Bereich der Schreibzugriff auf das Flash und das EEPROM sicher. Das ist insbesondere dann von Bedeutung, wenn die Kalibrierung in einem und für einen Bootloader erfolgt.


/* uC: ATMega48PA */

#ifndef F_CPU
  #error F_CPU not defined.
#endif
#define REF_CLOCK 32768L		
#if F_CPU == 921600
  #define COUNT_PRE (1 << CS10)
  #define COUNT_PREDIV 1
#else
  #if F_CPU == 7372800
    #define COUNT_PRE (1 << CS11)
    #define COUNT_PREDIV 8
  #else
    #error "No Baudrate Frequency defined"
  #endif
#endif 
#define REF_VAL F_CPU / (REF_CLOCK / 256) / COUNT_PREDIV
#define REF_MIN REF_VAL - 50
#define REF_MAX REF_VAL + 10

GPIOR0 = OSCCAL;
unsigned char nTimeOut = 1;
//Timer anhalten, Prescaler Reset
GTCCR |= (1 << TSM) | (1 << PSRASY);
ASSR = (1 << AS2);
TCCR2B = (1 << CS20);
GTCCR &= ~(1 << TSM);//Timer starten
while(!(TIFR2 & (1 << TOV2)));
//Clear Interrupt Flags
TIFR2 = (1 << OCF2B) | (1 << OCF2A) | (1 << TOV2);
TCCR1B = COUNT_PRE;

while(nTimeOut)
{
  if(TIFR2 & (1 << TOV2))//Poll Timer2
  {
    //Stop Timer1, Prescaler Reset
    GTCCR = (1 << TSM) | (1 << PSRSYNC);//*
    TIFR2 = (1 << OCF2B) | (1 << OCF2A) | (1 << TOV2);
    unsigned int nTcnt1 = TCNT1;

    if(!(nTimeOut & (1 << 0)))
    {
      if((nTcnt1 >= REF_MIN) && (nTcnt1 <= REF_MAX)) break;
      else
      {
        if(nTcnt1 < REF_VAL) OSCCAL++;
        if(nTcnt1 > REF_VAL) OSCCAL--;
      }
      TCNT1 = 0;
    }
    else GTCCR = 0;//Start Timer1 *
    nTimeOut++;
  }
}
if(!nTimeOut)
{
  //Error, Kalibrierung abgebrochen
}
//Register wiederherstellen (Reset-Zustand)
ASSR = 0;
TCCR2B = 0;
TCCR1B = 0;			
TCNT2 = 0;
OSCCAL = GPIOR0;    //ggf. zurück setzen auf 8Mhz
GPIOR0 = 0;
// Clear Interrupt Flags
TIFR1 = (1 << ICF1) | (1 << OCF1B) | (1 << OCF1A) | (1 << TOV1);
//Start Timer1
GTCCR = 0;

Die Messung erfolgt bei beiden Frequenzen mit einem Takt von 1MHz bis 921,6KHz für Timer1. Entweder durch CLKDIV8 (F_Timer = F_CPU) oder durch Prescaler = 8 des Timers (F_Timer = F_CPU/8). Dadurch ergeben sich bei der Kalibrierung die selben Parameter für beide Frequenzen.

Wird F_CPU auf 7,3728Mhz kalibriert, wird Timer1 mit Prescaler = 8 getaktet. Dieser muß vor der Messung zurück gesetzt werden, da der 1. Timertakt sonst zufällig nach 1 bis 8 CPU-Takten auftritt. Für die Genauigkeit der Messung muß dieser aber immer nach 8 Takten auftreten. Ansonsten kann das soweit führen, daß der Sollwert gar nicht erreicht wird und die Kalibrierung mit Timeout abgebrochen wird.

Die Kalibrierung war erfolgreich, wenn nTimeOut > 0 ist.

Das Wiederherstellen der verwendeten Register in den Reset-Zustand ist nur bei einem Bootloader wichtig. In einer normalen Anwendung ist es Sache des Programmierers, die veränderten Register entsprechend zu berücksichtigen und beispielsweise die Initialisierung der beiden Timer für eine andere Verwendung im Programm mit fester Zuweisung (=) statt mit Veroderung (|=) vorzunehmen. GTCCR ist in jedem Fall auf 0 zu setzen, da sowohl Timer0 als auch Timer1 beim Verlassen der Kalibrierung "stehen". Auch die Interrupt-Flags von Timer1 sollten gelöscht werden.

Die Kalibrierung verwendet keine Interrupts sondern pollt nur das TOV2-Flag des Timer2. Die Flags des Timer2 werden in der Kalibrierungsschleife direkt nach der Abfrage gelöscht.

Sollte die Kalibrierung in einem Bootlaoder erfolgen, kann man den ermittelten OSCCAL-Wert, sofern der ursprüngliche Wert (1 oder 8 MHz)wiederhergestellt wurde, in einem der GPIORx-Register an die Anwendung übergeben. Dann braucht diese eine evtl. notwendige Kalibrierung nur durchführen, wenn sie ohne Bootloader gestartet wurde. Ansonsten kann der Wert aus GPIORx ins OSCCAL geschrieben werden. Die GPIORx-Register werden (bislang) vom GCC nicht angetastet.

High-Speed-Timer

Einige AVRs enthalten einen 8- oder 10-Bit-High-Speed-Timer, der von einer besonderen internen Taktquelle (RC-Oszillator) mit 64 MHz (bei 5 V Betriebsspannung) bzw. 32 MHz (bei 3 V Betriebsspannung) getaktet werden kann. Im Datenblatt PCK genannt.

Sein Hauptanwendungsgebiet ist die (BLDC-)Motorsteuerung mit Halbbrücken-Endstufen, daher ist ein Dead-Time-Generator (Totzeit-Generator) nachgeschaltet, der aus einem PWM-Ausgang zwei gegenphasige mit einstellbarer Totzeit macht. Die Totzeit dient zur sicheren Vermeidung eines Brückenkurzschlusses im Umschaltmoment. Leider lässt sich die Gegenphasigkeit der Ausgänge nicht ändern.

Außerdem lässt sich damit eine qualitativ akzeptable (10-Bit-)Tonausgabe realisieren; die Halbbrücke kann verlustleistungsarm direkt einen Lautsprecher treiben: Bei 64 MHz und 10 Bit liegt die Trägerfrequenz bei 64 kHz und ist weitab unhörbar sowie leicht mit Drosseln filterbar. 10 Bit sind für Sprachausgabe, synthetische Klänge und Geräusche aus einem Samplespeicher gut genug und bietet Reserve für eine Laustärke-Einstellung von 8-Bit-Samples. Zum Vergleich: ISDN-Telefonqualität entsprechen 13 Bit, per A-Law (USA+Japan: µ-Law) aus 8 Bit exponentiell expandiert. Das ist auch das übliche für GSM- und Internet-Telefonie. Echte Musik wird man mit einem 8-Bit-AVR kaum wiedergeben wollen.

Ein weiteres Anwendungsgebiet sind Schaltregler mit hoher Arbeitsfrequenz für kleine Drosseln. Gleichphasigkeit mehrerer Steller bietet bessere Kontrollierbarkeit von gegenseitigen Störungen. Durch Begrenzung des Zählumfangs auf TOP=63 sind 1 MHz kein Problem, was den Abschied vom MC34063 erleichtert.

Folgende AVRs enthalten einen solchen Timer:

  • ATtiny25, ATtiny45, ATtiny85: 8-Bit-Timer 1, 2 PWM-Ausgänge
  • ATtiny261, ATtiny461, ATtiny861: 10-Bit-Timer 1, 3 PWM-Ausgänge, dazu 3 Analogvergleicher-Eingänge (typ. zur Schnellabschaltung bei Überstrom eines Brückenzweigs einer dreiphasigen BLDC-Steuerung zu verwenden)
  • ATmega16U4, ATmega32U4 (Arduino Leonardo): 10-Bit-Timer 4, 3 PWM-Ausgänge
  • (Liste möglicherweise unvollständig, aber es fällt schwer weitere zu finden)

Gemeinsame Register

Verschiedene Register beinhalten Zustände und Einstellungen, welche sowohl für den 8-Bit, als auch für den 16-Bit Timer/Counter in ein und demselben Register zu finden sind.

Dies gilt für viele ATTiny und die älteren ATMega, wie z.B. Atmega8. Die neueren ATMegas und einige ATTiny haben bei einigen mit Ausnahme des GIMSK sowie dem korrespondierenden GIFR exklusive Register für jeden Timer.

TIMSK Timer/Counter Interrupt Mask

Register

Bit 7 6 5 4 3 2 1 0
Name TOIE1 OCIE1A - - TICIE - TOIE0 -
R/W R/W R/W R R R/W R R/W R
Initialwert 0 0 0 0 0 0 0 0

TOIE1 (Timer/Counter Overflow Interrupt Enable Timer/Counter 1)

Wenn dieses Bit gesetzt ist, wird bei einem Überlauf des Datenregisters des Timer/Counter 1 ein Timer Overflow 1 Interrupt ausgelöst. Das Global Enable Interrupt Flag muss selbstverständlich auch gesetzt sein.

OCIE1A (Output Compare Match Interrupt Enable Timer/Counter 1)

Beim Timer/Counter 1 kann zusätzlich zum Überlauf ein Vergleichswert definiert werden.
Wenn dieses Bit gesetzt ist, wird beim Erreichen des Vergleichswertes ein Compare Match Interrupt ausgelöst. Das Global Enable Interrupt Flag muss selbstverständlich auch gesetzt sein.

TICIE (Timer/Counter Input Capture Interrupt Enable)

Wenn dieses Bit gesetzt ist, wird ein Capture Event Interrupt ausgelöst, wenn ein entsprechendes Signalereignis am Pin PD6(ICP) auftritt. Das Global Enable Interrupt Flag muss selbstverständlich auch gesetzt sein, wenn auch ein entsprechender Interrupt ausgelöst werden soll.

TOIE0 (Timer/Counter Overflow Interrupt Enable Timer/Counter 0)

Wenn dieses Bit gesetzt ist, wird bei einem Überlauf des Datenregisters des Timer/Counter 0 ein Timer Overflow 0 Interrupt ausgelöst. Das Global Enable Interrupt Flag muss selbstverständlich auch gesetzt sein.
TIFR Timer/Counter Interrupt Flag Register
Bit 7 6 5 4 3 2 1 0
Name TOV1 OCF1A - - ICF1 - TOV0 -
R/W R/W R/W R R R/W R R/W R
Initialwert 0 0 0 0 0 0 0 0

TOV1 (Timer/Counter Overflow Flag Timer/Counter 1)

Dieses Bit wird vom Controller gesetzt, wenn beim Timer 1 ein Überlauf des Datenregisters stattfindet.
In der PWM-Betriebsart wird das Bit gesetzt, wenn die Zählrichtung von auf- zu abwärts und umgekehrt geändert wird (Zählerwert = 0).
Das Flag wird automatisch gelöscht, wenn der zugehörige Interrupt-Vektor aufgerufen wird. Es kann jedoch auch gelöscht werden, indem eine logische 1 (!) in das entsprechende Bit geschrieben wird.

OCF1A (Output Compare Flag Timer/Counter 1)

Dieses Bit wird gesetzt, wenn der aktuelle Wert des Datenregisters von Timer/Counter 1 mit demjenigen im Vergleichsregister OCR1 übereinstimmt.
Das Flag wird automatisch gelöscht, wenn der zugehörige Interrupt-Vektor aufgerufen wird. Es kann jedoch auch gelöscht werden, indem eine logische 1 (!) in das entsprechende Bit geschrieben wird.

ICF1 (Input Capture Flag Timer/Counter 1)

Dieses Bit wird gesetzt, wenn ein Capture-Ereignis aufgetreten ist, welches anzeigt, dass der Wert des Datenregisters des Timer/Counter 1 in das Input Capture Register ICR1 übertragen wurde.
Das Flag wird automatisch gelöscht, wenn der zugehörige Interrupt-Vektor aufgerufen wird. Es kann jedoch auch gelöscht werden, indem eine logische 1 (!) in das entsprechende Bit geschrieben wird.

TOV0 (Timer/Counter Overflow Flag Timer/Counter 0)

Dieses Bit wird vom Controller gesetzt, wenn beim Timer 0 ein Überlauf des Datenregisters stattfindet.
Das Flag wird automatisch gelöscht, wenn der zugehörige Interrupt-Vektor aufgerufen wird. Es kann jedoch auch gelöscht werden, indem eine logische 1 (!) in das entsprechende Bit geschrieben wird.