Timer und PWM – Teil 1 (8 Bit Timer0/2)

Über diesen Beitrag

In diesem Beitrag über Timer und Pulsweitenmodulation (PWM) tauche ich in die Tiefen des Arduino UNO bzw. des ATmega 328P ein. Der Arduino ist eine tolle Erfindung, die es einem leicht macht, in die Welt der Microcontroller einzutreten. Allerdings ist der Preis für die Vereinfachung, dass nicht alle Fähigkeiten des zugrunde liegenden Microcontrollers in die Arduino Welt übernommen wurden. Timer und PWM gehören definitiv dazu. Diese Features sind (mit wenigen Einschränkungen) immer noch zugänglich, aber nur über die etwas kryptisch anmutenden Binäroperationen.

Ich werde versuchen, das Thema mit vielen kleinen Beispielsketchen verständlich zu erklären. Dadurch ist der Beitrag ausgesprochen lang geworden – eher ein Buchkapitel als ein Blogbeitrag. Aber ich möchte gerade auch unerfahrenere Arduinoisten „abholen“. Dabei setze ich allerdings die Kenntnis von Binärlogik und Portmanipulation voraus. Falls ihr da noch Nachholbedarf habt, schaut in meinen letzten Beitrag

Wegen des Umfanges beschränke ich mich in diesem Beitrag auf die 8 Bit Timer. Den 16 Bit Timer behandele ich in Teil 2

Inhalt

Also, das steht euch bevor:

  • Was sind Timer und PWM
  • Welche Timer besitzt der Arduino UNO (bzw. ATmega 328 P)
  • Die vier Timer Modi:
    • Normal
    • CTC (clear timer on compare match)
    • Fast PWM
    • Phasenkorrekte PWM
  • C – Sketche in Atmel Studio

Was ist ein Timer, was ist PWM?

Ich fange ganz einfach an: zunächst einmal gehört zu einem Timer ein Counter. Und der macht, was sich für einen Counter gehört – nämlich zählen. Er zählt wiederholend bis zu einem Limit hoch oder runter oder bis zu einstellbaren Grenzen. Ein Timer wird daraus, wenn er zeitabhängig zählt. Und einen Sinn erhält der Timer schließlich erst dann, wenn das Erreichen des Limits oder eines Zwischenwertes ein Ereignis auslösen kann.

PWM ist einfach ausgedrückt die Technik, die digitalen Ausgänge eures Microcontrollers periodisch HIGH und LOW zu schalten, also Rechtecksignale mit bestimmten Mustern zu erzeugen. Und da das zum Teil sehr schnell und im Hintergrund passieren soll, verwendet man dafür Timer. Ihr habt bestimmt schon PWM Signale erzeugt, z.B. über analogWrite, Servosteuerung oder die tone Funktion. Das ist Fertigkost, sozusagen. Hier lernt ihr nun, wie ihr euch PWM Signale selber zubereiten könnt.

Die Timer des Arduino UNO

In diesem Beitrag (und dem Teil 2) behandele ich die folgenden Timer:

  • Timer0: 8 Bit
  • Timer1: 16 Bit (Teil 2)
  • Timer2: 8 Bit

Es gibt noch mehr spezialisierte Timer (z.B. Watchdog Timer). Die hier genannten sind aber diejenigen, die für PWM und ähnlich Zwecke genutzt werden.

Die korrespondierenden Counter, Kontrollregister und I/O Pins

Die Timer/Counter Register TCNTx

Jetzt wird es erst einmal trocken und theoretisch. Aber keine Sorge, mit den Beispielen wird das Ganze klarer. Nehmt die Dinge erst einmal hin.

Für die Timer gibt es je nach Größe ein oder zwei Counter Register, nämlich TCNT0 (Timer/Counter 0), TCNT1L, TCNT1H und TCNT2. Da der Timer1 16 Bit breit ist, ist er auf zwei Register verteilt. Im Folgenden beschränke ich mich nun aber auf die beiden 8 Bit Timer.

Die Kontrollregister TCCRxy

Die wesentlichen Einstellungen für die Timer werden in den Timer/Counter Control Registern vorgenommen. Zum Timer0 gehören TCCR0A und TCCR0B, zum Timer2 gehören TCCR2A und TCCR2B. Hier als Beispiel die Timer2 Kontrollregister:

TCCR2A Kontrollregister für Timer 2
TCCR2A Kontrollregister für Timer2
TCCR2B Kontrollregister für Timer2
TCCR2B Kontrollregister für Timer2

Ihr findet die Register und ihre Beschreibung auch im recht länglichen Datenblatt zur ATmega 48 / 88 / 168 / 328 Familie.

