The objective of this post is to explain how to handle external interrupts using the ESP32 and 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 handle external interrupts using the ESP32 and the Arduino core.
The tests were performed on a DFRobot’s ESP-WROOM-32 device integrated in a ESP32 FireBeetle board.
The setup code
We will start by declaring the pin where the interrupt will be attached on a global variable. Note that depending on your ESP32 board the pin numbering of the ESP32 microcontroller and the one labeled on the board may not match. In the FireeBeetle board, the pin used below (digital pin 25) matches with the one labeled IO25/D2.
const byte interruptPin = 25;
We will also declare a counter that will be used by the interrupt routine to communicate with the main loop function and signal that an interrupt has occurred. Note that this variable needs to be declared as volatile since it will be shared by the ISR and the main code. Otherwise, it may be removed due to compiler optimizations.
volatile int interruptCounter = 0;
Additionally we will declare a counter to keep track of how many interrupts have already occurred globally since the start of the program. So this counter will be incremented each time an interrupt occurs.
int numberOfInterrupts = 0;
Finally, we will declare a variable of type portMUX_TYPE, which we will need to take care of the synchronization between the main code and the interrupt. We will see how to use it later.
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
Moving to the setup function, we start by opening a serial connection, in order to be able to output the results of our program.
Serial.begin(115200); Serial.println("Monitoring interrupts: ");
Next, since we are going to be working with an external pin interrupt, we need to configure the previously declared pin number as an input pin. To do so we call the pinMode function, passing as argument the the number of the pin and the operating mode.
In order to know the state of the input when no electric signal is applied to our pin, we use the INPUT_PULLUP mode. So, when no signal is applied, it will be at a voltage level of VCC instead of floating, avoiding the detection of non existing external interrupts.
pinMode(interruptPin, INPUT_PULLUP);
Next we attach the interrupt to the pin with a call to the attachInterrupt function. As first argument, we pass the result of a call to the digitalPinToInterrupt function, which converts the pin number used to the corresponding internal interrupt number.
Next we pass the function that will handle the interrupts or, in other words, that will be executed when an interrupt on the specified pin occurs. We will call it handleInterrupt and specify its code later.
Finally we pass the interrupt mode, which basically specifies which type of change in the pin input signal triggers the interrupt. We will use FALLING, which means that the interrupt will occur when a change from VCC to GND is detected on the pin.
attachInterrupt(digitalPinToInterrupt(interruptPin), handleInterrupt, FALLING);
The final setup code can be seen below.
const byte interruptPin = 25; volatile int interruptCounter = 0; int numberOfInterrupts = 0; portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; void setup() { Serial.begin(115200); Serial.println("Monitoring interrupts: "); pinMode(interruptPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(interruptPin), handleInterrupt, FALLING); }
The main loop
Now we will move to the main loop. There we will simply check if our interrupts counter is greater than zero. If it does, it means that we have interrupts to handle.
So, if an interrupt has occurred we first take care of decrementing this interrupts counter, signalling that the interrupt has been detected and will be handled.
Note that this counter approach is better than using a flag since if multiple interrupts occur without the main code being able to handle them all, we will not loose any events. On the other hand if we use a flag and multiple interrupts occur without the main code being able to handle them, then the flag value will keep being set to true in the ISR and the main loop handler will only interpret as if only one has occurred.
Other important aspect to keep in mind is that we should disable interrupts when writing on a variable that is shared with an interrupt. This way we ensure that there is no concurrent access to it between the main code and the ISR.
In the Arduino environment, we usually have the NoInterrupts and Interrupts function to disable and re-enable interrupts. Nonetheless, at the time of writing, these functions were not yet implemented in the ESP32 Arduino core.
So, we perform the decrement of the variable inside a critical section, which we declare using a portENTER_CRITICAL and a portEXIT_CRITICAL macro. These calls both receive as input the address of the previously declared global portMUX_TYPE variable.
if(interruptCounter>0){ portENTER_CRITICAL(&mux); interruptCounter--; portEXIT_CRITICAL(&mux); //Handle the interrupt }
After taking care of decrementing the counter, we will now increment the global counter that holds the number of interrupts detected since the beginning of the program. This variable doesn’t need to be incremented inside a critical section since the interrupt service routine will not access it.
After that, we will print a message indicating an interrupt was detected and how many interrupts have happened so far. Note that sending data to the serial port should never be done inside an interrupt service routine due to the fact that ISRs should be designed to execute as fast as possible. If you do this, you will most likely run into runtime problems.
This way, in our architecture, the ISR only takes care of the simple operation of signaling the main loop that the interrupt has occurred, and then the main loop handles the rest.
You can check the full main loop code below.
void loop() { if(interruptCounter>0){ portENTER_CRITICAL(&mux); interruptCounter--; portEXIT_CRITICAL(&mux); numberOfInterrupts++; Serial.print("An interrupt has occurred. Total: "); Serial.println(numberOfInterrupts); } }
The interrupt handling function
To finish the code, we will declare our interrupt handling function. As previously mentioned, it will only take care of incrementing the global variable that is used to signalize to the main loop that an interrupt has occurred.
We will also enclose this operation in a critical section, which we declare by calling the portENTER_CRITICAL_ISR and portExit_CRITICAL_ISR macros. They also both receive as input the address of the global portMUX_TYPE variable.
This is needed because the variable we are going to use is also changed by the main loop, as seen before, and we need to prevent concurrent access problems.
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 the interrupt handling function is shown below.
void IRAM_ATTR handleInterrupt() { portENTER_CRITICAL_ISR(&mux); interruptCounter++; portEXIT_CRITICAL_ISR(&mux); }
The final code
The final source code can be seen below. You can copy and paste it to your Arduino environment to test it.
const byte interruptPin = 25; volatile int interruptCounter = 0; int numberOfInterrupts = 0; portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; void IRAM_ATTR handleInterrupt() { portENTER_CRITICAL_ISR(&mux); interruptCounter++; portEXIT_CRITICAL_ISR(&mux); } void setup() { Serial.begin(115200); Serial.println("Monitoring interrupts: "); pinMode(interruptPin, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(interruptPin), handleInterrupt, FALLING); } void loop() { if(interruptCounter>0){ portENTER_CRITICAL(&mux); interruptCounter--; portEXIT_CRITICAL(&mux); numberOfInterrupts++; Serial.print("An interrupt has occurred. Total: "); Serial.println(numberOfInterrupts); } }
Testing the code
To test the code, simply upload it to your ESP32 and open the Arduino IDE serial monitor. The easiest way to trigger interrupts is to use a wire to connect and disconnect the digital pin where the interrupt was attached to GND.
Since the pin was declared as INPUT_PULLUP, then this will trigger a transition from VCC to GND and an external interrupt will be detected. Please be careful to avoid connecting the ground pin to the wrong GPIO and damaging the board.
You should get an output similar to figure 1, which shows the interrupts being triggered and the global counter being printed.
Figure 1 – Output of the interrupt handling program.
Has the FALLING edge construct been fixed in the attachInterrupt function ?
Back to the code I posted on Sept 6, I get two interrupts per RPM pulse on a magnetic sensor.
Hi!
Unfortunately I’m not sure, I’ve not been working with interrupts for a while.
My suggestion is to ask around the Arduino core GitHub page, they probably can confirm it 🙂
Best regards,
Nuno Santos
Has the FALLING edge construct been fixed in the attachInterrupt function ?
Back to the code I posted on Sept 6, I get two interrupts per RPM pulse on a magnetic sensor.
Hi!
Unfortunately I’m not sure, I’ve not been working with interrupts for a while.
My suggestion is to ask around the Arduino core GitHub page, they probably can confirm it 🙂
Best regards,
Nuno Santos
Mto interessante…
Como poderei usar este codigo num sensor de fluxo?
Sorry for my bad english.
Muito Obrigado 🙂
Como funciona o seu fluxo de sensor? É analógico ou digital?
Cumprimentos,
Nuno Santos
Mto interessante…
Como poderei usar este codigo num sensor de fluxo?
Sorry for my bad english.
Muito Obrigado 🙂
Como funciona o seu fluxo de sensor? É analógico ou digital?
Cumprimentos,
Nuno Santos
Pins 12,13,14,15 are routed to JTAG debugging hardware on ESP32 at boot up. So you cannot use them for general GPIO (or interrupts) unless you first “steal back the pins”. https://github.com/cspwcspw/ESP32_CamToLCD/blob/master/ESP32_Pin_Usage.pdf
Pins 12,13,14,15 are routed to JTAG debugging hardware on ESP32 at boot up. So you cannot use them for general GPIO (or interrupts) unless you first “steal back the pins”. https://github.com/cspwcspw/ESP32_CamToLCD/blob/master/ESP32_Pin_Usage.pdf
I reckon the logic in this tutorial is wrong. One cannot use locks and critical sections of code as a substitute for disabling interrupts.
Let’s assume you execute line 26 in the sample (lock and enter the critical section). Then an interrupt occurs. Now your interrupt handler will block, waiting for the lock, and your code is deadlocked – neither the interrupt handler nor the main loop can make progress. (And then the Watchdog Timer will bite you!)
Interrupts can be disabled / masked, which would be the correct thing to do at line 26. Then you won’t need the mux: both the ISR and the main loop run on the same core, on a single thread in the Arduino environment.
https://www.esp32.com/viewtopic.php?t=2467 tells us how to mask interrupts.
Hi!
Thanks for your feedback.
I understand your point but if you read IDF’s documentation, it specifically states (in bold) that disabling interrupts is not a valid protection method against simultaneous access to shared data as it leaves the other core free to access the data even if the current core has disabled its own interrupts:
https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/freertos-smp.html?highlight=portENTER_CRITICAL_ISR#critical-sections-disabling-interrupts
It also clearly states (in the same link) that the “enterCritical” macros were design for that protection.
Note that even the name implies it can be used in interrupts:
portENTER_CRITICAL_ISR
I’m not sure how the ESP32 core handles that situation you have mentioned under the hood because in a regular synchronization scenario it would indeed end up in a deadlock.
But it’s an interesting question to ask, probably in IDF’s GitHub page.
Note that Arduino’s interrupt example also uses the same approach of critical sections:
https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/Timer/RepeatTimer/RepeatTimer.ino
Since this example has as contributor me-no-dev, it’s another strong indication that this is the correct way of doing, since he is probably one of the guys that knows more about how the ESP32 works 🙂
Furthermore, take in consideration that FreeRTOS has specific APIs to handle locks from interrupts:
https://www.freertos.org/xSemaphoreTakeFromISR.html
https://www.freertos.org/a00124.html
Regarding the assumption about the fact that both the Arduino loop and setup run in the same core, that can change in the future without we even notice, since there is no coupling between our code and where those two functions will run. We don’t indicate explicitly if setup and main loop run in a given core, so I would say it’s a much better approach to not rely on that and protect data access anyway.
Furthermore, it’s completely reasonable that someone using the Arduino core also wants to work with FreeRTOS tasks and multi-core, since it’s really simple to do (FreeRTOS primitives are available in the arduino core)
This is also why, in this tutorial, I’ve covered the use of critical sections.
Nonetheless, I’ve not extensively tested all the use cases of synchronization between tasks and interrupts using the critical sections, so I cannot guarantee you that there is no deadlock situation I’m not aware about.
I can only comment that so far I did not run into any troubles using this approach 🙂
Did you run into any problem so far using critical sections?
This is indeed an interesting problematic, so let me know your thoughts on this information, and thanks for contributing with more insight 🙂
Best regards,
Nuno Santos
More information:
https://www.esp32.com/viewtopic.php?t=1703
It seems like the critical section disables interrupts on the current core (therefore, when the loop enters the critical section, there’s no danger of interrups being called in the same core, so they will not try to get a locked mutex and stay locked forever) and then even if an interrupt in the other core tries to take the mutex, eventually the first core will release it, thus not deadlocking.
Probably if the first core as a blocking or very big critical section it will cause problems, but it’s a general rule that critical sections should be as short as possible.
Best regards,
Nuno Santos
I reckon the logic in this tutorial is wrong. One cannot use locks and critical sections of code as a substitute for disabling interrupts.
Let’s assume you execute line 26 in the sample (lock and enter the critical section). Then an interrupt occurs. Now your interrupt handler will block, waiting for the lock, and your code is deadlocked – neither the interrupt handler nor the main loop can make progress. (And then the Watchdog Timer will bite you!)
Interrupts can be disabled / masked, which would be the correct thing to do at line 26. Then you won’t need the mux: both the ISR and the main loop run on the same core, on a single thread in the Arduino environment.
https://www.esp32.com/viewtopic.php?t=2467 tells us how to mask interrupts.
Hi!
Thanks for your feedback.
I understand your point but if you read IDF’s documentation, it specifically states (in bold) that disabling interrupts is not a valid protection method against simultaneous access to shared data as it leaves the other core free to access the data even if the current core has disabled its own interrupts:
https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/freertos-smp.html?highlight=portENTER_CRITICAL_ISR#critical-sections-disabling-interrupts
It also clearly states (in the same link) that the “enterCritical” macros were design for that protection.
Note that even the name implies it can be used in interrupts:
portENTER_CRITICAL_ISR
I’m not sure how the ESP32 core handles that situation you have mentioned under the hood because in a regular synchronization scenario it would indeed end up in a deadlock.
But it’s an interesting question to ask, probably in IDF’s GitHub page.
Note that Arduino’s interrupt example also uses the same approach of critical sections:
https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/Timer/RepeatTimer/RepeatTimer.ino
Since this example has as contributor me-no-dev, it’s another strong indication that this is the correct way of doing, since he is probably one of the guys that knows more about how the ESP32 works 🙂
Furthermore, take in consideration that FreeRTOS has specific APIs to handle locks from interrupts:
https://www.freertos.org/xSemaphoreTakeFromISR.html
https://www.freertos.org/a00124.html
Regarding the assumption about the fact that both the Arduino loop and setup run in the same core, that can change in the future without we even notice, since there is no coupling between our code and where those two functions will run. We don’t indicate explicitly if setup and main loop run in a given core, so I would say it’s a much better approach to not rely on that and protect data access anyway.
Furthermore, it’s completely reasonable that someone using the Arduino core also wants to work with FreeRTOS tasks and multi-core, since it’s really simple to do (FreeRTOS primitives are available in the arduino core)
This is also why, in this tutorial, I’ve covered the use of critical sections.
Nonetheless, I’ve not extensively tested all the use cases of synchronization between tasks and interrupts using the critical sections, so I cannot guarantee you that there is no deadlock situation I’m not aware about.
I can only comment that so far I did not run into any troubles using this approach 🙂
Did you run into any problem so far using critical sections?
This is indeed an interesting problematic, so let me know your thoughts on this information, and thanks for contributing with more insight 🙂
Best regards,
Nuno Santos
More information:
https://www.esp32.com/viewtopic.php?t=1703
It seems like the critical section disables interrupts on the current core (therefore, when the loop enters the critical section, there’s no danger of interrups being called in the same core, so they will not try to get a locked mutex and stay locked forever) and then even if an interrupt in the other core tries to take the mutex, eventually the first core will release it, thus not deadlocking.
Probably if the first core as a blocking or very big critical section it will cause problems, but it’s a general rule that critical sections should be as short as possible.
Best regards,
Nuno Santos
If the CLK pin of a rotary encoder is connected to pin 25, then as the rotary encoder is moved through one “click” in either direction the “An interrupt has occurred” statement is printed several times with the “Total” incrementing accordingly.
With a 0.1microF capacitor, the encoder bouncing is reduced as indicated by the print statement occurring two or three times.
How did you debounce the switch?
Hi!
Sorry for the delay.
I’ve never worked with rotary encoders, so I cannot help much unfortunately.
I did not use a switch, I simply connect a wire between the pin and GND and disconnect, to trigger some interrupts.
It also suffers from bouncing, which was not covered in this tutorial.
When I want to debounce a switch, I do it on the software side. Basically, when an interrupt occurs, I act accordingly and register the time when it as occurred (with the micros function).
On new interrupts, I compare the current time with the time I’ve registered from the previous interrupt and if it is lesser than a “debouncing” interval, I simply discard it and do not act on it.
Not sure if this applies to your use case, but this is a simple way of debouncing on the software side.
Hope this helps!
Best regards,
Nuno Santos
If the CLK pin of a rotary encoder is connected to pin 25, then as the rotary encoder is moved through one “click” in either direction the “An interrupt has occurred” statement is printed several times with the “Total” incrementing accordingly.
With a 0.1microF capacitor, the encoder bouncing is reduced as indicated by the print statement occurring two or three times.
How did you debounce the switch?
Hi!
Sorry for the delay.
I’ve never worked with rotary encoders, so I cannot help much unfortunately.
I did not use a switch, I simply connect a wire between the pin and GND and disconnect, to trigger some interrupts.
It also suffers from bouncing, which was not covered in this tutorial.
When I want to debounce a switch, I do it on the software side. Basically, when an interrupt occurs, I act accordingly and register the time when it as occurred (with the micros function).
On new interrupts, I compare the current time with the time I’ve registered from the previous interrupt and if it is lesser than a “debouncing” interval, I simply discard it and do not act on it.
Not sure if this applies to your use case, but this is a simple way of debouncing on the software side.
Hope this helps!
Best regards,
Nuno Santos