ESP32-Hardware-, Software- und LVGL-Timer
Was sind Timer eigentlich und wozu braucht man sie?
Die Idee der Timer sind uralt. Es gab sie schon in der MOS 6502 CPU aus den 1970ern. Im Grunde sind Timer Zähler, die mit jedem Taktzyklus hochzählen. Außerdem kann man einen Alarm-Zählerstand (und eine Speicheradresse eines Unterprogrammes) definieren, der einen Interrupt (IRQ) auslöst, wenn dieser Zählerstand erreicht wird. Dann wird die Ausführung des normalen Programmes, dass die CPU gerade ausführt, kurz unterbrochen, die Interrupt-Routine kurz ausgeführt, und danach wird wieder in das normale Programm zurückgekehrt und es wird ganz normal weitergemacht. Siehe auch Artikel 8-Bit-Breadboard-Computer auf Basis einer 6502-CPU - selektives Interrupt Handling über den VIA 6522.Mit Timern kann man zum Beispiel auch wiederholt etwas aufrufen, etwa, wenn man bei einem Computer ständig abrufen will, ob eine Taste gedrückt wurde. Dann springt der Timer z. B. alle Millisekunde in die Interrupt-Routine checkKey() und schaut dann dort nach, ob eine Taste niedergedrückt wurde. Die Interrupt-Routine darf nicht lange dauern, sonst würden zum Beispiel Animationen sichtlich ruckeln, weil der Computer gerade mit etwas anderem (eben der Interrupt-Routine) beschäftigt ist. Für checkKey() wäre das also nur: Leitungen überprüfen, gedrückten Key oder NULL in eine Speicherstelle schreiben. Und evtl. noch einen "Unterbrecher-Key" - beim Commodore 64 war das RUN/STOP-RESTORE - mit dem man die Ausführung des übergeordneten Programmes (beim C64 den BASIC-Interpreter) abbrechen kann.
Eine Timer-Wiederholung realisiert man, in dem man nach einem abgelaufenen Timer automatisch einen neuen startet. Mit ein bisschen Software drumherum kann man natürlich auch "rufe alle x Sekunden die Routine y auf" schreiben. Aber Grundlage sind immer Hardware-Timer: Zähler, die rauf- oder runterzählen. Meist werden sie auf einen bestimmten Wert (etwa der Zeit in Mikrosekunden bis zur Auslösung) gesetzt und dann wird heruntergezählt, weil es in Maschinensprache schneller geht, gegen Null als gegen einen Wert zu prüfen.
Hardware-Timer auf dem ESP32
Genau diesen rudimentären Zählern entsprechen die Hardware-Timer des ESP32. Davon hat der ESP32 ingesamt 4: zwei Timer-Gruppen (TIMERG0, TIMERG1), je Gruppe 2 Timer. Jeder Timer ist ein 64-Bit Zähler (mit 16-Bit Prescaler) und kann hoch- oder runter zählen, Alarme auslösen und optional Auto-Reload verwenden. Der Standard-Taktgeber ist dabei APB_CLK (typischerweise 80 MHz).Der Prescaler, auch Divider genannt, definiert, wie oft gezählt wird. Ist der Prescaler auf 1, dann jeden einzelnen Taktzyklus. Das wäre 80 mal pro Mikrosekunden. Das macht natürlich überhaupt keinen Sinn, weil die MCU (Micro Controller Unit, die CPU des Mikrocontrollers) dann nur noch mit dem Timer beschäftigt wäre. Aber ein Divider von 80 wäre möglich und würde so alle Mikrosekunde (alle 80 Taktzyklen) den Zähler eins herunterzählen. Bei einem Alarm würde man den Zähler z. B. bei 1000 loslaufen lassen und hätte dann alle Millisekunde einen Alarm, bei dem ein Interrupt ausgelöst und eine Routine ausgeführt werden kann.
Der Prescaler hat übrigens eine Breite von 16 Bit (ulong), was einen Maximalwert von 65'536 (im Bereicht von 2...65536) ergibt. Bei einem Divider von 65'536 würde nur jeder 65'536 Taktzyklus gezählt, dementsprechend immerhin noch 1220.7 mal pro Sekunde.
Die Timer-Zähler sind 64-Bit breit. Bei einem Divider von 80 wäre also erst ein Überlauf in 264 (ca. 18.4 Trillionen) Mikrosekunden zu erwarten, das sind 584'554 Jahre. Darüber müssen wir uns also keine Sorgen machen ;)
Bei Auto-Reload wird automatisch ein konfigurierter Start-Wert wieder geladen. Nach einer Auslösung ist der Alarm allerdings standardmäßig deaktiviert und muss wieder eingeschaltet werden. Aber im Prinzip entspricht das einer Wiederholung.
Hardware-Timer bieten gegenüber Software-Timern (die komfortabler sind und gleich noch besprochen werden) eine höhere Präzision, sind also ideal, um zum Beispiel bestimmte Frequenzen zu erzeugen.
Befehle für Hardware-Timer
Wie benutzt man nun Hardware-Timer? Nun, als erstes müssen wir erst einmal einen samt der Randbedingungen in einem timer_config_t-Struct definieren und dann mit timer_init() initialisieren:esp_err_t timer_init(timer_group_t group_num, timer_idx_t timer_num, const timer_config_t *config)
group_num ist die Timergruppennummer: 0 für TIMERG0 oder 1 für TIMERG1
timer_num ist der Timer-Index: 0 für hw_timer[0] und 1 für hw_timer[1]
timer_config_t ist ein Zeiger auf die Initialisierungsparameter des Timers mit
timer_alarm_t alarm_en: Timer-Alarm de-/aktivieren, Enum mit
TIMER_ALARM_DIS = 0: disabled oder
TIMER_ALARM_EN = 1: enabled oder
TIMER_ALARM_MAX
timer_start_t counter_en: Zähler de-/aktivieren, Enum mit
TIMER_PAUSE = 0: Timer pausieren oder
TIMER_START = 1: Timer aktiv
timer_intr_mode_t intr_type: Interrupt Modus, Enum mit
TIMER_INTR_LEVEL = 0: Interrupt mode ist level mode
TIMER_INTR_MAX
timer_count_dir_t counter_dir: Zähl-Richtung (hoch / Runter), Enum mit
TIMER_COUNT_DOWN = 0
TIMER_COUNT_UP = 1
TIMER_COUNT_MAX
timer_autoreload_t auto_reload: Automatisches Neuladen des Timers, Enum mit
TIMER_AUTORELOAD_DIS = 0
TIMER_AUTORELOAD_EN = 1
TIMER_AUTORELOAD_MAX
uint32_tdivider: Teiler des Zählertakts. Der Prescaler reicht von 2 bis 65536.
Was wäre ein Timer ohne einen Alarm? Eine verdammt genaue Uhr, die wir mit timer_get_counter_value() abfragen können. Aber eigentlich wollen wir ja eine Interrupt-Routine nach einer bestimmten Zeit ausführen, nicht? Dazu müssen wir einen Alarm definieren.
esp_err_t timer_set_alarm_value(timer_group_t group_num, timer_idx_t timer_num, uint64_t alarm_value)
group_num: wie bei timer_init()
timer_num: wie bei timer_init()
alarm_value: Der Zähler, bei dem etwas passieren soll (max. 264)
esp_err_t timer_isr_callback_add(timer_group_t group_num, timer_idx_t timer_num, timer_isr_t isr_handler, void *arg, int intr_alloc_flags)
group_num: wie bei timer_init()
timer_num: wie bei timer_init()
isr_handler: Adresse der CallBack Function
arg: Parameter für die CallBack Function
intr_alloc_flags: Flags, die angeben, in welcher Speicherart sich die CallBack Function befindet. Genaueres in esp_intr_alloc.h
ESP32 High-resolution Software Timer
Es gibt ein Software-Timer-System, dass uns viel Arbeit bei der Definition abnimmt, eine Auflösung von 1 Mikrosekunden bei einem Zähle bis 264 bietet. Intern verwendet das Software-Timer-System natürlich auch Hardware-Timer. Aber es schafft es, gleich mehrere Software-Timer auf einen Hardware-Timer zu legen und ist damit ressourcenschonender. Es nutzt Callback-Funktionen von einem speziellen esp_timer-Task auf und liegt damit im Task-Kontext statt im ISR-Kontext, es sei denn wählt explizit ISR-Dispatch.Ein Software-Timer legen wir an mit:
esp_timer_create(&esp_timer_create_args_t, &handle)
esp_timer_create_args_t ist ein Struct mit
esp_timer_cb_t callback: Die CallBack-Function, die bei Ablauf des Timer aufgerufen werden soll.
void *arg: Die Parameter für die CallBack-Function
esp_timer_dispatch_t: Dispatch-Methode:
Dispatch callback vom Task or ISR; wenn nicht spezifiziert: esp_timer task
const char *name?: Name des Timer, der in esp_timer_dump() ausgegeben wird
bool skip_unhandled_events: Unvorgesehene Ereignisse werden ignoriert
handle: hier wird das Timer-Handle zurückgegeben, auf das wir bei weiteren Calls Bezug nehmen.
Schon viel einfacher, nicht? Und für die allermeisten Anwendungen vollkommen ausreichend.
LVGL-Timer
Ich will noch auf eine weitere Timer-Variante zu sprechen kommen, die LVGL-Timer. LVGL ist ein Dialog-Framework für den ESP32 und andere Mikrocontroller, die einem viel Arbeit beim Design von Dialogen abnehmen kann.Man definiert Buttons und andere Steuerelemente und muss dann nur noch regelmäßig eine LVGL-Routine aufrufen. Diese sorgt dann dafür, dass eine kleine Animation beim Niederdrücken eines Buttons ausgeführt wird und das Tap-Ereignis an eine eigene Routine weitergeleitet wird. Dort wird dann ein eigener Programm-Code ausgeführt und wenn der beendet ist, übernimmt das LVGL-System wieder und lässt den Button mit einer kleinen Animation wieder los.
Wenn der eigene Programm-Code jetzt länger dauert, dann bleibt auch der Button länger niedergedrückt. Eigentlich kein Problem. Was aber, wenn das Dialogsystem auf weitere Buttons reagieren soll? Das geht dann nicht.
Und diesen Fall hatte ich bei meinem On/Off-Counter, den ich mittels LVGL auf einem Cheap Yellow Display mit ESP32 programmiert habe: Da gibt es einen Button "Go", bei dem dann der Countdown startet. Bei Beginn und Ende des Countdown soll etwas passieren. Aber es soll auch möglich sein, den Dialog weiter zu benutzen, um den Countdown abzubrechen.
Dazu benötigt es dann etwas, das parallel läuft, und dafür ist ein Timer wie geschaffen. Der soll alle Sekunde eine Callback-Funktion aufrufen, die den Countdown-Zähler runterzählt und die Anzeige aktualisiert. Ansonsten funktioniert der Hauptdialog ganz normal weiter und ich kann Abbruch drücken, das in einer globalen Variablen speichern und dann den Timer stoppen.
Wir besprechen das gleich anhand eines praktischen Beispiels, und auch, wie man mit einem zweiten Timer ein PowerSave oder Sleep-Modus nach einer bestimmtes Zeit Nichtstun realisieren kann. Doch vorher noch kurz der Grund, warum ich hier die LVGL-Timer benutze und einen Code-Überblick:
Die LVGL-Timer und nicht die normalen ESP32-Timer benutze ich, damit das LVGL-System Kenntnis davon hat und weiß, dass diese Timer existieren. LVGL kann dann die Timer viel besser in sein Öko-System integrieren und zum Beispiel für ruckelfreie Animationen sorgen. LVGL-Timer laufen in einem eigenen Timer-Subsystem und werden nicht preemptiv ausgeführt (ein Timer kann z. B. keinen anderen unterbrechen) und werden in lv_timer_handler() ausgeführt. Damit sind LVGL-Timer dafür gedacht, LVGL-spezifische Arbeiten sicher auszuführen (UI-Erzeugung, Redraw, Input-Poll, Animationen etc.).
Die LVGL-Timer-Callbacks laufenwie gesagt im Kontext von lv_timer_handler() (also in dem Thread/Task, der lv_timer_handler() aufruft). Weil LVGL-Timer non-preemptiv sind, dürfen sie LVGL-APIs direkt aufrufen (anders als externe Timer, die nicht LVGL-safe sein müssen). Was bedeutet, dass man in LVGL-Timer-Callbacks den LVGL-Dialog verändern kann.
Die LVGL-Timer funktionieren aber ähnlich der ESP32-Timer und ich glaube auch, dass die LVGL-Timer auf den ESP32-Timern basieren, halt mit ein bisschen "Drumherum", um das LVGL "geschmeidig" zu halten. Allerdings ist die Zeitbasis eine Millisekunde (nicht eine Mikrosekunde wie bei ESP32-Timern) und es muss regelmäßg lv_tick_inc(ms) ausgerufen werden. Aber solange das LVGL-Dialogsystem am Laufen gehalten wird, läuft das Alles mit.
Eine Timer legen wir wir folgt an:
lv_timer_t *TimerName = lv_timer_create(lv_timer_cb_t cb, uint32_t period_ms, void *user_data)
TimerName: Der Handle für den Timer
cb: Die Callback-Funktion
period_ms: Die Zeitspanne in Millisekunden für den Timer, die er wiederholend aufgerufen wird
user_data: zusätzliche Informationen, die der Programmierer hier speicher kann
zu pausieren: void lv_timer_pause(lv_timer_t *timer)
ein- und auszuschalten: void lv_timer_enable(bool en)
weiterlaufen zu lassen: void lv_timer_resume(lv_timer_t *timer)
wieder von Null losrennen zu lassen: void lv_timer_reset(lv_timer_t *timer)
zu löschen: void lv_timer_delete(lv_timer_t *timer)
Das ist auch schon alles, was man an LVGL-Timer-Befehler benötigt. Will man den Timer nur einmal laufen lassen, kann man ans Ende der Callback-Funktion void lv_timer_delete(lv_timer_t *timer) schreiben. Damit ist der Timer dann gelöscht und wird nicht wieder ausgeführt.
Kommen wir nun zu einer praktischen Anwendung von LVGL-Timern, eben dem schon erwähnten On/Off-Timer-Projekt, um meine Mikrowelle für eine bestimme Zeit einzuschalten und den Bildschirm nach einer bestimmten Zeit auszuschalten, damit sich der Dialog nicht einbrennt und um Strom zu sparen.
Praktisches Beispiel für mein On/Off-Timer Projekt