Die Bezeichnungen für die Register und die Bits sind in den AVR Bibliotheken über #define Anweisungen hinterlegt. Und da diese Bibliotheken ein fester Bestandteil der Arduino IDE sind, macht das den Zugriff sehr einfach.

Im Grunde sind die Bits der Register Schalter: gesetztes Bit = 1 =  Schalter an, nicht gesetztes Bit = 0 = Schalter aus. Wie man die Schalter sinnvoll kombiniert, ist Gegenstand dieses Beitrages.

Die Timer/Counter Interrupt Mask Register TIMSKx

Wenn ein Timer/Counterüberlauf oder das Erreichen eines Vergleichswertes (Compare Match) einen Interrupt auslösen soll, stellt ihr das in den entsprechenden Registern TIMSK0 bzw. TIMSK2 ein. Hier als Beispiel TIMSK2:

Das Setzen von TOIE2 (Timer/Counter2 Overflow Interrupt Enable) bewirkt, dass ein Registerüberlauf von TCNT2 (bei 8 Bit ist das nach 255) einen Interrupt auslöst. Gesetzte OCIE2A und OCIE2B Bits (= Timer/Counter2 Output Compare Match Interrupt Enable A bzw. B) bewirken, dass ein Interrupt ausgelöst wird, wenn TCNT2 den Vergleichswerten in den Output Compare Registern entspricht. 

TIMSK0 ist entsprechend organisiert. Einfach jeweils 2 durch 0 ersetzen.

Die Output Compare Register OCRxy

Für den Timer0 und den Timer2 gibt es die Output Compare Register OCR0A und OCR0B bzw. OCR2A und OCR2B. Wenn ihr diese nutzt, wird der Inhalt in TCNT0 bzw. TCNT2 ständig mit den OCR0A/OCR0B bzw OCR2A/OCR2B Registerwerten verglichen. Was bei einer Übereinstimmung (Compare Match) passiert, wird in den Kontrollregistern (TCCRxy) festgelegt.

Die Output Compare Pins OCxy

Zu den Timer/Countern gehören jeweils zwei Pins, OCxA und OCxB. In den Kontrollregistern (TCCRxy) legt ihr fest, was an den Pins im Falle eines Compare Match oder Timerüberlaufes passieren soll.

Die folgende Grafik zeigt euch, wo diese Pins am ATmega 328P liegen und welches die entsprechenden Arduino Pins sind.

Pinout des Atmega 328P vs. Arduino UNO

Übersicht über die Einstellungen

Haltet durch – ich muss euch noch ein bisschen mit weiteren Informationen quälen bis wir zu den Beispielen kommen. 

Zunächst legt ihr in den Kontrollregistern (TCCRxy) den Wave Form Generation Mode fest. Dafür sind die drei Bits WGMx0, WGMx1 und WGMx2 zuständig. Warum man diese Bits unbedingt auf zwei Register verteilen musste, ist mir ein Rätsel. Hier die Übersicht für den Timer2:

Wave Form Generation Mode Beschreibung für Timer 2
Wave Form Generation Mode Beschreibung für Timer2

Die Output Compare Modi

Der Compare Output Mode, also die Wirkung der Bits COMxyz (mit x = 0 oder 2, y = A oder B, z = 0 oder 1) hängt vom gewählten WGM Modus ab.

Compare Output Mode für non-PWM (Normal Mode 0), Timer 2, COM2Ax
1a) Compare Output Mode für non-PWM (Normal Mode 0), Timer2, COM2Ax
Compare Output Mode für non-PWM (Normal Mode 0), Timer 2, COM2Bx
1b) Compare Output Mode für non-PWM (Normal Mode 0), Timer2, COM2Bx
Compare Output Mode für Fast PWM (WGM Modi 3 und 7), Timer 2, COM2Ax
2a) Compare Output Mode für Fast PWM (WGM Modi 3 und 7), Timer2, COM2Ax
Compare Output Mode für Fast PWM (WGM Modi 3 und 7), Timer 2, COM2Bx
2b) Compare Output Mode für Fast PWM (WGM Modi 3 und 7), Timer2, COM2Bx
Compare Output Mode für Phase Correct PWM (WGM Modi 1 und 5), Timer 2, COM2Ax
3a) Compare Output Mode für Phase Correct PWM (WGM Modi 1 und 5), Timer2, COM2Ax
 Compare Output Mode für Phase Correct PWM (WGM Modi 1 und 5), Timer 2, COM2Bx
