Timer-API 3.0.x

In­halts­ver­zeich­nis

Handle

Allokation

timerBegin

Anwendungsbeispiel

timerEnd

Anwendungsbeispiel

timerGetFrequency

Taktgeber

timerStart

timerStop

Counter

timerRead

timerReadMicros

timerReadMilis

timerReadSeconds

timerWrite

timerRestart

Interrupt

timerAlarm

timerAttachInterrupt

timerAttachInterruptArg

timerDetachInterrupt

Sinnvolle Vorgehensweise

ESP-IDF

Beispiel-Programm

Download

Die offizielle Dokumentation des Timer-API findet man unter https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/timer.html.

Die genaue Funktion der Timer findet man im Technischen Referenzhandbuch im Kapitel Timer Group (TIMG) (Kapitel 18 im Technischen Referenzhandbuch Version 5.2 für den ESP32).

Ein Timer ist eigentlich nicht mehr als ein Zähler (Counter) der die Impulse eines einstellbaren Taktgebers zählt. Die Zähler besitzen 64 Bit (54 Bit beim ESP32-C3), können also bis 18.446.744.073.709.551.615 ≈ 18 Trillionen (18.014.398.509.481.983 ≈ 18 Billarden) zählen, bevor sie überlaufen. Wenn man den Taktgeber auf 1 MHz einstellt, also μs-Auflösung besitzt, dauert es knapp 600 Tsd. Jahre, bevor der Zähler überläuft. Beim ESP32-C3 sind auch immerhin noch knapp 600 Jahre.

Das Arduino-API verbirgt die vielen Details, die zur Konfiguration des Timers notwendig sind.

Hinweis: Alle Angaben beziehen sich auf die Version 3.0.x (konkret 3.0.4) der Arduino-Board-Library für den ESP32 und dem ESP-IDF Version 5.1.4.

Hinweis: Die Benutzung der Arduino-Funktionen innerhalb einer Interrupt-Service-Routine ist kritisch. Sie besitzen nicht das IRAM_ATTR-Flag.

Grundsätzlich erlaubt es der ESP auch, Interrupts ohne IRAM_ATTR zu realisieren. Bei der Registrierung eines Interrupts können Flags mitgegeben werden, die das Verhalten des IRQ bestimmen. Eines dieser Flags ist das IRAM-ATTR Flag, mit dem man dem System sagt, dass die Funktion im IRAM liegt. Ist das Flag nicht gesetzt, so wird die Funktion während eines Flash-Zugriffs nicht aufgerufen. Bei ISR, die auch mal eine Verzögerung vertragen können, könnte man das machen.

Siehe auch IRAM (Instruction RAM) und Concurrency Constraints for Flash on SPI1.

Handle

Das Timer-API arbeitet mit einem Handle-Mechanismus. Die Methode timerBegin stellt ein Handle bereit, d.h. einen Zeiger auf eine interne, nicht näher definierte Struktur, in der das System alle benötigten Daten zu Ansteuerung des Timers verwahrt. Dieses Handle muss bei allen Methoden zur Manipulation des Timers angegeben werden.

Mit der Methode timerBegin wird ein neues Handle für einen neuen Timer angefordert. Mit timerEnd wird das Handle ungültig und der zugehörige Timer wieder freigegeben. Beim nächsten Aufruf von timerBegin kann der freigegebene Timer wieder zugewiesen werden.

Die Methoden zur Ansteuerungen des Timers fangen einen nullptr, also einen nicht initialisierten Pointer, ab. Die Verwendung von Handles, deren Timer mit timerEnd freigegeben wurden, werden nicht ausgeführt oder führen zu einem Programmabbruch (Guru Mediation Error). Es wird jedoch eine entsprechende Meldung in das Log geschrieben.

Allokation

Mit der Methode timerBegin wird ein neues Handle für einen neuen Timer angefordert und die Frequenz des Taktgebers eingestellt. Das System ermittelt selbständig den nächsten freien Timer. Es können so viele Timer angefordert werden, wie der Chip Hardware-Timer besitzt.

ESP32 SoC Anzahl Timer
ESP32 4
ESP32-S2 4
ESP32-S3 4
ESP32-C3 2
ESP32-C6 2
ESP32-H2 2

 Mit timerEnd wird das Handle ungültig und der zugehörige Timer wieder freigegeben.

timerBegin

hw_timer_t * timerBegin(uint32_t frequency);

Parameter frequency: Angabe der Taktfrequenz in Hz.

timerBegin versucht einen freien Timer zu allokieren und die Frequenz dessen Taktgebers möglichst exakt gemäß der Angabe einzustellen. Wenn ein freier Timer allokiert werden konnte, wird ein gültiges Handle (ein Zeiger auf die interne Struktur hw_timer_t) zurück geliefert. Sind alle Timer belegt, wird null (nullptr) zurück gegeben. Fehlergründe für das Fehlschlagen einer Allokation werden in das Log geschrieben.

