ESP32: Ticker library

Introduction

In this post we are going to learn how to get started with the Ticker library, using the ESP32 and the Arduino core.

The Ticker library allows to very easily setup callback functions to run periodically, without having to worry about the lower level details of setting up a timer (you can check an example on how to setup timer interrupts here). This could be useful, for example, to periodically gather measurements from a sensor.

The tests shown below were performed on a ESP32-E FireBeetle board from DFRobot.

A simple example

We will start our code by including the Ticker.h library (you can check both the header and implementation files here). This will expose to us the Ticker class, which we will use below.

#include <Ticker.h>

In our example we will cover how to setup a function to execute periodically and another to fire only once, after a time interval. As such, we will create two Ticker objects, one for each use case.

Ticker periodicTicker;
Ticker onceTicker;

Moving on to the Arduino setup, we will first take care of opening a serial connection. The functions that will be triggered by the Ticker objects will make use of it to print a different message each.

Serial.begin(115200);

After this we will take care of configuring the Ticker object that will execute the function periodically. So, to configure a callback function to execute periodically, we can call the attach_ms method.

This method receives as first argument an unsigned integer, with the interval between periodic invocations, in milliseconds. As second argument it receives the callback function, which we will call periodicPrint and define later.

As an additional note, there’s also a similar method called attach, which receives the interval in seconds instead, as a float. Under the hood, if we use the attach method instead, the library multiplies the float value by 1000 and passes it to an internal function that also works with milliseconds.

periodicTicker.attach_ms(5000, periodicPrint);

To configure the other Ticker object to execute the function only once after a predefined interval, we need to call the once_ms method instead. The signature is similar: the first argument is the interval in milliseconds and the second is the callback function to be invoked once after that interval elapses.

Similarly to the attach_ms and attach methods, there’s also a method called once that receives an interval in seconds, as a float. Like before, under the hood, the value is converted to milliseconds and passed to an internal function that works in milliseconds.

onceTicker.once_ms(10000, oncePrint);

The whole setup is shown below.

void setup() {
  Serial.begin(115200);
  
  periodicTicker.attach_ms(5000, periodicPrint);
  onceTicker.once_ms(10000, oncePrint);
}

Now we will take care of our functions. Note that they should return void and receive no parameters (we will cover how to pass an argument to these functions in the section below). Like mentioned before, they will simply print a message to the serial port, so we can distinguish which one is executing.

void periodicPrint() {
  Serial.println("printing in periodic function.");
}

void oncePrint() {
  Serial.println("printing in once function.");
}

The complete code is seen below. Note that we don’t need to add anything to our main loop, as the underlying implementation is based on timers.

#include <Ticker.h>

Ticker periodicTicker;
Ticker onceTicker;

void periodicPrint() {
  Serial.println("printing in periodic function.");
}

void oncePrint() {
  Serial.println("printing in once function.");
}


void setup() {
  Serial.begin(115200);
  
  periodicTicker.attach_ms(5000, periodicPrint);
  onceTicker.once_ms(10000, oncePrint);
}

void loop() {}

As usual, to test the code, simply compile it and upload it to your ESP32 using the Arduino IDE. Once the procedure finishes, open the IDE serial monitor.

You should get a result similar to figure 1. As can be seen, we get the prints from the periodically executing function and we have only one print from the function that was scheduled to run once.

Output of the program, showing the prints from both the periodic and the to be executed once function.
Figure 1 – Output of the program, showing the prints from both the periodic and the to be executed once functions.

Passing function arguments and detach

In this section we will explore how to pass an argument to our callback function, and also how to detach the periodic execution of a callback.

In our example, we will configure a callback function that will execute a maximum number of times (this threshold will be the argument of the callback) and suspend itself after passing that number of executions. Please note that the code shown below is merely illustrative, and we could have defined a global variable to hold this threshold rather than passing it as argument, pretty much like we will do with the current iterations counter.

Like before, we start with the Ticker.h library include.

#include <Ticker.h>

After that we will instantiate an object of class Ticker. This time, we will use only an object, as we want to configure a single callback function to execute periodically.

Ticker periodicTicker;

We will also define a global counter, which will keep track of the number of times the callback function is already executed. Naturally, we will initialize it with a value of zero and later increment it in the callback.

int executionsCount = 0;

We will then jump right to the Arduino setup. We will open a Serial connection, so our callback can later print the current iteration value.

Serial.begin(115200);

Now we will define a variable that will hold the maximum number of executions. This is the variable we will pass to our callback. I’m going to use a value of 10.

int maxExecutionsCount=10;

Finally, we will call the attach_ms method on our Ticker object, passing as input the 3 following parameters:

  • The interval between executions, in milliseconds. We are going to pass a value of 5000 milliseconds (5 seconds).
  • The callback function. We will define the function later, but we will call it periodicPrint.
  • The argument to be passed to the callback function. We will use our variable.

Important: Don’t get mislead by the name of the maxExecutionsCount variable and our use case. The third argument of the attach_ms method doesn’t have any meaning for this method and will simply be passed to the callback variable. At the time of writing, the library doesn’t support any kind of maximum number of executions of the periodic function, and we are actually going to implement that mechanism ourselves.

