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

Advertisements

10 Replies to “ESP32 Arduino: Timer interrupts”

  1. Hi
    Me again, Thank you for the informative tutorials. I am measuring and storing an analog within the ISR, (Val[interruptCounter] = AnalogRead(AnalogPin))
    all goes well until i change the the analog pin in void().The ISR stops responding. Any suggestions how to fix?

    Liked by 1 person

    1. Hi!

      You’re welcome, and thanks for the feedback 🙂

      I’ve never faced that issue and it is weird since the two things don’t seem to be related.

      Nonetheless, my first suggestion to try to solve the problem is moving the Analog Read to outside of the ISR.

      ISRs are designed to run as fast as possible, so typically a good architecture style is using the ISR to simply set a variable (a flag or a counter, for example) that you then check in your main code.

      Then, in your main code, if that variable indicates that the IRS occurred, you do the actual operation, which in your case is reading from the analog pin.

      In principle, reading from the analog pin should be something fast, but in the implementation of the function there may be some lower level call that is messing with the ISR.

      Inside ISRs there are some primitive calls that we can’t do and thus I’ve found the simplest solution is moving the handling code to outside the ISR.

      Depending on how experienced you are with the ESP32 and threading, you can even leverage FreeRTOS tasks and block a task in some syncronization primitive and unlock when the ISR occurs.

      That way, you are not wasting CPU constantly polling some variable to check if the interrupt has occurred.

      One final note, maybe you are already aware, but the ESP32 ADCs are not linear, so if you compare the voltages you obtain from the ADC with the real value using, for example, a multimeter, then they will most likely differ.

      Not sure when or if this will be fixed in the software side.

      Hope this helps 🙂

      Best regards,
      Nuno Santos

      Like

  2. Thanks for the tutorial! I don’t have ESP32 hardware (yet?) – how fast is the CPU, can it execute a simple IRQ routine at, say, 100 kHz without affecting the WIFI operation?

    Liked by 1 person

    1. Hi,

      You’re welcome 🙂

      The ESP32 has a dual core processor and it can run at 240 MHz, but I’m not sure if all the modules out there use the same clock frequency.

      You can check more detailed info about the CPU at section 3.1.1 of the datasheed (age 12):
      https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf

      I would say it can, but I never did many load tests on it, so I cannot confirm if some problem will arise.

      Nonetheless, given the fact that it has two cores, you should be able to run your code in the core that is not handling the WiFi without any problems.

      From this GitHub post, WiFi should be running on core 0:
      https://github.com/espressif/esp-idf/issues/968

      But let us know when you receive your ESP and have a chance to give it a try 🙂

      Best regards,
      Nuno Santos

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s