ESP32. How to use Timers and Alarms with Arduino Code

How to trigger an alarm with an ESP32 timer. Example of Arduino code to take measurements at regular intervals with a BME280
Share on facebook
Share on twitter
Share on linkedin
Share on pinterest
Share on email
Share on whatsapp

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

Warning, some Timers may interfere with other features such as PWM outputs.

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

esp32 clock signal trigger edge 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.

The execution of the code in the RAM of the ESP32 is not imperative. However, it is advisable to do this as soon as the project becomes complex.

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 .

ESP32 Timer Alarm blink LED

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.

See other assortments

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

ESP32 Timer Alarm LED BMP180

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

By default, on ESP32 boards, the SDA pin is 21 . SCL pin is 22

#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

Click to rate this post!
[Total: 1 Average: 5]

Are you having a problem with this topic?

Maybe someone has already found the solution, visit the forum before asking your question
Ask your question

Share on facebook
Share on twitter
Share on linkedin
Share on pinterest
Share on email
Share on whatsapp

Did you like this project ? Don't miss any more projects by subscribing to our weekly newsletter!

We will be happy to hear your thoughts

      Leave a Reply

      DIY Projects