Interrupts – Teil 3: Timer Interrupts

Über den Beitrag

Im dritten und letzten Teil dieser Beitragsreihe kommen wir zu den Timer Interrupts. Da Timer Interrupts ausgesprochen hardwarespezifisch sind, werde ich ihre Anwendung auf verschiedenen Mikrocontrollern erklären, und zwar dem ATmega328P / LGT8F328P, der ATtinyx5-Serie, dem ESP8266 und dem ESP32.

Timer Interrupts werden häufig verwendet, um PWM Signale zu erzeugen. In diesem Beitrag geht es aber nur um die Nutzung der Timer Interrupts für andere Zwecke. PWM mit dem ATmega328P habe ich schon einmal separat in einem zweiteiligen Beitrag behandelt (Teil 1 und Teil 2).

Folgendes kommt auf euch zu:

Was ist ein Timer Interrupt?

Jeder Mikrocontroller besitzt Zählregister, die nichts anderes machen als stur hoch- oder herunterzuzählen, bis sie überlaufen oder einen definierten Wert erreicht haben, um dann wieder von vorn zu beginnen. Die Zählfrequenz entspricht dabei dem Systemtakt oder dem Systemtakt geteilt durch einen Vorteiler (Prescaler). Über den Zählerstand und die Taktfrequenz können wir Aussagen über die Zeit treffen. Damit wird der Zähler zu einem Zeitmessinstrument und heißt deswegen auch Timer Counter. Die Timer Counter verschiedener Mikrocontroller unterscheiden sich in ihrer Anzahl, ihrer Größe (z.B. 8, 16 oder 64 Bit) und der Auswahl der Prescaler.

Timer Interrupts können entweder für den Überlauf (Overflow) oder für das Erreichen eines Zielwertes (Compare Match) eingerichtet werden.

Wozu braucht man Timer Interrupts?

Wenn euer Mikrocontroller eine Aktion regelmäßig ausführen soll, dann ist es am einfachsten, das über delay() zu steuern. Diese Vorgehensweise lernt der Anfänger mit seinem ersten Blink-Sketch. Der Nachteil ist, dass delay() blockierend wirkt, d. h. der Mikrocontroller kann in der Wartezeit nichts anderes tun. Später lernt man dann, delay() zu vermeiden und durch millis()-Konstruktionen in dieser Art zu ersetzen:

if(millis() - lastAction > timeBetweenActions){
    ....
    lastAction = millis();
}

Die millis()-Methode stellt aber nicht sicher, dass die Aktion exakt zum richtigen Zeitpunkt ausgeführt wird, da die if-Abfrage durch andere Vorgänge verzögert werden könnte. Hier kommen nun die Timer Interrupts ins Spiel, denn ihre Interrupt Service Routinen (ISR) unterbrechen das Programm und werden sofort ausgeführt. Zumindest gilt das, wenn sich das Programm nicht gerade einer noInterrupts()-Umgebung oder in einer anderen ISR befindet.

Auch, wenn es euch vielleicht nicht bewusst ist, habt ihr schon Timer Interrupts eingesetzt, beispielsweise beim Gebrauch von delay(), analogWrite(), bei der Steuerung von Servo-Motoren oder beim Einsatz der tone() Funktion.

Einige Mikrocontroller ermöglichen auch, Timer durch externe Signale hochzählen zu lassen. Das könnt ihr nutzen, um die Signale zu zählen oder um den Timer mit einer bestimmten Frequenz zählen zu lassen.

Timer Interrupts der AVR-Mikrocontroller am Beispiel ATmega328P (Timer1)

Der ATmega328P besitzt zwei 8-Bit Timer (Timer0 und Timer2) und einen 16-Bit Timer (Timer1). Der Timer0 kann in der Arduino-Umgebung nicht genutzt werden, da er für die Zeitmessung (delay() / millis()) benötigt wird.

Ich beschränke mich in diesem Beitrag auf den Timer1. Die Übertragung auf den Timer2 ist aber nicht schwierig (siehe z. B. hier).