Der Counter des Timers wird auf 0 gesetzt und der Taktgeber wird gestartet. Der Zähler des Timers beginnt hochzulaufen. Die Ausführung dieser Funktion dauert ca. 10..20 μs.

Das Einstellen der Frequenz ist nicht immer exakt möglich. Mit timerGetFrequency kann die tatsächlich eingestellte Frequenz abgerufen werden. Je größer die Frequenz gewählt wird, desto größer ist  die mögliche Ungenauigkeit.

Bei einem ESP32 mit Standardeinstellungen ist die kleinste einstellbare Frequenz 1.221 Hz, die größte ist 40.000.000 Hz (=40 MHz). Wird dieser Bereich verlassen, wird kein Timer allokiert (Rückgabewert nullptr). Im Log erhält man die Fehlermeldung:

Error timerBegin

Jeder Timer verwendet den APB-Takt (APB_CLK, normalerweise 80 MHz) als Basistakt. Dieser Takt wird dann durch einen durch einen 16-Bit-Prescaler heruntergeteilt, der den Takt  erzeugt. Die Prescaler dividiert den APB-Takt um ganzzahlige Werte zwischen 2 (≙ 40 MHz) und 65536 (≙ 1221 Hz).

Anwendungsbeispiel

hw_timer_t* timer1 = nullptr;   // Timer