3b) Compare Output Mode für Phase Correct PWM (WGM Modi 1 und 5), Timer2, COM2Bx

Das waren die Einstellungen für Timer2. Die gute Nachricht: alle bisherigen Tabellen des Timer2 entsprechen denen des Timer0. Ihr müsst lediglich dort, wo eine 2 den Timer repräsentiert, die 2 durch eine 0 ersetzen.

Prescaler Einstellungen

Die Geschwindigkeit mit der das Timer-/Counterregister TCNTx zählt, orientiert sich am Systemtakt. Beim Arduino UNO sind das 16 MHz. Für viele Anwendungen ist das viel zu schnell. Deshalb gibt es den Prescaler, der bewirkt, dass der Zähler nur jeden n-ten Takt inkrementiert wird. Die Einstellungen dafür nehmt ihr im Timer/Counter Control Register B (TCCRxB) mit den Clock Select Bits (CSx0, CSx1,CSx2) vor. Für den Timer2 gilt:

Einstellung des Prescalers mit den Clock Select Bits an Timer 2
Einstellung des Prescalers mit den Clock Select Bits an Timer2

Für den Timer0 ist die Tabelle unterschiedlich, dazu später mehr. Dass ich mit dem Timer2 beginne, hat seinen Grund. Auch dazu später mehr.

Der Timer im Normal Mode

Geschafft, jetzt geht es in die Praxis. So komplex die Einstellungen, so einfach die Sketche. Die Verwirrung wird sich legen.

Wir starten mit dem Normal Mode. Dieser ist für eher langsame Anwendungen geeignet.

Der erste Sketch im Normal Mode

Für den folgenden Sketch schließt ihr eine LED an den Arduino Pin 7 (PD7) an. Ihr setzt das Kontrollregister A des Timer 2, also TCCR2A auf 0. Damit ist der Pin OC2A inaktiv. Im Kontrollregister B (TCCR2B) werden alle CS2x Bits gesetzt. Der Prescaler beträgt damit 1024. Es ist kein WGM2x Bit gesetzt, damit ist der Normal Mode aktiv. Im Timer/Counter2 Interrupt Mask Register wird das Bit für den Timer Overflow Interrupt gesetzt. Das heißt, dass jedesmal, wenn der TCNT2 überläuft (> 255), ein Interrupt ausgelöst wird. Was Ihr mit dem Interrupt anfangt, legt ihr in der ISR (Interrupt Service Routine) fest. Die ISR behandelt TIMER2_OVF_vect, also den Timer2 Overflow Interrupt. Eine Tabelle mit verfügbaren Interrupts gibt es im Datenblatt.

In diesem Fall führt der Interrupt dazu, dass der Pin 7 invertiert wird. Hinweis: mit DDRD |= (1<<PD7) wurde Pin 7 als Output definiert. Diese Anweisung sollte immer nach den anderen Registeranweisungen erfolgen.

void setup(){ 
  TCCR2A = 0x00; // Wave Form Generation Mode 0: Normal Mode; OC2A disconnected
  TCCR2B = (1<<CS22) + (1<<CS21) + (1<<CS20); // prescaler = 1024
  TIMSK2 = (1<<TOIE2); // interrupt when TCNT2 is overflowed
  DDRD |= (1<<PD7);  // Portmanipulation: replaces pinMode(7, OUTPUT);
} 

void loop() { 
 // do something else
}

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

 

Ladet den Sketch hoch und schaut was passiert. So sieht das Ergebnis am Oszilloskop aus:

Minimale Frequenz im Normal Mode. Die Frequenz bezieht sich auf ein HIGH/LOW Paar.
Minimale Frequenz im Normal Mode. Die Frequenz bezieht sich auf ein HIGH/LOW Paar.

Wenn ihr den Sketch auf den Timer0 übertragt, bekommt ihr eine Fehlermeldung:

Das Problem ist, dass in der Arduino Umgebung der Timer0_OVF_vect für andere Dinge eingesetzt wird. Das ist der Grund, weswegen ich mit dem Timer2 begonnen habe. In anderen Umgebungen, wie z.B. in Atmel Studio, gibt es das Problem nicht. Am Ende des Beitrages komme ich darauf zurück.

Berechnung der Frequenz

Ihr seht, dass die LED bei Verwendung des letzten Sketches sehr schnell blinkt. Der Arduino taktet 16 Millionen mal pro Sekunde. Aufgrund der Prescaler Einstellung wird TCNT2 jeden 1024sten Takt erhöht. TNCT2 beginnt bei 0 und läuft nach 255 über. Das sind 256 Schritte (wie bei einem for(i = 0; i<256; i++)). Die allgemeine Formel für die Frequenz f lautet deshalb:

