Zum Inhalt springen

Timer und Counter

Aus Wikibooks
WTFPL-2
Hinweis: Wenn du diese Seite bearbeitest, stimmst du zu, dass deine Bearbeitungen zusätzlich unter den Bedingungen der WTF Public License veröffentlicht werden.
WTFPL-2


Worum geht's?

[Bearbeiten]

Mit der Funktionseinheit Timer und Controller verfügen AVR Mikrocontroller über eine Komponente, die vielseitig verwendet werden kann.

Als einfache Zähler eingesetzt können sie für die Zeitmessung verwendet werden. Über für diesen Zweck vorgesehene Ausgabepins können sie für die Erzeugung elektrischer Signale herangezogen werden. Schließlich können Timer und Controller auch so konfiguriert werden, dass sie periodisch Interrupts auslösen und so für die Ausführung nebenläufiger Tätigkeiten herangezogen werden.

Bei einem so großen Leistungsspektrum ist es kein Wunder, dass die Beschreibung von Timern einen beinahe einschüchternden Umfang von Seiten im Datenblatt verschlingt. Ziel dieses Kapitels ist es, Dir zu helfen Dich in diesem dichten Papierdschungel ein wenig schneller zurecht zu finden. Ein Ersatz für die Lektüre des Datenblatts ist es nicht.

In diesem Kapitel kannst die folgenden Dinge lernen:

  • Ein/Ausgabepins von Timern lokalisieren
  • Zählerstände von Timern auslesen und vorgeben
  • Die Betriebsart von Timern vorgeben
  • Zählintervalle und Taktquellen von Timern konfigurieren
  • Das Verhalten der Ausgabepins von Timern festlegen
  • Timer für die Erzeugung periodischer Interrupts verwenden

Der Streifzug in die Welt der Timer und Counter in diesem Kapitel orientiert sich am Funktionsumfang eines AtMega328p Microcontrollers. Angaben zu Seitenzahl und Sektion beziehen sich auf das zugehörige Datenblatt [M328p].

Anzahl und Funktionsumfang verfügbarer Timer können bei anderen AVR Modellen abweichen. In diesem Kapitel beschriebene Grundprinzipien und Funktionsweisen sollten aber übertragbar sein. Unterschiede in den Details müssen in diesem Fall mit dem Datenblatt abgeglichen werden.

Grundlagen

[Bearbeiten]

Der AtMega328p verfügt über 3 Timer/Counter. Die Timer haben die Namen TC0 TC1 und TC2.

Jeder der Timer kann in einer Reihe verschiedener Betriebsarten verwendet werden. Nach einem Reset befinden sich alle Timer in einer Betriebsart, die im Handbuch Normal Mode genannt wird.

In diesem Abschnitt wird es ausschließlich um diese Betriebsart gehen. Eigenschaften und Konzepte, die Du beim Studium dieser Betriebsart in diesem Abschnitt kennen lernst, werden in folgenden Abschnitten auf andere Betriebsarten übertragen und verallgemeinert werden.

Stromsparmodus

[Bearbeiten]

Nach einem Reset befinden sich alle Timer im Stromsparmodus und sind daher deaktiviert.

Bevor es daran gehen kann, mit einem der Timer zu arbeiten, muss er also zunächst erst einmal aktiviert werden. Um die Timer TC0, TC1 bzw. TC2 zu aktivieren, muss das zugehörige Bit PRTIM0, PRTIM1 bzw. PRTIM2 im PRR (power reduction) Register auf den Wert 0 geschrieben werden.[1]

Achtung! Die Information im Handbuch [M328p] ist leider widersprüchlich formuliert:

Zu Timer TC0 heißt es: [2]

The TC0 is enabled by writing the PRTIM0 bit in ”Minimizing Power Consumption” to '0'.
The TC0 is enabled when the PRTIM0 bit in the Power Reduction Register (PRR.PRTIM0) is written to '1'.

Die erste Angabe ist die richtige.

Zu Timer TC1 heißt es: [3]

The Power Reduction TC1 bit in the Power Reduction Register (PRRPRR.PRTIM1) must be written to zero to enable the TC1 module.

Die Abkürzung für das Power Reduction Register ist PRR nicht PRRPRR, ansonsten ist die Angabe korrekt.

Zu Timer TC2 heißt es: [4]

The TC2 is enabled when the PRTIM2 bit in the Power Reduction Register (PRR.PRTIM2) is written to '1'.

Diese Angabe ist leider falsch. Auch hier muss der Wert 0 in das PRTIM2 Bit geschrieben werden.

Das zugehörige Bit PRTIMn muss also stets auf den Wert 0 geschrieben werden.

Im C Quellcode kann das Aktivieren und Deaktivieren der einzelnen Timer wie folgt implementiert werden.

void t0_power(uint8_t on) {
  if (on) cbi(PRR, PRTIM0);
  else    sbi(PRR, PRTIM0);
}
void t1_power(uint8_t on) {
  if (on) cbi(PRR, PRTIM1);
  else    sbi(PRR, PRTIM1);
}
void t2_power(uint8_t on) {
  if (on) cbi(PRR, PRTIM2);
  else    sbi(PRR, PRTIM2);
}

Zählregister

[Bearbeiten]

Jeder Timer verfügt über einen Zähler, der von einer Taktquelle gespeist wird. Bei jedem Eintreffen eines Taktsignals schreitet der Zählerstand um einen Schritt voran.

Die Zählrichtung (aufwärts oder abwärts) und der maximale Stand eines Zählers sind dabei von der gewählten Betriebsart abhängig.

