ESP32 GPIO Interrupts using ESP-IDF

In this ESP-IDF tutorial, we will learn to use ESP32 GPIO Interrupts using ESP-IDF. We will demonstrate GPIO Interrupts through an example that uses a push button and an LED. The external interrupt ISR routine will provide a signal to FreeRTOS task functions to control the onboard LED. One of the GPIO pin will be initialized and configured as an interrupt source. It will trigger an interrupt on a positive edge of an input signal. This will be created using the GPIO driver available in esp-idf which will then toggle the LED. This guide will revolve around GPIO configuration for input/output, interrupt initialization and usage, and using FreeRTOS queues to provide a signal from an interrupt service routine.

Before we move ahead, ensure you have the latest version of VS Code installed on your system with the ESP-IDF extension configured.

If you are new to ESP32 ESP-IDF and don’t know how to use its GPIO pins, you can refer to this getting started guide:

ESP32 GPIO Interrupts

A GPIO interrupt is a form of an external interrupt where an external trigger signal occurs when a key is pressed down (for example). When an interrupt is triggered, the processor halts the execution of the main program. At this point, the Interrupt Service Routine commonly known as ISR is called. The processor then temporarily works on a different task (ISR) and then gets back to the main program after the handling routine has ended. The figure below illustrates the process:

Interrupt Handling Process
Working of Interrupt

Using these types of interrupts is very handy as we do not have to constantly monitor the digital input pin state. In this guide, we will use a push button to trigger a GPIO interrupt. The diagram below shows the GPIO pins of ESP32 that we can use for external interrupts. Note that GPIO6, GPIO7, GPIO8, GPIO9, GPIO10, and GPIO11 can not be used.

ESP32 GPIO Interrupt Pins
ESP32 Interrupt Pins

ESP32 ESP-IDF GPIO Interrupt Example 

Let’s create and build an ESP-IDF project to demonstrate how to use GPIO interrupt for ESP32 using driver/gpio.h and involve FreeRTOS tasks and queues with it.

Create ESP32 GPIO Interrupts Example Project

Open your VS Code and head over to View > Command Palette. Type ESP-IDF: New Project in the search bar and press enter.

Specify the project name and directory. We have named our project ‘ESP32_GPIO_INTERRUPT.’ For the ESP-IDF board, we have chosen the custom board option. For ESP-IDF target, we have chosen ESP32 module. Click ‘Choose Template’ button to proceed forward.

ESP32 GPIO Interrupt using ESP-IDF

In the Extension, select ESP-IDF option:

ESP-IDF in VS Code New Project 2

We will click the ‘sample_project’ under the get-started tab. Now click ‘Create project using template sample_project.’

ESP-IDF in VS Code New Project 3

You will get a notification that the project has been created. To open the project in a new window, click ‘Yes.’

This opens the ESP32_GPIO_INTERRUPT project that we created inside the EXPLORER tab. There are several folders inside our project folder. This is the same for every project which you will create through ESP-IDF Explorer. Lets head over to the main.c file. The main folder contains the source code meaning the main.c file will be found here.

Now go to main > main.c and open it. Copy the code given below in that file and save it.

ESP32 GPIO Interrupts using ESP-IDF Code

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"

#define INPUT_PIN 15
#define LED_PIN 2

int state = 0;
xQueueHandle interputQueue;

static void IRAM_ATTR gpio_interrupt_handler(void *args)
{
    int pinNumber = (int)args;
    xQueueSendFromISR(interputQueue, &pinNumber, NULL);
}

void LED_Control_Task(void *params)
{
    int pinNumber, count = 0;
    while (true)
    {
        if (xQueueReceive(interputQueue, &pinNumber, portMAX_DELAY))
        {
            printf("GPIO %d was pressed %d times. The state is %d\n", pinNumber, count++, gpio_get_level(INPUT_PIN));
            gpio_set_level(LED_PIN, gpio_get_level(INPUT_PIN));
        }
    }
}