Ich benutze Timer in meinem Projekt ESP32-CYD (Cheap Yellow Display) Projekt mit LVGL: 433 MHz On/Off-Timer Funk-Steckdosen-Steuerung, das uns hier als praktisches Beispiel dienen soll.
Rechts eine Abbildung des Dialoges, damit klarer wird, was durch die Timer gewollt ist, nämlich zwei Dinge:
1. Wenn Go gedrückt wird, soll switchOn() aufgerufen werden, um die Mikrowelle einzuschalten. Gleichzeitig wird auf dem grünen Go ein rotes Stop und der Go-Button wird zu einem Abbruch-Button.
Timer 1 soll jetzt sekündlich den Countdown-Zähler herunterzählen... von 1m26s auf 1m25s auf 1m24s usw. usf. und den Zähler natürlich auch anzeigen.
Erreicht der Zähler Null, dann wird switchOff() aufgerufen und die Mikrowelle geht aus. Gleichzeitig wird Stop wieder zu Go und der Dialog ist klar für eine neue Runde.
Wird allerdings während des Countdown der Abbruch-Button betätigt, dann wird sofort switchOff() aufgerufen und die Mikrowelle ausgeschaltet. Der Countdown-Timer und die Countdown-Anzeige werden gelöscht.
2. Es wird ein 30-Sekunden-Timer gestartet. Jedesmal, wenn man auf Display tappt, wird Timer2 auf Null gesetzt und muss wieder 30 Sekunden erreichen. Nach 30 Sekunden Nichtstun wird dann der Timer2 gelöscht und der PowerSave-Modus eingeleitet. Ist der PowerSave zuende, weil jemand wieder auf den Touchscreen tappt, startet ein neuer 30 Sekunden-Timer2.
Countdown-Timer definieren
Damit wir auf die Timer von überall Zugriff haben, müssen diese als globale Variablen definiert sein:lv_timer_t * SleepTimer = NULL;
const uint32_t SleepTimeout = 30000; // 30 Sekunden
lv_timer_t * CountdownTimer = NULL;
Countdown-Timer für die Zeit, nach der Bildschirm ausgeht und das CYD in PowerSave geht
Für den Auto-Sleep nach 30 Sekunden Nicht-Eingabe sind folgende Funktionen zuständig:Einmal die Ausführung selbst, die nach 30 Sekunden stattfindet:
void sleep_timer_cb(lv_timer_t * timer) {
lv_timer_del(SleepTimer);
SleepTimer=NULL;
enter_powersave();
}
void setup_auto_sleep() { // beim ersten mal wird der Timer neu gestartet
reset_sleep_timer();
}
void reset_sleep_timer() {
// Alten Timer löschen, falls vorhanden
if(SleepTimer != NULL) {
lv_timer_del(SleepTimer);
SleepTimer=NULL;
delay (10);
}
// Neuen Timer starten
SleepTimer = lv_timer_create(sleep_timer_cb, SleepTimeout, NULL);
if(SleepTimer != NULL) lv_timer_set_repeat_count(SleepTimer, 1); // Nur einmal ausführen
}Countdown-Timer für die Zeit, die die Mikrowelle läuft
void countdown_timer_cb(lv_timer_t * timer)
{
int m=0; int s=0;
SeksToGo--;
// Display aktualisieren
refreshCounterFromSeksToGo();
// Countdown beendet?
if(SeksToGo <= 0) {
lv_timer_del(CountdownTimer);
CountdownTimer = NULL;
CountdownActive = false;
switchOff();
for (int i = 0; i <5; i++) { // Essen Fertig!
delay(1000);
beep(1000);
}
// Button wieder grün und Go
lv_obj_set_style_bg_color(btnGo, lv_color_hex(0x00c000), 0);
lv_label_set_text(lab_btnGo, "Go");
Counter="";
lv_label_set_text(lab_counter, Counter.c_str());
refreshLVGL();
}
reset_sleep_timer(); // Bei jeder Aktivität und wenn aktiv bleiben soll: Sleep-Timer zurücksetzen
}
void evt_btn(lv_event_t *e) {
...
reset_sleep_timer(); // Bei jeder Aktivität und wenn aktiv bleiben soll: Sleep-Timer zurücksetzen
...
switch(*btn_id) {
...
case 'G': // GO!
if(!CountdownActive) {
...
CountdownSeconds=SeksToGo;
CountdownActive = true;
// Timer wird alle 1000 ms aufgerufen
CountdownTimer = lv_timer_create(countdown_timer_cb, 1000, NULL);
...
}
Sleep-Timer (light_sleep und deep_sleep)
Sleep-Timer benutzt man dafür, um das Gerät schlafen zu legen. im Schlafmodus verbraucht es dann nur noch sehr wenig Strom. Aber irgendwie muss man dem ESP32 auch sagen, wann der wieder aufwachen soll. Das geschieht mit einem Hardware-Interrupt, der z. B. durch einen Tastendruck ausgelöst werden kann. Sleep-Timer sind damit irgendwie das Gegenteil der normalen Timer: Statt eine Interrupt-Routine nach einer bestimmten Zeit aufzurufen wartet es bei einem Sleep-Timer quasi unendlich lange, bis von außen ein Interrupt ausgelöst wird.Dieses Thema habe ich in einem separaten Artikel behandelt: Den ESP32 schlafen legen, um Strom zu sparen.
Demo und Video
Hier ein kleines Demonstrations-Video für die On/Off-Timer-App für das CYD gemacht, dass die Dialoge und Timer in Aktion zeigt:Quellen, Literaturverweise und weiterführende Links
- ESP32-2432S028 mit 2.8" Touchscreen (Cheap Yellow Display) - Vorstellung Hardware und Pinout Yellow Display) - Einbinden der LVGL Library
- ESP32-2432S028 mit 2.8" Touchscreen (Cheap Yellow Display) - Dialoge entwerfen mit der LVGL Library
- ESP32-2432S028 mit 2.8" Touchscreen (Cheap Yellow Display) - Utility V5 WebRadio und MP3-Player
- LVGL-Dokumentation für Version 9.1

- ESP32-CYD (Cheap Yellow Display) Projekt mit LVGL: 433 MHz On/Off-Timer Funk-Steckdosen-Steuerung
- Espressif ESP32-Doku: General Purpose Timer
- Espressif ESP32-Doku: ESP Timer (High Resolution Timer) en
- LVGL-Doku zu TimerHandler

- LVGL-Doku zu Timer (lv_timer)
