Do. Apr 25th, 2024
Beitrag befindet sich in Bearbeitung

1. Grundlagen der Timer-Programmierung

Ein Timer ist ein Funktionsbaustein eines Mikrocontrollers und kann verwendet werden, um Zeitereignisse zu messen. Vom Aufbau her ist ein Timer ist im Grunde nichts anderes als ein Register im Mikrocontroller, das hardwaregesteuert fortlaufend um 1 erhöht (oder verringert) wird. Dazu wird üblicherweise der Timer mit dem Systemtakt verbunden, um so die Genauigkeit des Quarzes auszunutzen, um ein regelmäßiges und vor allen Dingen vom restlichen Programmfluss unabhängiges Zählen zu ermöglichen.  

Um die verschiedenen Timer, die ein Microcontroller zur Verfügung stellt verwenden zu können, müssen bestimmte Eigenschaften konfiguriert werden. Dazu sind jedem Timer bestimmte Register zugeordnet. Zum Teil sind diese Timer spezifisch, es gibt aber auch Register, die von allen Timern gemeinsam genutzt werden. Im nachfolgender Abbildung sind diese Register am Beispiel des 16-Bit Timers1 dargestellt:

Übersicht aller dem Timer1 zugeordneten Register

Neben diesen spezifischen Registern die alle Timer haben, gibt es wie gemeinsam genutzte Register. Dabei handelt es sich zum einen um das sogenannte TIMSK-Register. Das TIMSK-Register definiert, bei welchen Ereignissen welche Art Interrupts ausgelöst werden. Das Register hat für den Timer1 den nachfolgend dargestellten Aufbau.

Dazu kommt noch ein weiteres Register das gemeinsam genutzt wird. Im TIFR- Register wird festgelegt, wie reagiert werden soll, wenn ein Zähler überläuft, bzw. sein Zielwert erreicht hat.

Sind die jeweiligen Interrupts aktiviert, können diese dann über die Interrupt Service Routinen (ISRs) benutzt werden. Dazu müssen die entsprechenden Interruptvektoren an die ISR übergeben werden:

  • TIMER1_CAPT_vect für Input Capture
  • TIMER1_COMPA_vect / TIMER1_COMPB_vect für Compare Match
  • TIMER1_OVF_vect für Timer Overflow

Das ist die Theorie der Timer Programmierung. Für unerfahrene Programmierer sicher alles andere als einfach nachzuvollziehen. Aus diesem Grund soll die Arbeit mit Timern nachfolgend Schritt für Schritt erläutert werden. Dazu ist es wichtig, zu wissen, wie die eben beschriebenen Register im Zusammenhang stehen.

2. Kontrollregister

Um die Timer Funktion zu spezifizieren, werden bestimmte Bits in den für die Timer-Konfiguration vorgesehenen Registern gesetzt. Die wesentlichen Register sind :

  • TCCRx – Timer/Counter Control Register
  • TCNTx – Timer/Counter Register
  • OCRx – Output Compare Register
  • ICRx – Input Capture Register (only for 16bit timer)
  • TIMSKx – Timer/Counter Interrupt Mask Register
  • TIFRx – Timer/Counter Interrupt Flag Register

Im folgenden werden diese Register erklärt und aufgezeigt wie deren innere Bit-Struktur aussieht. Um die Timer zu konfigurieren, müssen diese Register mit entsprechenden Werten gesetzt werden. Das mutet auf den ersten Blick vielleicht etwas verwirrend an, wird am Ende aber hoffentlich transparent.

2.1 Das Timer Control Register

In den Timer Control Registern werden die wesentlichen Einstellungen für das Timer-Verhalten vorgenommen. Dazu gehören u.a.:

  • Wahl des Wave Form Generation Modes über die WGMx Bits
  • Festlegung, was bei einem Compare Match passiert (COM xy Bits)
  • Prescaler bzw. external Clock über die Chip Select Bits CS1x

Im folgenden ist eine Übersicht über Anzahl und den Aufbau sämtlicher Timer Kontroll-Register der Timer des Arduino UNO dargestellt.

Übersicht der Kontrollregister der Timer0, Timer1 und Timer2

2.2 Das Counter Register TCNTx