Grafisch sieht das ganze so aus:

Grafik 1: TNTC vs. Pin Level
Grafik 1: TNTC vs. Pin Level

TCNT beginnt bei Bottom (hier 0) und zählt bis Top. In der WGM Tabelle seht ihr für den Normal Mode: Top = 0xFF und TOV Flag set on MAX (=TOP). Bei jedem Overflow invertiert PD7.

Weiteres Herunterregeln der Frequenz

Selbst mit dem maximalen Prescaler ist die Frequenz für einen Blinksketch also immer noch sehr hoch. Um den Vorgang weiter zu verlangsamen, führen wir einen Scalefaktor in die ISR ein (counter = 60). Damit erhalten wir eine Frequenz von ungefähr 1. Genau genommen 61,035… / 60 [s-1].

void setup(){ 
  TCCR2A = 0x00; // Wave Form Generation Mode 0: Normal Mode, OC2A disconnected
  TCCR2B = (1<<CS22) + (1<<CS21) + (1<<CS20); // prescaler = 1024
  TIMSK2 = (1<<TOIE2); // interrupt when TCNT2 is overflowed
  DDRD |= (1<<PD7);
} 

void loop() { 
 // do something else
}

ISR(TIMER2_OVF_vect){  // Interrupt Service Routine 
  static int counter = 0;
  counter++;
  if(counter==60){
    PORTD ^= (1<<PD7); 
    counter = 0; 
  }
}

 

Einstellen einer genauen Frequenz

Im nächsten Schritt soll eine Frequenz von genau 1 Hz eingestellt werden. Dafür nutzen wir den Umstand, dass das TCNTx Register auch mit einem Startwert beschrieben werden kann. Es werden dann nur noch 256 - Startwert Schritte bis zum Überlauf durchgeführt. Die allgemeine Formel lautet:

Mit fWunsch = 1 ergibt sich umgestellt:

Der Systemtakt ist 16 MHz. Bleibt aber immer noch eine Gleichung mit drei Unbekannten. Was wir aber wissen ist, dass der Prescaler nur bestimmte Werte annehmen kann, dass der Startwert kleiner 256 sein muss und er muss ganzzahlig sein. Also probieren wir ein bisschen:

Der Prescaler 1024 gibt zu „krumme“ Werte. Mit einem Prescaler von 256 sieht es besser aus. Nehmen wir einen Scalefaktor von 500 dazu, kommen wir auf einen Startwert von 131. Mit einem Prescaler von 64 und einem Startwert von 1000 ergibt sich ein Startwert von 6. Beides gut. 

Wenn ihr keine Lust habt zu rechnen, dann gibt es im Netz Kalkulatoren dafür, z.B. hier oder hier. Zwar berücksichtigen diese Kalkulatoren keine Scalefaktoren, aber eine Hilfe sind sie trotzdem.

Der vorherige Sketch wird geringfügig verändert. TCNT2 muss zu Beginn und nach jedem Überlauf wieder auf den Startwert eingestellt werden.

byte counterStart = 131;  // alternative: 6
unsigned int scaleFactor = 500; // alternative: 1000

void setup(){ 
  TCCR2A = 0x00; // OC2A and OC2B disconnected; Wave Form Generation Mode 0: Normal Mode
  TCCR2B = (1<<CS22) + (1<<CS21); // prescaler = 256
  // TCCR2B = (1<<CS22); // prescaler = 64; 
  TIMSK2 = (1<<TOIE2); // interrupt when TCNT2 is overflowed
  TCNT2 = counterStart;
  DDRD |= (1<<PD7);
} 

void loop() { 
 // do something else
}

ISR(TIMER2_OVF_vect){
  static int counter = 0;
  TCNT2 = counterStart;
  counter++;
  if(counter==scaleFactor){
    PORTD ^= (1<<PD7);
    counter = 0; 
  }
}

 

Eine Anwendung im Normal Mode: asynchrone LEDs

Vielleicht ist euch aufgefallen, dass bei den bisherigen Sketchen die loop Schleife leer war. Das LED Blinken passiert automatisch im Hintergrund. Stünde der Blink Code mit einer delay Funktion in der loop Schleife, müsste jeder zusätzliche Code „drumherum“ gebastelt werden.

Als ganz simples Beispiel lassen wir im nächsten Sketch zwei LEDs asynchron blinken. Versucht einmal, das ohne Timer zu programmieren.

byte counterStart = 131;  // alternative: 6
unsigned int scaleFactor = 500; // alternative: 1000