void app_main()
{
    gpio_pad_select_gpio(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);

    gpio_pad_select_gpio(INPUT_PIN);
    gpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT);
    gpio_pulldown_en(INPUT_PIN);
    gpio_pullup_dis(INPUT_PIN);
    gpio_set_intr_type(INPUT_PIN, GPIO_INTR_POSEDGE);

    interputQueue = xQueueCreate(10, sizeof(int));
    xTaskCreate(LED_Control_Task, "LED_Control_Task", 2048, NULL, 1, NULL);

    gpio_install_isr_service(0);
    gpio_isr_handler_add(INPUT_PIN, gpio_interrupt_handler, (void *)INPUT_PIN);
}

How the Code Works?

Firstly, we will start by including the necessary libraries for this project. This includes the driver/gpio.h library as we have to work with GPIO pins and FreeRTOS libraries as we want to create tasks and queues in order to blink the LED.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"

We have connected the push button at GPIO15. Therefore, we will define a variable called ‘INPUT_PIN’ that will hold the GPIO pin 15. This will be used later on in the code to read the digital input.

The onboard LED of ESP32 is connected with GPIO2. Therefore, we will define a variable called ‘LED_PIN’ that will hold the GPIO pin 2. This will be used later on in the code to control the digital output.

#define INPUT_PIN 15
#define LED_PIN 2

The gpio_interrupt_handler is the interrupt handler or ISR. It is called whenever an interrupt occurs.

Inside the interrupt service routine we will call the xQueueSendFromISR() function. It will send an item from a queue. This function takes in three parameters. The first parameter is ‘xQueue’ which is the queue handle on which the item is to be posted. In our case it is ‘interuptQueue.’ The second parameter is the ‘pvItemToQueue’ which is the pointer to the item that is placed on the queue. In our case it is ‘&pinNumber.’ The third parameter is the ‘pxHigherPriorityTaskWoken’ which is NULL in our case.

static void IRAM_ATTR gpio_interrupt_handler(void *args)
{
    int pinNumber = (int)args;
    xQueueSendFromISR(interputQueue, &pinNumber, NULL);
}

The LED_Control_Task function is given below. Inside this function, we first create two integer variables ‘pinNumber’ and ‘count’ and set their value to 0. Then inside the infinite while function, we check if an item is received from the queue by using the xQueueReceive() function. This function takes in three parameters. The first parameter is the ‘xQueue’ which is the queue handle from which the item is to be received. It is ‘interuptQueue’ in our case. The second parameter is the ‘pvBuffer’ which is the pointer to the buffer into which the received item will be copied. In our case it is ‘&pinNumber.’ The third parameter is the ‘xTicksToWait’ which is the maximum amount of time the task should block waiting for an item to receive should the queue be empty at the time of the call. In our case it is set as ‘portMAX_DELAY.’ Thus, when the interrupt occurs then gpio_set_level() function is called to set the level of the LED_PIN according to the level of the INPUT_PIN. At this time the ESP-IDF terminal prints the message that the GPIO was pressed, its state, the pin Number and count.


void LED_Control_Task(void *params)
{
    int pinNumber, count = 0;
    while (true)
    {
        if (xQueueReceive(interputQueue, &pinNumber, portMAX_DELAY))
        {
            printf("GPIO %d was pressed %d times. The state is %d\n", pinNumber, count++, gpio_get_level(INPUT_PIN));
            gpio_set_level(LED_PIN, gpio_get_level(INPUT_PIN));
        }
    }
}

app_main()

Inside the app_main() function, we will first configure the input and output GPIOs. First, we will configure the LED_PIN as a GPIO. To do that, we will use the gpio_pad_select_gpio() function. Specify the LED_PIN as a parameter inside this function. Then, we will set the direction of the pin as an output using the gpio_set_direction() function. This function takes in two arguments. The first argument is the GPIO pin and the second argument is the mode (input or output) we want to set the pin in. In our case, we want to set the LED_PIN as an output pin.

gpio_pad_select_gpio(LED_PIN);
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);

Similarly, we will configure the INPUT_PIN as a GPIO and then set its direction as an input. Moreover, we will enable the pulldown resistor and disable the pullup resistor on the input pin. This way when the push button will be pressed, the state of the INPUT_PIN will go HIGH and when it will be released then the state of the INPUT_PIN will go LOW. Then, we will set the interrupt trigger type on the INPUT_PIN using gpio_set_intr_type() function. This function takes in two parameters. The first parameter is the GPIO pin number which is ‘INPUT_PIN’ in our case. The second parameter is the trigger type which is set as ‘GPIO_INTR_POSEDGE’ which means the interrupt will be triggered when the the state of the pin changes from LOW to HIGH. This occurs whenever the push button is pressed.

    gpio_pad_select_gpio(INPUT_PIN);
    gpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT);
    gpio_pulldown_en(INPUT_PIN);
    gpio_pullup_dis(INPUT_PIN);
    gpio_set_intr_type(INPUT_PIN, GPIO_INTR_POSEDGE);