Grundlagen – Registereinstellungen

Das Timer Counter Register für den Timer1 ist TCNT1. Wenn keine weiteren Einstellungen vorgenommen werden, zählt es brav im Systemtakt von 0 bis 216 – 1, also 65535.

Die Einstellungen für den Timer1 nehmt ihr in den beiden Timer1 Counter Control Registern TCCR1A und TCCR1B vor:

Timer1 Counter Control Register A – TCCR1A
Timer1 Counter Control Register B – TCCR1B

Die Compare Output Mode Bits COM1xy steuern den PWM Ausgabemodus. Das ist hier nicht Thema, also lassen wir die Bits ungesetzt. Ebenso wenig kümmern wir uns – im Moment – um ICNC1 (Input Capture Noise Canceller) und ICES1 (Input Capture Edge Select).

Die Wave Form Generator Mode Bits WGM1x legen den (PWM-)Modus fest. Es gibt 12 PWM-Modi und 3 Nicht-PWM Modi. Wir betrachten nur die Letzteren:

  • Normal Mode 0: der Timer Counter zählt bis 216 – 1 = 65535.
    • Um diesen Modus einzustellen, setzt ihr kein WGM1x Bit.
  • Clear Timer on Compare (CTC) Mode 4: der Timer Counter zählt bis OCR1A (Output Compare Register A).
    • Um diesen Modus einzustellen, setzt ihr WGM12.
  • Clear Timer on Compare Mode 12: der Timer Counter zählt bis ICR1 (Input Capture Register 1). Um diesen Modus einzustellen, setzt ihr WGM12 und WGM13.

Für die vollständige Liste der Modi klickt hier: WGM1-Tabelle.

Um den Prescaler auszuwählen, setzt ihr die CS1x Bits nach folgendem Schema:

Timer1 Prescaler Einstellung über die Clock Select Bits
Timer1 Prescaler Einstellung über die Clock Select Bits

Schließlich aktiviert ihr noch die Timer Interrupts, die ihr nutzen wollt, im Timer1 Interrupt Mask Register TIMSK1.

Timer1 Interrupt Mask Register TIMSK1
Timer1 Interrupt Mask Register TIMSK1
  • TOIE1: Timer Overflow Interrupt Enable – wird ausgelöst, wenn der Timer überläuft.
  • OCIE1A: Output Compare A Match Interrupt Enable – wird ausgelöst, wenn der Timer Counter mit dem Wert in OCR1A (Output Compare Register A) übereinstimmt.
  • OCIE1B: Output Compare B Match Interrupt Enable – wird ausgelöst, wenn der Timer Counter mit dem Wert in OCR1B (Output Compare Register B) übereinstimmt.
  • ICIE1: Durch das Setzen des Input Capture Interrupt Enable Bit erreicht ihr, dass ein Interrupt ausgelöst wird, wenn ein Signal an ICP1 (PB0 / Pin 8) detektiert wird. Das eröffnet die Möglichkeit, ICP1 wie einen zusätzlichen externen Interruptpin zu nutzen (siehe Anhang).

Na, komplett verwirrt? Keine Sorge, mit den Beispielen sollte es klarer werden. Aber ein wenig Theorie fehlt noch.

Berechnung der Interruptfrequenz

Die Frequenz, in der das Timer Counter Register TCNT1 hochzählt, ist der Systemtakt, geteilt durch den Prescaler:

f_{TCNT1} = \frac{system\_clock}{prescaler}

Der Maximalwert, bis zu dem der Timer hochzählt, wird als „Top“ bezeichnet. Im Normal Mode ist Top 65535. Da der Timer Counter bei 0 beginnt, sind das 65536 Schritte. Daraus folgt für die Überlauf-Frequenz:

f_{timer1\_overflow} = \frac{system\_clock}{65536\cdot prescaler}

Interrupt Frequenz im CTC Modus (4)

Im CTC Modus (4) ist Top gleich OCR1A. Entsprechend gilt für die Frequenz der Compare Matches: 

