Using ESP32 timers in Arduino code
In this article, we will explore the operation of a timer on the ESP32 using Arduino code. We’ll look at how to set up and use a timer effectively, using practical examples to help you understand the basic concepts. We will discover the steps to configure a timer on the ESP32, with the important parameters for optimal operation. Here we go 😊.
How ESP32 timers work ?
The theoretical functioning of the timer is not presented in this article to avoid overloading it. If you are a beginner and you don’t know the internal working of a timer, I strongly encourage you to read the theoretical article on how it works . It will allow you to better understand how to choose the values of the parameters to use it in your Arduino code.
Configure and use an ESP32 timer with Arduino code
Here is the minimal skeleton code to use a timer on the ESP32 with Arduino code. It allows to trigger an interrupt when the timer reaches the threshold value threashold
threshold value, which is commonly called the autoreload
.
hw_timer_t * timer = NULL;
void IRAM_ATTR timer_isr() {
// This code will be executed every 1000 ticks, 1ms
}
void setup() {
Serial.begin(115200);
uint8_t timer_id = 0;
uint16_t prescaler = 80; // Between 0 and 65 535
int threshold = 1000000; // 64 bits value (limited to int size of 32bits)
timer = timerBegin(timer_id, prescaler, true);
timerAttachInterrupt(timer, &timer_isr, true);
timerAlarmWrite(timer, threshold, true);
timerAlarmEnable(timer);
}
void loop() {
}
Selection and basic configuration of the timer
We define an object hw_timer_t
object outside the setup()
function to be able to access it from different functions. The function timerBegin(uint8_t id, uint16_t prescaler, bool countUp)
allows to configure the timer :
On the ESP32 there are 4 completely independent timers, they are chosen via an id
between 0 and 3. Then, we choose the prescaler
that we want to apply to the timer clock signal. On the ESP32, it is the clock APB_CLK
clock clocked at 80 MHz is used.
Warning
If you use external libraries in your code, they may use timers. In this case you have to be careful not to choose one that is already used by them, otherwise your program will certainly have bugs!
In most of the available examples, a prescaler
of 80
is applied to have a counting period of 1 µs. From a period of the timer in microseconds, we can directly know the value of the corresponding autoreload without doing complicated calculations:
The argument countUp
argument specifies in which direction we want to count: true
in ascending order, false
in decreasing order.
Configure the alarm and the triggering of an interrupt routine
We attach to the timer an interrupt which will be triggered each time the threshold value is exceeded with timerAttachInterrupt(hw_timer_t *timer, void (*fn)(void), bool edge)
.
In the middle argument you have to put the name of the interrupt routine that will be executed (here timer_isr()
). You can also choose if the interrupt will be triggered on the rising or falling edge: we generally opt for a rising edge for timers.
The threshold value of the counter is defined by timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload)
. We activate the mode autoreload
mode by setting autoreload
à true
: once the timer has exceeded the value alarm_value
value, it starts counting again from zero and the interrupt function is triggered.
Note
With a prescaler
of 80, the value of alarm_value
corresponds directly to the global period of the timer in microseconds.
Once the timer has been completely configured, you can activate it with its alarm with timerAlarmEnable(timer)
.
If we execute this code, the function timer_isr()
function will be executed every second. Since the function is empty in this “skeleton” code, nothing will actually happen. I propose you a version which makes the blue LED of the ESP32 blink on the pin GPIO2
.
Code blink with only one timer
Here is the famous “blink” rewritten using only a hardware timer that will flash the blue LED every second:
#define PIN_LED 2
hw_timer_t * timer = NULL;
volatile uint8_t led_state = 0;
void IRAM_ATTR timer_isr(){
led_state = ! led_state;
digitalWrite(PIN_LED, led_state);
}
void setup() {
Serial.begin(115200);
pinMode(PIN_LED, OUTPUT);
uint8_t timer_id = 0;
uint16_t prescaler = 80;
int threashold = 1000000;
timer = timerBegin(timer_id, prescaler, true);
timerAttachInterrupt(timer, &timer_isr, true);
timerAlarmWrite(timer, threashold, true);
timerAlarmEnable(timer);
}
void loop() {
// put your main code here, to run repeatedly
}
We use the keyword volatile
keyword for the variable led_state
variable to force the compiler not to optimize it. Indeed, for the compiler, the function timer_isr ()
function is never called in the code because it is called via an external interrupt (that the compiler is not aware of).
Without the attribute volatile
attribute, the compiler could remove the variable that is not used to free up memory space. By putting the keyword volatile
attribute, we force the compiler to ignore this variable for optimization.
To make the LED blink, we use a little trick here to invert the state of the LED with the operator !
. When used on a variable, all zeros in its binary representation are replaced by 1s (and vice versa).
Thus the value of led_state will be inverted at each entry in the interrupt routine: 0b0000000
→ 0b11111111
→ 0b0000000
… Since the function digitalWrite()
function only looks at the value of the first bit, we will have an alternation between 0 and 1 .
Note
We add the attribute IRAM_ATTR
attribute when declaring the function, to tell the compiler to load the whole code of the interrupt routine in the internal RAM of the ESP32, so that it runs faster.
With this program, the blue LED starts blinking, without using the loop()
function, which can be used to do other tasks.
Increment a variable and generate flags
The interrupt routine must be executed as quickly as possible to avoid disturbing the main program. So we commonly use flags
which will change state (usually a boolean : true
or false
) in the``isr`` . These changes will then be processed in the main loop:
hw_timer_t * timer = NULL;
volatile boolean tick_flags = false;
void IRAM_ATTR timer_isr() {
tick_flags = true;
}
void setup() {
Serial.begin(115200);
uint8_t timer_id = 0;
uint16_t
This section is available to premium members only. You still have 77% to discover.
Subscribe for only 5$/monthAlready subscribed? Sign in