Im Normal Mode, mit dem wir uns in diesem Abschnitt befassen, ist die Zählrichtung immer aufwärts. Bei jedem eintreffen eines Taktsignals wird der aktuelle Zählerstand um eins erhöht. Für die Timer TC0 und TC2 liegt der maximale Zählerstand bei 0xFF, für den Timer TC1 beträgt der maximale Zählerstand 0xFFFF. Nach Erreichen des maximalen Zählerstands wird die Zählung beim Eintreffen des nächsten Taktsignals mit Zählerstand 0 fortgesetzt.

Der aktuelle Stand eines Zählers kann über zugehörige Zählregister sowohl gelesen, als auch geschrieben werden. Die Timer TC0 und TC2 verfügen je über einem 8-bit Zähler, dessen Stand über das Register TCNT0, bzw. TCNT2 manipuliert werden kann. Der Timer TC1 verfügt über einen 16-bit Zähler. Zugriff auf den Zählerstand des Timers TC1 ist über die Register TCNT1L und TCNT1H möglich, über die hochwertiges und niederwertiges Byte manipuliert werden können.

Achtung! Beim Zugriff auf 16-bit Register ist eine feste Reihenfolge einzuhalten:[5]

  • Beim Schreiben muss das high Byte vor dem low Byte geschrieben werden.
  • Beim Lesen muss das low Byte vor dem high Byte gelesen werden.

Abschließend sind die Eigenschaften der Zählregister in folgender Tabelle wiedergegeben.

Timer Bitlänge Zählregister max
TC0 8-bit TCNT0 0xFF
TC1 16-bit TCNT1H TCNT1L 0xFFFF
TC2 8-bit TCNT2 0xFF

Taktquelle

[Bearbeiten]

Das Tempo, mit dem der Stand eines Zählers voranschreitet, wird durch eine Taktquelle vorgegeben, mit der der zugehörige Timer versorgt wird. Nach einem Reset ist keiner der Timer mit einer Taktquelle verbunden. Bevor es mit dem Zählen los gehen kann, muss der betreffende Timer also zunächst mit einer Taktquelle versorgt werden.

Die Timer TC0 und TC1 können sowohl von einer externen Taktquelle, als auch von einer internen Taktquelle mit einem Takt versorgt werden. Der Timer TC2 kann nur von einer internen Taktquelle versorgt werden. In diesem Abschnitt wird es ausschließlich um die intern zur Verfügung stehenden Taktquellen gehen. Die genaue Lage der Pins, mit denen die Timer TC0 und TC1 extern versorgt werden können, werden wir uns zu einem späteren Zeitpunkt ansehen.

Alle internen Taktquellen werden von zwei prescalern bereit gestellt. Über sie können fest vorgegebene Bruchteile des Systemtakts abgegriffen werden. Ein prescaler Wert von N gibt an, dass der prescaler nur für jeden N-ten CPU Takt ein eigenes Taktsignal erzeugt. Die Timer TC0 und TC1 teilen sich den selben prescaler, an dem sie unabhängig von einander Taktwerte abgreifen können. Der Timer TC2 verfügt über einen eigenen prescaler.

Die Taktquellen der Timer können mit einem 3-bit Wert vorgegeben werden. Die Taktquelle des Timers TCn kann mit den Bits CSn[0:2] (clock source) im zugehörigen TCCRnB (timer counter control register B) Register festgelegt werden.

Über zur Verfügung stehende Wahlmöglichkeiten gibt folgende Tabelle Auskunft. .[6]

CSn[0:2] TC0 / TC1 TC2
0 000 deaktiviert deaktiviert
1 001 clk / 1 clk / 1
2 010 clk / 8 clk / 8
3 011 clk / 64 clk / 32
4 100 clk / 256 clk / 64
5 101 clk / 1024 clk / 128
6 110 fallende Flanke an TO/T1 clk / 256
7 111 steigende Flanke an TO / T1 clk / 1024

Die Wahl der Taktquelle kann im C Quellcode mit folgendem Code Schnipsel implementiert werden.

void t0_cs(uint8_t cs) {
  uint8_t tccr_b = TCCR0B & 0b11111000;
  TCCR0B = tccr_b | cs;
}
void t1_cs(uint8_t cs) {
  uint8_t tccr_b = TCCR1B & 0b11111000;
  TCCR1B = tccr_b | cs;
}
void t2_cs(uint8_t cs) {
  uint8_t tccr_b = TCCR2B & 0b11111000;
  TCCR2B = tccr_b | cs;
}

Codebeispiel

[Bearbeiten]

Mit Aktivierung eines Timers und Vorgabe der zugehörigen Taktquelle haben wir bereits alle Schritte durchgeführt, die für den Betrieb eines Timers zwingend erforderlich sind. Davon, dass wir einen Timer mit diesen Schritten tatsächlich in Gang setzen können, können wir uns mit einem kurzen Programm überzeugen. Für die Ausgabe machen wir dabei Gebrauch von der im Kapitel Tipps Tricks und kleine Helferlein vorgestellten Möglichkeit Ausgaben über die serielle Schnittstelle zu versenden und am PC zu empfangen.

void setup() {
  stdout = log_init();

  // TIMER 0
  t0_power(1);  // power on
  t0_cs(1);     // clock source: clk/1
}

void loop() {
  printf("%d ", TCNT0);
  _delay_ms(500);   // delay ~ 500 ms
}

Nachdem Du das Programm übersetzt und auf den Microcontroller übertragen hast, sollte der Microcontroller über die serielle Schnittstelle nun kontinuierlich neue Zahlen ausgeben:

8 204 91 95 104 232 109 237 109 242 114 247 119 247 124 252 129 1 197 84 88 97 101 229 106 234 111

Glückwunsch! Du hast den Timer 0 erfolgreich zum laufen gebracht.

Noch ist das Programm zwar nicht sehr nützlich. Aber mit ihm hast Du bereits den wichtigsten Grundstein für die Erstellung umfangreicherer Programme mit Timern gelegt. Die Schritte, die Du bis hier hin durchgeführt hast, müssen in jedem Programm, das mit Timern arbeitet, ausgeführt werden.

In den folgenden Abschnitten wirst Du weitere Möglichkeiten kennen lernen, Timer zu nutzen und mit ihrer Hilfe komplexere Programme zu erstellen.

Timer und Interrupts

[Bearbeiten]

Überlauf

[Bearbeiten]

Wie Du bereits erfahren hast, setzt ein Zähler, der seinen maximalen Zählerstand erreicht hat, die Zählung bei erneutem Eintreffen eines Taktsignal bei 0 fort. Ein weiteres Detail ist dabei bisher außer Acht geblieben. Zusätzlich zur bekannten Arbeitsweise wird bei einem solchen Überlauf des Zählers ein Statusflag gesetzt.

Der aktuelle Zustand des Overflow Flags des Timers TC0 kann über das Bit TOV0 (timer overflow) im Register TIFR0 (timer interrupt flag) ausgelesen werden. In analoger Art und Weise sind die Bits TOV1 und TOV2 in den Registern TIFR1 bzw. TIFR2 für die Timer TC1 und TC2 zuständig. Einmal gesetzt bleibt das Overflow Flag eines Timers TCn so lange gesetzt, bis es durch Schreiben einer 1 in das zugehörige TOCn Bit manuell zurückgesetzt wird.

Interrupt

[Bearbeiten]

Eine besondere Bedeutung gewinnt das Overflow Flag TOVn eines Timers TCn erst dann, wenn es zum Auslösen von Interrupts verwendet wird. Um diese zu erreichen muss sowohl das zugehörige Bit TOIEn (timer overflow interrupt enable) Bit im TIMSKn (timer interrupt mask) Register gesetzt werden, als auch die Zustellung von Interrupts global aktiviert werden.

Bei Ansprung der zugehörigen Service Routine wird das Overflow Flag automatisch gelöscht. Ein manuelles Rücksetzen ist in diesem Fall also nicht nötig.

Zum Aktivieren und Deaktivieren des Interruptbetriebs kann der folgende Code Schnipsel verwendet werden.

void t0_interrupt_ov(uint8_t on) {
  if (on) sbi(TIMSK0, TOIE0);
  else    cbi(TIMSK0, TOIE0);
}
void t2_interrupt_ov(uint8_t on) {
  if (on) sbi(TIMSK2, TOIE2);
  else    cbi(TIMSK2, TOIE2);
}
void t1_interrupt_ov(uint8_t on) {
  if (on) sbi(TIMSK1, TOIE1);
  else    cbi(TIMSK1, TOIE1);
}

Die zugehörigen Interrupt Service Routinen können mit dem ISR() Makro der Header Datei avr/interrupt.h definiert werden. Die Indices der zugehörigen Einträge in der Interruptvektortabelle können mit den Präprozessormakros TIMER0_OVF_vect, TIMER1_OVF_vect bzw. TIMER2_OVF_vect angegeben werden.

Codebeispiel

[Bearbeiten]

Das Arbeiten mit dem Overflow Interrupt können wir mit einem kleinen Programm ausprobieren. Durch zählen der zwischen zwei Überläufen des Zählers verstrichenen Taktschritte soll es die seit dem Systemstart verstrichene Zeit in Sekunden ausgeben. Die Anzahl der Taktschritte, die pro Sekunde ausgeführt werden, kann dem Makro F_CPU entnommen werden.

void setup() {
  stdout = log_init();

  // TIMER 0
  t0_power(1);       // power on
  t0_cs(5);          // clock source: clk/1024

  t0_interrupt_ov(1); // enable overflow interrupt
  sei();              // global interrupt enable
}

volatile uint32_t uptime = 0;

void loop() {
  printf("\r%5ld s", uptime);
  _delay_ms(500);   // delay ~ 500 ms
}

ISR(TIMER0_OVF_vect) {
  static uint32_t clocks = 0;
  clocks += 1024L * 256;
  if (clocks > F_CPU) {
    uptime += 1;
    clocks -= F_CPU;
  }
}

Nachdem Du das Programm übersetzt und auf den Microcontroller übertragen hast, sollte der Microcontroller über die serielle Schnittstelle nun kontinuierlich die aktuelle Anzahl Sekunden seit dem Systemstart anzeigen.

Vergleichsregister

[Bearbeiten]

Zusätzlich zu den Registern, die Du bereits kennen gelernt hast, verfügt jeder Timer über zwei Vergleichsregister OCRnA (output compare A) und OCRnB (output compare B).

Der Inhalt dieser beiden Vergleichsregister wird permanent mit dem aktuellen Zählerstand des zugehörigen Timers verglichen. Bei einer Übereinstimmung werden die Flags OCRFnA (output compare flag A) bzw. OCRFnB (output compare flag B) im TIFRn (timer interrupt flag register) Register gesetzt.

Die Timer TC0 und TC2 verfügen jeweils über zwei 8-bit Vergleichsregister OCR0A und OCR0B bzw. OCR2A und OCR2B. Der Timer TC1 verfügt über zwei 16-bit Vergleichsregister, mit zughörigen Werten in den Registern OCR1AL und OCR1AH sowie OCR1BL und OCR1BH.

