ESP32 Arduino: Timer interrupts

The objective of this post is to explain how to configure timer interrupts on the ESP32, using the Arduino core. The tests were performed on a DFRobot’s ESP-WROOM-32 device integrated in a ESP32 FireBeetle board.


Introduction

The objective of this post is to explain how to configure timer interrupts on the ESP32, using the Arduino core. The code shown here is based on this example from the Arduino core libraries, which I encourage you to try.

So, in this tutorial, we will check how to configure the timer to periodically generate an interrupt and how handle it.

The tests were performed on a DFRobot’s ESP-WROOM-32 device integrated in a ESP32 FireBeetle board.

 

The alarms

The ESP32 has two timer groups, each one with two general purpose hardware timers. All the timers are based on 64 bits counters and 16 bit prescalers [1].

The prescaler is used to divide the frequency of the base signal (usually 80 MHz), which is then used to increment / decrement the timer counter [2]. Since the prescaler has 16 bits, it can divide the clock signal frequency by a factor from 2 to 65536 [2], giving a lot of configuration freedom.

The timer counters can be configured to count up or down and support automatic reload and software reload [2]. They can also generate alarms when they reach a specific value, defined by the software [2]. The value of the counter can be read by the software program [2].


Global variables

We start our code by declaring some global variables. The first one will be a counter that will be used by the interrupt service routine to signal the main loop that an interrupt has occurred.

We have seen this use of a counter in the previous tutorial about external interrupts because, as explained, ISRs should run as fast as possible and should not perform long operations, such as writing to the serial port. Thus, a good way of implementing interrupt handling code is making the ISR only signal the occurrence of the interrupt and defere the actual handling (which may contain operations that take a while) to the main loop.

The counter is also useful since because if by some reason the handling of an interrupt in the main loop takes longer than expected and more interrupts occur in the meantime, then they are not lost because the counter will be incremented accordingly. On the other hand, if a flag is used as signaling mechanism, then it will keep be setting to true, and interrupts will be lost since the main loop will only assume that an additional one has occurred.

As usual, since this counter variable will be shared amongst the main loop and the ISR, then it needs to be declared with the volatile keyword, which avoids it being removed due to compiler optimizations.

volatile int interruptCounter;

We will have an additional counter to track how many interrupts have already occurred since the beginning of the program. This one will only be used by the main loop and thus it doesn’t need to be declared as volatile.

int totalInterruptCounter;

In order to configure the timer, we will need a pointer to a variable of type hw_timer_t, which we will later use in the Arduino setup function.

hw_timer_t * timer = NULL;

Finally, we will need to declare a variable of type portMUX_TYPE, which we will use to take care of the synchronization between the main loop and the ISR, when modifying a shared variable.

portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;


Setup function

As usual, we will start our setup function by opening a serial connection, so we can later output the results of our program to be available in the Arduino IDE serial monitor.

Serial.begin(115200);

Next, we will initialize our timer with a call to the timerBegin function, which will return a pointer to a structure of type hw_timer_t, which is the one of the timer global variable we declared in the previous section.

As input, this function receives the number of the timer we want to use (from 0 to 3, since we have 4 hardware timers), the value of the prescaler and a flag indicating if the counter should count up (true) or down (false).

For this example we will use the first timer and will pass true to the last parameter, so the counter counts up.

Regarding the prescaler, we have said in the introductory section that typically the frequency of the base signal used by the ESP32 counters is 80 MHz (this is true for the FireBeetle board). This value is equal to 80 000 000 Hz, which means means the signal would make the timer counter increment 80 000 000 times per second.

Although we could make the calculations with this value to set the counter number for generating the interrupt, we will take advantage of the prescaler to simplify it. Thus, if we divide this value by 80 (using 80 as the prescaler value), we will get a signal with a 1 MHz frequency that will increment the timer counter 1 000 000 times per second.

From the previous value, if we invert it, we know that the counter will be incremented at each microsecond. And thus, using a prescaler of 80, when we call the function to set the counter value for generating the interrupt, we will be specifying that value in microseconds.

timer = timerBegin(0, 80, true);

But before enabling the timer, we need to bind it to a handling function, which will be executed when the interrupt is generated. This is done with a call to the timerAttachInterrupt function.

This function receives as input a pointer to the initialized timer, which we stored in our global variable, the address of the function that will handle the interrupt and a flag indicating if the interrupt to be generated is edge (true) or level (false). You can read more about the difference between edge and level interrupts here.

So, as mentioned, we will pass our global timer variable as first input, as second the address of a function called onTimer that we will later specify, and as third the value true, so the interrupt generated is of edge type.

timerAttachInterrupt(timer, &onTimer, true);

Next we will use the timerAlarmWrite function to specify the counter value in which the timer interrupt will be generated. So, this function receives as first input the pointer to the timer, as second the value of the counter in which the interrupt should be generated, and as third a flag indicating if the timer should automatically reload upon generating the interrupt.

So, as first argument we pass our timer global variable again, and as third argument we will pass true, so the counter will reload and thus the interrupt will be periodically generated.

