A Timer is an internal interrupt that can trigger an alarm and an associated action at a specific time repeatedly. A Timer is considered an interrupt because it “interrupts” the main thread to execute the code associated with it. Once the associated code has been executed, the program continues where it left off.
The ESP32 contains two groups of hardware timers. Each group has two general purpose hardware timers, so the ESP32 has a total of 4 timers which are numbered 0-3. These are all generic 64-bit based timers. Each Timer has a 16-bit Prescaler (from 2 to 65536) as well as 64-bit ascending / descending counters which can be automatically reloaded (reload option of the timerAlarmWrite function ).
If you need to trigger actions from an external event (button, PIR motion detector, radar …), read this tutorial dedicated to external interrupts
Install the ESP-IDF SDK for ESP32 on IDE Arduino and PlatformIO
If you are new to ESP32 development boards you must first install the ESP-IDF development kit. Here are two tutorials to get started depending on your code editor
Follow the instructions in this tutorial for the Arduino IDE
And this one for PlatformIO (ideally with VSCode)
Introduction to Timers
Before integrating Timers into your programs, you must take into account certain technical constraints
- The code should be extremely quick to execute. It is preferable to update the state of a variable and to do the processing in the loop(). For example, we will avoid publishing a message on an MQTT server or writing to the serial port.
- The code executes as soon as the timer is exceeded
- Each code is attached to a dedicated Timer. The ESP32 has 4 Timers
- It is possible to share the content of variables declared as volatile
How to share a variable between the Timer and the rest of the code
The idea is therefore to update the value or state of a variable and then to carry out the associated processing in the main loop().
For that, it must be declared with the volatile keyword. This turns off code optimization. Indeed, by default, the compiler will always try to free the space occupied by an unused variable, which we do not want here.
volatile int count;
Prescaler (Time divider) and Tics
The Timer uses the processor clock to calculate the elapsed time. It is different for each microcontroller. The quartz frequency of the ESP32 is 80MHz.
The ESP32 has two groups of timers. All timers are based on 64-bit Tic counters and 16-bit time dividers (prescaler). The prescaler is used to divide the frequency of the base signal (80 MHz for an ESP32), which is then used to increment or decrement the timer counter.
To count each Tic, all you have to do is set the prescaler to the quartz frequency. Here 80. For more details, read this excellent article .
The timer simply counts the number of Tic generated by the quartz. With a quartz clocked at 80MHz, we will have 80,000,000 Tics.
By dividing the frequency of the quartz by the prescaler, we obtain the number of Tics per second
80,000,000 / 80 = 1,000,000 tics / sec
How to add a Timer to an Arduino project for ESP32?
In order to configure the timer, we will need a pointer to a variable of type hw_timer_t .
hw_timer_t * timer = NULL;
The timerbegin(id, prescaler, flag) function is used to initialize the Timer. It requires three arguments
- id the Timer number from 0 to 3
- prescale the value of the time divider
- flag true to count on the rising edge, false to count on the falling edge
timer = timerBegin(0, 80, true);
Before activating the timer, it must be linked to a function which will be executed each time the interrupt is triggered. For this, we call the timerAttachInterrupt(timer, function, trigger) function. This method has three parameters:
- timer is the pointer to the Timer we have just created
- function the function that will be executed each time the Timer alarm is triggered
- Trigger indicates how to synchronize the Timer trigger with the clock.
2 types of triggers are possible. More info here.
- Edge (true) The Timer is triggered on detection of the rising edge
- Level (false) The Timer is triggered when the clock signal changes level
timerAttachInterrupt(timer, &onTime, true);
Trigger an alarm
Once the Timer is started, all that remains is to program an alarm which will be triggered at regular intervals.
For this we have the timerAlarmWrite(timer, frequency, autoreload) method which requires 3 parameters (source code)
- timer the pointer to the Timer created previously
- frequency the frequency of triggering of the alarm in ticks. For an ESP32 , there are 1,000,000 tics per second
- autoreload true to reset the alarm automatically after each trigger.
timerAlarmWrite(timer, 1000000, true);
Finally we start the alarm using the timerAlarmEnable(timer) method
timerAlarmEnable(timer);
How to make the code “Real time” (optional)?
The ESP-IDF framework that we use to develop the Arduino program is built on a modified version of FreeRTOS , a real-time operating system suitable for micro-controllers and on-board systems in general.
In order for the code to run deterministically in real time, it is possible to frame certain critical portions.
To do this, you must define an object of type portMUX_TYPE
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
Then, we frame the critical code portion like this
portENTER_CRITICAL_ISR(&timerMux);
... critical code
portEXIT_CRITICAL_ISR(&timerMux);
Run the function in IRAM with the IRAM_ATTR attribute
As with any interrupt, it is best to place the code executed by the Timer in the internal RAM of the ESP32 which is much faster than the Flash memory of the development board.
To do this, simply place the IRAM_ATTR attribute just before the name of the function like this
void IRAM_ATTR mafonctionrapide(){
... code executed in the RAM of the ESP32
}
It is also possible to make the execution of the code in RAM critical
void IRAM_ATTR mayfastfunction(){
portENTER_CRITICAL_ISR(&timerMux);
... critical code executed in the RAM of the ESP32
portEXIT_CRITICAL_ISR(&timerMux);
}
Example of a Timer flashing an LED
Let’s start with a simple example of an alarm triggered every second. Each time the alarm is triggered, a counter is incremented. If the counter is even, the LED is turned on. The LED is turned off if the counter is odd.
Circuit
The LED is connected to output 32 .
The LED must be protected by a resistor, the value of which depends on the output voltage and current of the pin (3.3V – 40mA) and the maximum supply voltage of the LED.
You can use this calculator to determine the required resistance value for your circuit.
PlatformIO configuration for a LoLin D32
Here is an example platformio.ini configuration file for a LoLin32 Pro development board
[env:lolin_d32_pro]
platform = espressif32
board = lolin_d32_pro
framework = arduino
monitor_speed = 115200
Upload the Arduino code of the project
Create a new sketch on the Arduino IDE or a new PlatformIO project.
On the Arduino IDE you can remove the first line #include <Arduino.h> .
Each time the onTimer function is executed, the value of the volatile variable count is increased . In the main thread of the loop() , as soon as the trigger is greater than zero, we increment the totalInterrupts counter and we make the LED blink if it is even or not.
#include <Arduino.h>
volatile int count; // Trigger
int totalInterrupts; // counts the number of triggering of the alarm
#define LED_PIN 32
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// Code with critica section
void IRAM_ATTR onTime() {
portENTER_CRITICAL_ISR(&timerMux);
count++;
portEXIT_CRITICAL_ISR(&timerMux);
}
// Code without critical section
/*void IRAM_ATTR onTime() {
count++;
}*/
void setup() {
Serial.begin(115200);
// Configure LED output
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// Configure the Prescaler at 80 the quarter of the ESP32 is cadence at 80Mhz
// 80000000 / 80 = 1000000 tics / seconde
timer = timerBegin(0, 80, true);
timerAttachInterrupt(timer, &onTime, true);
// Sets an alarm to sound every second
timerAlarmWrite(timer, 1000000, true);
timerAlarmEnable(timer);
}
void loop() {
if (count > 0) {
// Comment out enter / exit to deactivate the critical section
portENTER_CRITICAL(&timerMux);
count--;
portEXIT_CRITICAL(&timerMux);
totalInterrupts++;
Serial.print("totalInterrupts");
Serial.println(totalInterrupts);
if ( totalInterrupts%2 == 0) {
// Lights up the LED if the counter is even
digitalWrite(LED_PIN, HIGH);
} else {
// Then swith off
digitalWrite(LED_PIN, LOW);
}
}
}
Open the serial monitor to view the triggering of alarms by the Timer.
Measure the temperature every second with a BMP180 or BME280
We will now apply the principle to regularly trigger the temperature recording using a BMP180. Whatever the sensor, the principle will remain the same.
Circuit
Add a BMP180 , BME280 or BME680 barometer to the previous circuit. Here, the pin SDA is connected to pin 18 and SCL on 5 . The LED is always connected to output 32
PlatformIO configuration for a LoLin32 Pro
Here is the platformio.ini configuration file for a LoLin D32 Pro development board which automatically installs the Adafruit_BMP085 (525) library
[env:lolin_d32_pro]
platform = espressif32
board = lolin_d32_pro
framework = arduino
monitor_speed = 115200
lib_deps =
525
Upload Arduino code
Create a new PlatformIO sketch or project. Change the pins in the code before uploading:
- PIN_LED by default 32
- PIN_SDA SDA pin of the I2C bus, in code 18
- PIN_SCL SCL pin of the I2C bus, in code 5
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BMP085.h>
Adafruit_BMP085 bmp;
bool BMP180connected = false;
volatile bool get_temp;
#define PIN_SDA 18
#define PIN_SCL 5
#define PIN_LED 32
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// With critical section
void IRAM_ATTR onTime() {
portENTER_CRITICAL_ISR(&timerMux);
get_temp = true;
portEXIT_CRITICAL_ISR(&timerMux);
}
void blinkLED(){
digitalWrite(PIN_LED, HIGH);
delay(500);
digitalWrite(PIN_LED, LOW);
}
void setup() {
Serial.begin(115200);
// Attribute Pins for I2C bus
Wire.begin(18, 5);
if (!bmp.begin()) {
Serial.println("Could not find a valid BMP085 sensor, check wiring!");
//while (1) {}
} else {
BMP180connected = true;
}
// Configure LED Output
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, LOW);
// Configure Prescaler to 80, as our timer runs @ 80Mhz
// Giving an output of 80,000,000 / 80 = 1,000,000 ticks / second
timer = timerBegin(0, 80, true);
timerAttachInterrupt(timer, &onTime, true);
// Fire Interrupt every 1s (1 million ticks)
timerAlarmWrite(timer, 2000000, true);
timerAlarmEnable(timer);
}
void loop() {
if ( get_temp ) {
// You can comment Enter/ Exit critical section
portENTER_CRITICAL(&timerMux);
get_temp = false;
portEXIT_CRITICAL(&timerMux);
if ( BMP180connected ) {
blinkLED();
Serial.printf("Temperature is %.1f°C \n", bmp.readTemperature());
}
}
}
Open the serial monitor to view the acquisition of a measurement each time the alarm is triggered by the Timer. The LED flashes then the measurement is published on the serial port.
Explanation of the code
The operation of the Timer and the alarm is identical to the previous code.
Here we use pins 18 and 5 of a LoLin32 development board to connect the I2C bus
Wire.begin(PIN_SDA, PIN_SCL);
Each time the alarm is triggered, we set the get_temp flag to true.
We test if the sensor is available using the BMP180connected flag.
If this is the case, we make the LED flash for 500ms by calling the blinkLED method
void blinkLED(){
digitalWrite(PIN_LED, HIGH);
delay(500);
digitalWrite(PIN_LED, LOW);
}
The temperature measurement is constructed by formatting the measurement as a single significant digit before publishing it to the serial monitor using the Serial.printf() method .
Serial.printf("Temperature is %.1f°C \n", bmp.readTemperature());
Updates
6/10/2020 Publication of the article
- How to store data on a micro SD card. Arduino code compatible ESP32, ESP8266
- Getting started Arduino. Receive commands from the serial port (ESP32 ESP8266 compatible)
- C++ functions print•println•printf•sprintf for Arduino ESP32 ESP8266. Combine•format → serial port
- Getting started Arduino. Character string functions (ESP32 ESP8266 compatible)
- ESP32. How to connect to local WiFi network with Arduino code