Timer und PWM – Teil 2 (16 Bit Timer1)

Über den Beitrag

In meinem letzten Beitrag hatte ich die 8-Bit Timer des Arduino UNO (bzw. ATmega328P), nämlich Timer0 und Timer2, beschrieben. In diesem zweiten Teil möchte ich mich dem 16-Bit Timer1 widmen. Im Prinzip ist er den beiden 8-Bit Timern sehr ähnlich, weist aber eine höhere Flexibilität und zusätzliche Funktionen auf.

Ihr müsst nicht unbedingt den Teil 1 lesen, ich werde aber weniger weit ausholen. Folgendes wird in diesem Beitrag behandelt:

  • Struktur und Komponenten des Timer1
  • Normal Mode
  • CTC Mode
  • PWM Modi
    • Fast PWM
    • Phase Correct PWM
    • Phase and Frequency Correct PWM
  • Verwendung des Input Capture Pins
  • Externer Taktgeber

Struktur und Komponenten des Timer1

Die Register des Timer1

Timer/Counter1 Register TCNT1

Das Timer/Counter1 Register TCNT1 ist 16 Bit breit. Das Register zählt im Systemtakt oder verlangsamt über den gewählten Prescaler. Die untere Grenze ist Bottom (Null), die obere Grenze ist Top. Top ist, je nach Modus, festgelegt oder kann variabel definiert werden.

Output Compare Register OCR1x

In den beiden Output Compare Registern OCR1A und OCR1B könnt ihr Werte definieren, die permanent mit dem TCNT1 Register verglichen werden. Je nach Einstellung und Modus löst eine Übereinstimmung (Compare Match) bestimmte Aktionen aus. In bestimmten WGM1 Modi ist OCR1A Top. Die Register sind natürlich wie TCNT1 auch 16 Bit breit. 

Timer/Counter1 Control Register TCCR1x

In den Timer/Counter1 Control Registern TCCR1A, TCCR1B und TCCR1C werden die wesentlichen Einstellungen vorgenommen. Dazu gehören u.a.:

  • Wahl des Wave Form Generation Modes über die WGM1 Bits
  • Festlegung, was bei einem Compare Match passiert (COM1xy Bits)
  • Prescaler bzw. external Clock über die Chip Select Bits CS1x
Timer / Couter1 Control Register TCCR1A
Timer / Couter1 Control Register TCCR1B
Timer / Couter1 Control Register TCCR1C
Timer/Couter1 Control Register TCCR1A/B/C

ICES1, das Input Capture Edge Select Bit in TCCR1B, wird uns noch bei der Besprechung der Input Capture Funktion begegnen. Für alle weiteren Bits schaut bitte ins Datenblatt der ATmega 48 / 88 / 168 / 328 Familie.

Input Capture Register ICR1

Dieses Timer1 Register hat zwei Funktionen.

  1. Bei einem Ereignis an ICP1 wird der Zählerstand von TCNT1 in ICR1 geschrieben. Das eben erwähnte ICES1 Bit legt fest, ob dies bei steigender (ICES1 = 1) oder fallender Flanke (ICES1 = 0) passieren soll.
  2. 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. 

Timer/Counter1 Interrupt Mask Register TIMSK1

In TIMSK1 aktiviert ihr die Interrupts für die Input Capture Funktion (ICIE1), die Output Compare Matches (OCIE1B, OCIE1A) und den Timer Overflow (TOIE1). Das „IE“ steht dabei für „Interrupt Enable“.

Timer/Counter1 Interrupt Mask Register
Timer/Counter1 Interrupt Mask Register

Sind die jeweiligen Interrupts aktiviert, dann könnt ihr sie in Interrupt Service Routinen (ISRs) nutzen. Dabei übergebt ihr der ISR die jeweiligen Interruptvektoren:

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

Timer/Counter1 Interrupt Flag Register TIFR1

Sofern die Interrupts aktiviert sind, werden im Falle eines Interrupts die entsprechenden Bits in TIFR1 gesetzt. Die Ausführung eines Interruptvektors löscht das Bit. Alternativ löscht ihr die Bits durch Überschreiben mit 0. 

Output Compare Pins OC1x

Dem Timer1 sind zwei Output Compare Pins zugeordnet, OC1A (PB1, Arduino UNO Pin 9) und OC1B (PB2, Arduino UNO Pin 10). Wenn ich im Folgenden von Arduino Pins spreche, meine ich den Arduino UNO. Außerdem beziehe ich mich immer auf digitale Pins, wenn ich nichts anderes schreibe. 

Pinbezeichnungen ATmega 328P vs. Arduino UNO
Pinbezeichnungen ATmega328P vs. Arduino UNO

Das Verhalten der Output Compare Pins hängt von den WGM1 Bits und den Compare Output Bits in TCCR1A und TCCR1B ab. 

Input Capture Pin ICP1

Der Input Capture Pin entspricht PB0 (Arduino Pin 8). Mithilfe der Input Capture Funktion lassen sich elegant schnelle Ereignisse zeitlich vermessen. Ich komme gegen Ende des Beitrages darauf zurück.

External Clock Pin T1

Alternativ zum internen Taktgeber könnt ihr einen externen Taktgeber an T1 (PD4, Arduino Pin 4) hängen. 

Einstellungen in TCCR1x

Wave Form Generation Modes

Im Gegensatz zu den Timer0 und Timer2 stehen für den Timer1 vier WGM Bits zur Verfügung, d.h. es gibt 16 Einstellungsmöglichkeiten. Die Wahl des WGM ist immer die Grundlage für alle weiteren Einstellungen.

WGM1 Einstellungen an Timer1
Übersicht über die WGM Einstellungen des Timer1

Clock Select Bits / Prescaler

Die Zählfrequenz von TCNT1 ergibt sich aus dem Systemtakt und dem Prescaler:

    \[ f_{Wunsch}=\frac{Systemtakt}{prescaler} \]

Prescalerauswahl über die Clock Select Bits des Timer1
Prescalerauswahl über die Clock Select Bits des Timer1

Compare Output Mode Bits

Die Wirkung der COM1xy Bits hängt davon ab, welcher Mode gewählt wurde:

Compare Output Mode (Timer1) für non-PWM Modes
1. Compare Output Mode für non-PWM Modes
Compare Output Mode (Timer1) für Fast PWM Modes
2. Compare Output Mode für Fast PWM Modes
Compare Output Mode (Timer1) für Phase Correct und Phase and Frequency Correct Modes
3. Compare Output Mode für Phase Correct und Phase and Frequency Correct Modes

Der Normal Mode

Im Normal Mode ist Top immer 0xFFFF (65535). Ein kompletter Durchlauf umfasst 65536 Schritte, da die 0 mitgezählt wird. Nach Erreichen von Top wird TNCT1 wieder auf 0 gesetzt. Die Frequenz für den Timer Overflow wird durch den Systemtakt und den Prescaler bestimmt. Zusätzlich könnt ihr TCNT1 mit einem Startwert versehen und so – im Rahmen der Auflösung und der Limits – beliebige Frequenzen für den Overflow erzeugen. Den Startwert müsst ihr allerdings nach jedem Overflow erneut in TCNT1 schreiben. Die Frequenz fWunsch berechnet sich nach der Formel:

    \[ f_{Wunsch}=\frac{Systemtakt}{prescaler\cdot (65536-Startwert)} \]

    \[ Startwert=65536-\frac{Systemtakt}{prescaler\cdot f_{Wunsch}} \]

