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. Hello.
    I tried this example, but with other prescaler values – I need the highest frequency to input a 64 bit counter, preferably 80MHz. When set to “timer = timerBegin (0, 1, true);” divide by two, just like setting “timer = timerBegin (0, 2, true);”.
    Any advice, please? Thank you.

  2. Hi,
    I’m interested in making a frequency counter and your project is a closest thing to what I want. Because instead of counting the internal clock, now it counts the external frequency. Any idea in implementing it with your existing code? Thanks for a great tutorial.

  3. I found this focused article helpful.

    Regarding, ” functions also placed in IRAM” Might be a good topic for a video to explain IRAM.

    And the link in, “as can be seen here in the IDF documentation” does not go to public documentation.

  4. I need a Arduino code for Quectel M95 GSM Module about the trigger powerkey function. I have a high signal because of pull resistors. So I need to pull ground this signal for <1sec, after that the signal turn back its original high situation. I want to use timer interrupt. How can I accomplish this task ??

  5. Hi, great article – many thanks.

    I was just wondering if the arduino core uses any of the ESP timer interrupts and hence is is it safe to use all/any of the ESP32 hardware timers, or are some already used by existing Arduino core or library functions?

    Thanks

Leave a Reply

Discover more from techtutorialsx

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

Continue reading