f_{OCR1A\_match\_CTC4} = \frac{system\_clock}{(1 + OCR1A)\cdot prescaler}

Um eine bestimmte Interruptfrequenz (fdesired) bei vorgegebenem Systemtakt zu berechnen, löst ihr die Gleichung nach OCR1A auf.

OCR1A =  \frac{system\_clock}{prescaler\cdot f_{desired}}-1

Nur leider haben wir noch eine zweite Unbekannte, nämlich prescaler. Den Prescaler bestimmt ihr zuerst. Er muss so gewählt werden, dass OCR1A kleiner oder gleich 65535 ist. Entweder ihr probiert einfach ein wenig herum oder ihr nutzt zur Berechnung die folgende Formel:

prescaler \geq \frac{system\_clock}{65536\cdot f_{desired}}

Rechnet die rechte Seite aus und dann wählt ihr den nächstgrößeren, verfügbaren Prescaler.

Für den CTC Modus 12 müsst ihr lediglich OCR1A durch ICR1 ersetzen.

Interruptfrequenz im Normal Mode (0)

Wie schon erwähnt, ist 65535 das Top für den Timer1 im Normal Mode. Hier könnt ihr die Überlauffrequenz beeinflussen, indem ihr einen Startwert counter_start vorgebt. Die Berechnungen dazu sind:

f_{desired\_Normal\_Mode} = \frac{system\_clock}{(65536-counter\_start)\cdot prescaler}
counter\_start = 65536 - \frac{system\_clock}{prescaler\cdot f_{desired}}

Praktische Beispiele

Timer Interrupts im CTC Mode

Geschafft – jetzt kommt die Praxis. Zum „Aufwärmen“ erzeugen wir im CTC Modus alle zwei Sekunden einen Interrupt (fdesired = 0.5). Den Interrupt nutzen wir, um eine LED an Pin 7 zu toggeln, also je nach Zustand an- oder auszuschalten. Bei 16 MHz ist der einzig mögliche Prescaler 1024. OCR1A ist 31249. Der Interruptvektor ist TIMER1_COMPA_vect.

void setup(){ 
    TCCR1A = 0x00; // OC2A and OC2B disconnected; Wave Form Generator: Normal Mode
    TCCR1B = (1<<WGM12) | (1<<CS12) |(1<<CS10); // CTC-Mode 4, prescaler = 1024; 
    TIMSK1 = (1<<OCIE1A); // interrupt on OCR1A match
    OCR1A = 31249;
    DDRD |= (1<<PD7);  // pinMode(7, OUTPUT); 
} 

void loop() {} 

ISR(TIMER1_COMPA_vect){
    PORTD ^= (1<<PD7);  // toggle Pin 7
}

Timer Interrupts im Normal Mode

Und nun noch einmal dasselbe im Normal Mode. Für den Startwert errechnen wir 34286. Der Startwert muss nach jedem Interrupt erneut in der ISR gesetzt werden. Diesmal ist TIMER_OVF_vect als Interruptvektor zu wählen.

unsigned int counterStart = 34286; 

void setup(){ 
  TCCR1A = 0x00; // OC2A and OC2B disconnected; Wave Form Generator: Normal Mode
  TCCR1B = (1<<CS12) |(1<<CS10); // prescaler = 1024; 
  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);
}

Mehrere Timer1 Interrupts nutzen

Im nächsten Beispiel nutzen wir den Compare Match A, den Compare Match B und den Overflow Interrupt. Damit werden wir eine LED im 2-Sekundentakt toggeln, eine weitere im 1-Sekundentakt und schließlich noch eine Dritte im 2⁄3 Sekundentakt.

Wir nehmen den letzten Sketch als Basis und haben damit den 2-Sekundentakt abgedeckt. Die zweite LED toggeln wir auf der Hälfte des Weges zwischen dem Startwert (34286) und Top+1 (65536). Als Interrupt nutzen wir den Compare Match B, d. h. OCR1B ist 49911. Zusätzlich toggeln wir die zweite LED bei TOP.