Systemtakt und Frequenz sind bekannte Größen. Bleibt eine Gleichung mit zwei Unbekannten. Darauf bin ich in Teil 1 des Beitrages schon ausführlicher eingegangen. Deswegen will ich das hier nicht noch einmal wiederholen. Ich möchte aber noch einmal auf die hilfreichen AVR Timer Kalkulatoren im Netz hinweisen, z.B. hier oder hier.

Normal Mode Beispielsketch

Eine LED soll im Sekundentakt an- und ausgehen (1s an, 1s aus = 0.5 Hz Blinkfrequenz). Das bewerkstelligen wir, indem TCNT1 in genau diesem Takt überläuft und wir den Overflow Interrupt zum Invertieren des LED Pin Ausganges nutzen.

Bei Timer0 und Timer2 mussten wir für diese Anwendung noch einen weiteren Scalefaktor einführen (siehe letzter Beitrag). Wegen der 16 Bit Breite von TCNT1 ist das hier nicht notwendig. Der Arduino UNO hat einen Takt von 16 MHz. Daraus ergeben sich zwei mögliche Kombinationen: 1) Prescaler 256 / Startwert 3036 oder 2) Prescaler 1024 / Startwert 49911.

Die in TCCR1A und TCCR1B zu setzenden Bits ergeben sich aus den Tabellen weiter oben. TOIE1 muss gesetzt werden, damit wir einen Overflow Interrupt erhalten. Die LED hängt an PD7 (Arduino Pin 7). In der ISR wird der LED Pin invertiert und der Timer wieder auf den Startwert gesetzt.

unsigned int counterStart = 3036; // alternative: 49911

void setup(){ 
  TCCR1A = 0x00; // OC2A and OC2B disconnected; Wave Form Generator: Normal Mode
  TCCR1B = (1<<CS12); // prescaler = 256; alternative: 1024 (set CS12 and CS10)
  TIMSK1 = (1<<TOIE1); // interrupt when TCNT1 is overflowed
  TCNT1 = counterStart;
  DDRD |= (1<<PD7);
} 

void loop() { 
 // do something else
} 

ISR(TIMER1_OVF_vect){
  TCNT1 = counterStart;
  PORTD ^= (1<<PD7);
}

 

Ein Vorzug am Arbeiten mit Timern ist, dass die Prozesse im Hintergrund laufen. Die loop Schleife des Sketches ist immer noch leer. Normalerweise hättet ihr die blinkende LED wahrscheinlich mit einer delay Konstruktion erzeugt. Da kann es immer eine Herausforderung sein, zusätzlichen Code drum herum zu bauen, besonders, wenn dieser weitere Zeitabhängigkeiten enthält. Im letzten Beitrag hatte ich als Beispiel zwei LEDs asynchron blinken lassen. 

CTC Mode

CTC steht für „Clear Timer on Compare Match“ und genau das macht man in diesem Modus. Anstelle von 0xFFFF ist Top entweder OCR1A (WGM 4) oder ICR1 (WGM 12). Top bestimmt also die Frequenz.

Auch im CTC Modus wird nur aufwärts gezählt. Nach Erreichen von Top wird TCNT1 auf Null zurückgesetzt.

CTC Mode Beispielsketch

Wir machen dasselbe wie im Normal Mode: eine LED soll im Sekundentakt blinken (1s an, 1s aus, Blinkfrequenz 0.5 Hz). Für die Berechnung des Prescalers und Top gilt: 

    \[ f_{Wunsch}=\frac{Systemtakt}{prescaler\cdot (1+Top)} \]

    \[ Top = \frac{Systemtakt}{prescaler\cdot f_{Wunsch}}-1\;\;\;\;mit\;\;\;\;Top < 65536 \]

Auch hier gibt es wieder zwei mögliche Kombinationen für den Prescaler und Top. Wir nehmen den Prescaler 1024 und als Top 15624. Die Alternative wäre Prescaler 256 und Top 62499. Ob wir Mode 4 oder 12 verwenden, macht keinen Unterschied, außer in der Definition von Top. Wir wählen Mode 4, also ist OCR1A Top. Im TCCR1A Register setzen wir COM1A0. Dadurch definieren wir gemäß Tabelle: „Toggle OC1A on Compare Match“. Wir nutzen hier also nicht den Interrupt mittels einer ISR, sondern steuern direkt einen Output Compare Pin. Und in diesem Fall ist das OC1A = PB1 = Arduino Pin 9. OC1A muss auf Output gesetzt werden, denn das passiert nicht automatisch. Dieser Schritt sollte immer nach allen anderen Einstellungen erfolgen.

void setup(){ 
  TCCR1A = (1<<COM1A0); // Toggle OC1A on Compare Match; Wave Form Generator: CTC Mode 4, Top = OCR1A
  TCCR1B = (1<<WGM12) + (1<<CS12) + (1<<CS10); // prescaler = 1024; 
  OCR1A = 15624;
  DDRB |= (1<<PB1);
} 

void loop() { 
 // do something else
} 

 

Graphisch sieht das dann so aus:

Timer1: CTC - "Toggle Modus"
Grafik 1: CTC – „Toggle Modus“ mit Timer1

Fast PWM

Im Fast PWM Modus, wie auch allen anderen PWM Modi, bestimmt der Top Wert die Frequenz. TCNT1 zählt von Bottom bis Top und wird dann auf null zurückgesetzt (eine Flanke pro Periode). Für den Timer1 gibt es ggü. dem Timer0 und dem Timer2 eine erweiterte Auswahl für Top. Je nach WGM1 Bitkombination ist das:

  • 0x00FF, 0x01FF, 0x03FF – Modus 5 bis 7
  • ICR1 (Input Capture Register 1) – Modus 14
  • OCR1A – Modus 15

Normalerweise steuert ihr in den PWM Modi die dem jeweiligen Timer zugehörigen Output Compare Pins OC1A (PB1, Arduino Pin 9) und OC1B (PB2, Arduino Pin 10). Alternativ könnt ihr natürlich auch die Compare Match Interrupts verwerten.

In der Compare Output Mode Tabelle für Fast PWM wählt ihr das gewünschte Verhalten der Output Compare Pins. In der nächsten Grafik ist beispielhaft OCR1A als Top gewählt. Für OCR1B wurde die Option „Clear OC1B at Compare Match, set OC1B at Bottom“ gewählt. OCR1B bestimmt hier deshalb den Duty Cycle, OCR1A die Frequenz. Im Modus 5 bis 7 ist die Frequenz nur durch festen Top Wert und den Prescaler festgelegt. Dem entsprechend könnt ihr also keine beliebigen Frequenzen in Modus 5 bis 7 wählen. 

Timer1: TNTC1 vs. OC1x bei Fast PWM
Grafik 2: TNTC1 vs. OC1x bei Fast PWM