void setup(){ 
  TCCR2A = 0x00; // OC2A and OC2B disconnected; Wave Form Generator: Normal Mode
  TCCR2B = (1<<CS22) + (1<<CS21); // prescaler = 256
  // TCCR2B = (1<<CS22); // prescaler = 64; 
  TIMSK2 = (1<<TOIE2); // interrupt when TCNT2 is overflowed
  TCNT2 = counterStart;
  DDRD |= (1<<PD7) + (1<<PD6); // Pin 6 und Pin 7 als Ausgang
} 

void loop() { 
 PORTD ^= (1<<PD6);
 delay(723);
}

ISR(TIMER2_OVF_vect){
  static int counter = 0;
  TCNT2 = counterStart;
  counter++;
  if(counter==scaleFactor){
    PORTD ^= (1<<PD7);
    counter = 0; 
  }
}

 

Der Timer im CTC Mode

Im „Clear Timer on Compare Match“ Modus, kurz CTC, wird TCNTx nicht nach 256 Schritten, sondern nach Erreichen des in OCRxA hinterlegten Wertes auf Null zurückgesetzt. Wieder wollen wir eine LED im Sekundentakt blinken lassen. Die Formel für die Frequenzberechnung lautet:

Und wieso (1 + Top) und nicht nur Top? Ganz einfach: weil die 0 mitzählt! Aufgelöst nach Top und mit fWunsch = 1:

Ein Systemtakt von 16 MHz, ein Prescaler von 256 und ein Scalefaktor von 500 ergibt einen Top-Wert von 124. Diesen Wert tragen wir in das OCRxA Register ein, hier: OCR2A = 124.

Gemäß der WGM Tabelle müssen wir für den CTC Mode das WGM21 Bit setzen. Für den Prescaler 256 setzen wir CS22 und CS21. Im Timer/Counter Interrupt Mask Register setzen wir OCIE2A (Output Compare Interrupt Enable A, Timer 2), da diesmal kein Timer Overflow stattfindet, sondern ein Compare Match. Entsprechend müssen wir auch noch die ISR Routine abändern und als Argument TIMER2_COMPA_vect übergeben.

unsigned int scaleFactor = 500;

void setup(){ 
  TCCR2A = (1<<WGM21); // Wave Form Generation Mode 2: CTC, OC2A disconnected
  TCCR2B = (1<<CS22) + (1<<CS21) ; // prescaler = 256
  TIMSK2 = (1<<OCIE2A); // interrupt when Compare Match with OCR2A
  OCR2A = 124;
  DDRD |= (1<<PD7);
  
} 

void loop() { 
 // do something else
}

ISR (TIMER2_COMPA_vect){  // Interrupt Service Routine 
  static int counter = 0;
  counter++;
  if(counter==scaleFactor){
    PORTD ^= (1<<PD7); // 
    counter = 0; 
  }
}

 

Wenn wir den Normal Mode mit dem CTC Mode vergleichen, dann haben wir einmal von einem bestimmten TCNT Wert bis 255 gezählt, im anderen Fall von 0 bis zu dem in OCRA hinterlegten Wert.

Der Timer im Fast PWM Mode

Im Fast PWM Modus arbeitet man für gewöhnlich mit den Pins, die dem Timer zugeordnet sind. Das ist für den

  • Timer0: OC0A (=PD6, Arduino Pin 6) / OC0B (=PD5, Arduino Pin 5)
  • Timer2: OC2A (=PB3, Arduino Pin 11) / OC2B (=PD3, Arduino Pin 3)

Der PWM Modus arbeitet im Mode 3 mit einem Timer Overflow nach 255 (0xFF). Im Mode 7 arbeitet der PWM Modus mit einem Compare Match. Dabei ist Top der in OCRxA hinterlegte Wert.

Fast PWM an OC2B

Als Beispiel nehmen wir den Mode 7. Ziel ist ein Rechtecksignal mit einer Frequenz von 1 kHz an OC2B. In einer Periode soll das Signal 20% der Zeit HIGH sein und 80% LOW. Man sagt auch: der Duty Cycle ist 20%.

Wir setzen dazu das COM2B1 Bit. Das bedeutet laut Tabelle: „Clear OC2B at Compare Match, Set OC2B at BOTTOM“. Der Compare Match bezieht sich dabei auf den in OCR2B hinterlegten Wert. Grafisch sieht das so aus:

Grafik 2: PWM Signal an OC2B, Timer 2
Grafik 2: PWM Signal an OC2B