Für LED Nr. 3 teilen wir die Strecke Start → Top+1 in drei Stücke. Einmal toggeln wir wieder bei TOP, bräuchten also noch zwei Interrupts, haben aber nur noch den Compare Match A. Dieses Problem lösen wir, indem wir den Wert von OCR1A nach jedem Compare Match A Interrupt neu zuweisen.

So sieht das schematisch aus:

Schema zu 3_timer_at_timer1.ino
Schema zu 3_timer_at_timer1.ino, LEDx = Togglepunkt

Das Schema ist insofern irreführend, als der Eindruck entsteht, dass mit dem Erreichen von TOP (65535) zwei Sekunden vergangen sind. Das ist aber nicht richtig, denn erst beim Umspringen auf 0 sind die zwei Sekunden vorbei. Und dies ist auch der Grund, weswegen man den Startwert von 65536 und nicht von 65535 abziehen muss, um auf die Zahl der Schritte zu kommen. Das wird klarer, wenn man die Schritte als Treppenstufen darstellt. Wäre der Startwert 65533, wären es drei ganze Schritte bis zum Überlauf:

Treppenstufenschema für TCNT1
Treppenstufenschema für TCNT1

Mit 5, 6 und 7 (bzw. PD5, PD6 und PD7) als LED Pins sieht der Sketch dann folgendermaßen aus:

unsigned int counterStart = 34286; 

void setup(){ 
    TCCR1A = 0x00; // OC2A and OC2B disconnected; Wave Form Generator: Normal Mode
    TCCR1B = (1<<CS12)|(1<<CS10); // prescaler = 1024; 
    OCR1A = 44703; 
    OCR1B = 49911;
    TIMSK1 = (1<<OCIE1B) | (1<<OCIE1A) | (1<<TOIE1);  // interrupt when TCNT1 is overflowed and on commpare matches
    TCNT1 = counterStart;
    DDRD |= (1<<PD7) | (1<<PD6) | (1<<PD5);
} 

void loop() {} 

ISR(TIMER1_COMPA_vect){
    PORTD ^= (1<<PD5);
    if(OCR1A==44703){
        OCR1A = 55119;
    }
    else
        OCR1A = 44703;
}

ISR(TIMER1_COMPB_vect){
    PORTD ^= (1<<PD6);
}

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

 

Wenn es nur darum geht, Pins zu toggeln, dann gibt es einen wesentlich einfacheren Weg, und zwar über OC1A und OC1B (Timer1 Output Compare Match A/B Output). Diese Ausgänge lassen sich automatisch bei einem Compare Match toggeln, und zwar ohne, dass ihr dafür jedes Mal eine ISR aufrufen müsst. Damit befinden wir uns auf halbem Wege zu PWM.

Arduino Web Timers nutzen

Ihr habt keine Lust, immer wieder die richtigen Bits aus den Tabellen der Datenblätter herauszusuchen? Dann empfehle ich das Tool Arduino Web Timers. Ihr klickt einfach nur an, was ihr haben wollt und stellt die gewünschte Frequenz über einen Schieber ein. Der Code wird automatisch erstellt.

Arduino Web Timers in Action
Arduino Web Timers in Action

Arduino Web Timers funktioniert nicht nur mit dem ATmega328P, sondern auch mit dem LGT8F328P.

AVR Timer Bibliothek TimerOne

Wer es ganz bequem haben möchte, könnte die Bibliothek TimerOne nutzen. Sie ist mit AVR-MCUs bzw. darauf basierenden Boards und mit dem LGT8F328P kompatibel. Die Bibliothek kann über die Bibliotheksverwaltung der Arduino IDE installiert werden. 

Hier ein einfaches Beispiel, das selbsterklärend sein sollte:

#include <TimerOne.h>
const int ledPin = 5;  

void setup(void) {
    pinMode(ledPin, OUTPUT);
    Timer1.initialize(500000);  // Time in µs
    Timer1.attachInterrupt(blinkLED); 
    Serial.begin(9600);
}

void blinkLED(void) {
    digitalWrite(ledPin,!digitalRead(ledPin));
}