Achtung! Bei der Arbeit mit den Vergleichsregistern sind folgende Besonderheiten zu beachten:

  • Bei schreibendem Zugriff auf das Zählregister eines Timers wird der Vergleich für einen Taktzyklus ausgesetzt.
  • Bei Zugriff auf die 16-bit Register OCR1A und OCR1B ist dieselbe Reihenfolge, wie bei Zugriff auf die 16-bit Zählerstände einzuhalten.

Zum Setzen der Werte der Vergleichsregister kann folgender Code Schnipsel verwendet werden

void t0_ocra(uint8_t ocra) {
  OCR0A = ocra;
}
void t0_ocrb(uint8_t ocrb) {
  OCR0B = ocrb;
}
void t2_ocra(uint8_t ocra) {
  OCR2A = ocra;
}
void t2_ocrb(uint8_t ocrb) {
  OCR2B = ocrb;
}
void t1_ocra(uint16_t ocra) {
  OCR1AH = (uint8_t) (ocra >> 8);
  OCR1AL = (uint8_t) (ocra & 0xFF);
}
void t1_ocrb(uint16_t ocrb) {
  OCR1BH = (uint8_t) (ocrb >> 8);
  OCR1BL = (uint8_t) (ocrb & 0xFF);
}

Interrupts

[Bearbeiten]

Analog zur Arbeitsweise des Overflow Flags können auch diese beiden Flags zum Auslösen von Interrupts verwendet werden. Damit ein Interrupt ausgelöst wird müssen die Bits OCIEnA (output compare interrupt enable A) bzw. OCIEnB (output compare interrupt enable B) im TIMSKn (timer interrupt mask) Register auf den Wert 1 gesetzt werden.

Um den Interruptbetrieb zu aktivieren / deaktivieren kann der folgende Code Schnipsel verwendet werden:

void t0_interrupt_ocra(uint8_t on) {
  if (on) sbi(TIMSK0, OCIE0A);
  else    cbi(TIMSK0, OCIE0A);
}
void t0_interrupt_ocrb(uint8_t on) {
  if (on) sbi(TIMSK0, OCIE0B);
  else    cbi(TIMSK0, OCIE0B);
}

void t2_interrupt_ocra(uint8_t on) {
  if (on) sbi(TIMSK2, OCIE2A);
  else    cbi(TIMSK2, OCIE2A);
}
void t2_interrupt_ocrb(uint8_t on) {
  if (on) sbi(TIMSK2, OCIE2B);
  else    cbi(TIMSK2, OCIE2B);
}

void t1_interrupt_ocra(uint8_t on) {
  if (on) sbi(TIMSK1, OCIE1A);
  else    cbi(TIMSK1, OCIE1A);
}
void t1_interrupt_ocrb(uint8_t on) {
  if (on) sbi(TIMSK1, OCIE1B);
  else    cbi(TIMSK1, OCIE1B);
}

Die Indices der zugehörigen Interruptvektor Einträge können mit den Präprozessormakros TIMER0_COMPA_vect, TIMER0_COMPB_vect, TIMER1_COMPA_vect, TIMER1_COMPB_vect, TIMER2_COMPA_vect bzw. TIMER2_COMPB_vect angegeben werden.

Codebeispiel

[Bearbeiten]

Auch am Ende dieses Abschnitts soll ein Teil der neu gewonnen Möglichkeiten anhand eines kleinen Beispiels demonstriert und überprüft werden.

In diesem Programm soll die Leutdiode L in schnellem Wechsel ein- und ausgeschaltet und somit gedimmt werden. Um das zu bewerkstelligen, können wir zwei Interrupts verwenden. Wenn wir sowohl den Overflow Interrupt, als auch den Compare Match A Interrupt verwenden, können wir die zugehörigen Service Routinen so implementieren, dass die Leuchtdiode bei jedem Überlauf eingeschaltet und bei jedem Compare Match wieder ausgeschaltet wird.

Über das zugehörige Vergleichsregister kann dann die Leuchtdauer festgelegt werden.

void setup() {
  stdout = log_init();

  sbi(DDRB, DDB5);      // LED: PORTB5 out 
  
  // TIMER 0
  t0_power(1);          // power on
  t0_cs(5);             // clock source: clk/1024

  t0_interrupt_ocra(1); // enable compare match A interrupt
  t0_interrupt_ov(1);   // enable overflow interrupt
  sei();                // global interrupt enable
}

void loop() {
  static uint8_t i;

  i += 1;
  if (i == 128)
    i = 0;
    
  t0_ocra(i);
  _delay_ms(10);
}

ISR(TIMER0_OVF_vect) {
  sbi(PORTB, PORTB5);     // high LED
}

ISR(TIMER0_COMPA_vect) {
  cbi(PORTB, PORTB5);     // low LED
}

Glückwunsch! Du hast nun alle Möglichkeiten kennen gelernt, die Timer im 'normal mode zur Verfügung stellen.

Im nächsten Abschnitt wird es darum gehen, dieses Wissen auch auf die anderen Betriebsarten von Timern zu übertragen.

Betriebsarten

[Bearbeiten]

Bisher wurde ausschließlich die Arbeitsweise im normal mode betrachtet. In diesem Abschnitt wird es darum gehen einen Überblick über die verschiedenen anderen Modi zu erhalten, in denen die Timer betrieben werden können. Die verschiedenen Betriebsarten von Timern werden im Handbuch unter der Bezeichnung Wave Generation Modes geführt, da sie vor allem für die Signalerzeugung von besonderer Bedeutung sind.