Verändert ihr im Programm OCR1A, dann erfolgt die Aktualisierung des Top Wertes erst nachdem der vorherige Top Wert erreicht wurde. Diese verzögerte Aktualisierung ist möglich, da die OCR1x Register gepuffert sind, d.h. es gibt sozusagen eine Zwischenablage für die zu aktualisierenden Werte.

ICR1 besitzt keinen Puffer und das hat eine entscheidende Konsequenz. Stellt euch vor, ihr verwendet ICR1 als Top und weist ihm einen neuen Wert zu, der kleiner ist als der aktuelle TCNT1 Zählerstand. TCNT1 kann in diesem Fall im aktuellen Zyklus keine Übereinstimmung mehr mit Top finden und zählt bis 0xFFFF. Und das kann je nach Prescaler und Zählerstand ziemlich lange dauern. Deswegen gilt: Wollt ihr die Frequenz während des Programmablaufs verändern, dann verwendet OCR1A als Top (Modus 15). Oder noch besser: wählt den Phase- and Frequency Correct Mode 9.

Fast PWM Beispielsketch

Als Beispiel wollen wir ein 1 kHz Signal mit 25% DutyCycle an OC1A erzeugen. Dabei soll ICR1 als Top dienen. Erstmal müssen wir rechnen:

    \[ f_{Wunsch}=\frac{Systemtakt}{prescaler\cdot(1+Top)} \]

    \[ Top=\frac{Systemtakt}{prescaler\cdot f_{Wunsch}}-1 \]

    \[ Top=\frac{1600000}{1\cdot 1000}-1=15999\;\;\;\;mit\;\;\;\;prescaler=1 \]

Top ist 15999 und das sind 16000 Schritte. 25 % sind 4000 Schritte, die mit Vollendung von 3999 erreicht sind. Wir wählen die WGM1 Bits für den Mode 14, da dieser ICR1 als Top definiert. Durch Setzen von CS10 wählen wir den Prescaler 1, also keinen Prescaler. OC1A ist PB1 (Arduino Pin 9) und muss auf Ausgang gesetzt werden. 

void setup(){ 
  // Clear OC1A on Compare Match / Set OC1A at Bottom; Wave Form Generator: Fast PWM 14, Top = ICR1
  TCCR1A = (1<<COM1A1) + (1<<WGM11); 
  TCCR1B = (1<<WGM13) + (1<<WGM12) + (1<<CS10); // prescaler = none; 
  ICR1 = 15999;
  OCR1A = 3999;
  DDRB |= (1<<PB1);
} 

void loop() { 
 // do something else
} 

 

Das Schöne daran ist, dass der Timer1 ICR1 als Top zulässt ist, dass wir sowohl OCR1A wie auch OCR1B als Compare Match für die Steuerung der Output Pins zur Verfügung haben. Wir erweitern den Sketch um ein Signal an OC1B mit 75% Duty Cycle:

void setup(){ 
  // Clear OC1A and OC1B on Compare Match / Set OC1A and OC1B at Bottom; 
  // Wave Form Generator: Fast PWM 14, Top = ICR1
  TCCR1A = (1<<COM1A1) + (1<<COM1B1) + (1<<WGM11); 
  TCCR1B = (1<<WGM13) + (1<<WGM12) + (1<<CS10); // prescaler = 1 (none) 
  ICR1 = 15999;
  OCR1A = 3999;
  OCR1B = 11999;
  DDRB |= (1<<PB1)|(1<<PB2);
} 

void loop() { 
 // do something else
} 

 

So sieht es dann am Oszilloskop aus:

Als Übung könnt ihr mal die Einstellungen so verlangsamen, dass ihr das PWM Signal an zwei LEDs sichtbar macht. Falls ihr kein Oszilloskop habt, ist das ja auch befriedigender. Wie schon im letzten Beitrag möchte ich aber noch mal auf das DSO 138, ein Oszilloskop für unter 30 Euro, hinweisen. Als Einstieg kann ich das sehr empfehlen. Bei Amazon, ebay und Co bekommt ihr es bei vielen Anbietern.

Verwendung von Fast PWM

Der Name weist schon darauf hin, dass Fast PWM schnell ist. Deshalb wird dieser Modus vor allem dort eingesetzt, wo es auf diese Eigenschaft ankommt. Warum er doppelt so schnell ist wie die anderen PWM Modi, erfahrt ihr gleich. Auch sind die Einschränkungen von Fast PWM gegenüber den anderen PWM Modi besser verständlich, wenn man sie sich im Vergleich anschaut.

Phase Correct PWM

Bei der phasenkorrekten PWM zählt TCNT1 erst von Bottom bis Top hoch und dann wieder herunter (zwei Flanken pro Periode). Als Top Werte stehen wieder

  • 0x00FF, 0x01FF, 0x03FF – Modus 1 bis 3
  • ICR1 – Modus 10
  • oder OCR1A – Modus 11

zur Auswahl. Im Folgenden möchte ich die Besonderheiten dieses Modus an einem konkreten Beispiel erklären. Dabei ist OCR1A als Top und OCR1B als Compare Match definiert:

Timer1: TNTC1 vs. OC1x bei der Phase Correct PWM
Grafik 3: TNTC1 vs. OC1x bei Phase Correct PWM

Die Periode verdoppelt sich gegenüber Fast PWM, bzw. die Frequenz halbiert sich. Ein Compare Match mit OCR1B findet sowohl beim Herauf- wie beim Herunterzählen statt.

Da ihr nun die Einstellungen selbst vornehmen können solltet, und weil der Beitrag nicht noch länger werden soll, habe ich auf weitere Beispielsketche verzichtet.

Phasenkorrekte PWM bei konstanter Frequenz

OCR1A und OCR1B werden aktualisiert, wenn TCNT1 Top erreicht. Dadurch ist, solange nur OCR1B variiert wird, sichergestellt, dass die Mitte des Pulses immer in der Mitte der Periode liegt. Bei Fast PWM wandert die Mitte des Pulses bei Änderung der Pulsweite durch die Periode. Klingt komplex, ist es aber nicht:

Fast PWM vs. Phase Correct PWM bei Änderung der Pulsweite und konstanter Frequenz
Grafik 4: Fast PWM vs. Phase Correct PWM bei Änderung der Pulsweite und konstanter Frequenz

Eine schöne Animation dazu gibt es hier.

Eine typische Anwendung für phasenkorrekte PWM ist die Steuerung von Servomotoren, da diese ein symmetrisches Signal mögen. Über Servomotoren werde ich vielleicht noch einmal separat etwas schreiben.

Phasenkorrekte PWM bei variierender Frequenz

Bei Änderung des Top Wertes, also einer Frequenzänderung, haben wir immer noch das Aktualisierungsproblem, wenn ICR1 als Top verwendet wird. Wird ICR1 als Top beim Heraufzählen aktualisiert und ist dieser Wert kleiner als der aktuelle TCNT1 Wert, findet im aktuellen Zyklus kein „Top Match“ mehr statt.

