ESP32 Using the HW timers
When I started programming on the ESP32 there was no Ticker library available as I was used to from the ESP8266 and Arduino. So I decided to write some code that makes it easy for me to use the ESP32’s hardware timers.
But before I explain the code, some essentials about the HW timers. There are only 4 HW timers available, so the code snippet I made is supporting only 4 timers.
The code snippet for the HW timer wrapper and the example code is available on my Bitbucket repo.
How to setup the HW timers of the ESP32
To use the HW timers we need to include the header file with all the definitions
1 |
#include <esp32-hal-timer.h> |
To setup the timers we first need to define which timer we want to use and the divider for the prescaler (see ESP32 Technical Reference Manual for more info). This is done by calling
1 2 3 4 |
hw_timer_t * timerBegin(uint8_t num, uint16_t divider, bool countUp) num = the number of the timer to use, valid values 0 to 3 divider = clock divider, default value is 80 (ESP32 Technical Reference Manual) countUp = defines if the timer counts up or down |
To use the HW timer 0 with standard settings the call will look like
1 |
hw_timer_t ourTimer0 = timerBegin(0, 80, true); |
Then we need to tell the HW timer which function to call when the time has expired with the call
1 2 3 4 |
void timerAttachInterrupt(hw_timer_t *timer, void (*fn)(void), bool edge) timer = the pointer to the hw_timer_t we got from the timerBegin call fn = the function to call when the time is expired edge = defines what type of interrupt is generated, we just use edge here |
To attach the ISR (interrupt service routine) void IRAM_ATTR isrTimer1() to the timer the call looks like
1 |
timerAttachInterrupt(ourTimer0, (callback_t) isrTimer0, true); |
Now we tell the HW timer the time it should run and if it should run only once or repeatedly
1 2 3 4 |
void timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload) timer = the pointer to the hw_timer_t we got from the timerBegin call alarm_value = the time in micro seconds autoreload = defines repeating call if true or single shot if false |
To prepare a repeating timer with a 2 seconds interval the call is
1 |
timerAlarmWrite(ourTimer0, 2000000, true); |
for a single shot timer with a 500 milli seconds duration the call is
1 |
timerAlarmWrite(ourTimer0, 500000, false); |
Note the difference of true and false in the call for a repeating or single shot timer.
All is done now, we can start the timer
1 2 |
void timerAlarmEnable(hw_timer_t *timer) timer = the pointer to the hw_timer_t we got from the timerBegin call |
In this example the call looks like
1 |
timerAlarmEnable(ourTimer0); |
Of course the ISR needs to be written as well. Keep in mind that ISR must be short and should not do too much work, because while the code of the ISR is executed other interrupts and tasks are disabled. Here is a example for an ISR that just sets a flag that can be processed from the main loop of the application
1 2 3 4 5 6 7 |
bool isrWasTriggered = false; void IRAM_ATTR isrTimer0() { portENTER_CRITICAL_ISR(&timerMux0); isrWasTriggered = true; portEXIT_CRITICAL_ISR(&timerMux0); } |
Now with these calls you can setup a repeating or single-shot HW timer. But I wanted to have a wrapper around this so that I do not need to keep track which of the 4 HW timers I already use.
HW Timer wrapper
As there are 4 HW timers available I first made some definitions to handle the 4 timers with the same group of functions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** Number of available timers */ int availTimers = 4; /** Flags for timers in use */ bool timerInUse[4] = {false, false, false, false}; /** Pointers to timer structures */ hw_timer_t *attachedTimer[4] = {NULL, NULL, NULL, NULL}; /** ISR locks */ portMUX_TYPE timerMux0 = portMUX_INITIALIZER_UNLOCKED; portMUX_TYPE timerMux1 = portMUX_INITIALIZER_UNLOCKED; portMUX_TYPE timerMux2 = portMUX_INITIALIZER_UNLOCKED; portMUX_TYPE timerMux3 = portMUX_INITIALIZER_UNLOCKED; /** User callback functions */ callback_t usrCallback[4] = {NULL, NULL, NULL, NULL}; /** Pointer to the ISR routine */ callback_t isrPtr[4] = {(callback_t) isrTimer0, (callback_t) isrTimer1, (callback_t) isrTimer2, (callback_t) isrTimer3}; /** Flag if timer was set to repeat */ bool doRepeat[4] = {false, false, false, false}; |
It sets the number of available timers (availTimers), an array that shows which timers are in use (timerInUse), an array to hold the pointers to the timer structures (attachedTimer), an array for user defined callbacks (usrCallback) and an array for the timer ISR routines (isrPtr). The last array defines for each timer if it is used as repeating or single-shot timer (doRepeat).
initTimer() initTimerSec() initTimerMsec()
To start a timer the function initTimer() is used. It’s parameters are
the time in micro seconds as an uint64_t variable, a pointer to the callback function and a flag to define a repeating or single shot timer. Inside initTimer() the first thing is to check if there is any timer available. If all timers are used up, a NULL pointer is returned. This should be checked by the calling code to see if a timer was started successfully. If one of the 4 timers is available the above explained functions are used to setup the timer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
hw_timer_t *initTimer (uint64_t usTriggerTime, callback_t callback, bool repeat) { // Find free timer uint8_t timerToUse; for (timerToUse = 0; timerToUse < availTimers; timerToUse++) { if (timerInUse[timerToUse] == false) { break; } } if (timerToUse == availTimers) { // No free timer found! return NULL; } // Use timerNum timer of 4 (counted from zero). // Save pointer to user callback function usrCallback[timerToUse] = callback; // Set flag when timer is repeating doRepeat[timerToUse] = true; // Set 80 divider for prescaler (see ESP32 Technical Reference Manual for more // info). attachedTimer[timerToUse] = timerBegin(timerToUse, 80, true); // Attach onTimer function to our timer. timerAttachInterrupt(attachedTimer[timerToUse], (callback_t) isrPtr[timerToUse], true); // Set alarm to call onTimer function every usTriggerTime microseconds. // Repeat the alarm (third parameter) timerAlarmWrite(attachedTimer[timerToUse], usTriggerTime, repeat); timerInUse[timerToUse] = true; return attachedTimer[timerToUse]; } |
There are two alternative calls to initialize a timer with the time in seconds or milli seconds, they simple convert the given time into micro seconds and then call initTimer()
1 2 3 4 5 6 7 |
hw_timer_t *initTimerMsec (uint64_t msTriggerTime, callback_t callback, bool repeat) { return initTimer (msTriggerTime*1000, callback, repeat); } hw_timer_t *initTimerSec (uint64_t sTriggerTime, callback_t callback, bool repeat) { return initTimer (sTriggerTime*1000*1000, callback, repeat); } |
startTimer()
To start a specific timer, the function startTimer() is called with a pointer to the timer structure as argument
1 2 3 |
void startTimer(hw_timer_t *timerToStart) { timerAlarmEnable(timerToStart); } |
restartTimer()
To restart a single-shot timer or to restart a repeating timer from 0 the routine restartTimer() can be used. It takes as an argument to pointer to the timer structure, stops that timer and starts it again with the settings given by startTimer()
1 2 3 4 5 6 7 8 9 10 11 12 |
void restartTimer(hw_timer_t *timerToRestart) { uint8_t timerIndex; for (timerIndex = 0; timerIndex < availTimers; timerIndex++) { if (attachedTimer[timerIndex] == timerToRestart) { if (doRepeat[timerIndex]) { timerStop(timerToRestart); } timerRestart(timerToRestart); break; } } } |
stopTimer()
The stopTimer() function stops a single timer defined by the parameter timerToStop
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void stopTimer(hw_timer_t *timerToStop) { uint8_t timerIndex; for (timerIndex = 0; timerIndex < availTimers; timerIndex++) { if (attachedTimer[timerIndex] == timerToStop) { timerStop(timerToStop); timerEnd(timerToStop); attachedTimer[timerIndex] = NULL; timerInUse[timerIndex] = false; doRepeat[timerIndex] = false; break; } } } |
stopAllTimers()
To stop all running timers I added a function to check which timers are active and stop all active timers
1 2 3 4 5 6 7 8 9 10 11 12 |
void stopAllTimers() { uint8_t timerIndex; for (timerIndex = 0; timerIndex < availTimers; timerIndex++) { if (attachedTimer[timerIndex] != NULL) { timerStop(attachedTimer[timerIndex]); timerEnd(attachedTimer[timerIndex]); attachedTimer[timerIndex] = NULL; timerInUse[timerIndex] = false; doRepeat[timerIndex] = false; } } } |
Interrupt Service Routines
For the ISR (interrupt service routine) for each of the timers I had two possible ways to do it.
- Take the user callback function and attach it to the timer
- Create a “standard” ISR that is then calling the user callback function
I decided to go for the second solution. This way I made sure that in my callbacks I do not forget to inform the operating system that an ISR is executed and should not be interrupted.
There are four ISR defined, one for each timer. The ISR locks the OS, then calls the user callback function, unlocks the OS and returns. As all of the 4 ISR are basically the same, here is only the code for the ISR for timer 0
1 2 3 4 5 6 |
void IRAM_ATTR isrTimer0() { portENTER_CRITICAL_ISR(&timerMux0); callback_t thisCallBack = (callback_t) usrCallback[0]; thisCallBack(); portEXIT_CRITICAL_ISR(&timerMux0); } |
How to use the HW timer wrapper
Everybody likes to see some example code, so here is a short program that uses 4 HW timers to display some messages on the Serial port. The example code is available on my Bitbucket repo. To run the code, copy the files hw_timer.cpp and hw_timer.h into your project or sketch folder and create an test_timer.ino (ArduinoIDE) or test_timer.cpp (PlatformIO) file with this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
/** * Example code for HW timer wrapper * Read details on https://desire.giesecke.tk/index.php/2018/04/22/using-the-hw-timers-of-the-esp32 * Check README.md for usage * Code released under GNU GENERAL PUBLIC LICENSE Version 3 */ #include <Arduino.h> #include "hw_timer.h" // HW timer structures hw_timer_t *myTimer0; hw_timer_t *myTimer1; hw_timer_t *myTimer2; hw_timer_t *myTimer3; hw_timer_t *myTimer4; // Flags for timer callbacks volatile bool timer0Triggered = false; volatile bool timer1Triggered = false; volatile bool timer2Triggered = false; volatile bool timer3Triggered = false; void timer0cb () { timer0Triggered = true; } void timer1cb () { timer1Triggered = true; } void timer2cb () { timer2Triggered = true; } void timer3cb () { timer3Triggered = true; } void setup() { Serial.begin(115200); Serial.println(); Serial.println("HW timer wrapper example"); // Start first timer repeating with 5 seconds time if ((myTimer0 = initTimerSec((uint64_t)5, timer0cb, true)) == NULL) { Serial.println("Could not start first timer"); } else { Serial.println("Started first timer at " + String(millis())); } // Start second timer repeating with 8 seconds time if ((myTimer1 = initTimerSec((uint64_t)8, timer1cb, true)) == NULL) { Serial.println("Could not start second timer"); } else { Serial.println("Started second timer at " + String(millis())); } // Start third timer repeating with 11 seconds time if ((myTimer2 = initTimerSec((uint64_t)11, timer2cb, true)) == NULL) { Serial.println("Could not start third timer"); } else { Serial.println("Started third timer at " + String(millis())); } // Start forth timer single shot with 15 seconds time if ((myTimer3 = initTimerSec((uint64_t)15, timer3cb, false)) == NULL) { Serial.println("Could not start forth timer"); } else { Serial.println("Started forth timer at " + String(millis())); } // Just for checking, try to start a fifth timer // This will always fail as there are only 4 HW timers // Start fifth timer repeating with 15 seconds time if ((myTimer4 = initTimerSec(15, timer3cb, true)) == NULL) { Serial.println("Could not start fifth timer"); } else { Serial.println("Started fifth timer at " + String(millis())); } if (myTimer0 != NULL) { startTimer(myTimer0); } if (myTimer1 != NULL) { startTimer(myTimer1); } if (myTimer2 != NULL) { startTimer(myTimer2); } if (myTimer3 != NULL) { startTimer(myTimer3); } } void loop() { if (timer0Triggered) { Serial.println("First timer was triggered at " + String(millis())); timer0Triggered = false; } if (timer1Triggered) { Serial.println("Second timer was triggered at " + String(millis())); timer1Triggered = false; } if (timer2Triggered) { Serial.println("Third timer was triggered at " + String(millis())); timer2Triggered = false; } if (timer3Triggered) { Serial.println("Forth timer was triggered at " + String(millis())); Serial.println("Timer was single-shot so this message comes only once!"); timer3Triggered = false; } } |