Eigenschaften

[Bearbeiten]

Die Timer TC0 und TC2 verfügen über den gleichen Satz an Betriebsarten. Der gewünschte Wave Generation Mode kann für jeden der beiden Timer mit einem 3-bit Wert konfiguriert werden. Der Timer1 verfügt über einen umfangreicheren Satz von Betriebsarten. Für ihn kann der gewünschte Wave Generation Mode mit einem 4-bit Wert vorgegeben werden.

Die verschiedenen Wave Generation Modes unterscheiden sich in den folgenden Eigenschaften:

  • maximaler Zählerstand
  • Zählintervall
  • Pufferung der Vergleichsregister

Der maximale Zählerstand kann, so wie Du es bereits vom Normal Mode kennst, durch einen für den jeweiligen Modus spezifischen festen Wert vorgegeben sein. In einigen Modi kann der maximale Stand eines Zählers stattdessen durch Vorgabe des gewünschten Werts im OCRA bzw. dem ICR Register vorgegeben werden.

Das Zählintervall eines Timers durchläuft entweder das Intervall von 0 bis zum Maximalwert, oder es durchläuft zunächst aufsteigend die Werte von 1 bis zum Maximalwert um danach schrittweise bis auf den Wert 0 abzusinken.

Im Normal Mode fand keine Pufferung der Vergleichsregister statt. In die Register OCRnA und OCRnB geschriebene Werte wurden sofort übernommen und haben sich somit sofort auf das Verhalten der Timer ausgewirkt. In anderen Modi können die die OCRnx Register stattdessen gepuffert sein. Werte in diesen Registern werden dann nur zu bestimmten, durch den jeweiligen Modus bestimmten Zeitpunkten übernommen.

Überblick

[Bearbeiten]

Achtung! Die Benennung der Betriebsarten im Handbuch [M328p] folgt keinem einheitlichen Schema:

  • Bei Modi mit gepufferten OCRx Registern erfolgt die Benennung anhand des Zählintervalls und des Zeitpunkts der Übernahme der gepufferten Werte. Bei diesen Modi wird zwischen Fast PWM Mode, Phase Correct PWM Mode und Phase and Frequency Correct PWM Mode unterschieden.
  • Bei ungepufferten OCRx Registern wird abhängig vom maximalen Zählerstand zwischen Normal Mode und CTC Mode unterschieden.

In der folgenden Übersicht wurden Normal Mode und Clear Timer on Compare Match Mode zur Kategorie Non-PWM Mode zusammengefasst. Die folgenden Kombinationen aus Zählintervall und Pufferung der Vergleichsregister sind möglich. Das Overflow Flag wird dabei stets beim Überschreiten des in der Spalte Zählintervall ganz rechts angegebenen Werts gesetzt.

Kategorie Zählintervall OCRx update
n/ctc Non-PWM Mode 0...max sofort
f Fast PWM Mode 0...max 0
pc Phase Correct PWM Mode 1...max...0 max
pfc Phase and Frequency Correct PWM Mode 1...max...0 0

Durch die Kategorie ist das Verhalten eines Timers bereits bis auf den maximalen Zählerstand bestimmt. Abhängig davon, um welchen Timer es sich handelt, sind folgende Kombinationen aus Kategorie und maximalem Zählerstand möglich.

Timer 0 / 2

[Bearbeiten]
WGM 0 0000 1 0001 2 0010 3 0011 4 0100 5 0101 6 0110 7 0111
Typ n pc ctc f - pc - f
max 0xFF 0xFF OCRA 0xFF - OCRA - OCRA

Timer 1

[Bearbeiten]
WGM 0 00000 1 0001 2 0010 3 0011 4 0100 5 0101 6 0110 7 0111
Typ n pc pc pc ctc f f f
max 0xFFFF 0x00FF 0x01FF 0x03FF OCRA 0x00FF 0x01FF 0x03FF
WGM 8 10000 9 1001 10 1010 11 1011 12 1100 13 1101 14 1110 15 1111
Typ pfc pfc pc pc ctc - f f
max ICR OCRA ICR OCRA ICR n/a ICR OCRA

Konfiguration

[Bearbeiten]

Die Modi der Timer TC0 und TC2 können mit einem 3-bit Wert konfiguriert werden. Die zugehörigen Bits liegen über zwei Register verteilt. Die Bits WGMn0, WGMn1 finden sich im Register TCCRnA, das Bit WGMn2 findet sich im TCCRnB

Der Modus von Timer1 kann mit einem 4-bit Wert konfiguriert werden. Auch hier sind die Bits über zwei Register verteilt. Die Bits WGM10 und WGM11 finden sich im Register TCCR1A, die Bits WGM12 und WGM13 im Register TCCR1B.

Um die Modi der Timer vorgeben zu können kann folgender Code Schnipsel verwendet werden.

