- ESP32 Arduino: External interrupts
- Introduction
- The setup code
- The main loop
- The interrupt handling function
- The final code
- Testing the code
- ESP32 External Interrupts using Arduino IDE
- Prerequisites
- Interrupts
- External Interrupts
- ESP32 Interrupt Pins
- Configuring Interrupts in Arduino IDE
- External Interrupt using a Push Button to toggle LED
- How the Push Button works with External Interrupt?
- Arduino Sketch External Interrupt
- How the Code Works?
- Demonstration
- ESP32 Interrupt Latency
- What is ISR routine?
- ESP32 Interrupt Latency Measurement
ESP32 Arduino: External interrupts
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.
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.
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.
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.
Moving to the setup function, we start by opening a serial connection, in order to be able to output the results of our program.
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.
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.
The final setup code can be seen below.
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.
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.
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.
The final code
The final source code can be seen below. You can copy and paste it to your Arduino environment to test it.
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.
ESP32 External Interrupts using Arduino IDE
In this ESP32 tutorial, we will learn how to configure and use external interrupts with ESP32 GPIO pins in Arduino IDE. We will demonstrate this through an example with a push button and an LED. Additionally, we will also show you how to measure ESP32 interrupt latency via an oscilloscope measurement.
Table of Contents
Prerequisites
We will use Arduino IDE to program our ESP32 development boards. Thus, you should have the latest version of Arduino IDE. Additionally, you also need to install the ESP32 plugin. If your IDE does not have the plugin installed you can visit the link below:
Interrupts
Interrupts are used to handle events that do not happen during the sequential execution of a program. For example, we want to perform certain tasks and these tasks execute sequentially in your Arduino program. But there are few tasks that only execute when a special event occurs such as an external trigger signal to the digital input pin of a microcontroller.
External Interrupts
An external interrupt or a ‘hardware interrupt’ is caused by the external hardware module. For example, there is a Touch Interrupt which happens when touch is detected and a GPIO interrupt when a key is pressed down. In this tutorial, we will focus on this type of interrupt.
With interrupt, we do not need to continuously check the state of the digital input pin. When an interrupt occurs (a change is detected), the processor stops the execution of the main program and a function is called upon known as ISR or the Interrupt Service Routine. The processor then temporarily works on a different task (ISR) and then gets back to the main program after the handling routine has ended. This is shown in the figure below.
An example can be that of pressing a push button or motion detection with a PIR Sensor. In both cases, a push button or a PIR motion sensor can be used to trigger an interrupt. Therefore, when an external event occurs, the processor stops what it is doing and executes the interrupt service routine which we define for the respective event. After that, it returns to the current program. External Interrupts are extremely useful because with their help we do not have to constantly monitor the digital input pin state.
In a previous tutorial, we used the example of PIR sensor to demonstrate external interrupts with ESP32 in Arduino IDE as well in Micro Python. Follow the links below to access the guides:
This time however we will use a push button to handle the external interrupt.
ESP32 Interrupt Pins
For ESP32 we can use all GPIO pins for external interrupt except for GPIO6, GPIO7, GPIO8, GPIO9, GPIO10, and GPIO11. The diagram below shows the pinout of the GPIO pins in ESP32 that can be used.
Configuring Interrupts in Arduino IDE
Now let us look at how to set up external interrupts in our ESP32 development boards using Arduino IDE. The following steps need to be followed.
We will use the following function to configure an interrupt in Arduino IDE:
The attachInterrupt() function takes in three arguments:
- digitalPinToInterrupt(pin): This is a function which takes in the GPIO pin of the ESP32 board as a parameter inside it. The pin denotes the GPIO associated with the pin which will cause an interrupt to occur. For example if setting GPIO2 as an interrupt pin the function will be specified as digitalPinToInterrupt(2). You can use any of the ESP32 interrupt pins shown in the diagram above, as a parameter inside this function.
- ISR: This is the second argument used to set up an interrupt. It is a special kind of function known as the Interrupt Service Routine which takes in no parameters and also returns nothing. Whenever the interrupt will occur this function will be called.
- mode: This denotes the triggering action for the interrupt to occur. The following four parameters are used to specify the mode:
LOW: This is used to trigger the interrupt when the pin is in a low state.
CHANGE: This is used to trigger the interrupt when the pin changes its state (HIGH-LOW or LOW-HIGH)
RISING: This is used to trigger the interrupt when the pin goes from LOW to HIGH.
FALLING: This is used to trigger the interrupt when the pin goes from HIGH to LOW.
External Interrupt using a Push Button to toggle LED
Now we will learn how to handle interrupts in the ESP32 board using a push button to toggle an LED. The push button will be connected to an interrupt pin of ESP32 and configured as an input. Whereas the LED will be set up as a digital output. The LED will be toggled on each rising edge.
The following components are required:
- ESP32 development board
- Push button
- One 5mm LED
- One 220 ohm resistor
- One 10k ohm resistor
- Breadboard
- Connecting Wires
Assemble your circuit as shown below:
In the above schematics, we can see that GPIO22 is connected with the anode pin of LED, and the cathode pin is connected with the common ground through the 220-ohm resistor.
The push button has four terminals. One terminal is powered by 3.3 volts from ESP32 and the other terminal is connected by GPIO15 and the 10k ohm resistor which acts as a pull-down resistor. The other end of the resistor is connected with the common ground.
When the pushbutton is not pressed, logic low will appear on GPIO15, or push button state will be low and when the push button is pressed, a logic high will be on GPIO15. That means a rising edge occurs when a push button is pressed. We can detect this rising edge with the help of interrupt pins of ESP32. You can use any appropriate ESP32 interrupt GPIO pin which we showed you at the start.
How the Push Button works with External Interrupt?
The push button acts as a source for the external interrupt. That means we connect the output of the push button with the GPIO pin of ESP32. Furthermore, we attach the rising edge-triggered interrupt to this GPIO pin. That means this GPIO pin will trigger the interrupt whenever it will sense a rising edge on its input.
When the push button will be pressed, an external interrupt is caused, the LED will toggle. If initially it was OFF, it will turn ON and vice versa.
Arduino Sketch External Interrupt
Open your Arduino IDE and go to File > New to open a new file. Copy the code given below in that file and save it.
How the Code Works?
First, we will define the GPIO pin through which the LED is connected. It is GPIO22 in our case. Also, we will define the GPIO pin through which the push button is connected. It is GPIO15 in our case.
Next, we will define the function which will act as the Interrupt Service Routine(ISR). It is called toggleLED(). Whenever the push button will be pressed, this function will be called. It will toggle the state of the LED. This will be achieved by using the digitalWrite() function and using the led_pin and !digitalRead(LED_pin) as parameters inside it. The digitalRead() function is used to read the state of the input pin. This digitalRead() function returns either logical HIGH or logical LOW input. If input state of the pin is HIGH, it will return HIGH and otherwise low.
Inside the setup() function, we will first configure the LED pin as an output pin and the push button pin as an input pin. Then, we will set the interrupt by using the attachInterrupt() function and pass three arguments inside it. These include the ‘pushButton_pin’ that configures the interrupt with the pin connected with the push button, the ISR which is the ‘toggleLED’ function which we defined previously, and lastly RISING which is the mode for the interrupt set up. The pushButton_pin state will go through a change (LOW to HIGH) when the push button will be pressed and call the toggleLED function. This will in return change the state of the LED_pin.
Demonstration
Choose the correct board and COM port before uploading your code to the ESP32 board. Go to Tools > Board and select ESP32 Dev Module.
Next, go to Tools > Port and select the appropriate port through which your board is connected.
Click on the upload button to upload the code into the ESP32 board. After you have uploaded your code to the board press its ENABLE button.
Now press the push button. The LED will turn ON. Press the push button again. The LED will turn OFF. Likewise, keep on pressing a push button and the LED will keep on toggling.
Watch the demonstration video below:
Sometimes the LED will toggle inconsistently as well. This is because of the push button bouncing which can be interpreted as a single button press as many. To counter that, we can include a debouncing circuit to our circuit design.
ESP32 Interrupt Latency
Interrupt Latency is the time when the interrupt was triggered to the time the event handler started execution. Ideally, we would want this time to be less. There are several factors that affect the interrupt latency including the microcontroller’s architecture/design, clock speed, type of interrupt controller used and the operating system itself.
We will show you to measure the time taken when the external interrupt was triggered till the start of the ISR routine.
What is ISR routine?
An interrupt service routine (ISR) is also called an interrupts handler. There are many different types of interrupt handlers which may handle many different types of interrupts. Like the clock in a system has its own interrupt handler same as the keyboard it also has its own interrupt handler. Every device which is existing has its interrupt handler.
Generally, ISR will use a volatile variable which can still be used between other pieces of code. Also, ISR should be as short and fast as possible. Interrupts execute immediately and stop everything that the program is currently doing in order to jump into the interrupts function and execute the code. Furthermore, the interrupts will return to the same point within the software where it had previously left off.
Above is an example of execution. So line by line the code will execute until the interrupt is called on Line3. Then the function jumps down to the ISR and starts executing line5 and line 6. After executing the lines within the ISR it jumps back to line4 and finishes the execution as routine. If it is in a loop then it goes back to line1.
ESP32 Interrupt Latency Measurement
Now let us measure the interrupt latency of the example which we provided above where we used a push button to trigger the interrupt and toggled the LED pin. We will basically measure the time the CPU took to handle the external interrupt by changing the state of the LED pin.