Erstmal müssen wir aber wieder rechnen. Wir brauchen den Top-Wert für die Frequenz und den Wert für OCR2B für den Duty Cycle. Wegen der kleinen Frequenz brauchen wir keinen Scalefaktor. Es gilt:

Ein Prescaler von 64 ergibt 249 für Top. Das sind 250 Schritte. Ein Fünftel davon sind 50. Da der Zähler bei 0 beginnt, ist OCR2B 49, jedenfalls theoretisch. Praktisch müsst ihr das mal ausprobieren. Ich habe das gewünschte Signal besser mit der Paarung 249 / 50 getroffen. Es kommt natürlich auch darauf an, wie genau der Microcontroller taktet.

So sieht der Sketch aus:

// Period = 1 ms => Frequenz = 1kHz
void setup(){ 
  // WGM22/WGM21/WGM20 all set -> Mode 7, fast PWM
  TCCR2A = (1<<COM2B1) + (1<<WGM21) + (1<<WGM20); // Set OC2B at bottom, clear OC2B at compare match
  TCCR2B = (1<<CS22) + (1<<WGM22); // prescaler = 64; 
  OCR2A = 249;
  OCR2B = 49;
  DDRD |= (1<<PD3);
} 

void loop() {}

 

… und so sieht es dann am Oszilloskop aus:

PWM Signal an OC2B
PWM Signal an OC2B

Fast PWM an OC2A

Für ein PWM Signal an OC2A ist Mode 7 nur bedingt geeignet, da der Compare Match gleich Top ist. Es fehlt die Möglichkeit das Signal in einen HIGH und einen LOW Teil zu unterteilen. Schaut in die Tabellen, um das nachzuvollziehen. In Mode 3 zählt der Timer bis 255, deshalb lassen sich dort beliebige Duty Cycles realisieren. Der Nachteil ist jedoch, dass sich nicht jede beliebige Frequenz einstellen lässt, da Top fix ist.

Was sich hingegen an OC2A gut realisieren lässt, sind PWM Signale mit 50% Duty Cycle. Das macht ihr mit der Kombi: Mode 7 / gesetztes COM2A0. Gemäß Tabelle heißt das: Toggle O2CA on Compare Match. Dabei verdoppelt sich die Periode bzw. die Frequenz halbiert sich.

// Period = 2 ms / Frequenz = 500 Hz. 
void setup(){ 
  // WGM22/WGM21/WGM20 all set -> Mode 7, fast PWM, TOP = OCR2A
  TCCR2A = (1<<COM2A0) + (1<<WGM21) + (1<<WGM20); // Toggle OC2A at compare match
  TCCR2B = (1<<CS22) + (1<<WGM22); // prescaler = 64; 
  OCR2A = 249;
  DDRB |= (1<<PB3); // PB3 = OC2A = Arduino Pin 11
} 

void loop() { 
}

 

AnalogWrite – eine PWM Anwendung

Dass ein analogWrite kein analoges, sondern ein PWM Signal ist, erkennt ihr am Oszilloskop:

Ein analogWrite(pin, 50) am Oszilloskop

Ihr habt kein Oszilloskop?

Den einen oder anderen mag es frustrieren, dass er das Ergebnis der Fast PWM Sketche nicht überprüfen kann, da er kein Oszilloskop besitzt. Drei Vorschläge dazu:

  • auch im Fast PWM Modus könnt ihr mit Compare Match Interrupts arbeiten, in der ISR einen Counter einfügen und damit quasi in Zeitlupe das PWM Signal beobachten. Ihr könnt sogar OCIE2A und OCIE2B setzen und zwei ISR Routinen nutzen (TIMER2_COMPA_vect / TIMER2_COMPB_vect).
  • nutzt die Technik aus meinem Beitrag über IR Fernbedienungen, um schnelle Signale zu analysieren
  • kauft euch ein DSO 138 Oszilloskop für unter 30 Euro. Sucht danach bei Amazon oder eBay. Die Teile funktionieren erstaunlich gut:
DSO 138 Oszilloskop in Aktion

Der Timer im Phase Correct PWM Mode

Im phasenkorrekten PWM Modus (Phase Correct PWM Mode) zählt der Counter von BOTTOM bis TOP hoch und dann wieder herunter bis BOTTOM. Als Beispiel nehmen wir den Modus 5 /  gesetztes COM2B1, also: „Clear OC2B when up-counting, set OC2B when down-counting“. Grafisch sieht das so aus:

Phase Correct PWM mit dem Timer 2 an OC2B.
Grafik 3: Phase Correct PWM mit dem Timer 2 an OC2B

Damit wird die Frequenz gegenüber der Fast PWM Methode halbiert.