Next, we will create a queue instance using xQueueCreate(). This function will return the handle through which we will reference the new queue. This is named as ‘interuptQueue.’ The xQueueCreate() function takes in two parameters. The first parameter is the maximum items that the queue can hold and the second parameter is the number of bytes for each item. In our case, we have set the maximum number of items as 10 and the number of bytes as the sizeof(int).

  interputQueue = xQueueCreate(10, sizeof(int));

Then we will create the task for LED control. To create a task we use the function xTaskCreate(). This function takes in several arguments.

  • The first argument is the name of the function. In our case, we have set it to LED_Control_Task.
  • The second argument is the name of the task for descriptive purposes. In our case we set the second argument as “LED_Control_Task.”
  • The third argument specifies the stack size of the task. This indicates the amount of memory we want to reserve for the particular task. We have set it to ‘2048’.
  • The fourth argument is the parameter. It is a value that is passed as the parameter to the created task. In our case, we will set this argument as NULL which indicates that we are not using this property.
  • The fifth argument is the priority of the task. We have set it to ’1.’
  • The last argument is the handle which is used to change the function of the task eg. suspend, delete, resume, get or set a config of task. It is optional so we have set it as NULL.
xTaskCreate(LED_Control_Task, "LED_Control_Task", 2048, NULL, 1, NULL);

Then we will call gpio_install_isr_service() to install the interrupt service routine service. It takes in a single parameter which denotes the flags for interrupt allocation. in our case we have specified the parameter as ‘0’. Finally, we will add the ISR handler for the INPUT_PIN using gpio_isr_handler_add(). This function takes in three parameters. The first parameter is the GPIO pin number which we have specified as ‘INPUT_PIN’. The second parameter is the ISR handler function for our GPIO which is gpio_interrupt_handler in our case. The third parameter is the parameter for the ISR handler which is (void *)INPUT_PIN in our case.

    gpio_install_isr_service(0);
    gpio_isr_handler_add(INPUT_PIN, gpio_interrupt_handler, (void *)INPUT_PIN);

Hardware Setup

Set up the circuit as shown in the diagram below:

ESP32 GPIO Interrupts using ESP-IDF

Here the push button is connected with the interrupt pin of ESP32. We have used GPIO15 as the input pin in code, hence one terminal of the push button is connected with GPIO15. As we have already configured a pulldown resistor on this GPIO, there is no need to add a physical resistor ourselves. When the push button is pressed, the state of GPIO15 will turn from LOW to HIGH (a positive rising edge occurs) and an interrupt will be triggered. Another terminal of the pushbutton is connected with 3.3 volts from ESP32 to give an active high signal when pressed.

We are using the onboard LED, hence there is no need to add a physical LED that acts as the digital output.

Compiling the Sketch

To flash your chip, type the following command in the serial terminal. Remember to replace the COM port with the one through which your board is connected.

idf.py -p COMX flash monitor

After the code flashes successfully press the push button connected with GPIO5. Its state changes from LOW to HIGH and the interrupt occurs. This causes the onboard LED to turn ON. The ESP-IDF terminal displays the number of times the GPIO was pressed and its current state.

ESP32 GPIO Interrupt using ESP-IDF Terminal

Video demo:

You may also like to read:

2 thoughts on “ESP32 GPIO Interrupts using ESP-IDF”

  1. Good job of yours. I have one significent question:
    Is it possible to fire an interrupt when a high state will be set on one of the GPIO, but this high state is another source of power.
    Imagine situation when esp32 board is powerd from a car battery(12V addjusted to 3.3V) and when we switch on ignition we provide(12V addjusted to 3.3V) to one of GPIO pin in order to fire the interrupt??????

    Reply
  2. What is the argument for gpio_install_isr_service(0); is ESP_INTR_FLAG_SHARED if using interrupt handler for multiple GPIO pins?

    Reply

Leave a Comment