void loop() {
//    delay(2000);  // will not delay the blinking
}

Timer Interrupts mit dem ATtiny85 / ATtiny45 / ATtiny25

Als Beispiel für einen weiteren AVR-Mikrocontroller nehme ich den ATtiny85 bzw. ATtiny45 bzw. ATtiny25. Diese MCUs besitzen zwei 8-Bit Timer, nämlich Timer0 und Timer1.

Wieder soll ein Pin mithilfe des Timer1 im 2-Sekundentakt toggeln. Die ATtinys dieser Reihe bieten zwar einen großen, maximalen Prescaler von 16384, jedoch ist die minimale Timer-Überlauffrequenz durch die 8 Bit bei 8 MHz auf ~1.9 Hz beschränkt. Wir behelfen uns mit einem Zähler, den wir bei jedem Überlauf inkrementieren. 

Ich will den Beitrag nicht zu lang werden lassen. Deswegen werde ich nicht alle Details durchgehen, so wie ich das beim ATmega328P getan habe. Schaut am besten in das Datenblatt der ATtinyx5 Reihe. Dort findet ihr alle Register und Bits zum Timer1 in Kapitel 12.3.

Normal Mode

So sieht der Sketch für den Normal Mode aus:

volatile int isrCounter = 0;
const byte counterStart = 12;  // at 8 MHz, overflow frequency is: ~2.0012 Hz

void setup(){ 
    TCCR1 = (1<<CS13) | (1<<CS12) | (1<<CS11) | (1<<CS10); // Normal Mode, prescaler 16384
    TIMSK = (1<<TOV1); // interrupt on TCNT1 overflow
    DDRB |= (1<<PB4);  // pinMode(4, OUTPUT); 
} 

void loop() {} 

ISR(TIMER1_OVF_vect){
    isrCounter++;
    TCNT1 = counterStart; 
    if(isrCounter == 4){ // to achieve 0.5 Hz
        PORTB ^= (1<<PB4);  // toggle Pin 4
        isrCounter = 0;
    }
}

Da die gewünschten 0.5 Hertz nicht darstellbar sind, habe ich erst einmal ausgerechnet, wie viele Schritte der Timer Counter bei 8 MHz und einem Prescaler von 16384 theoretisch zählen müsste. Das sind 8000000 / (16384 * 0.5) = ca. 976. Die maximale Schrittzahl ist aber 256. 2 Hertz würden hingegen passen (~244 Schritte). Damit ist der Timer Counter Startwert 12 und die Überlauffrequenz ~2.0012 Hertz.

CTC Mode

Der Vollständigkeit halber hier noch der entsprechende Sketch im CTC Modus. Das funktioniert anders als beim ATmega328P. Der Interrupt wird bei einem OCR1A Compare Match ausgelöst. Bei einem Compare Match mit OCR1C wird der Counter zurückgesetzt. Dadurch, dass wir OCR1A und OCR1C denselben Wert zuweisen, kreieren wir im Prinzip ein PWM Signal mit einem Duty-Cycle von 100 %.

volatile int isrCounter = 0;

void setup(){ 
/* CTC-Mode -> TCNT1 set to 0 at compare match with OCR1C; 
 * PWM1A -> enables PWM mode based on comparator OCR1A in Timer/Counter1 and 
 *          the counter value is reset to 0 in the CPU clock cycle after a 
 *          compare match with OCR1C register value
 * prescaler = 16384;  
 */
    TCCR1 = (1<<CTC1) | (1<<PWM1A) | (1<<CS13) | (1<<CS12) | (1<<CS11) | (1<<CS10); 
    TIMSK = (1<<OCIE1A); // interrupt on OCR1A match
    OCR1A = 243; // at 8 MHz, compare match frequency is: ~2.0012 Hz
    OCR1C = 243; 
    DDRB |= (1<<PB4);  // pinMode(4, OUTPUT); 
} 

void loop() {} 

ISR(TIMER1_COMPA_vect){
    isrCounter++;
    if(isrCounter == 4){ // to achieve 0.5 Hz
        PORTB ^= (1<<PB4);  // toggle Pin 4
        isrCounter = 0;
    }
}