Phasenkorrekt bedeutet, dass die Umkehrpunkte von HIGH nach LOW (bzw. umgekehrt) im gleichen Abstand vor und hinter Bottom bzw. Top des Timers liegen. Dadurch ergibt sich ein symmetrisches Signal, auch wenn der OCR2B Compare Wert dynamisch verändert wird. Eine sehr hilfreiche Animation, die den Unterschied zum Fast PWM deutlich macht, findet ihr hier. Die phasenkorrekte PWM wird vor allem bei Motorsteuerungen angewendet. Wer mehr über die PWM Modi wissen möchte, schaut z.B. hier. Es gibt noch einen weiteren PWM Modus und zwar den „phasen- und frequenzkorrekten PWM“ Modus. Dieser wird uns bei dem 16 Bit Timer in Teil 2 begegnen. Dort werde ich auch etwas detaillierter auf die Unterschiede der PWM Modi eingehen.

Und hier nun ein Sketch Beispiel:

// Period = 2 ms 
void setup(){ 
  // WGM22/WGM20 all set -> Mode 5, phase correct PWM
  TCCR2A = (1<<COM2B1) + (1<<WGM20); // Set OC2A at bottom, clear OC2B at compare match
  TCCR2B = (1<<CS22) + (1<<WGM22); // prescaler = 64; 
  OCR2A = 249;
  OCR2B = 49;
  DDRD |= (1<<PD3);
} 

void loop() { }

 

Timer 0 vs. Timer 2

Wie schon weiter oben erwähnt, sind die Strukturen des Timer0 und Timer2 sehr ähnlich. Den wesentlichen Unterschied seht ihr in der Clock Select Bit Tabelle für den Timer0:

Prescaler / Clock Select mit den Clock Select Bits an Timer 0
Prescaler / Clock Select mit den Clock Select Bits an Timer0

Es stehen weniger Prescaler zur Auswahl, stattdessen könnt ihr hier auch einen externen Taktgeber verwenden. Diesen schließt ihr an T0 (PD4, Arduino Pin 4) an. Darüber hinaus könnt ihr auswählen, ob auf die steigende oder die fallende Kante gezählt wird.

Einfaches Beispiel für externen Taktgeber

Als Taktgeber verwenden wir einen schlichten Taster. An den Ausgabepin OC0A (PD6, Arduino Pin 6) kommt eine LED.

Timer 0 mit Taster als externem Taktgeber

Jetzt wählen wir Mode 7 und setzen alle CS0x Bits (Clock on rising edge). OCR0A setzen wir auf 10.

void setup(){ 
  TCCR0A = (1<<COM0A0) + (1<<WGM01) + (1<<WGM00); // WGM 7: fast PWM; since WGM02 = 1 --> Toggle OC0A on Compare Match;
  TCCR0B = (1<<CS02) + (1<<CS01) + (1<<CS00) + (1<<WGM02);  // External clock source on T0 (rising edge)
  OCR0A = 10;
  DDRD |= (1<<PD6);
} 

void loop() { 
}

 

Theoretisch sollte die LED bei jedem elften Tasterdruck an- bzw. ausgehen. Durch das Tasterprellen schaltet die LED entsprechend früher um.

Das Zählen externer Ereignisse im Hintergrund kann für bestimmte Anwendungen sehr nützlich sein. Dafür gibt es übrigens auch spezielle Counter ICs, die ich hier beschrieben habe.

Wenn ihr einen Uhrenquarz als Taktgeber verwendet, dann könnt ihr euch eine Quarzuhr bauen. Im Detail ist das hier beschrieben. Ziemlich cool.

Verwendung von Atmel Studio

Ganz zu Beginn hatte ich erwähnt, dass der Timer0 Overflow Interrupt in der Arduino Umgebung nicht zugänglich ist. Verwendet ihr z.B. die (kostenlose) Software Atmel Studio, dann habt ihr diese Einschränkung nicht. Eine Einführung in Atmel Studio findet ihr hier. Das ist nichts, was man mal so eben nebenbei macht, aber es lohnt sich aus meiner Sicht.

Der Sketch für die zwei asynchron blinkenden LEDs sieht, übertragen auf den Timer0, in Atmel Studio so aus:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
uint8_t counterStart = 131;
uint16_t scaleFactor = 500;

int main(void)
{
  TCCR0A = 0x00; // OC0A and OC0B disconnected; Wave Form Generation Mode 0: Normal Mode
  TCCR0B = (1<<CS02); // prescaler = 256
  TIMSK0 = (1<<TOIE0); // interrupt when TCNT0 is overflowed
  TCNT0 = counterStart;
  DDRB |= (1<<PB1) + (1<<PB0);
  sei();  // activate interrupts
      
  while (1)
  {	
    PORTB ^= (1<<PB0);
    _delay_ms(723);	
  }
}