periodicTicker.attach_ms(5000, periodicPrint, maxExecutionsCount);

With the attach_ms method call we conclude our setup, which is summarized in the snippet below.

void setup() {
  Serial.begin(115200);

  int maxExecutionsCount=10;
  
  periodicTicker.attach_ms(5000, periodicPrint, maxExecutionsCount);
}

Now let’s look at the implementation of our callback function. Once again, it should return void but, this time, it will receive an integer parameter.

void periodicPrint(int maxExecutionsCount) {
   // callback implementation
}

The first thing we will do is printing the current execution number. We will be summing one to the executionsCount so we display a counter starting at 1 to the user.

Serial.print("printing in periodic function. Exec nr: ");
Serial.println(executionsCount+1);

After this we will increment the counter and check if we reached the threshold. In case we did, we will stop further executions simply by calling the detach method on our Ticker object. This method takes no arguments and returns void.

executionsCount++;

if(executionsCount>=maxExecutionsCount){
    periodicTicker.detach();
}

The whole callback can be seen below.

void periodicPrint(int maxExecutionsCount) {
  Serial.print("printing in periodic function. Exec nr: ");
  Serial.println(executionsCount+1);

  executionsCount++;

  if(executionsCount>=maxExecutionsCount){
    periodicTicker.detach();
  }
}

The complete code is available on the snippet below.

#include <Ticker.h>

Ticker periodicTicker;
int executionsCount = 0;

void periodicPrint(int maxExecutionsCount) {
  Serial.print("printing in periodic function. Exec nr: ");
  Serial.println(executionsCount+1);

  executionsCount++;

  if(executionsCount>=maxExecutionsCount){
    periodicTicker.detach();
  }
}

void setup() {
  Serial.begin(115200);

  int maxExecutionsCount=10;
  
  periodicTicker.attach_ms(5000, periodicPrint, maxExecutionsCount);
}

void loop() {}

Once again, simply compile and upload the code, and open the serial monitor once the procedure finishes. You should see a result similar to figure 2, which evidences that the periodic callback execution will halt after 10 calls, as we defined in our code.

Output of the code, showing the 10 configured executions of the callback function, configured with the Ticker library.
Figure 2 – Output of the code, showing the 10 configured executions of the callback function.

Important considerations

Now that we covered the basics of the Ticker library, it is important for us to be aware of how it works under the hood. Arduino abstractions are great to get things up and running fast, but when we want to use some of them for more complex scenarios, we may run into unexpected problems if we are not aware about the details.

The Ticker library that we just covered uses an IDF API under the hood called esp_timer, defined in the esp_timer.h file and documented here. If we go to the Ticker.h file, we can easily see the include for the esp_timer.h at the beginning of the file.

Under the hood, the esp_timer.h library is based on hardware timers that can provide microsecond accuracy [1], which can easily be seen in the signature of the function that is used to setup a periodic callback. As we saw by the coding sections, we can only go to the millisecond accuracy, in case we use the Ticker library. So, if you need something more granular, it is useful to know that the underlying functions are more flexible.

Furthermore, we cannot blindly trust that our callback functions are going to be executed right away when the hardware timer fires. The actual implementation of the IDF API uses an auxiliary task, which is notified from the timer interrupt function [1]. This means that some time is needed for the ISR to notify the task and then to execute the callback [1].

It is also interesting that one callback is only be called after the other returns [1], in case more than one needs to be called. This is important because the more callbacks that are registered to run periodically, the more delay there will be for the callbacks that are called later.

Other interesting assumption that we can take from here is that, in the code section where I’m accessing a global variable and incrementing it, I should not need to worry about synchronization as, even if I had the callbacks firing very close together, there shouldn’t be any concurrent access to the variable because they execute in sequence. Note however that I did not validate this, which is why I’m mentioning this as an assumption.

I also did not dig deep enough in the API to confirm if this assumption is reasonable in case different callbacks associated to different timer handles access a same global variable (for example, it is not clear if under the hood each handle is tied to its own task, meaning that there would be no guarantee that different callbacks are executed concurrently). If you you have such use case, I highly recommend you to check it, as it may cause hard to debug issues in your application.

Other important recommendation that we can read in esp_timer.h library is that our callbacks should not do much work, but rather use some RTOS notification mechanism to offload the work to other task [1]. This is a general rule that should be applied to interrupt handlers and, even though these callbacks are not directly executed as ISR functions, we already seen that they impact when other callbacks are executed, and we don’t to keep them waiting while doing some long or blocking operation.

As a final note, I would like to mention the existence of a configuration called skip_unhandled_events, also only accessible if we use the IDF API directly. This configuration exists because of the fact that the API is designed to not loose events, and to be resilient to handle delayed ones [2]. As such, when the timer has not been processed for more than one period (for periodic timers), the callbacks will be called one after the other without waiting for the defined period [2].

Nonetheless, since this may not be compatible with some applications, this configuration was introduced [2]. When set, a periodic timer that has expired multiple times without being able to call the callback will still result in only one callback event once processing is possible [2].

References

[1] https://github.com/espressif/esp-idf/blob/8131d6f/components/esp_timer/include/esp_timer.h

[2] https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html

1 thought on “ESP32: Ticker library”

Leave a Reply