Timer Interrupts mit dem ESP32

Nun kommen wir zu den Timer Interrupts mit dem ESP32. Die gute Nachricht zuerst: Im Vergleich zu den AVR basierten Arduinos ist das Einrichten wesentlich einfacher.

Der ESP32 besitzt vier 64-Bit Timer (Ausnahme: der ESP32-C3 hat zwei) und für jeden von ihnen könnt ihr einen Interrupt einrichten. Der Timer zählt in der Systemfrequenz (also normalerweise 80 MHz) und kann durch einen Prescaler verlangsamt werden.

Einen einzigen Timer Interrupt einrichten

Hier ein einfacher Beispielsketch:

const int ledPin = 16;

hw_timer_t * timer = NULL;

void IRAM_ATTR timer_isr(){
    digitalWrite(ledPin, !digitalRead(ledPin));
}

void setup() {
    pinMode(ledPin, OUTPUT);
    byte timer_id = 0;
    unsigned int prescaler = 80;
    unsigned int limit = 1000000; // int is 4 bytes on the ESP32
    timer = timerBegin(timer_id, prescaler, true);
    timerAttachInterrupt(timer, &timer_isr, true);
    timerAlarmWrite(timer, limit, true);
    timerAlarmEnable(timer);
}

void loop() {}

Den Sketch habe ich übrigens mit verschiedenen ESP32 Development Boards getestet, und zwar mit einem ESP32-WROOM-32, einem ESP32C3, einem ESP32S3 und dem ESP32S Dev Kit C.

Erläuterungen zu esp32_1_timer.ino

  • hw_timer_t * timer = NULL; erzeugt die Variable timer vom Typ hw_timer_t (Hardware Timer) als Zeiger.
  • Mit timer = timerBegin(timer_id, prescaler, true); bekommt timer seine Bedeutung.
    • timer_id: Nummer des Timers, also 0, 1, 2 oder 3.
    • prescaler: Bei der Wahl des Prescalers seid ihr frei. Der Wert 80 macht Sinn, um in Mikrosekunden rechnen zu können.
    • true: Der Timer zählt aufwärts.
  • timerAttachInterrupt: Ordnet dem Timer eine ISR zu. Hier bedeutet das true, dass der Interrupt bei der steigenden Flanke ausgelöst wird, was beim Timer Sinn macht.
  • timerAlarmWrite: Definiert die Interruptbedingung, sprich den Zählerstand, bei dem der Interrupt ausgelöst wird. Das true bedeutet hier, dass der Timer nach Erreichen des Limits neu gestartet wird.

Vier Timer Interrupts einrichten

Im Prinzip ist es ganz einfach, vier Timer Interrupts einzurichten. Ihr müsst lediglich für jeden Timer eine eigene Variable, eine eigene ISR und die individuellen Interruptbedingungen festlegen. Um viel Schreibarbeit zu vermeiden, können wir Arrays verwenden. Das ist eigentlich auch einfach, aber da vielleicht nicht jeder weiß, wie man ein Array von Funktionen anlegt, gibt es hier noch einen Sketch dazu:

const int ledPin[] = {16, 17, 18, 19};

hw_timer_t * timer[4] = {NULL};

void IRAM_ATTR timer_isr_0(){
    digitalWrite(ledPin[0], !digitalRead(ledPin[0]));
}

void IRAM_ATTR timer_isr_1(){
    digitalWrite(ledPin[1], !digitalRead(ledPin[1]));
}

void IRAM_ATTR timer_isr_2(){
    digitalWrite(ledPin[2], !digitalRead(ledPin[2]));
}

void IRAM_ATTR timer_isr_3(){
    digitalWrite(ledPin[3], !digitalRead(ledPin[3]));
}

void (*timerISRs[4])(void) = {timer_isr_0, timer_isr_1, timer_isr_2, timer_isr_3};  // array of functions