void t0_wgm(uint8_t wgm) {
  uint8_t wgm_01 = wgm & 0b00000011;
  uint8_t wgm_2  = (uint8_t) ( (wgm & 0b00000100) << 1 );
  
  uint8_t tccr_a = TCCR0A & 0b11111100;
  uint8_t tccr_b = TCCR0B & 0b11110111;
  
  TCCR0A = tccr_a | wgm_01;
  TCCR0B = tccr_b | wgm_2;
}
void t2_wgm(uint8_t wgm) {
  uint8_t wgm_01 = wgm & 0b00000011;
  uint8_t wgm_2  = (uint8_t) ( (wgm & 0b00000100) << 1 );
  
  uint8_t tccr_a = TCCR2A & 0b11111100;
  uint8_t tccr_b = TCCR2B & 0b11110111;
  
  TCCR2A = tccr_a | wgm_01;
  TCCR2B = tccr_b | wgm_2;
}
void t1_wgm(uint8_t wgm) {
  uint8_t wgm_01 = wgm & 0b00000011;
  uint8_t wgm_23 = (uint8_t) ( (wgm & 0b00001100) << 1 );
  
  uint8_t tccr_a = TCCR1A & 0b11111100;
  uint8_t tccr_b = TCCR1B & 0b11100111;
  
  TCCR1A = tccr_a | wgm_01;
  TCCR1B = tccr_b | wgm_23;
}

Codebeispiel

[Bearbeiten]

Im Codebeispiel am Ende dieses Abschnitts geht es noch einmal darum die LED L zu dimmen. Im Normal Mode, in dem wir den Timer im letzten Codebeispiel betrieben hatten, mussten wir dafür zwei Interrupt Routinen verwendet. Wenn wir den Timer im Phase Correct PWM Mode betreiben, genügt eine Interrupt Routine. Sowohl beim Aufwärtszählen, als auch beim Abwärtszählen wird ein Compare Match mit dem Vergleichsregister OCRA ausgelöst. Es genügt also bei einem Compare Match den Zustand der LED zu toggeln.

void setup() {
  stdout = log_init();

  sbi(DDRB, DDB5);      // LED: PORTB5 out 
  
  // TIMER 0
  t0_power(1);          // power on
  t0_cs(5);             // clock source: clk/1024
  t0_wgm(1);

  t0_interrupt_ocra(1); // enable overflow interrupt
  sei();                // global interrupt enable
}

void loop() {
  static uint8_t i;

  ++i;
    
  t0_ocra(i);
  _delay_ms(10);
}

ISR(TIMER0_COMPA_vect) {
  sbi(PINB, PINB5);            // toggle LED
}

Signalerzeugung

[Bearbeiten]

Die verschiedenen Betriebsarten von Timern sind vor allem dann von Nutzen, wenn sie zum Erzeugen periodischer Signale eingesetzt werden. Zeitgesteuerte Aufgaben wie diese können, wie Du bereits gesehen hast, mit Interrupts in Software ausgeführt werden. Kosten in diesem Fall aber auch Rechnzeit des Microcontrollers.

Eine elegante Möglichkeit, mit der Aufgaben wie diese erledigt werden können, auch ohne die CPU des Micorcontrollers zu belasten, kannst Du in diesem Abschnitt kennen lernen.

Ausgabepins konfigurieren

[Bearbeiten]

Für jeden Timer TCn sind zwei Ausgabepins OCnA und OCnB vorgesehen. Wenn ein Timer nur zum zählen verwendet wird, können die zugehörigen Ausgabepins für einen anderen Zweck benutzt werden.

Die genaue Lage der Pins kann im Datenblatt nachgesehen werden.[7]

Timer Taktquelle (Tn) output A (OCRnA) output B (OCRnB) capture (ICP1)
TC0 PD4 PD6 PD5 -
TC1 PD5 PB1 PB2 PB0
TC2 - PB3 PD3 -

Damit die entsprechenden Pins als Ausgabepins verwendet werden können, muss zunächst die Datenrichtung auf Ausgabe gestellt werden.

Das kann mit folgendem Code Schnipsel realisiert werden.

void t0_a_out() { // OC0A = PD6
  sbi(DDRD, DDB6);
};
void t0_b_out() { // OC0B = PD5
  sbi(DDRD, DDB5);
};
void t2_a_out() { // OC2A = PB3
  sbi(DDRB, DDB3);
};
void t2_b_out() { // OC2B = PD3
  sbi(DDRD, DDD3);
};
void t1_a_out() { // OC1A = PB1
  sbi(DDRB, DDB1);
};
void t1_b_out() { // OC1B = PB2
  sbi(DDRB, DDB2);
};

Ausgabemodus

[Bearbeiten]

Das Verhalten der Ausgabepins von Timer TC0 kann kann über die Bits COM0A[1:0] (compare output mode A), bzw. COM0B[1:0] (compare output mode B) im TCCR0A (timer counter control register A) Register vorgegeben werden. Analog kann das Verhalten der Ausgabepins der Timer TC1 und TC2 über die Bits COM1A[1:0] und COM1B[1:0] im TCCR1A Register, bzw. die Bits COM2A[1:0] und COM2B[1:0] im TCCR2A Register vorgegeben werden.

Die Ausgabepins können so konfiguriert werden, dass sie abhängig vom Zählerstand des zugehörigen Timers gesetzt, gelöscht oder getoggelt werden. Die Konfigurationsmöglichkeiten hängen dabei von dem Modus ab, in dem der jeweilige Timer betrieben wird.

M:match B:bottom U:match when up-counting D: match when down counting

COMxA/B normal/ctc fast PWM phase (and frequency) correct PWM
0 00 disconnected disconnected disconnected
1 01 M:toggle siehe Tabelle X siehe Tabelle Y
2 10 M:clear M:clear B:set U:clear D:set
3 11 M:set M:set B:clear U:set D:clear

Der Wahl des Ausgabemodus kann im C Quellcode mit folgendem Code Schnippsel implementiert werden.