Das Timer-Register zählt im Systemtakt oder verlangsamt über den gewählten Prescaler. Die untere Grenze wird als Bottom (Null) bezeichnet, die obere Grenze als Top. Top ist, je nach Modus, festgelegt oder kann variabel definiert werden. Für alle Timer gibt es je nach Größe ein oder zwei 8Bit Counter Register. Sie haben die Bezeichnungen: TCNT0, TCNT1H und TCNT1L sowie TCNT2.

2.3 Das Output Compare Register OCRx

In den beiden Output Compare Registern OCR1A und OCR1B kann man Werte definieren, die permanent mit dem Timer Daten Register TCNTx verglichen werden. Je nach Einstellung und Modus löst eine Übereinstimmung (Compare Match) bestimmte Aktionen aus. Die entsprechenden Aktionen werden über die Timer/Counter 1 Control und Status Register konfiguriert.

Der Compare Output Mode ist in alle Timer integriert. Die Bits COMx0 und COMx1 steuern das Verhalten des OCx-Pins (der Ausgabepin des CompareOutput Modes). Wenn beide Bits auf 0 gesetzt sind, behält der OCx-Pin seine Standardfunktion im Mikrocontroller.

Ist das COMx1 Register gesetzt wird der OCx-Pin bei einem Compare Match auf 0 gesetzt (bei PWM wird er bei Erreichen vom maximalen Wert wieder auf 1 gesetzt). Sind beide Register gesetzt ist diese Funktion invertiert (also bei jedem Compare Match wird OCx auf 1 gesetzt). 

Output-Compare-Pins

Jedem Timer sind im Timer-Kontroll-Register sogenannte Output-Compare-Pins zugeordnet. Dem Timer1 sind beispielsweise beim Arduino UNO die zwei Output Compare Pins OC1A (Pin 9) und OC1B (Pin 10) zugeordnet. Die Tabelle gibt eine Übersicht der Zuordnung für UNO und MEGA:

Übersicht der OutputCompare-Pins

2.4 Das Input Capture Register ICR1

Dieses Register hat nur der Timer1. Es hat zwei Funktionen: Bei einem Ereignis an ICP1 wird der Zählerstand von TCNT1 in ICR1 geschrieben. Das ICES1 Bit (Bit6 im TCCR1B Kontroll-Register) legt fest, ob dies bei steigender (ICES1 = 1) oder fallender Flanke (ICES1 = 0) passieren soll.

ICR1 dient, wie OCR1A, in einigen WGM1 Modi als Top Wert. In diesen Fällen ist die Input Capture Register Funktion deaktiviert. Im Gegensatz zu OCR1A ist ICR1 nicht gepuffert, sondern wird sofort überschrieben. Welche Folgen das hat, besprechen wir bei den PWM Modi. 

2.5 Das TIMSK Register

Dieses Register wird von allen Timern verwendet. Das TIMSK-Register definiert, bei welchen Ereignissen welche Art Interrupts ausgelöst werden. Das Register hat den nachfolgend dargestellten Aufbau. Die Bedeutung der einzelnen Bits wird unten beschrieben:

Struktur des TIMSK Register
  • OCIE2 (Timer/Counter2 Output Compare Match Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter2 Compare Match Interrupt aktiviert (vorrausgesetzt die Interrupts sind global aktiviert).
  • TOIE2 (Timer/Counter2 Overflow Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter2 Overflow Interrupt aktiviert.
  • TICIE1 (Timer/Counter1, Input Capture Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Input Capture Interrupt aktiviert.
  • OCIE1A (Timer/Counter1 Output Compare A Match Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Output Compare A Match Interrupt aktiviert .
  • OCIE1B (Timer/Counter1 Output Compare B Match Interrupt Enable)
    Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Output Compare B Match Interrupt aktiviert.
  • TOIE1 (Timer/Counter1 Overflow Interrupt Enable)
  • Wenn dieses Bit gesetzt wird, wird der Timer/Counter1 Overflow Interrupt aktiviert.
  • OCIE0 (Timer/Counter0 Output Compare Match Interrupt Enable)
    Wenn dieses Bit gesetzt wird, so wird der Timer/Counter0 Compare Match Interrupt aktiviert.
  • TOIE0 (Timer/Counter0 Overflow Interrupt Enable)
    Wenn dieses Bit gesetzt wird, so wird der Timer/Counter0 Overflow Interrupt aktiviert.

Sind die jeweiligen Interrupts aktiviert, können diese dann über die Interrupt Service Routinen (ISRs) benutzt werden. Dazu müssen die entsprechenden Interruptvektoren an die ISR übergeben werden:

  • TIMER1_CAPT_vect für Input Capture
  • TIMER1_COMPA_vect / TIMER1_COMPB_vect für Compare Match
  • TIMER1_OVF_vect für Timer Overflow

2.6 Das Timer Interrupt Flag Register TIFRx

Die Timer des ATMega16 können verschiedene Interrupts auslösen um auf verschiedene Timerereignisse zu reagieren. Um die Interrupts zu aktivieren, muss das das entsprechende Bit im TIMSK (Timer/Counter Interrupt Mask Register) gesetzt werden. Bei einem Interrupt Request wird das entsprechende Flag (Bit) im TIFR (Timer Interrupt Flag Register) gesetzt, welches dann den Interrupt auslöst. Man kann also auch durch Setzen des entsprechenden Bits im TIFR einen Interrupt „von Hand“ auslösen.

2.6.1 Output Compare Match Interrupt (OCI)

Erreicht das Register TCNTx den Wert, der im Register OCRx voreingestellt wurde, so tritt ein Timer Compare Match ein. Dabei wird ein Output Compare Interrupt ausgelöst. Auf Wunsch wird der Counter dabei zurückgesetzt. Dazu dient das CTC-Bit (Clear Timer on Compare) im TCCRx-Register.

2.6.2 Timer Overflow Interrupt (TOC)

Ist das Register TCNTx voll, so wird ein Timer/Counter Overflow und damit ein Timer/Counter Overflow Interrupt ausgelo ̈st. Der Timer zählt dabei von 0 aufwärts weiter. So kann periodisch eine Interruptroutine ausgeführt werden.

Bei PWM kann bei Bedarf der Timer Overflow Interrupt aktiviert werden um z.B. das OCRx Register zu ändern, wodurch man einen sinusförmigen Ausgang generieren kann.

2.6.3 Timer Input Capture Interrupt (TICI)

Der TICI wird bei einem Input Capture Event ausgelöst, welches eintritt, wenn am ICP1-Pin eine steigende/fallende Flanke (je nach Einstellung) gemessen wird. Es besteht die Möglichkeit einen Noice Canceler einzuschalten, der dafür sorgt, dass der TICI erst ausgelöst wird, wenn das Signal 4 Takte lang stabil anliegt. Dieser Modus kann genutzt werden um Ereignisse zu zählen oder auf sie zu reagieren.

3. Festlegung der Timer-Betriebsart

Timer können in verschiedenen Modi konfiguriert werden. Man unterscheidet dabei zwischen folgenden Modi:

  • Normal-Mode
    Der einfachste Betriebsmodus ist der normale Modus. 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. Der Timer kann so konfiguriert werden, dass beim Erreichen dieses Maximums der TIMERn_OVF_vect ausgelöst wird. Die Frequenz, mit der ein Overflow bei Verwendung des Prozessortakts als Taktquelle auftritt ergibt sich mit:

        \[f=\frac{Systemtakt}{Prescaler\cdot N}\]


    wobei N =256 bei 8Bit Zählern und N=65536 bei 16Bit-Zählern gesetzt wird.
  • CTC-Mode
    Hier zählt der Timer nach oben bis zum Erreichen des OCRn Registers. Das Register TCNTn wird beim Erreichen zurückgesetzt. Der Timer kann so konfiguriert werden, dass beim Erreichen des OCRn Wertes der TIMERn_COMPx_vect ausgelöst wird. Die Frequenz, mit der ein Compare Match bei Verwendung des Prozessortakts als Taktquelle auftritt ergibt sich mit:

        \[f=\frac{Systemtakt}{Prescaler\cdot OCR_n}\]

  • PWM-Mode
    Im PWM Modus werden die OCxy Ausgabe-Pins verwendet um PWM Signale zu erzeugen und auszugeben. Die jeweiligen Pins sind genau festgelegt und lassen sich aus der oben stehenden Übersicht entnehmen.
  • Fast-PWM-Mode
    Beim Fast PWM zählt der Timer bis zum Maximum seines Zählberreichs. Das Register OCRn dient als Vergleich und abhängig davon, ob TCNTn kleiner oder größer OCRn ist, kann der OCn Pin auf logisch 0 oder 1 gesetzt werden.

Um die beschriebenen Modi zu setzen sind im Timer-Kontroll-Register die WGMx-Bits vorgesehen. der verschiedenen Timer sogenannte Waveform Bits vorgesehen. Für den Timer2 sind das die Bits WGM20 und WGM21. Diese befinden sich im TCCR2A Register an der Position 0 und 1 siehe Abbildung:

Festlegen des Betriebsmodus „Normal“ im Kontroll-Register des Timers 2

Wenn man den Modus bei Timer0 und Timer1 konfigurieren möchte, würde man analog vorgehen, und die entsprechenden WGM0x- und WGM1x-Bits mit den WaveForm-Bit-Kombinationen belegen. Da wie oben erwähnt jeder Timer sein spezifischer zugeordnetes Kontrollregister besitzt.

4. Erstes Beispiel

Als Beispiel soll der Timer 2 so konfiguriert werden, dass eine LED Leuten soll. Der Timer2 soll dabei im Normal-Modus betreiben werden mit einem Prescaler von 1024. Dazu müssen die Parameter in den zughörigen Kontroll-Rister entsprechend gesetzt werden:

Als erstes wird Kontrollregister A des Timer 2, also TCCR2A auf 0 setzen, damit ist der Pin OC2A inaktiv und der Timer aktiviert:

Die für das Beispiel relevanten Kontroll-Register des Timers 2

Anschliessend werden im Kontrollregister TCCR2B alle CS2x Bits auf HIGH gesetzt (Gelb markiert). Der Prescaler ist damit auf 1024 gesetzt.

Da die WGM2x Bits standardmässig auf 0 gesetzt sind, braucht man hier keine weiteren Aktivitäten durchführen, um den Normal-Modus zu konfigurieren.

Als nächstes wird das Bit OIE2 für den Timer Overflow Interrupt gesetzt. Dadurch wird jedesmal, wenn das TCNT2-Register überläuft (> 255), ein Interrupt ausgelöst.

Nun fehlt noch die Festlegung des LED-Pins. Hierzu nutzen wir die Port-Manipulations-Anweisungen, um Pin 7 als Ausgang zu definieren.

Und abschliessend wird noch die Interrupt-Service Routine definiert, die den Timer2 Overflow Interrupt TIMER2_OVF_vect abfängt.

Das Programm ist dann trotz der vielen Vorüberlegungen alles in allem sehr Kompakt. Das liegt natürlich auch daran, dass die ganzen Bezeichnet bereits vordefiniert wurden, und man dadurch sehr effizient auf die Register und die Bits zugreifen kann:

void setup(){ 
  TCCR2A = 0x00;        // Set register TCCR2A from Timer2 to LOW; 
  TCCR2B = (1<<CS22) + (1<<CS21) + (1<<CS20); // prescaler = 1024
  TIMSK2 = (1<<TOIE2);  // if TCNT2 > 255, wird ein Interrupt ausgelöst
  DDRD |= (1<<PD7);     // Portmanipulation: replaces pinMode(7, OUTPUT); 
} 

void loop() { 
}

ISR(TIMER2_OVF_vect){
    PORTD ^= (1<<PD7); // toggle PD7
}

Nach dem Start des Programms erkennt man, dass die LED sehr schnell leuchtet. Nach der oben genannten Formel lässt sich die Frequenz auch leicht berechnen:

    \[f=\frac{Systemtakt}{1024\cdot 256} = 61,03 \,Hz\]

Wenn man einen kleinere Frequenz haben möchte, müsste man weitere Methoden zum verlangsamen, wie beispielsweise einen Scalefaktor, einführen.

4. Timer im CTC Modus

Im Modus Clear-Timer on Compare wird ein Vergleichswert in ein Compare-Register geschrieben. Für den Timer0 wird das Register OCR0 verwendet. Sobald der Timer0 diesen Wert erreicht (TOP-Wert) wird ein Flag gesetzt. Der Timer0 zählt also nicht mehr von 0 bis 255 (MAX-Wert), wie im Normalmodus, sondern kann irgendwo zwischen 0 und 255 zum Zählabbruch gezwungen werden, um dann wieder bei 0 anzufangen. Analog verhält es sich natürlich bei den anderen Timern. Mit dem Unterschied, dass beim 16Bit-Timer1 die Top- und Max-Werte entsprechend größer sind.

Eine zweite Variante im Normalmodus, die noch nicht angesprochen wurde, besteht darin, dass man den Timer nicht bei 0 sondern bei einem Wert k zwischen 0 und 255 starten lässt. Damit lassen sich ebenfalls die Zählzeiten verkürzen.

4. 1 Timer Interrupt Berechnung

Da die Timer wie oben beschrieben, immer an den Systemtakt gekoppelt sind, werden die Zählregister beim Arduino UNO und MEGA mit einer Takt-Frequenz von 16 Mhz erhöht. Dies bedeutet, dass die Incrementierung eines Registers T=\frac{1}{16\cdot 10^6}=62,5  ns benötigt. Dementsprechend würde ein 8 Bit Register nach 16 Microsekunden überlaufen und ein 16 Bit Register nach 4,9 ms, wie aus den nachfolgenden Rechnungen hervorgeht.

Register BreiteZeit bis Überlauf
8 Bit Timer Register256 * 62,5 ns = 16 µs
16 Bit Timer Register65536 * 62,5 ns = 4,90 ms
Maximale Laufzeiten der Zählregister

Für die meisten Anwendungen ist das natürlich zu schnell. Deshalb gibt es den so genannten Vorteiler oder auch Prescaler genannt, mit dem die Zeitbasis reduziert werden kann.

Ein Prescaler ist einfach ein binärer Teiler für die Taktfrequenz. Mögliche Werte liegen bei 8, 32, 64, 256 oder 1024.  Seine Aufgabe ist es, den Systemtakt um den angegebenen Faktor zu teilen. Steht der Vorteiler also auf 1024, so wird nur bei jedem 1024-ten Impuls vom Systemtakt das Timerregister um 1 erhöht. Entsprechend weniger häufig kommen dann natürlich die Overflows. Die allgemeine Formel für die Frequenz f mit der ein Zähler einen Interrupt auslöst lautet:

 

(1)   \begin{equation*} f=\frac{Systemtakt}{2^{TimerBit}\cdot Prescaler}\end{equation*}

In der folgenden Tabelle sind für den Timer1 (16Bit) und Timer 2 (8Bit) mögliche Prescaler-Werte und die daraus folgenden Zeiten für einen Register Überlauf zusammengestellt. Die Idee hinter der Berechnung ist folgende: Bei 16 MHz benötigt ein Zähl-Impuls t_T = 62,5 ns. Dementsprechend benötigt ein 8-Bit Register 256\cdot t_T Zeit für einen Überlauf und somit einen Interrupt.

Prescaler18642561024
8 Bit Reg16,00 µs128,00 µs1,024 ms4,096 ms16,38 ms
16 Bit Reg4,90 ms32,76 ms256,14 ms1,048 s4,19 s
Zeiten in Abhängigkeit vom prescale Faktor bis Überlauf

4.2 Setzen der Prescalerwerte

Die Prescaler Werte werden über die CSx-Bits im TCCRx Register definiert. Das x in obiger Tabelle steht wie immer für die Timer-Nummer. Für den Timer1 sind folgende Bits definiert: CS12, CS11 und CS10.

Um nun die Zählgeschwindigkeitfestzulegen, ist der jeweilige Pressale-Wert zu wählen und die entsprechende Bit-Kombination in dem Kontrollregister des Timers zu setzen. Ein entsprechender C-Code wie diese Bits konfiguriert werden ist nachfolgend dargestellt:

TCCR1B | = (0 << CS12) | (0 << CS11) | (1 >> CS10); //kein Prescale
TCCR1B | = (0 << CS12) | (1 << CS11) | (0 >> CS10); //Prescale auf 8
TCCR1B | = (0 << CS12) | (1 << CS11) | (1 >> CS10); //Prescale auf 64
TCCR1B | = (1 << CS12) | (0 << CS11) | (0 >> CS10); //Prescale auf 256
TCCR1B | = (1 << CS12) | (0 << CS11) | (1 >> CS10); //Prescale auf 1024

Weiteres Beispiel

Das von oben bekannte Beispiel mit dem Timer2, soll nun auf den 16Bit Timer 1 übertragen werden. Nun aber mit der Vorgabe, dass die LED mit einer vorgegebenen Taktzeit von 1,5s aufleuchten soll.

void setup(){ 
  DDRD |= (1<<PD7);             // Portmanipulation: replaces pinMode(7, OUTPUT);  
  TCCR1A = 0;			// clear ctrl register A
  TCCR1B = 0;			// clear ctrl register B
  TCCR1B |= (1 << WGM12);	// set bit for CTC mode
  TCCR1B |= (1 << CS12);	// set bit 2 of prescaler for 1024x
  TCCR1B |= (1 << CS10);	// set bit 0 of prescaler for 1024x
  OCR1A = 23437;		// set L & H bytes to 23437 (1.5 sec)
  TIMSK1 |= (1 << OCIE1A);	// enable interrupt on OCR1A
  TCNT1 = 0;			// reset counter to zero
} 

void loop() { 
}

ISR(TIMER2_OVF_vect){
    PORTD ^= (1<<PD7); // toggle PD7
}

Betrachtet man das Ergebnis des obigen Programms am Oszillographen, so erkennt man dass saubere Flanken in einer Frequenz von rund 30,6 Hz angezeigt werden.

Gemäß obiger Formel

 

(2)   \begin{equation*} f=\frac{Systemtakt}{2^{TimerBit}\cdot Prescaler} = \frac{16 000000}{2^8\cdot 1024} = 61.0352  Hz\end{equation*}

Möchte man nun genaue Frequenzen einstellen, benötigt man einen weiteren Skalierungsfaktor und einen Startwert, mit denen sich dann folgende Formel ergibt:

 

(3)   \begin{equation*} f_{wunsch} \cdot ScaleFaktor =\frac{Systemtakt}{(2^{TimerBit}-Startwert)\cdot Prescaler} \end{equation*}

Für eine Wunsch-Frequenz von 1 Hz nutzen wir den Umstand, dass das TCNTx Register auch mit einem Startwert beschrieben werden kann. Es werden dann nur noch (2^{TimerBit}-Startwert) Schritte bis zum Überlauf durchgeführt. Wie in der obigen Formel beschrieben, bleibt aber immer noch eine Gleichung mit mehreren Unbekannten:

 

(4)   \begin{equation*} f_w \cdot ScaleFaktor\cdot (2^{TimerBit}-Startwert)\cdot Prescaler=Systemtakt\end{equation*}

 

(5)   \begin{equation*} (2^{TimerBit}-Startwert)=\frac{Systemtakt}{f_w \cdot ScaleFaktor\cdot Prescaler}\end{equation*}

 

(6)   \begin{equation*}   Startwert = 2^{TimerBit} -\frac{Systemtakt}{f_w \cdot ScaleFaktor\cdot Prescaler}\end{equation*}

Wir kennen aber den Systemtakt, die möglichen Prescalewerte und das der Startwert ganzzahlig und kleiner 2^{TimerBit} in diesem Beispiel 256 sein muss. Mit nachfolgendem Zusammenhang und etwas rumprobieren erhält man folgende Ergebnisse:

Etwas systematischeres rumprobieren mit Mathemathematica

Aus der Tabelle lassen sich 3 gute Kombinationen entnehmen: Mit einem Prescaler von 256 finden sich die Startwerte 250 und 500. Bei einem Presclaewert von 64 würde nur der Startwert 1000 passen. Bei einem Prescaler von 1024 haben wir nur annähernd passende Werte, die man aufrunden müsste.

Mit f_{wunsch} :=1 und einem 8-Bit Timer ergibt sich dann

 

(7)   \begin{equation*}Startwert = 256 -\frac{16 000000}{500\cdot 256}=131\end{equation*}

Damit folgt folgendes überarbeitete Programm, dass die Diode mit genau 1 Hz zum leuchten bringt:

byte counterStart = 131;  
unsigned int scaleFactor = 500; 

void setup(){ 
  TCCR2A = 0x00; 
  TCCR2B = (1<<CS22) + (1<<CS21); // prescaler = 256
  TIMSK2 = (1<<TOIE2); // interrupt when TCNT2 is overflowed
  TCNT2 = counterStart;
  DDRD |= (1<<PD7);
} 

void loop() { 
}

ISR(TIMER2_OVF_vect){
  static int counter = 0;
  TCNT2 = counterStart;
  counter++;
  if(counter==scaleFactor){
    PORTD ^= (1<<PD7);
    counter = 0; 
  }
}
Programm-Sequenz um Timer zu programmieren
// Timmer aktivieren
TCCR0A |= (1 <<  WGM01);   // enable timer0 CTC mode
TIMSK0 |= (1 <<  OCIE0A);  // enable timer0 compare interrupt

TCCR1B |= (1  <<  WGM12);   // enable timer1 CTC mode
TIMSK1 |= (1  <<  OCIE1A);  // enable timer1 compare interrupt

TCCR2A |= (1  <<  WGM21);   // enable timer2 CTC mode
TIMSK2 |= (1  <<  OCIE2A);  // enable timer2 compare interrupt

// Timmer Rücksetzen
// reset a timer unit (replace X by timer number)
TCCRXA = 0;  // set TCCRXA register to 0
TCCRXB = 0;  // set TCCRXB register to 0
TCNTX  = 0;  // reset counter value

//Timer Vergleichswert setzen
OCR0A = 124;   // set compare match register of timer 0 (max. value: 255 = 2^8 - 1)
OCR1A = 20233; // set compare match register of timer 1 (max. value: 65535 = 2^16 - 1)
OCR2A = 20;    // set compare match register of timer 2 (max. value: 255 = 2^8 - 1)

//Prescaler Werte setzen 
//am Beispiel Timer1
TCCR1B | = (0 << CS12) | (0 << CS11) | (1 >> CS10); //kein Prescale
TCCR1B | = (0 << CS12) | (1 << CS11) | (0 >> CS10); //Prescale auf 8
TCCR1B | = (0 << CS12) | (1 << CS11) | (1 >> CS10); //Prescale auf 64
TCCR1B | = (1 << CS12) | (0 << CS11) | (0 >> CS10); //Prescale auf 256
TCCR1B | = (1 << CS12) | (0 << CS11) | (1 >> CS10); //Prescale auf 1024

TCCR0B |= (1  <<  CS00);  // no prescaling for timer0

TCCR2B |= (1  <<  CS22) | (1  <<  CS20);  //Prescale auf 1024 for timer2
void setup(){
//set pins as outputs
  pinMode(8, OUTPUT);
  pinMode(9, OUTPUT);
  pinMode(13, OUTPUT);

cli();//stop interrupts

//set timer0 interrupt at 2kHz
  TCCR0A = 0;// set entire TCCR2A register to 0
  TCCR0B = 0;// same for TCCR2B
  TCNT0  = 0;//initialize counter value to 0

// set compare match register for 2khz increments
  OCR0A = 124;// = (16*10^6) / (2000*64) - 1 (must be <256)
  // turn on CTC mode
  TCCR0A |= (1 << WGM01);
  // Set CS01 and CS00 bits for 64 prescaler
  TCCR0B |= (1 << CS01) | (1 << CS00);   
  // enable timer compare interrupt
  TIMSK0 |= (1 << OCIE0A);

//set timer1 interrupt at 1Hz
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = 15624;// = (16*10^6) / (1*1024) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS12 and CS10 bits for 1024 prescaler
  TCCR1B |= (1 << CS12) | (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);

//set timer2 interrupt at 8kHz
  TCCR2A = 0;// set entire TCCR2A register to 0
  TCCR2B = 0;// same for TCCR2B
  TCNT2  = 0;//initialize counter value to 0
  // set compare match register for 8khz increments
  OCR2A = 249;// = (16*10^6) / (8000*8) - 1 (must be <256)
  // turn on CTC mode
  TCCR2A |= (1 << WGM21);
  // Set CS21 bit for 8 prescaler
  TCCR2B |= (1 << CS21);   
  // enable timer compare interrupt
  TIMSK2 |= (1 << OCIE2A);


sei();//allow interrupts

}//end setup

// Interrupt Service Routine 

ISR(TIMER0_COMPA_vect){
//timer0 interrupt 2kHz toggles pin 8
}

ISR(TIMER1_COMPA_vect){
//timer1 interrupt 
}
  
ISR(TIMER2_COMPA_vect){
//timer1 interrupt 8kHz toggles pin 9
}


void loop(){
  //do other things here
}

Quellen

  1. AVR-GCC-Tutorial/Die Timer und Zähler des AVR https://www.mikrocontroller.net/articles/AVR-GCC-Tutorial/Die_Timer_und_Zähler_des_AVR
  2. Die Timer/Counter des AVR (Uni Regensburg)
  3. ATMega Datenblatt (deutsche Version)
  4. Timer Teil1 und Teil2 Wolles Elektronik Kiste https://wolles-elektronikkiste.de/tag/timer
  5. und die vielen übrigen Beiträge in Foren