void setup() {
    byte timer_id[] = {0,1,2,3};
    unsigned int prescaler = 80;
    unsigned int limit[] = {1000000, 500000, 250000, 125000}; // int size is 4 bytes n the ESP32!
    for(int i=0; i<4; i++){
        pinMode(ledPin[i], OUTPUT);
        timer[i] = timerBegin(timer_id[i], prescaler, true);
        timerAttachInterrupt(timer[i], timerISRs[i], true);
        timerAlarmWrite(timer[i], limit[i], true);
        timerAlarmEnable(timer[i]);      
    }
}

void loop() {}

 

Weitere nützliche Funktionen für die Timer des ESP32 findet ihr hier in der Arduino ESP32 API – Dokumentation.

Timer Interrupts mit dem ESP8266

Der ESP8266 besitzt zwei Timer, nämlich Timer0 und Timer1, wobei der Timer0 für WiFi benötigt wird und deswegen nicht zur freien Verfügung steht. Die Einrichtung eines Timer Interrupts auf dem ESP8266 ist wieder etwas weniger komfortabel, sofern man keine Bibliothek benutzt.

Timer Interrupts ohne Bibliothek

Hier erst einmal ein Beispiel für einen Timer Interrupt ohne Einsatz einer Bibliothek (getestet auf dem Wemos D1 mini Board).

const int ledPin = D2;

void IRAM_ATTR timer1ISR() {
    digitalWrite(ledPin, !digitalRead(ledPin));
}

void setup(){
    pinMode(D2, OUTPUT);
    timer1_attachInterrupt(timer1ISR); 
   
    /* Prescalers:
        TIM_DIV1      80MHz => 80 ticks/µs => Max: (2^23 / 80) µs =  ~0.105s
        TIM_DIV16     5MHz  => 5 ticks/µs => Max: (2^23 / 5) µs = ~1.678s
        TIM_DIV256    0.3125MHz => 0.3125 ticks/µs => Max: (2^23 / 0,3125) µs = ~26.8s
      Interrupt TYPE:
        TIM_EDGE      no other choice here
      Repeat?:
        TIM_SINGLE  0 => one time interrupt, you need another timer1_write(ticks); to restart
        TIM_LOOP    1 => regular interrupt 
    */
    timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);
    timer1_write(2500000); // 2500000 / 5 ticks/µs => 0.5s interval 
}

void loop(){}

Erläuterungen zu esp8266_timer.ino

Der Timer1 Counter hat eine Größe von 23 Bit. Das klingt viel, aber bei 80 MHz ist läuft er ca. alle 0.105 Sekunden über. Mit den Prescalern (TIM_DIV) 16 und 256 lässt sich diese Zeitspanne auf ~1.678 bzw. ~26.8 Sekunden verlängern, allerdings lässt sich damit nicht so gut rechnen.

  • timer1_attachInterrupt() definiert die ISR für den Interrupt.
    • timer1_detachInterrupt() deaktiviert den Interrupt.
  • Mit timer1_enable() legt ihr den Prescaler fest und ob der Timer einmalig oder dauerhaft aktiviert werden soll. 
    • timer1_disable() deaktiviert den Timer1.
  • timer1_write() startet den Countdown bei dem übergebenen Zählerstand. Der Counter zählt also rückwärts und löst den Interrupt bei 0 aus.
    • Mit timer1_read() könnt ihr den Timer1 Counter auslesen.

Timer Interrupts mit Ticker Bibliothek

Wer es einfacher haben möchte, sollte die Ticker Bibliothek verwenden. Diese müsst ihr nicht gesondert installieren, da sie Teil des Arduino ESP8266 Paketes ist. Ich denke, das Beispiel ist selbsterklärend: 

#include <Ticker.h>  
Ticker blinker;

const int ledPin = 2; 

void blinkLED(){
    digitalWrite(ledPin, !(digitalRead(ledPin)));    
}

void setup(){
    pinMode(ledPin, OUTPUT);
    //Initialize Ticker every 0.5s
    blinker.attach(0.5, blinkLED); // use attach_ms for milliseconds
}

void loop() {}