void setup() {

   timer1 = timerBegin(1'000'000); // Init the timer. Frequency 1 MHz
   if (timer1)
      Serial.printf("Timer 1 frequency is %lu Hz.\n", timerGetFrequency(timer1)); 
   else {
      Serial.println("Cannot initialize the timer.\nProgram stops.");
      Serial.flush(); // while(true) blocks output
      while (true);
   }
...

timerEnd

void timerEnd(hw_timer_t * timer);

timerEnd stoppt den angegebenen Timer und gibt ihn wieder frei. Der mehrfache Aufruf dieser Funktion ist unkritisch.

Fehlergründe für das Fehlschlagen dieser Funktion werden in das Log geschrieben.

Anwendungsbeispiel

// Start timer1 with new frequency.
timerEnd(timer1);
timer1 = timerBegin(frequency);

timerGetFrequency

uint32_t timerGetFrequency(hw_timer_t * timer);

timerGetFrequency liefert die tatsächlich eingestellte Frequenz des Taktgebers des angegebenen Timers in Hz.

Hinweis: Der in der Dokumentation angegebene Rückgabewerttyp uint16_t ist falsch.

Taktgeber

timerStart und timerStop startet bzw. stoppt den Taktgeber ohne den Counter zu beeinflussen.

timerStart

void timerStart(hw_timer_t * timer);

Parameter: timer Handle.

imerStart startet den Taktgeber. Der Counter zählt mit dem zuletzt vorhandenen Wert weiter.

timerStop

void timerStop(hw_timer_t * timer);

Parameter: timer Handle.

timerStop stoppt den Taktgeber. Der Counter zählt nicht weiter.

Counter

timerRead

uint64_t timerRead(hw_timer_t * timer);

Parameter: timer Handle.

Liest den aktuellen Zählerstand aus.

timerReadMicros

uint64_t timerReadMicros(hw_timer_t * timer);

Parameter: timer Handle.

Liest den aktuellen Zählerstand aus und rechnet ihn anhand der eingestellten Frequenz in Microsekunden um.

timerReadMilis

uint64_t timerReadMilis(hw_timer_t * timer);

Parameter: timer Handle.

Liest den aktuellen Zählerstand aus und rechnet ihn anhand der eingestellten Frequenz in Millisekunden um.

timerReadSeconds

double timerReadSeconds(hw_timer_t * timer);

Parameter: timer Handle.

Liest den aktuellen Zählerstand aus und rechnet ihn anhand der eingestellten Frequenz in Sekunden um.

timerWrite

void timerWrite(hw_timer_t * timer, uint64_t val);

Parameter: timer Handle.

Beschreibt den Counter mit einem neuen Wert. Von diesem an wird dann weiter gezählt.

timerRestart

void timerRestart(hw_timer_t * timer);

Parameter: timer Handle.

Schreibt 0 in den Counter. Identisch mit void timerWrite(hw_timer_t * timer, 0);

Interrupt

Mit timerAlarm wird der Interrupt konfiguriert. timerAttachInterrupt und timerAttachInterruptArg legen die Interrupt-Service-Routine fest und geben die Interrupt-Auslösung frei. timerDetachInterrupt sperrt die Interrupt-Auslösung wieder.

timerAlarm

void timerAlarm(hw_timer_t * timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count);

Parameter:

Konfiguriert die Interrupt-Auslösung.

Wenn autoreload den Wert false hat, wird der Interrupt nur einmal ausgelöst (One-Shot-Timer) und der Zähler zählt unbeeinflusst weiter. Der Interrupt wird auch dann nicht ein zweites mal ausgelöst, wenn der Zähler auf einen Wert kleiner als der Alarmwert zurück gesetzt wird.

Wenn autoreload den Wert true hat, wir nach jedem Interrupt der Zähler auf 0 zurück gesetzt und beim erneuten Erreichen des Alarmwerts wird wieder ein Interrupt ausgelöst. Der Parameter reload_count ist in der Version 3.0.4 der Arduino-Library auf Basis des ESP-IDF 5.1.4 wirkungslos.

timerAttachInterrupt

void timerAttachInterrupt(hw_timer_t * timer, void (*userFunc)(void));

Parameter:

Registriert die Interrupt-Service-Routine und gibt die Interrupt-Auslösung frei. Wenn der Timer bereits gestartet ist, wird er zum festlegen der ISR kurz gestoppt.

Die ISR muss diese Form haben:

IRAM_ATTR void handleTimerInterrupt(); 

Wenn der Counter den durch timerAlarm gesetzten gesetzten Alarmwert bereits überschritten hat, wir kein Interrupt ausgelöst.

timerAttachInterruptArg

void timerAttachInterruptArg(hw_timer_t * timer, void (*userFunc)(void*), void * arg);

Parameter:

Registriert die Interrupt-Service-Routine und gibt die Interrupt-Auslösung frei. Außerdem wird ein Zeiger auf beliebiges Element übergeben. Dieser Zeiger wird der ISR als Argument übergeben. Typischerweise kann man hier den this-Pointer einer Klasseninstanz übergeben.

Wenn der Timer bereits gestartet ist, wird er zum festlegen der ISR kurz gestoppt.

Die ISR muss diese Form haben:

IRAM_ATTR void handleTimerInterrupt(void* arg); 

Wenn der Counter den durch timerAlarm gesetzten gesetzten Alarmwert bereits überschritten hat, wir kein Interrupt ausgelöst.

 

timerDetachInterrupt

void timerDetachInterrupt(hw_timer_t * timer);

Parameter: timer Handle.

Sinnvolle Vorgehensweise

Funktion Wirkung
timerBegin Handle für Timer anfordern. Problem: der Taktgeber ist freigegeben, d.h. der Counter zählt hoch.
timerStop Taktgeber anhalten.
timerRestart / timerWrite Zähler auf definierten Anfangswert 0 setzen. Alternativ: auf einen Wert setzen, der kleiner als der Alarmwert ist. Dies verkürzt die Dauer bis zum ersten Interrupt.
timerAlarm Counter-Wert für Interrupt festlegen (Alarmwert)
timerAttachInterrupt Adresse der ISR festlegen und Interrupt freigeben.
timerStart Timer starten.

ESP-IDF

siehe General Purpose Timer (GPTimer)

Beispiel-Programm

Mir dem folgenden Programm ESP32-3.0.x-TimerTest.ino lassen sich Timer-Funktionen über die serielle Schnittstelle auslösen. Es wird laufend der Zählerstand eines Timers und die Anzahl der durchgeführten Interrupts angezeigt.

 Hinweise:

/*
 Name:		ESP32-3.0.x-TimerTest.ino
 Created:		13.08.2024
 Author:	   	Ullis Roboter Seite


 Dieses Programm dient zum Test des neuen ESP32 Timer-API in der Board-Library Version 3.0.x.
 Das Programm ezeugt in regelmäßigen Abständen einen Interrupt in dem jeweils ein Pin den Zustand wechselt.

 This program is used to test the new ESP32 Timer-API in the board library version 3.0.x.
 The program generates an interrupt at regular intervals in which a pin changes its state.

 API docs: https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/timer.html
  */

#include <UrsEsp.h>
#include "UrsStringHelper.h"

using namespace UrsCPU;

hw_timer_t* timer1 = nullptr;   // Timer
volatile uint32_t interruptCount = 0; // Interrupt-Counter
uint64_t nextDiplayAt;          // When to generate output (millis)


// interrupt handler
IRAM_ATTR void handleTimerInterrupt()   {
   interruptCount = interruptCount + 1;  // count changes/second
}

void setup() {
   Serial.begin(115200);
   Serial.println("\nESP32-3.0.x-TimerTest");

   // Check reset cause
   auto resetReason = UrsESP.getResetReason(0);
   Serial.printf("CPU0 reset reason: %i (%s) %s\n", resetReason, UrsESP.getResetReasonName(resetReason).c_str(), UrsESP.getResetReasonDescription(resetReason).c_str());

   // If not power-on reset then stop.
   if (resetReason != 1) // Otherwise a new reset will be performed continuously.
      while (1);


   timer1 = timerBegin(1'000'000); // Init the timer. Frequency 1 MHz
   if (timer1)
      Serial.printf("Timer 1 frequency is %lu Hz.\n", timerGetFrequency(timer1));
   else {
      Serial.println("Cannot initialize the timer.\nProgram stops.");
      Serial.flush(); // while(1) blocks output
      while (1);
   }

    Serial.println("Terminate input with (CR)LF");

   Serial.println("'f9999' set frequency and start the timer");
   Serial.println("'start' start the timer");
   Serial.println("'stop' stop the timer");
   Serial.println("'restart' restart the timer");
   Serial.println("'i9999' restart the timer and enable interrupts");
   Serial.println("'o9999' restart the timer and enable one-shot interrupt");
   Serial.println("'n' stops interrupts");

   while (Serial.available()) // clear buffers
      Serial.read();

   SHelper.setThousandSeparator('.');
   nextDiplayAt = millis() + 1000;
}


void loop() {
   if (millis() > nextDiplayAt) {
      Serial.printf("%15s %3lu\n",SHelper.valToStr(timerRead(timer1)).c_str(), interruptCount);
      nextDiplayAt += 1000;
   }


   // handle Serial input
   if (!Serial.available())
      return;

   String inp = Serial.readStringUntil('\n');
   inp.toLowerCase();

   // Frequency setup
   if (inp.startsWith("f")) {
      inp.replace(".", ""); // German thousand separator
      inp.replace(",", ""); // English thousand separator
      inp.replace("'", ""); // C++ thousand separator
      uint32_t frequency = inp.substring(1).toInt(); // Get Value
      timerEnd(timer1);
      timer1 = timerBegin(frequency);
      if (timer1)
         Serial.printf("\nTimer 1 frequency is %s Hz.\n", SHelper.valToStr(timerGetFrequency(timer1)).c_str());
      else {
         Serial.println("\nCannot initialize the timer.\nProgram stops.");
         Serial.flush(); // while(1) blocks output
         while (1);
      }
      return;
   }

   // Timer stop
   if (inp.startsWith("stop")) { // German "stopp"
      timerStop(timer1);
      Serial.println("\nTimer 1 stopped.");
      return;
   }

   // Timer start
   if (inp == "start") {
      timerStart(timer1);
      Serial.println("\nTimer 1 started.");
      return;
   }

   // Timer restart
   if (inp == "restart") {
      timerRestart(timer1);
      auto t = timerRead(timer1);
      Serial.println("\nTimer 1 restarted.");
      Serial.println(SHelper.valToStr(t));
      return;
   }


   // Interrupt setup
   if (inp.startsWith("i")) {
      inp.replace(".", ""); // German thousand separator
      inp.replace(",", ""); // English thousand separator
      inp.replace("'", ""); // C++ thousand separator
      uint32_t alarm = inp.substring(1).toInt(); // Get Value

      timerStop(timer1);
      timerRestart(timer1);
      timerAlarm(timer1, alarm, true, 0);
      timerAttachInterrupt(timer1, handleTimerInterrupt);
      interruptCount = 0;

      Serial.printf("\nInterrupts at %s.\n", SHelper.valToStr(alarm).c_str());

      timerStart(timer1);
      return;
   }

   // One shot setup
   if (inp.startsWith("o")) {
      inp.replace(".", ""); // German thousand separator
      inp.replace(",", ""); // English thousand separator
      inp.replace("'", ""); // C++ thousand separator
      uint32_t alarm = inp.substring(1).toInt(); // Get Value

      timerStop(timer1);
      timerRestart(timer1);
      timerAlarm(timer1, alarm, false, 0);
      timerAttachInterrupt(timer1, handleTimerInterrupt);
      interruptCount = 0;

      Serial.printf("\nOne shot interrupt at %s.\n", SHelper.valToStr(alarm).c_str());

      timerStart(timer1);
      return;
   }

   // Detach Interrupt
   if (inp == "n") {
      timerDetachInterrupt(timer1);
      Serial.println("\nInterrupts stopped.");
      return;
   }
   Serial.println("Invalid command");
}

Beispiel-Ausgabe:

Beispiel-Programm

Download

Dass Zip-Archiv ESP32-3.0.x-TimerTest enthält das komplette Visual Micro Projekt. Benutzer der Arduino-IDE verwenden nur die Quelldateien im Verzeichnis ESP32-3.0.x-TimerTest.