Mit OCR1A als Top gibt es ein anderes Problem, wenn ihr diesen Wert im laufenden Programm verändert. Stellt euch vor, ihr aktualisiert Top während TCNT1 herunterzählt. Die Länge der abfallenden Flanke ist dann immer noch durch den alten Top Wert bestimmt. Die Länge der steigenden Flanke hingegen wird durch den neuen Top Wert bestimmt. Dadurch bekommt das Signal wieder eine Asymmetrie. Verhindern könnt ihr das, indem ihr auf den phasen- und frequenzkorrekten PWM Modus wechselt.

Phase- and Frequency correct PWM

Auch bei der phasen- und frequenzkorrekten PWM zählt TCNT1 von Bottom bis Top und wieder runter bis Bottom. Es gilt dasselbe Schema wie bei der phasenkorrekten PWM (Grafik 3). Der Unterschied besteht darin, dass OCR1A und OCR1B bei Bottom aktualisiert werden. Dadurch ist sichergestellt, dass immer zwei symmetrische Flanken vorliegen. Im Datenblatt der ATmega 48 / 88 / 168 / 328 Familie gibt es dazu noch weitere Grafiken. 

Zusammenfassung: welcher PWM Modus ist geeignet?

  • Fast PWM: bei konstanter Frequenz und Pulsweite oder Anwendungen die nicht allergisch auf die mit der Pulsweitenänderung verbundenen Wanderung der Pulsmitte reagieren
  • Phase Correct PWM: bei Variation der Pulsweite und konstanter Frequenz
  • Phase and Frequency Correct PWM: bei Variation der Frequenz

Hilfe für die Timer- und PWM-Programmierung

Die Timer- und PWM-Programmierung kann schon etwas nervig und verwirrend sein. Eine großartige Hilfe ist das Tool „Arduino Web Timers“ von David Buezas, das ihr hier findet. Ich kann es wirklich wärmstens empfehlen.

Den Input Capture Pin verwenden

Wie oben angekündigt, möchte ich nochmal auf die Input Capture Funktion zurückkommen. Sie einfach nur darauf zu reduzieren, dass man mit ICR1 einen weiteren Top Wert zur Verfügung hat, wäre schade.

Mithilfe des Input Capture Pins lassen sich Ereignisse hinsichtlich ihrer zeitlichen Abfolge bequem im Hintergrund messen. Das folgende Beispiel soll das Prinzip verdeutlichen. Es ist nicht besonders schön programmiert mit seinen vielen globalen Variablen, aber darauf soll es hier nicht ankommen.

Ich habe einen Taster an ICP1 (PB0, Arduino Pin8) gehängt. Gedrückt ist er HIGH, sonst LOW. Nach zehnmaligem Drücken wird ausgegeben, zu welchen Zeitpunkten der Taster gedrückt wurde. 

Über den Timer1 im Normal Mode registriert der Sketch die Sekunden. Dazu habe ich den Prescaler 256 gewählt und den Startwert 3036. Die Frequenz ist damit 1 Hz und 1 Sekunde entspricht 62500 Schritten. Bei jedem Timer Overflow werden die Sekunden inkrementiert. Durch das Setzen von ICES1 wird die steigende Kante des Signals an ICP1 ausgewählt. ICIE1 und TOIE1 aktivieren die benötigten Interrupts. 

Bei einem Tasterdruck wird der TCNT1 Zählerstand in ICR1 gespeichert. Abziehen des Startwertes und teilen durch 62.5 ergibt die Millisekunden. Die Tasterdrücke werden in einem array gespeichert.

unsigned int counterStart = 3036;
unsigned int eventTime[10][2];
volatile unsigned int seconds = 0;
unsigned int ms = 0;
unsigned int eventCounter = 0;
volatile bool event = false;
const float ms_const = 62.5;

void setup(){ 
  Serial.begin(9600);
  TCCR1A = 0; // Normal Mode
  TCCR1B = (1<<ICES1) + (1<<CS12);  
  TIMSK1 = (1<<ICIE1) + (1<<TOIE1);
  TCNT1 = counterStart;
} 

void loop() { 
  if(event){
    eventTime[eventCounter][1] = seconds;
    eventTime[eventCounter][2] = round((ICR1 - 3036) / ms_const);
    eventCounter++;
    if(eventCounter==10){
      ausgabeZeiten();
      eventCounter=0;
    }
    event=false;
  }   
} 

void ausgabeZeiten(){
  Serial.println("Tastendruck nach: ");
  for(int i=0; i<10; i++){
    Serial.print(i);
    Serial.print(": ");
    Serial.print(eventTime[i][1]);
    Serial.print(" Sekunden, ");
    Serial.print(eventTime[i][2]);
    Serial.println(" Millisekunden");
  }
}

ISR(TIMER1_OVF_vect){
  TCNT1 = counterStart;
  seconds++;
}

ISR(TIMER1_CAPT_vect){
  event = true;
}

 

Ausgabe des Input Capture Beispielsketches

Ich habe den Taster alle 2-3 Sekunden gedrückt. In der Ausgabe erkennt man schön das Tasterprellen. Natürlich hätte man dieses Beispiel auch einfach mit einer digitalRead / millis Kombination programmieren können. Das Schöne an der Input Capture Methode ist aber, dass man den Zeitpunkt des Events nicht sofort abfragen muss. Er ist in ICR1 gespeichert und läuft einem erst einmal nicht weg. Außerdem lässt sich der Zeitpunkt des Events in sehr hoher Auflösung, nämlich taktgenau ermitteln. 

Externe Taktgeber an T1

Wie der Clock Select Bit Tabelle zu entnehmen ist, könnt ihr an T1 (PD5, Arduino Pin 5) einen externen Taktgeber hängen. Ihr habt dabei die Wahl, ob die steigende oder die fallende Flanke das Taktsignal gibt. In Teil 1 dieses Beitrages hatte ich als Taktgeber einen Taster an T0 gehängt und habe damit eine LED „getoggled“. Umso erstaunter war ich, dass das mit T1 am Arduino nicht funktionierte. Dabei kam folgender Sketch zum Einsatz:

// geht nicht!!!!
void setup(){ 
  TCCR1A |= (1<<COM1A0) + (1<<WGM11) + (1<<WGM10); 
  TCCR1B |= (1<<WGM13) + (1<<WGM12) + (1<<CS12) + (1<<CS11) + (1<<CS10); 
  OCR1A = 10;
  DDRB |= (1<<PB1);
}  

void loop() { 
 // do something else
} 

 

Eigentlich müsste die LED and OC1A (PB1, Arduino Pin9) nach jedem zehnten Tasterdruck an- bzw. ausgehen. Warum das nicht passiert, ist mir nicht klar. Hat jemand eine Idee? Aber es scheint am Setup des Arduino zu liegen, denn derselbe Sketch, übersetzt in C funktionierte mit Atmel Studio am „nackten“ ATmega328P ohne Probleme:

#include <avr/io.h>
#include <util/delay.h>
    
int main(void)
{
    TCCR1A |= (1<<COM1A0) + (1<<WGM11) + (1<<WGM10); 
    TCCR1B |= (1<<WGM13) + (1<<WGM12) + (1<<CS12) + (1<<CS11) + (1<<CS10); 
    OCR1A = 10;
    DDRB |= (1<<PB1);
  
    while (1)
    {
    }
}

 

Danksagung

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