void t0_coma(uint8_t coma) {
  uint8_t tccr_a = TCCR0A & 0b00111111;
  TCCR0A = tccr_a | (uint8_t) (coma << COM0A0);
}
void t0_comb(uint8_t comb) {
  uint8_t tccr_a = TCCR0A & 0b11001111;
  TCCR0A = tccr_a | (uint8_t) (comb << COM0B0);
}
void t2_coma(uint8_t coma) {
  uint8_t tccr_a = TCCR2A & 0b00111111;
  TCCR2A = tccr_a | (uint8_t) (coma << COM0A0);
}
void t2_comb(uint8_t comb) {
  uint8_t tccr_a = TCCR2A & 0b11001111;
  TCCR2A = tccr_a | (uint8_t) (comb << COM0B0);
}
void t1_coma(uint8_t coma) {
  uint8_t tccr_a = TCCR1A & 0b00111111;
  TCCR1A = tccr_a | (uint8_t) (coma << COM0A0);
}
void t1_comb(uint8_t comb) {
  uint8_t tccr_a = TCCR1A & 0b11001111;
  TCCR1A = tccr_a | (uint8_t) (comb << COM0B0);
}

Frequenz und Tastgrad

[Bearbeiten]

Allen Signalen, die mit die Ausgabepins generiert werden können, ist gemein, dass der Ausgabewert nur zwischen den beiden Pegeln low und high wechseln kann. Es können also immer nur Rechtecksignale erzeugt werden.

Typische Kenngrößen eines solchen Rechtecksignals sind:

Bei der Betrachtung dieser Kenngrößen stellt der Vergleichsmodus 1 einen Sonderfall dar. Aus diesem Grund wird er erst zu einem späteren Zeitpunkt betrachtet werden.

Vergleichsmodus 2 und 3

[Bearbeiten]
Non-PWM Mode
[Bearbeiten]

Hier wird gar kein Rechtecksignal erzeugt. Der Ausgabepegel wird einmalig auf high, bzw. low gesetzt und verbleibt auf diesem Stand.

Fast PWM Mode
[Bearbeiten]

Der Zähler durchläuft das Intervall in Schritten. Nach einem weiteren Schritt des Zählers beginnt die Zählung von vorn. Eine Periode ist somit genau nach Schitten des Zählers durchlaufen. Die Periodendauer beträgt somit Taktschritte. Um aus diesem Wert die Periodendauer in Sekunden zu erhalten, muss er mit dem CPU Takt verrechnet werden. Die Frequenz kann dann als Kehrwert der Periodenlänge berechnet werden.

s

Hz

Phase Correct / Phase and Frequency Correct PWM Mode
[Bearbeiten]

Der Zähler durchläuft das Intervall in Schritten. Nach einem weiteren Zählschritt, also insgesamt Schritten, ist eine Periode beendet. Die Periodendauer beträgt somit Taktschritte. Für die Umrechnung in Sekunden muss dieser Wert mit dem CPU Takt verrechnet werden. Die Frequenz kann dann als Kehrwert der Periodenlänge in Sekunden berechnet werden.

s

Hz

Vergleichsmodus 1

[Bearbeiten]
Non-PWM Mode
[Bearbeiten]

Der Zustand des Ausgabepins wird in diesem Vergleichsmodus bei jedem Erreichen des maximalen Zählerstands invertiert. In zwei aufeinander folgenden Durchläufen des Zählintervalls steht der Ausgabepin so einmal low und einmal high und erzeugt auf diese Weise ein Rechtecksignal.

Das Intervall, das für die Erzeugung einer Periode des Rechtecksignals durchlaufen wird, besteht aus Zählerständen . Erst bei Eintreffen des nächsten Taktsignals beginnt die Generierung von vorn. In dieser Zeit verstreichen Zählschritte. Die Periodendauer beträgt somit Taktschritte. Um hieraus die Periodendauer in Sekunden und die Frequenz zu berechnen, muss der CPU Takt verrechnet und der Kehrwert gebildet werden.

s

Hz

Fast PWM Mode
[Bearbeiten]

Der Zustand des Ausgabepins verhält sich in diesem Vergleichsmodus im Fast PWM Mode genauso wie im Non-PWM Mode. Für Periodenlänge und Frequenz ergeben sich somit dieselben Werte wie zuvor.

Phase Correct / Phase and Frequency Correct PWM Mode
[Bearbeiten]

TODO

Zusammenfassung

[Bearbeiten]

Eine Zusammenfassung findest Du in den folgenden Tabellen.

Output Compare Mode 1
[Bearbeiten]
Typ Frequenz Tastgrad
n/ctc 50 %
f 50 %
pc/pfc
Output Compare Mode 2 / 3
[Bearbeiten]
Typ Frequenz Tastgrad
n/ctc kein Rechtecksignal -
f
pc/pfc

Achtung! Im ctc Modus findet keine Doppelpufferung des OCRnA Registers statt.

Wird OCRnA auf einen Wert gesetzt, den der Zähler bereits überschritten hat, findet der nächste Match erst nach Überlauf des Zählers statt.

Rückschau und Ausblick

[Bearbeiten]

An dieser Stelle hast den Rundgang durch die Welt der Timer und Counter absolviert. Du hast nun alle Möglichkeiten von Timern kennen gelernt, die Dir bei der Realisierung eigener Projekte zur Verfügung stehen.

Gegebenenfalls hast Du die Informationen dieses Kapitels mit dem Datenblatt Deines Microcontrollers abgeglichen. Du kannst jetzt:

  • die Ein- und Ausgabepins von Timern lokalisieren
  • Zählerstände von Timern auslesen und vorgeben

Gegebenenfalls hast Du die Code Schnippsel dieses Kapitels an Deinen Microcontroller angepasst. Mit den Funktionen, die Du Dir in diesem Kapitel erarbeitet hast, kannst Du:

  • Timer aktivieren und deaktivieren