Regarding the second argument, remember that we set the prescaler in order for this to mean the number of microseconds after which the interrupt should occur. So, for this example, we assume that we want to generate an interrupt each second, and thus we pass the value of 1 000 000 microseconds, which is equal to 1 second.

Important: Take in consideration that this value is specified in microseconds only if we specify the value 80 for the prescaler. We can use different prescaler values and in that case we need to do the calculations to know when the counter will reach a certain value.

timerAlarmWrite(timer, 1000000, true);

We finish our setup function by enabling the timer with a call to the timerAlarmEnable function, passing as input our timer variable.

timerAlarmEnable(timer);

The final code for the setup function can be seen below.

void setup() {

  Serial.begin(115200);

  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000000, true);
  timerAlarmEnable(timer);

}


The main loop

As stated before, the main loop will be where we actually handle the timer interrupt, after it being signaled by the ISR. For simplicity, we will use polling to check the value of the interrupt counter, but naturally a much more efficient approach would be using a semaphore to lock the main loop, which would be then unlocked by the ISR. This is the approach used on the original example.

So, we will check if the interruptCounter variable is greater than zero and if it is, we will enter the interrupt handling code. There, the first thing we will do is decrementing this counter, signaling that the interrupt has been acknowledged and will be handled.

Since this variable is shared with the ISR, we will do it inside a critical section, which we specify by using a portENTER_CRITICAL and a portEXIT_CRITICAL macro. Both of these calls receive as argument the address of our global portMUX_TYPE variable.

if (interruptCounter > 0) {

    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);

    // Interrupt handling code
  }

The actual interrupt handling will simply consist on incrementing the counter with the total number of interrupts that occurred since the beginning of the program and printing it to the serial port. You can check below the full main loop code, which already includes this call.

void loop() {

  if (interruptCounter > 0) {

    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);

    totalInterruptCounter++;

    Serial.print("An interrupt as occurred. Total number: ");
    Serial.println(totalInterruptCounter);

  }
}


The ISR code

The interrupt service routine needs to be a function that returns void and receives no arguments.

Our function will be as simple as incrementing the interrupts counter that will signal the main loop that an interrupt as occurred. This will be done inside a critical section, declared with the portENTER_CRITICAL_ISR and portEXIT_CRITICAL_ISR macros, which both receive as input parameters the address of the portMUX_TYPE global variable we declared early.

Update: The interrupt handling routine should have the IRAM_ATTR attribute, in order for the compiler to place the code in IRAM. Also, interrupt handling routines should only call functions also placed in IRAM, as can be seen here in the IDF documentation. Thanks to Manuato for point this.

The full code for this function can be seen below.

void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);

}


The final code

The final source code for our periodic timer interrupt program can be seen below.

volatile int interruptCounter;
int totalInterruptCounter;

hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

void IRAM_ATTR onTimer() {
  portENTER_CRITICAL_ISR(&timerMux);
  interruptCounter++;
  portEXIT_CRITICAL_ISR(&timerMux);

}

void setup() {

  Serial.begin(115200);

  timer = timerBegin(0, 80, true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000000, true);
  timerAlarmEnable(timer);

}

void loop() {

  if (interruptCounter > 0) {

    portENTER_CRITICAL(&timerMux);
    interruptCounter--;
    portEXIT_CRITICAL(&timerMux);

    totalInterruptCounter++;

    Serial.print("An interrupt as occurred. Total number: ");
    Serial.println(totalInterruptCounter);

  }
}


Testing the code

To test the code, simply upload it to your ESP32 board and open the Arduino IDE serial monitor. You should get an output similar to figure 1, where the messages should be printed with a periodicity of 1 second.

ESP32 Arduino timer interrupt.png

Figure 1 – Output of the timer interrupts program.


Related posts

 

References

[1] https://esp-idf.readthedocs.io/en/v1.0/api/timer.html

[2] http://espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf

52 thoughts on “ESP32 Arduino: Timer interrupts”

  1. Pingback: ESP32 Arduino web server: Sending data to JavaScript client via websocket – techtutorialsx

  2. Pingback: ESP32 Arduino web server: Sending data to JavaScript client via websocket – techtutorialsx

  3. Pingback: ESP32 Arduino: Getting DHT22 sensor measurements with interrupts – techtutorialsx

  4. Pingback: ESP32 Arduino: Getting DHT22 sensor measurements with interrupts – techtutorialsx

  5. I’m trying to poll for button presses while doing a https POST. I moved my polling from setup() to a timer ISR that runs every 1ms but I miss presses that occur while doing network I/O. I believe this is because the timer task runs at priority 1 and that is less than the network stack.

    Is there a way to make the timer task run at a higher priority? I configTIMER_TASK_PRIORITY controls the priority but I don’t see any way to change it.

    1. Hi!

      Just as an initial suggestion, if you want to get changes from a button, then the most efficient approach is actually using an external interrupt rather than polling:
      https://techtutorialsx.com/2017/09/30/esp32-arduino-external-interrupts/

      If this is possible for your application, then it would probably solve your problems.

      When you mentioned “timer task”, are you referring to the handling function that is executed when the interrupt is triggered, or some Additional FreeRTOS task that you are running to read the button state?

      If you are creating a FreeRTOS task, then you can assign its priority:
      https://techtutorialsx.com/2017/05/06/esp32-arduino-creating-a-task/

      Note that an interrupt, as the name implies, is something that interrupts the normal flow of execution of the code.

      So, as far as I’m aware (sometimes there are some tricky exceptions hidden in the IDF framework), when an interrupt occurs, the running task halts, the interrupt service routine is executed and then control is returned back to the task.

      This is why interrupt service routines should be as short as possible and not do any blocking calls.

      So your problem may be related to something other than the network stack.

      Also, the ESP32 has two cores, so if you assign all your routines to the core that is not running the network stack, they should not interfere.

      You can read more about the lower level detail interrupts here:
      https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/intr_alloc.html

      Hope this helps getting you in the right path.

      Best regards,
      Nuno Santos

  6. I’m trying to poll for button presses while doing a https POST. I moved my polling from setup() to a timer ISR that runs every 1ms but I miss presses that occur while doing network I/O. I believe this is because the timer task runs at priority 1 and that is less than the network stack.
    Is there a way to make the timer task run at a higher priority? I configTIMER_TASK_PRIORITY controls the priority but I don’t see any way to change it.

    1. Hi!
      Just as an initial suggestion, if you want to get changes from a button, then the most efficient approach is actually using an external interrupt rather than polling:
      https://techtutorialsx.com/2017/09/30/esp32-arduino-external-interrupts/
      If this is possible for your application, then it would probably solve your problems.
      When you mentioned “timer task”, are you referring to the handling function that is executed when the interrupt is triggered, or some Additional FreeRTOS task that you are running to read the button state?
      If you are creating a FreeRTOS task, then you can assign its priority:
      https://techtutorialsx.com/2017/05/06/esp32-arduino-creating-a-task/
      Note that an interrupt, as the name implies, is something that interrupts the normal flow of execution of the code.
      So, as far as I’m aware (sometimes there are some tricky exceptions hidden in the IDF framework), when an interrupt occurs, the running task halts, the interrupt service routine is executed and then control is returned back to the task.
      This is why interrupt service routines should be as short as possible and not do any blocking calls.
      So your problem may be related to something other than the network stack.
      Also, the ESP32 has two cores, so if you assign all your routines to the core that is not running the network stack, they should not interfere.
      You can read more about the lower level detail interrupts here:
      https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/intr_alloc.html
      Hope this helps getting you in the right path.
      Best regards,
      Nuno Santos

  7. Thanks for your response (and also all of the fine information you’ve posted!)

    My project does https POSTs on button presses. I’m using 5 inputs to poll 5 buttons with digitalRead(). I’ve written a number of unix device drivers over the years and was expecting esp32/arduino timer interrupts to run at a high priority than the network.

    I found another one of your pages that shows how to create a task:

    https://techtutorialsx.com/2017/05/06/esp32-arduino-creating-a-task/

    I got that working but even at priority configMAX_PRIORITIES (25)
    my code still gets locked out long enough to miss button presses.

    I now suspect that the WiFi/ETH library code is doing something that locks out other tasks for 100’s of ms.

    1. You’re welcome 🙂

      That’s indeed a weird behavior since the interrupt handling routines should execute with priority, as you mention.

      I’m not sure how those lower level networking tasks work under the hood. But my recommendation is to expose your problem in the IDF GitHub page.

      IDF is the lower level framework (the official one from Espressif for the ESP32) on top of which the Arduino core is built.

      So, most likely somebody there knows how the networking tasks can affect the regular flow of your code.

      In case you find an answer please let us know since it will be useful for others 🙂

      Best regards,
      Nuno Santos

  8. Thanks for your response (and also all of the fine information you’ve posted!)
    My project does https POSTs on button presses. I’m using 5 inputs to poll 5 buttons with digitalRead(). I’ve written a number of unix device drivers over the years and was expecting esp32/arduino timer interrupts to run at a high priority than the network.
    I found another one of your pages that shows how to create a task:
    https://techtutorialsx.com/2017/05/06/esp32-arduino-creating-a-task/
    I got that working but even at priority configMAX_PRIORITIES (25)
    my code still gets locked out long enough to miss button presses.
    I now suspect that the WiFi/ETH library code is doing something that locks out other tasks for 100’s of ms.

    1. You’re welcome 🙂
      That’s indeed a weird behavior since the interrupt handling routines should execute with priority, as you mention.
      I’m not sure how those lower level networking tasks work under the hood. But my recommendation is to expose your problem in the IDF GitHub page.
      IDF is the lower level framework (the official one from Espressif for the ESP32) on top of which the Arduino core is built.
      So, most likely somebody there knows how the networking tasks can affect the regular flow of your code.
      In case you find an answer please let us know since it will be useful for others 🙂
      Best regards,
      Nuno Santos

Leave a Reply

Discover more from techtutorialsx

Subscribe now to keep reading and get access to the full archive.

Continue reading