MCU-übergreifende Timer-Bibliotheken

Aber gibt es nicht doch MCU-übergreifende Bibliotheken für Timer Interrupts? Leider nein, zumindest nicht, dass ich wüsste. Was es aber gibt, sind MCU-übergreifende Timer-Bibliotheken, die auf Basis von millis() funktionieren, wie z. B. arduino-timer oder TickTwo. Ihr könnt sie über den Bibliotheksmanager der Arduino IDE installieren.

Der Nachteil dieser Lösungen ist, dass ihr aktiv abfragen müsst, ob die definierte Zeitspanne abgelaufen ist. Wenn ihr die Delays in den Hauptschleifen der folgenden Beispielsketche entkommentiert, werdet ihr feststellen, dass die Blinkfrequenz entsprechend sinkt. 

arduino-timer

Hier der Beispielsketch für arduino-timer:

#include <arduino-timer.h>
const int ledPin = 5;  

auto timer = timer_create_default(); 

bool blinkLED(void *) {
    digitalWrite(ledPin, !digitalRead(ledPin)); // toggle the LED
    return true; // in case you want to stop the timer choose false
}

void setup() {
    pinMode(ledPin, OUTPUT); // set LED pin to OUTPUT
    timer.every(500, blinkLED);
}

void loop() {
    timer.tick(); 
//    delay(2000);  // will delay the blinking 
}

TickTwo

Und hier der Beipielsketch für TickTwo:

#include "TickTwo.h"
const int ledPin = 5;

void blinkLED() {
  digitalWrite(ledPin, !digitalRead(ledPin));
}

TickTwo timer(blinkLED, 500);

void setup() {
    pinMode(ledPin, OUTPUT);
    timer.start();
}

void loop() {
    timer.update();
//    delay(2000); // will delay the blinking   
}

Anhang – ICP1 als externen Interruptpin nutzen

Zum Schluss komme ich noch einmal auf den Timer1 des ATmega328P zurück. Und zwar wollte ich zeigen, wie ihr den Input Capture Interrupt nutzen könnt, sodass sich ICP1 (PB0/Pin 8) wie ein externer Interruptpin verhält.

const int ledPin = 13;
const int icp1Pin = 8;
volatile bool event = false;
 
ISR(TIMER1_CAPT_vect){
   event = true;
}

void setup() {
    pinMode(icp1Pin, INPUT);
    pinMode(ledPin, OUTPUT); 
//    TCCR1B = (1<<ICES1);  // rising edge
    TCCR1B =  (1<<ICNC1) |  (1<<ICES1);  // with noise cancelling
    TIMSK1 = (1<<ICIE1);
}
 
void loop() {
    if(event){
        digitalWrite(ledPin, HIGH);
        delay(500);
        digitalWrite(ledPin, LOW);
        event = false;
    }
}

Die folgenden Bits sind hier relevant:

  • ICES1: Ist das Bit gesetzt, wird der Interrupt bei einer steigenden Flanke ausgelöst.
  • ICNC1: Setzt ihr dieses Bit, wird der Interrupt nur dann ausgelöst, wenn ICP1 über vier Takte hinweg die Interruptbedingung erfülllt (NC = noise cancelling).
  • ICIE1: aktiviert den Input Capture Interrupt.

Danksagung

Mein Beitragsbild habe ich aus verschiedenen Komponenten zusammengesetzt. Die Time-Out Geste habe ich Gerd Altmann zu verdanken, die Sanduhr stammt von Felipe und das Hintergrundbild (Arduino UNO) von federicoag, alles gefunden auf Pixabay.

5 thoughts on “Interrupts – Teil 3: Timer Interrupts

  1. Hallo Wolfgang,

    vielen Dank für deine hervorragende Webseite. Bin restlos begeistert und habe sehr viel dadurch gelehrnt, was ich auf anderem Wege mir nur schwer aneignen konnte. Ich habe moch einige Seiten von dir vor mir aber beabsichtige mir alles anzuschauen.

    Viele Grüße

    André

Schreibe einen Kommentar

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