Du kannst das Zählintervall von Timern vorgeben, indem Du:

  • dem jeweiligen Timer eine Taktquelle zuweist
  • die Betriebsart des Timers festlegst
  • gegebenenfalls den maximalen Zählerstand vorgibst

Du kannst dafür sorgen, dass Timer periodisch Interrupts auslösen, indem Du:

  • den Overflow Interrupt von Timern aktivierst
  • den Compare Match Interrupt von Timern aktivierst
  • gewünschte Zeitpunkt über die zughörigen Vergleichsregister festlegst
  • die Zustellung von Interrupts global aktivierst

Du kannst die Ausgabepins von Timern für die Signalerzeugung verwenden, indem Du:

  • die Datenrichtung der Pins als Ausgabepin festlegst
  • den Ausgabemodus des zugehörigen Vergleichsregisters vorgibst
  • gewünschte Werte in das Vergleichsregister schreibst

In folgenden Kapiteln findest Du Beispiele und Anregungen zu Aufgaben, die mit Hilfe von Timern realisiert werden können.

Register Übersicht

[Bearbeiten]

Für Timer 0 und Timer 2

Kontroll Register
TCCRnA compare output mode A compare output mode B waveform generation mode
COMnA1 COMnA0 COMnB1 COMnB0 x x WGMn1 WGMn0
TCCRnB force output A force output B waveform gen mode clock select
FOCnA FOCnB x x WGMn2 CSn2 CSn1 CSn0
Interrupt Register
TIFRn compare B compare A overflow
x x x x x OCFnB OCFnA TOVn
TIMSKn compare A interrupt enable compare B interrupt enable overflow interrupt enable
x x x x x OCIEnB OCIEnA TOIEn
Vergleichsregister
OCRnA output compare A
OCRnA_7 OCRnA_6 OCRnA_5 OCRnA_4 OCRnA_3 OCRnA_2 OCRnA_1 OCRnA_0
OCRnB output compare B
OCRnB_7 OCRnB_6 OCRnB_5 OCRnB_4 OCRnB_3 OCRnB_2 OCRnB_1 OCRnB_0
Zählregister
TCNTn counter value
TCNTn_7 TCNTn_6 TCNTn_5 TCNTn_4 TCNTn_3 TCNTn_2 TCNTn_1 TCNTn_0


Kontroll Register
TCCR1A compare output mode A compare output mode B waveform generation mode
COM1A1 COM1A0 COM1B1 COM1B0 x x WGM11 WGM10
TCCR1B input capture noise cancel input capture edge select waveform generation mode clock select
ICNC1 ICES1 x WGM13 WGM12 CS12 CS11 CS10
TCCR1C force output A force output B
FOC1A FOC1B x x x x x x
Interrupt Register
TIFR1 input capture compare B compare A overflow
x x ICF x x OCF0B OCF0A TOV0
TIMSK1 input capture interrupt enable compare B interrupt enable compare A interrupt enable overflow
x x ICIE x x OCIE1B OCIE1A TOIE1
Vergleichsregister
OCR1AH output compare A high byte
OCR1AH7 OCR1AH6 OCR1AH5 OCR1AH4 OCR1AH3 OCR1AH2 OCR1AH1 OCR1AH0
OCR1AL output compare A low byte
OCR1AL7 OCR1AL6 OCR1AL5 OCR1AL4 OCR1AL3 OCR1AL2 OCR1AL1 OCR1AL0
OCR1BH output compare B high byte
OCR1BH7 OCR1BH6 OCR1BH5 OCR1BH4 OCR1BH3 OCR1BH2 OCR1BH1 OCR1BH0
OCR1BL output compare B low byte
OCR1BL7 OCR1BL6 OCR1BL5 OCR1BL4 OCR1BL3 OCR1BL2 OCR1BL1 OCR1BL0
Capture Register
ICR1H input capture high byte
ICR1H7 ICR1H6 ICR1H5 ICR1H4 ICR1H3 ICR1H2 ICR1H1 ICR1H0
OCR1AL input capture low byte
ICR1L7 ICR1L6 ICR1L5 ICR1L4 ICR1L3 ICR1L2 ICR1L1 ICR1L0
Zählregister
TCNT1H counter high byte
TCNT1H7 TCNT1H6 TCNT1H5 TCNT1H4 TCNT1H3 TCNT1H2 TCNT1H1 TCNT1H0
TCNT1L counter low byte
TCNT1L7 TCNT1L6 TCNT1L5 TCNT1L4 TCNT1L3 TCNT1L2 TCNT1L1 TCNT1L0

Fußnoten

[Bearbeiten]
  1. (siehe [M328p]: 14.12.3. Power Reduction Register, S.71)
  2. (siehe [M328p]: Sektion 19.2 Overview S.125)
  3. (siehe [M328p]: Sektion 20.3 Block Diagram S.149)
  4. (siehe [M328p]: Sektion 22.2 Overview S.189)
  5. (siehe [M328p]: Sektion 20.6. Accessing 16-bit Registers, S.152)
  6. (siehe [M328p]: Table 19-10. Clock Select Bit Description, S. 142, Table 20-7. Clock Select Bit Description, S. 173 sowie Table 22-10. Clock Select Bit Description, S. 206 )
  7. (siehe [M328p]: Sektion 5. Pin Configurations, S.14)

WTFPL-2
Du hast das Recht unter den Bedingungen der WTF Public License mit diesem Dokument anzustellen was zum Teufel auch immer Du willst.