37 thoughts on “Timer und PWM – Teil 2 (16 Bit Timer1)

  1. Hallo Wolfgang!

    Zunächst wieder mal einen schönen Dank für Deine tollen Beiträge!

    Ich möchte Dich auf einen kleinen Fehler in der Formel zur Berechnung der resultierenden Ausgangsfrequenz des Timers im CTC-Mode hinweisen. Die Periodendauer erstreckt sich über 2 Timer-Zyklen, weil der Ausgang beim CTC-Modus ge-toggled wird. Folglich ist die Ausgangsfrequenz nur halb so groß. Im Prinzip zeigt es Deine Grafik eh schon perfekt 😉 Die LED blinkt also nicht mit einer Frquenz von 1 Hz, sonder mit 0,5 Hz.
    Dies betrifft auch Dein Bsp. im vorherigen Beitrag (Timer 2) wo du die LED mithilfe eines Soft-Prescalers in der ISR auch im Sekundentakt blinkel lassen möchtest. Auch hier blinkt die LED mit 0,5 Hz.

    Selbst der Online-Rechner „Arduino Web Timers“ den Du in deinem Beitrag empfiehlst hat auch den Berechnungsfehler drinnen.
    …und auch bei dem Beitrag „AVR-GCC-Tutorial/Die Timer und Zähler des AVR“ bei mikrocontroller.net hat der Fehlerteufel zugeschlagen.

    Ich hab mir bei Ali einen Pro Mini bestellt, 5V/16MHz. Natürlich keine Markierung auf der Rückseite!
    Hat das Board wirklich 5V/16MHz? Lieber nachmessen…
    Die Spannung ist ja schnell erledigt, aber die Quarzfrequenz is ein bisserl aufwendiger.
    Also gut, schnell ein bisserl Code schreiben und die Quarzfrequenz / 1000 teilen und an einem PortPin ausgeben.
    Ja eh – schon lange nix mehr mit Timer gemacht, also nix mit schnell 😉
    Also bin ich wider mal hilfesuchend bei Deinen Beiträgen gelandet…
    Timer 1, CTC-Mode, Prescaler = 1, Auto-Reload auf 999 und das Oszi zeigt 8 kHz auf Pin 9???
    Watn nu? Doch n 8MHz Quarz drauf? Also nochmal die grauen Zellen angestrengt und dann is der Groschen gefallen.
    Der Pin wir ja getoggled! Das ist kein PWM-Mode. Also OCR1A auf 499 geändert und siehe da, jetzt zeigt auch das Oszi 16 KHz an.

    Nach langem Suchen bin ich bei Microchip selbst fündig geworden:
    https://developerhelp.microchip.com/xwiki/bin/view/products/mcu-mpu/8-bit-avr/peripherals/timers-counters/example-project/
    Abschnitt: „Clear Timer on Compare Match Mode“

    LG
    Rick

    1. Hallo Rick,

      vielen Dank für den Hinweis. Ich sollte es vielleicht genauer ausdrücken. Ich beziehe mich nicht die Blinkfrequenz, sondern die „Toggle-Frequenz“. Das gilt sowohl für das Normal Mode Beispiel, als auch für das CTC Beispiel. Also nicht falsch berechnet, sondern unsauber ausgedrückt.

      VG, Wolfgang

  2. Hallo Wolfgang,

    ich habe mit deinen Beiträgen (Teil1 und Teil2) hier ein stabiles 10kHz mit der Breite 5µs hinbekommen. Mit dem Timer1 am Pin9 lasse ich das Signal ausgeben und habe ein tolles stabiles Signal am Oszi. Wie kann ich mir ein gleiches Signal am Pin10 ausgeben lassen, welches 50µs versetzt zum Signal am Pin 9 ist, sonst aber auch mit 10kHz und Duty Cycle 5µs ist?

    Mit delayMicroseconds() in loop wird alles unstabil.

    Vielleicht hast du eine Idee.

    MfG
    Igor

    1. Hallo Igor,

      du hast meinen Ehrgeiz geweckt und ich habe eine Lösung gefunden. Was die Sache dabei vereinfacht hat, ist, dass der Shift von 50µs genau die Hälfte der PWM-Frequenz ist. Ich habe die PWM-Frequenz deshalb verdoppelt und schalte die PWM-Ausgabe an OC1A (Pin 9) und OC1B (Pin 10) versetzt jede zweite PWM-Periode aus. Das Ausschalten erfolgt, indem ich den OCR1x Wert auf einen Wert größer ICR1 setze. Gesteuert wird das über die Interrupts.

      void setup(){
        noInterrupts();
        TCCR1A = (1 << COM1A1) | (1 << COM1A0)| (1 << COM1B1) | (1 << COM1B0) | (1 << WGM11);
        TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10);
        DDRB = (1 << DDB1) | (1<< DDB2);
        TIMSK1 = (1 << OCIE1A) | (1 << OCIE1B);
        OCR1A = 720;
        OCR1B = 720;
        ICR1 = 800;
        interrupts();
      }
      void loop(){}
      
      ISR(TIMER1_COMPA_vect) {
          OCR1A = 5000; // d.h. kein Match
          OCR1B = 720;
      }
      ISR(TIMER1_COMPB_vect) {
         OCR1A = 720;
         OCR1B = 5000; // d.h. kein Match
      }
      

      VG, Wolfgang

  3. Schoon wieder ein großer Dank für einen Artikel.
    Der Teil „CTC Mode Beispielsketch“ ist genau dass, wass ich zur Schrittmotorsteuerung ohne Bibliothek gesucht habe.
    Das schöne ist, dass ich so durch das lesen des Beitrages auch andere Möglichkeiten immer besser verstehen lerne.
    VG
    Leif

  4. Vielen Dank für die Zusammenfassung, die mir zum Timer verholfen hat, den ich so brauchte.
    Ich habe nun ein Problem: in der Timer-IRPT Routine will ich ggf. den Prescaler ändern
    (in t wird der dynamische Timer-Wert gehalten):

    void setTimer() {
    tEff = t >> FACTSHIFT;
    if (tEff > 65535) {
    TCCR1B = (1 << WGM12) | (1 << CS11);
    tEff /= 8;
    } else {
    TCCR1B = (1 << WGM12) | (1 << CS10);
    }
    OCR1A = (unsigned short) tEff;
    }

    ISR(TIMER1_COMPA_vect) {

    setTimer();

    }

    Mit dem Setzen von TCCR1B scheint es, als ob der Timer Dauerfeuer gibt.
    Wenn ich TCCR1B einmal setze und es im setTimer auskommentiere, dann geht's.

    Bin ratlos…

    1. Hi , ich hatte bisher noch nicht versucht den Prescaler im Programmlauf zu ändern. Das habe ich jetzt mal nachgeholt, indem ich T1_normal_mode_1Hz_toggle.ino erweitert habe, indem nicht nur PD7 toggelt, sondern auch der Prescaler. Also prinzipiell ist das schon mal möglich.
      Was bezeichnest du als dynamischen Timer-Wert? Also was genau ist t und um welchen Datentyp handelt es sich? Und welcher Datentyp ist tEff und welche Wert hat FACTSHIFT?
      Viele Fragen. Du könntest du mir auch dein gesamtes Programm schicken, oder besser reduziert auf den problemrelevanten Teil, aber lauf- bzw. kompilierungsfähig (wolfgang.ewald@wolles-elektronikkiste. de). Dann würde ich mal versuchen, das Problem nachzuvollziehen und ein wenig herumspielen.

      1. Hallo Wolfgang,
        das ging ja fix 🙂 t ist ein unsigned long und FACTSHIFT ist 2. Dass es bei dir funktioniert deutet ja eher auf das Problem vor der Tastatur hier hin… Also ich probiere erst selbst noch mal. Wenn es gar nicht klappt, dann melde ich mich per Mail. Oder ich streue die Asche hier über mein Haupt 😉

        Danke auf jeden Fall!

      2. P.S. Ich brauche eigentlich einen ganzen Sack Asche. Habe das Ganze noch mal neu geschrieben und siehe da, es geht. Zu allem Überfluss habe ich nach den Tests festgestellt, dass ich die dynamische Änderung eigentlich gar nicht brauche und mit dem 8er Prescaler vollkommen auskomme.

        Also noch mal vielen Dank und mach weiter so 🙂

  5. Guten Tag. Gute Seiten zum Programmieren (mit Arduino getestet). Habe ein Problem beim debugging, beim abarbeite mit step into (F11) springt der Debugger normal an den Anfang. Aber dann muss ich alle Schritte des Prgramms abarbeiten.
    Z.B. im Blink Beispiel eine Led einschalten da werden ja hunderte Schritte im Hintergrund ausgeführt, die möchte ich nicht alle einzel abarbeiten, nur digItalWrite(LED_BUILTIN, HIGH);
    nächster step into
    digItalWrite(LED_BUILTIN, LOW);
    und nicht alle Zwischenschritte.
    Auch mit step over viel zu aufwendig.
    Kann man das im Atmel Studi einstellen?
    Danke

  6. Guten Tag Wolfgang
    Das ist ein klasse Artikel und hat mir sehr geholfen. Grund ist:
    Ich habe mir selber eine Wallbox zum Laden meines E-Autos gebaut. Dazu benötige ich ein PWM Signal von 1000 Hz.
    Mit dem normalen Prescaler komme ich nur auf 976 Hz. Mit dem PWM On-Duty wird die Höhe des Ladestroms eingestellt. Mit der normalen Frequenz von 976 Hz funktioniert es bei meinem Wagen wunderbar. Erstaunlich ist das es einige Fahrzeuge gibt, die eine genauere Frequenz benötigen. Ist wohl eine normale Streuung.
    Mit deinem Artikel bin ich nun in der Lage die Frequenz genauer einzustellen. Mein Rechner ist ein Arduino Nano der die Aufgaben der Wallbox prima erfüllt.
    Viele Grüße aus dem hohen Norden

  7. Hallo,

    ich habe mich lange nicht an das direkt Programmieren herangetraut, aber dank deiner Seiten habe ich nun mein erstes PWM Signal welches ich ohne Libary zum laufen bekommen habe. Ich kann mich nur anschließen und möchte mich bedanken für deine mühe eine derartig sauber organsierte Website zu haben, die mir sehr geholfen hat. Einfach und Schritt für Schritt erklärt.

    Ich habe für mich den Timer1 als Favorit entdeckt, da ich nun weiß das ich diesem mit 2 PWM Signale erzeugen kann ohne mit Interrupts arbeiten zu müssen.

    Nur ist mir eine Sache an meinem Oszilloskop aufgefallen die ich gerne mit dir teilen will wenn du es nicht eh schon weißt oder vielleicht ist es ja auch nur bei mir ?

    Ich hoffe ich drücke mich korrekt aus da ich noch nicht lange in diesen tiefen der Programmierung drin bin:

    Wenn ich ein PWM Signal zwischen 1 und 99% dutycycle erzeuge wird dies auch korrekt auf meinem Oszilloskop dargestellt, wenn ich aber 0% oder 100% ausgeben will habe ich Artefakte auf meinem Oszilloskop. Es sieht so aus das bei z.B. 0% das Signal ganz kurz auf HIGH geht (ca. 4ns) und dann wieder LOW ist und das im Takt der eingestellten Frequenz.

    Wenn ich die AnalogWrite() Funktion nutze gibt es dieses Artefakt nicht.
    Ich nehme mal an das es daran liegt es zwischen 2 Takten erst erkannt werden kann das er schon wieder Low sein müsste??

    Auf jeden Fall habe ich das Problem in den Griff bekommen, in dem ich bei 0% oder 100% den jeweiligen Port von COM1A1 oder COM1B1 trenne und dann ein High oder Low Signal ausgeben lasse.

    Ich habe hier mal meinen Code mit angehängt.
    Mit großer Sicherheit gibt es noch Optimierungsbedarf aber für mich reicht es aus. Es gibt natürlich kein Fehlerabfangen o.ä., aber vielleicht hilft es ja jemanden und vieleiecht findet mein Codeschnipsel findet in deiner Beschreibung ja einen Platz?

    Viele Grüße
    Dominic

    // PWM INIT ######################################################################################
    void pwmInit(uint16_t freq, bool aktivatePB1, bool aktivatePB2) {

    //Timer = Fast PWM und Prescaler = System Clock (1)
    TCCR1A = (1 << WGM11);
    TCCR1B = (1 << WGM13) + (1 << WGM12) + (1 << CS10);

    ICR1 = 16000000 / 1 / freq – 1;

    if (aktivatePB1 == true) {
    DDRB |= (1 << PB1);
    pwmWrite(9, 0);
    }
    if (aktivatePB2 == true) {
    DDRB |= (1 << PB2);
    pwmWrite(10, 0);
    }
    }

    // PWM WRITE #####################################################################################
    void pwmWrite(uint8_t pinNumber, uint16_t dutycycle) {

    switch (dutycycle) {
    case 0:
    if (pinNumber == 9) {
    TCCR1A &= ~(1 << COM1A1);
    PORTB &= ~(1 << PB1); //digitalWrite(9, LOW);
    }
    if (pinNumber == 10) {
    TCCR1A &= ~(1 << COM1B1);
    PORTB &= ~(1 << PB2); //digitalWrite(9, LOW);
    }
    break;

    case 1 … 999:
    if (pinNumber == 9) {
    TCCR1A |= (1 << COM1A1); // Clear OC1A on Compare Match, Set OC1A at Bottom
    //OCR1A = 16000 / 100 * dutycycle – 1;
    OCR1A = (ICR1 + 1) / 1000 * dutycycle – 1;
    }
    if (pinNumber == 10) {
    TCCR1A |= (1 << COM1B1); // Clear OC1A on Compare Match, Set OC1A at Bottom
    OCR1B = (ICR1 + 1) / 1000 * dutycycle – 1;
    }
    break;

    case 1000:
    if (pinNumber == 9) {
    TCCR1A &= ~(1 << COM1A1); //Normal Port Operation, OC1A Disconnected
    PORTB |= (1 << PB1); //digitalWrite(9, HIGH);
    }
    if (pinNumber == 10) {
    TCCR1A &= ~(1 << COM1B1); //Normal Port Operation, OC1B Disconnected
    PORTB |= (1 << PB2); //digitalWrite(9, HIGH);
    }
    break;

    default:
    // Bei unplausiblen Werten wird das Signal auf LOW geschaltet
    if (pinNumber == 9) {
    TCCR1A &= ~(1 << COM1A1);
    PORTB &= ~(1 << PB1); //digitalWrite(9, LOW);
    }
    if (pinNumber == 10) {
    TCCR1A &= ~(1 << COM1B1);
    PORTB &= ~(1 << PB2); //digitalWrite(9, LOW);
    }
    break;
    }
    }

    // SETUP ######################################################################################
    void setup() {
    pwmInit(1000, true, true); //250 Hz – 16000 Hz mit Prescaler System Clock (1) und einer Auflösung auf eine Kommastelle genau also 0,0% bis 100,0%
    }

    // LOOP ######################################################################################
    void loop() {
    pwmWrite(9, 250); //0,0%-100,0% dutycycle Aufösung 0-1000 50,3% = 503
    pwmWrite(10, 750); //0-100% dutycycle
    }

    1. Hallo Dominic,

      ich war mir des Problems bisher nicht bewusst und muss das selbst mal ausprobieren. Leider ist die Kommentarfunktion meines Blogs nicht gut für Code geeignet, wie du siehst. Ich muss mal nachforschen, ob ich dafür eine bessere Lösung finde – aber das ist eine anderes Thema. Auf jeden Fall erst einmal herzlichen Dank!

      VG, Wolfgang

  8. T1_external_clock.ino

    // geht nicht!!!! ===>
    // Vorschlag (typos nicht ausgeschlossen) ::

    void setup() {
    noInterrupts();
    // stop & reset timer
    TCCR1A = TCCR1B = ICR1 = TIMSK1 = TIFR1 = 0;
    // initialize and start timer
    OCR1A = 10;
    TCNT1 = 0;
    // CTC-Mode 4; Toggle OCA1; Select External Clock / Enable Timer
    TCCR1A = (1<<COM1A0);
    TCCR1B = (1<<WGM12) | (1<<CS12) | (1<<CS11) | (1<<CS10);
    DDRB |= (1<<PB1);
    Interrupts();
    }

    void loop() {
    // do something else
    }

    1. Hallo Joe, vielen Dank für den reichhaltigen Kommentar. Das muss ich mir in einer ruhigen Minute anschauen. Ich wollte aber nicht versäumt haben, mich schon einmal zu bedanken. VG, Wolfgang

  9. Hallo Wolfgang, prima Aufarbeitung und Zusammenstellung zum Thema !

    Zu Deinem Problem / nicht funktionierendem Sketch im Abschnitt “ Externe Taktgeber an T1 “ :
    „… Warum das nicht passiert, ist mir nicht klar. … Aber es scheint am Setup des Arduino zu liegen …“

    Die Arduino-Umgebung konfiguriert und verwendet einige Ressourcen des 328P – ohne – dass dies nach außen zunächst sichtbar oder offensichtlich ist – also sozusagen ‚unter der Haube‘.

    Unter anderem werden die Timer für PWM Betrieb vorkonfiguriert _und_ gestartet. Timer 0 wird zudem für interne Zwecke und die korrekte Arbeitsweise der millis() Funktion benötigt/verwendet.

    TIMER_1 läuft bereits, wenn Dein Code-Beispiel ausgeführt wird. TCNT1 hat dadurch einen beliebigen Wert.
    Wenn dieser größer 10 ist, müsstest Du mit dem Taster zunächst Pulse bis zum Überlauf bei 65535
    und folgend weitere 10 Impulse erzeugen bis ein Compare-Match auftritt.

    Ich würde für diesen Anwendungsfall zudem zum CTC Mode raten.

    Es ist daher zu empfehlen:

    1. Timer_0 im Anwenderprogramm eher nicht explizit zu verwenden.

    Wenn z.B. der TOP Wert umkonfiguriert oder auf externen Takt umgestellt wird,
    ist davon auszugehen, dass die milllis() zumindest nicht mehr „korrekt“ sind, aber im
    schlechtesten Fall ziemlicher Unsinn passiert.
    Man sollte sich dann schon sicher sein dass im eigenen Programm keine Funktionen der
    Arduino-Umgebung erforderlich sind (z.B. auch nicht von aufgerufenen Bibliotheksfunktionen) .

    Verwendung als PWM-Generator ohne Umkonfiguration des Zählbereichs (z.B. TOP), des Modes
    (Waveform Generation Mode) oder des Taktes sollte m.E. aber möglich sein.
    ( z.B. mittels analogWrite() )

    2. Während des Konfigurierens der Timer die Interrupts global zu deaktivieren.
    z.B. noInterrupts(); … TIMER_x_Konfiguration … interrupts();

    3. Die zu konfigurierenden Timer zunächst zu deaktivieren
    (clock select CSxx = 000b == „No Clock Source“ == Timer disabled)

    Dann neu zu initialisieren – zuerst ICR1, OCR1A, OCR1B, TCNT1
    ( für Deine Anwendung vor allem _AUCH_ TCNT1 (!) )

    Und erst danach Ziel-Mode und Clock-Select einstellen.
    TCCR1A, TCCR1B sowie anschließend
    OC1A Pin als Ausgang konfigurieren ( z.B. DDRB |= (1<<PB1); )

    z.B.
    noInterrupts();
    TCCR1A = TCCR1B = TIMSK1 = TIFR1 = 0;
    OCR1A = 10;
    TCNT1 = 0;
    // CTC Mode 4 mit TOP = OC1A; Toggle OC1A; external Clock
    TCCR1A = (1<<COM1A0);
    TCCR1B = (1<<WGM12) | (1<<CS12) | (1<<CS11) | (1<<CS10);
    DDRB |= (1<<PB1);
    interrupts();

  10. Hallo!
    Zunächst mal vielen Dank für die Bereitstellung dieser Hilfe – obwohl ich so manches noch nicht ganz verstanden habe…
    Meine Frage: Gibt es eine Möglichkeit, bei mehreren Arduinos (Nano) die PWM-Ausgabe zu synchronisieren? Dass also bei gleichem Dutycycle die PWM-Pulse mehrerer Arduinos zeitgleich erfolgen – keine Phasenverschiebung.
    Anwendungsfall: Bei einer Modelleisenbahn sollen mehrere Gleisabschnitte über H-Brücke und Arduino per PWM geregelt werden, dabei erhält der Zug beim Überfahren der Trennstellen das PWM-Signal beider Abschnitte. Sind die beiden PWM-Signale jetzt nicht zeitgleich, ergibt das eine Geschwindigkeitserhöhung bis auf maximal das Doppelte, je nachdem wie die Zeitverschiebung der beiden PWM-Signale ist.
    Es wäre somit sehr praktisch, wenn über ein zusätzliches SYNC-Signal alle Arduinos mit ihrer PWM-Ausgabe zeitgleich beginnen würden.
    VG, Uli

    1. Hallo Ulli,
      eine richtig ausgegorene Anleitung habe ich nicht, nur eine Idee. Man könnte inenen Arduino ein Signal geben lassen, z.B. einen
      Pin, der HIGH geht und einen Interrupt bei den anderen Arduinos auslöst. Das wiederum könnte das Startan könnte das Startsignal sein, damit alle Aurduinos gleichzeitig ihr PWM im selben Takt erzeugen. Allerdings laufen die Arduinos mit der Zeit wieder auseinander, da jeder ein wenig unterschiedlich schnell getaktet ist. D.h. nach einer gewissen Zeit müsste man das ganze wiederholen. Ob das in der Realität funktioniert, müsste man probieren. Etwas Besseres fällt mir leider im Moment nicht ein. VG, Wolfgang

      1. Hallo Ewald,
        vielen Dank für deine Idee. Dass das Synchronisieren nicht sehr lange stabil bleiben wird, ist mir bewußt, die Synchronisation muss wohl regelmäßig wiederholt werden. Aber wie bringe ich die Arduinos dazu, ihre PWM auf Befehl zu starten? Das ist mir nicht klar. Ich bin da noch in der Beginnerphase…
        VG, Uli

        1. Hallo, wie du das PWM Signal erzeugt, ist hoffentlich durch den Artikel selbst klar geworden. Synchronisieren kannst du das Signal, indem du die Zähler bei allen Arduinos gleichzeitig auf Null setzt:
          TCNT1 = 0;
          Und damit es gleichzeitig passiert, könntest einen Arduino nehmen, der das Startsignal einfach über einen Ausgangspin gibt. Den verbindest du mit Interruptpins aller anderen Arduinos. Und in der Interruptroutine setzt du den Zähler auf Null. Ich kann das in einem Kommentar nicht detaillierter erklären. Ob es so wirklich funktioniert und wann die Arduinos merklich auseinander laufen, das müsste man probieren.
          Hoffe das hilft ein wenig weiter. VG, Wolfgang

    2. Was mir da einfällt (immer vorausgesetzt identische Grundkonfiguration der Timer) :

      Verwendung eines gemeinsamen, externen TIMER_Taktes für alle beteiligten Arduinos.
      (Mit Nano/Uno und 328P nur für TIMER_1 möglich, da TIMER_0 vom „System“ verwendet wird).

      TIMER_1 sollte als 8-bit PWM-Timer im Phase-Correct-Mode betrieben werden.

      Der Master-Takt darf erst anlaufen, wenn alle Arduinos die Grundeinstellungen für Ihren TIMER vorgenommen haben. Zum Beispiel könnte einer der Arduinos den TIMER_Takt als 50%-PWM-Signal (TIMER_0 oder TIMER_2 basierend) erzeugen nachdem alle Arduinos „betriebsbereit“ sind.

      Sinnvolle Taktfrequenz und Settings müsste man durchrechnen und entsprechend festlegen.
      Für 8-bit Phase-Correct-PWM ist das 512-fache der PWM-Frequenz erforderlich.
      Das ergäbe bei 100 Hz PWM-Frequenz einen TIMER-Takt von 51,2 kHz.
      Klingt machbar, Leitungslängen sollten aber kurz bleiben und ggf. geschirmt sein.

      /Joe

  11. Vielen Dank. Durch Zufall bin ich über Deine toll erklärte Seite gestolpert. Ich habe mir als absoluter Rookie vorgenommen eine Steuerung für einen KFZ-Lüfter zu basteln (Arduino nano mit Arduino IDE). Dazu benötige ich für das PWM Signal ein 10Hz Signal, dass sich abhängig von einer eingelesenen Temperatur, zunächst mit einem DS18B20, später über den Can Bus, ändert. Nun weiss ich dass ich ein Phase Correct PWM brauche. Um den Duty Cycle zu ändern muss ich mich wohl noch besser einfuchsen, aber die Grundlagen sind gelegt. Danke schon mal.
    VG

    Helmut

    1. Hallo Helmut,

      danke erst einmal fürs Feedback. Das sollte mit der Phasenkorrekten PWM funktionieren. Viel Erfolg beim Rechnen. Entweder du probierst ein wenig mit den Gleichungen oder nimmst einen Internetrechner zu Hilfe. Man muss nur die Halbierung der Frequenz bei der phasenkorrekten Methode im Hinterkopf behalten. VG, Wolfgang

  12. Hallo Wolle,
    ein „Danke“ aus Berlin.
    Habe lange mit den Timern gehadert und mit Deiner WebSeite hats dann geklappt. Ziel ist die Nutzung des PWM für einen Spannungswandler. Der AVR hat ja alles an Bord: PWM-Generator und Messzeug für die Rückkopplung. Wenn man einmal den Timer eingerichtet hat kann man im Programm den OCR-Wert beliebig setzen und muss ich sonst um nix kümmern.

  13. Hallo, das Wichtigste hätte ich beinahe vergessen. Wenn du auf den Arduino eingehst und dafür Bsp. Code zeigst, dann musst du auch daraufhinweisen das erst alle Timerregister gelöscht werden müssen bevor man eigene Settings einstellt. Ansonsten macht der Timer nicht das was man möchte, weil die Timer für PWM analogWrite voreingestellt sind. Auch sollte man Timer0 in Ruhe lassen.

    1. Das ist ein guter Punkt, da muss ich mal sehen an welcher Stelle ich das noch einfließen lasse – und hoffe das so lange der Kommentar noch mitgelesen wird – vielen Dank dafür!

  14. Hallo, liest sich ganz gut, du solltest aber den Text nochmal überarbeiten bezüglich der Namensgebung der Register und Bitnamen. xy kommt im Datenblatt nicht vor. Im Datenblatt gibt es nur nx, bspw. ICRn oder TCCRnx. n steht für die Timernummer und x für den Compare Channel.

    1. Danke für das Feedback – ich meine das „xy“ habe ich nur in Verbindung mit den „COMnxy“ Bits getan, wobei x für A oder B und y für 0 oder 1 steht.

  15. Einen ganzen Tag habe ich Google und Foren umgegraben … bis ich endlich hier gelandet bin!
    DANKE für die übersichtliche Darstellung der Timer und der PWM-Modes.

    Danke auch für die sonst fehlenden Erklärung woher die CSxy und andere merkwürdige Konstanten kommen … als Arduino-Anfänger war ich bei blahblub |= (1 <<CS12) ziemlich überfordert anfangs … Ich finde das Konstrukt zwar immer noch schräg, aber jetzt verstehe ich es!

  16. Nach 4 Tagen Counter-Frust hat mir diese Seite endlich geholfen. Ich sitze an einem ATtiny841 und konnte ums Verrecken die 16-Bit-Timer nicht benutzen. Stellte sich heraus, dass ich nur an OCR1A herumgedreht habe, aber nicht an ICR1. Jetzt funktioniert mein Code endlich. Vermutlich hat der µC vorher tatsächlich bis 0xFFFF gezählt.

    Großen Dank und einen extra Karma-Punkt von mir!

    1. Hallo Christoph,

      schön wenn mein Beitrag hilfreich war. Jedesmal, wenn ich mit Timern programmiere, muss mich auch wieder ein bisschen „hineindenken“. Eigentlich ist es nicht schwierig, aber man kann doch viele (Denk-)Fehler einbauen.

      VG, Wolle

Schreibe einen Kommentar

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