ISR(TIMER0_OVF_vect)
{
  static int counter = 0;
  TCNT0 = counterStart;
  counter++;
  if(counter==scaleFactor)
  {
    PORTB ^= (1<<PB1);
    counter = 0;
  }
}

 

Abschließende Worte

So, ich hoffe der eine oder andere hat bis hier durchgehalten. Ich persönlich fand es jedenfalls sehr spannend als ich die ersten Erfahrungen mit den Timern gemacht habe und dadurch einen tieferen Einblick in die innere Struktur des ATmega 328P bekam.

Was folgt ist, wie oben schon angekündigt, Teil 2, der sich mit dem 16 Bit breiten Timer1 beschäftigt.

Danksagung

Die Sanduhr im Beitragsbild habe ich „derGestalterCottbus“ auf Pixabay zu verdanken.

7 thoughts on “Timer und PWM – Teil 1 (8 Bit Timer0/2)

  1. Hi, vielen Dank für die ausführliche Beschreibung! Ich habe eine Frage – und hoffe, ich habe die Antwort nicht bereits überlesen weiter oben.

    Ich möchte die Grundfrequenz eines PWM-Signals genau(-er) einstellen als mit den Prescalern der Counter möglich. Auch gerne auf Kosten der PWM-Auflösung. Eine Lösung wurde bereits genannt: nach einem Counter-Overflow mit einer ISR den Startwert des Zählers beliebig setzen (unter Beachtung, dass der „PWM-Umschaltpunkt“ innerhalb des verbleibenden Zählerbereichs liegt – ich hoffe, das soweit begriffen zu haben).

    Frage: gibt es einen Modus (den ich im Atmel-Datenblatt noch nicht gefunden habe), bei dem der TOP- bzw. BOTTOM-Wert aus einem Register gelesen wird, ohne nach einem Overflow diesen mit einer ISR jedesmal neu setzen zu müssen?

    Vielen Dank, Grüße, Michael

    1. Was das genaue Einstellen angeht, da würde ich dich erstmal fragen ob dich schon mit dem Timer1 beschäftigt hast. Der ist wesentlich genauer einstellbar, wegen der 16 Bit. Wegen des Ansonsten gibt es doch einige Modi in denen der Top Wert OCR0A bzw. OCR2A ist. Da musst du dann keinen neuen Startwert setzen. Und wie gesagt, wenn das zu ungenau ist, dann nimm den Timer1. Ich hoffe, ich habe deine Frage richtig verstanden, so dass die Antwort auch einen Sinn ergibt….

      1. Perfekt, Danke, und wer lesen kann… ich hätte einfach Deinen Teil 2 lesen sollen, da steht die Lösung. Super Seite, Ciao

  2. Hallo,
    Ich habe nach ihren Beispiel 2 Timer mit Fast PWM erstellt. Gibt es die Möglichkeit die PWM Signale zu verodern oder zu verunden?
    Vielen Dank Benni
    void setup() {
    //Timer 1 fast PWM
    TCCR2A = (1<<COM2B1) + (1<<WGM21) + (1<<WGM20); // Set OC2B at bottom, clear OC2B at compare match
    TCCR2B = (1<<CS22) + (1<<WGM22); // prescaler = 64;
    OCR2A = 249; //duty cycle
    OCR2B = 49;
    DDRD |= (1<<PD3);

    //timer 0 fast PWM
    TCCR0A = (1<<COM0B1) + (1<<WGM01) + (1<<WGM00); // Set OC2B at bottom, clear OC2B at compare match
    TCCR0B = (1<<CS01) + (1<<CS00)+ (1<<WGM02); // prescaler = 64;
    OCR0A = 249; // duty cycle
    OCR0B = 100;
    DDRD |= (1<<PD5);
    }
    void loop() {

    }

    1. Hi Benny,

      ich sage mal „du“. Ich bin mir nicht sicher was du meinst. Möchtest Du entscheiden können, ob das eine Signal an PD3(OC2B) kommt und das andere gleichzeitig an PD5(OC0B) oder das eine oder das andere? Also so wie Du es oben geschrieben hast sollten sie gleichzeitig laufen. Wenn du jetzt gezielt einen der Timer ausschalten willst, musst du einfach nur die WGM Bits auf null setzen, also z.B. für den Timer 2:
      TCCR2A &= ~((1<

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.