ESP32 GPIO Interrupts with Arduino IDE

Learn how to configure and handle interrupts with the ESP32 board to detect and respond to changes on its input GPIOs. We’ll build a project example using a pushbutton and another one using a PIR Motion Sensor.

ESP32 GPIO Interrupts with Arduino IDE

Setting an ESP32 Input Pin as an interrupt allows you to quickly detect events (changes in the GPIO state) and react to them by halting the main program and running a callback function (also called an interrupt service routine—ISR). This can be useful to detect the press of a pushbutton, when a sensor passes a certain threshold, when motion is detected, etc.

Table of Contents

In this tutorial, we’ll cover the following subjects:

Prerequisites

Before proceeding with this tutorial, you should have the ESP32 boards installed in your Arduino IDE. Follow this next tutorial to install the ESP32 on the Arduino IDE, if you haven’t already.

Introducing Interrupts

Interrupts are useful for making things happen automatically in microcontroller programs and can help solve timing problems.

Interrupts provide mechanisms to respond to external events, enabling your ESP32 boards to react quickly to changes without continuously polling (continuously checking the current value of a pin or variable).

What are Interrupts?

Interrupts are signals that pause the normal execution flow of a program to handle a specific event. When an interrupt happens, the processor stops the execution of the main program to execute a task and then gets back to the main program. That task is also referred to as an interrupt handling routine (or interrupt service routine, ISR), as shown in the figure below.

How interrupts work

In summary, with interrupts, you don’t need to constantly check the current value of a pin. With interrupts, when a change is detected, a callback function is triggered.

Types of Interrupts

There are different types of interrupts: external interrupts (hardware-based) and timer interrupts (software interrupts).

  1. External Interrupts: triggered by external signals and detected on the ESP32 GPIOs, such as a button press or a sensor reading—this is hardware-based and associated with a specific GPIO pin. When the state of a pin changes, it will trigger the interrupt service routine (ISR). We’ll focus on these in this tutorial.
  2. Timer Interrupts (software timers): initiated based on time intervals, enabling periodic actions—this is software-based.

Using Interrupts with the ESP32

To set an interrupt in the Arduino IDE, you use the attachInterrupt() function, that accepts as arguments: the GPIO pin, the name of the function to be executed, and the mode:

attachInterrupt(GPIO, callback_function, mode);

This instruction should be added to the setup() of your Arduino code.

Let’s take a look at the arguments you should pass to that function.

GPIO Interrupt

The first argument of the attachInterrupt() function is the GPIO number where we’ll detect the change. For example, if you want to use GPIO 27 as an interrupt, you can use:

digitalPinToInterrupt(27)

With an ESP32 board, all the pins that can act as inputs can be set as interrupts.

Callback Function

The second argument of the attachInterrupt() function is the name of the function that will be called when the interrupt is triggered.

Now, there are a few important rules you should be aware of when defining your ISR (callback function).

  1. The ISR should not return anything.
  2. ISRs should be as short and fast as possible because they halt the normal execution of the code.
  3. They should have the ARDUINO_ISR_ATTR attribute, so that they run in the ESP32 Internal RAM and not in Flash. IRAM access is much faster, which is critical for ISRs to run reliably without timing issues or crashes during interrupts.
  4. Variables that are used inside ISRs and throughout the code should preferably be volatile. This prevents the compiler from caching values in registers (and skipping memory access), so reads/writes always access the actual memory location and reflect unexpected changes caused by the interrupt.

Here’s an example of an ISR so that you can check its syntax:

void ARDUINO_ISR_ATTR my_callback() {
    // Any code you want to run
}

Another important thing about ISRs is that you should keep their code as fast and simple as possible and avoid things like complex operations, writing to the Serial Monitor, or using delay(). Instead, you should use a flag or counter to indicate that the interrupt happened, and then handle whatever you need to do in the main code or loop() section.

Mode

The third argument is the mode. There are 5 different modes:

  • LOW: to trigger the interrupt whenever the pin is LOW;
  • HIGH: to trigger the interrupt whenever the pin is HIGH;
  • CHANGE: to trigger the interrupt whenever the pin changes value – for example, from HIGH to LOW or LOW to HIGH;
  • FALLING: for when the pin goes from HIGH to LOW;
  • RISING: to trigger when the pin goes from LOW to HIGH.

The following picture will help you better understand the different trigger modes.

Interrupt modes

Attach an Interrupt with Arguments

Besides the attachInterrupt() function, you can alternatively use the attachInterruptArg() function instead. The function attachInterruptArg() is used to attach the interrupt to the defined pin using argumentsthis means you can pass arguments to the callback function.

attachInterruptArg(uint8_t pin, void callback_function, void * arg, int mode);
  • pin defines the GPIO pin number.
  • callback_function set the callback function.
  • arg pointer to the interrupt arguments.
  • mode set the interrupt mode.

Detaching/Disabling an Interrupt from a GPIO Pin

When you don’t want the ESP32 to no longer monitor the pin, you can call the detachInterrupt() function and pass as argument the GPIO pin.

detachInterrupt(digitalPinToInterrupt(interruptPin));

Example 1: ESP32 – Detecting a Button Press with an Interrupt

In this section, we’ll create a simple example to detect a button press using an interrupt. We’re using a pushbutton in this example, but you can use a sensor with a threshold, like a PIR Motion Sensor, for example (we’ll take a look at that later in this guide).

ESP32 with a pushbutton on a breadboard

Parts Required

You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!

Wiring Diagram

For this example, you need to connect a pushbutton to GPIO 18. We won’t use any resistors for the pushbutton because we’ll use the ESP32 internal pull-up resistors. Wire one lead of the pushbutton to GPIO 18 and the other one to GND, as shown in the diagram below.

ESP32 connected to a pushbutton - schematic diagram

Alternatively, you can use any other suitable GPIO as long as you modify the code. Make sure to check our ESP32 Pinout Guide.

Code

Upload the following code to your ESP32. It detects the pushbutton presses and prints the number of presses in the Serial Monitor.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-gpio-interrupts-arduino/
*********/
#include <Arduino.h>

// Global variables for the button
const uint8_t buttonPin = 18;
volatile int32_t counter = 0;
volatile bool pressed = false;

// Interrupt Service Routine (ISR)
void ARDUINO_ISR_ATTR buttonISR() {
  counter++;
  pressed = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  attachInterrupt(buttonPin, buttonISR, RISING);
  Serial.println("Press the button on GPIO 18.");
}

void loop() {
  if (pressed) {
    Serial.print("Button pressed ");
    Serial.print(counter);
    Serial.println(" times.");
    pressed = false;
  }
  delay(10);
}

View raw code

How Does the Code Work?

Let’s take a look at how the code works so that you can understand how to use interrupts in your code.

Pushbutton Global Variables

First, define global variables for the pushbutton. We’re defining the buttonPin as a const variable because it won’t change throughout the code. The counter variable will count the number of pushbutton presses. Finally, pressed is a boolean variable that will indicate whether the button was pressed or not. It starts as false.

// Global variables for the button
const uint8_t buttonPin = 18;
volatile uint32_t counter = 0;
volatile bool pressed = false;

Notice that counter and pressed are volatile variables because they will be used inside the ISR and also throughout the code (in the loop()).

Interrupt Service Routine

Define the interrupt service routine, which is the callback function that will run when the interrupt is triggered. In this case, the function is called buttonISR() and it has the ARDUINO_ISR_ATTR attribute so that the function runs on the ESP32 IRAM as we’ve seen previously.

void ARDUINO_ISR_ATTR buttonISR() {

In this function, we increase the counter variable and set the pressed variable to true, indicating that a button press happened.

counter++;
pressed = true;

setup()

In the setup(), initialize the Serial Monitor for debugging purposes.

void setup() {
  Serial.begin(115200);

Setting an Interrupt

Set the interrupt pin first as an input with an internal pull-up resistor as follows.

pinMode(buttonPin, INPUT_PULLUP);

Set the pin as an interrupt and assign it a callback function with RISING mode (this means the interrupt will be triggered when the interrupt pin goes from LOW to HIGH—when the pushbutton is pressed).

attachInterrupt(buttonPin, buttonISR, RISING);

loop()

In the loop(), we check whether the pushbutton was pressed. If it was pressed, we print how many times it was pressed so far.

void loop() {
  if (pressed) {
    Serial.print("Button pressed ");
    Serial.print(counter);
    Serial.println(" times.");

In the end, we set it to false again, so that we can print when there’s a new press.

pressed = false;

Testing the Example

Upload the code to your ESP32. After uploading, open the Serial Monitor at a baud rate of 115200. Press the ESP32 RST button so that it starts running the code.

Press the pushbutton.

Pressing a pushbutton connected to the ESP32

See the number of presses increasing in the Serial Monitor.

ESP32: detecting pushbutton presses with an interrupt without debouncing - results on the Serial Monitor

Issue: notice that when you press the pushbutton, sometimes it catches more presses than it should. This is an issue related to mechanical buttons called mechanical bouncing. This can be solved via software or via hardware.

debounce a pushbutton

This happens because the electrical contacts inside the button connect and disconnect very quickly before reaching a steady state, which will cause the system to register multiple press events, causing an inaccurate count.

We’ll show you how you can add debouncing to your code to prevent false pushbutton presses.

Detecting a Button Press with an Interrupt (with Debouncing)

The following code is similar to the previous one, but includes the lines of code required for debouncing the pushbutton.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-gpio-interrupts-arduino/
*********/
#include <Arduino.h>

// Global variables for the button
const uint8_t buttonPin = 18;
volatile uint32_t counter = 0;
volatile bool pressed = false;

// For bedouncing the pushbutton
const unsigned long DEBOUNCE_DELAY = 50;  // in milliseconds
volatile unsigned long lastPressTime = 0;

// Interrupt Service Routine (ISR)
void ARDUINO_ISR_ATTR buttonISR() {
  unsigned long now = millis();
  if (now - lastPressTime > DEBOUNCE_DELAY) {
    counter++;
    pressed = true;
  }
  lastPressTime = now;
}

void setup() {
  Serial.begin(115200);
  pinMode(buttonPin, INPUT_PULLUP);
  attachInterrupt(buttonPin, buttonISR, HIGH);
  Serial.println("Press the button on GPIO 18.");
}

void loop() {
  if (pressed) {
    Serial.print("Button pressed ");
    Serial.print(counter);
    Serial.println(" times.");
    pressed = false;
  }
  delay(10);
}

View raw code

In this code, we’ve added variables to handle debouncing for the pushbutton.

// For bedouncing the pushbutton
const unsigned long DEBOUNCE_DELAY = 50;  // in milliseconds
volatile unsigned long lastPressTime = 0;

DEBOUNCE_DELAY defines the minimum time required between button presses to register a valid event (preventing false triggers from mechanical bounce). The 50 milliseconds should be enough. If you continue to have false positives, increase the debounce time.

The lastPressTime saves the time of the last button press.

In the buttonISR() function, before considering a valid pushbutton press, we first check if at least 50 milliseconds have passed since the last press. We get the time that has passed since the program started with millis() (in milliseconds) and save it in the now variable.

unsigned long now = millis();
if (now - lastPressTime > DEBOUNCE_DELAY) {

If yes, we consider that we have a valid button press and we increase the counter variable and set the pressed variable to true.

counter++;
pressed = true;

After that, update the lastPressTime with the current time.

lastPressTime = now;

Testing the Example

Now, if you test this new example, you’ll see you no longer get false positive presses (if you do, increase the debounce delay in your code).

ESP32: detecting pushbutton presses with an interrupt with debouncing - results on the Serial Monitor

Example 2: ESP32 with a PIR Sensor – Detect Motion with an Interrupt

As we mentioned previously, you can also detect events caused by sensors that change their output state when they reach a certain threshold, like for example a PIR Motion sensor.

PIR Motion Sensors: AM312 and HC-SR501
Two of the most popular motion sensors used by hobbyists
electronics projects: mini PIR motion sensor (AM312) and PIR motion sensor
(HC-SR501).

These sensors output a HIGH signal when movement is detected, or a LOW signal if no movement is detected. The digital output from the PIR sensor can be read by an ESP32 GPIO pin, allowing you to program specific actions based on the detected motion status.

Example Overview

We’ll create a simple example that will light up an LED when motion is detected. After learning how it works, the same way of thinking can be applied to useful applications, such as sending an email or triggering an alarm.

ESP32 with a PIR Motion Sensor Example Overview

Here’s how the example works:

  • The sensor detects motion.
  • The ESP32 detects this event.
  • It prints in the Serial Monitor that motion was detected.
  • It turns on an LED for 20 seconds.
  • During those 20 seconds, we don’t print anything else to the Serial Monitor.
  • After those 20 seconds and if motion was not detected, we turn off the LED, print a message to the Serial Monitor indicating that motion has stopped.

Parts Required

For this example, you need the following parts:

Circuit Diagram

PIR sensors have a GND, VCC, and a data line. Connect the GND to the ESP32 GND, VCC to 3.3V, and the data line to an available GPIO. We’ll use the following pins:

PIR SensorGNDVCCData
ESP32GND3.3VGPIO 27

For this circuit, the LED is connected to GPIO 26, and the PIR motion sensor data pin is connected to GPIO 27. We’ll use the AM312 PIR motion sensor that works with 3.3V. The following figure shows the AM312 PIR motion sensor pinout.

AM312 PIR Motion Sensor Pinout labeled

You can use the following diagram as a reference to wire your circuit.

ESP32 connected to a PIR motion sensor and an LED

The Mini AM312 PIR Motion Sensor used in this project operates at 3.3V. However, if you’re using another PIR motion sensor like the HC-SR501, it operates at 5V. You can either modify it to operate at 3.3V or simply power it using the Vin pin.

Code

After wiring the circuit, upload the following code to your ESP32.

/*********
  Rui Santos & Sara Santos - Random Nerd Tutorials
  Complete project details at https://RandomNerdTutorials.com/esp32-pir-motion-sensor-interrupts-timers/
  ESP32 GPIO Interrupts with Arduino IDE: https://RandomNerdTutorials.com/esp32-gpio-interrupts-arduino/
*********/
#include <Arduino.h>

// Set GPIOs for LED and PIR Motion Sensor
const uint8_t led = 26;
const uint8_t motionSensor = 27;

// Timer: Auxiliary variables
unsigned long now;
volatile unsigned long lastTrigger = 0;
volatile bool startTimer = false;

bool printMotion = false;

const unsigned long timeSeconds = 20 * 1000UL;  //20 seconds in milliseconds

void ARDUINO_ISR_ATTR motionISR() {
  lastTrigger = millis();
  startTimer = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(motionSensor, INPUT_PULLUP);
  attachInterrupt(motionSensor, motionISR, RISING);

  // Set LED to LOW
  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);
}

void loop() {
  now = millis();

// Turn LED on immediately on new trigger
  if (startTimer && !printMotion) {
    digitalWrite(led, HIGH);
    Serial.println("MOTION DETECTED!!!");
    printMotion = true;
  }

// Turn off the LED after timeout
  if (startTimer && (now - lastTrigger > timeSeconds)) {
    Serial.println("Motion stopped...");
    digitalWrite(led, LOW);
    startTimer = false;
    printMotion = false;
  }
}

View raw code

How Does the Code Work?

Let’s take a quick look at the code to better understand how it works.

Defining Variables

We start by defining the pins for the LED and PIR Motion sensor. Adjust if you’re using different pins:

const uint8_t led = 26;
const uint8_t motionSensor = 27;

Create variables to track the duration of the LED in the on state. The now variable saves the current time (time elapsed since the program has started), the lastTrigger saves the last time motion was detected, and the startTimer is a boolean variable to indicate whether the timer to turn on the LED is currently running or not.

unsigned long now;
volatile unsigned long lastTrigger = 0;
volatile bool startTimer = false;

We also have another variable to keep track whether the Motion Detected text was already printed to the Serial Monitor.

bool printMotion = false;

The timeSeconds variable saves how long we want the LED on after motion is detected. You can adjust according to your preferences.

const unsigned long timeSeconds = 20 * 1000UL;  //20 seconds in milliseconds

motionISR()

The motionISR() will run when motion is detected. We save the current time on the lastTrigger variable to keep track when motion was detected, and we set the startTimer variable to true to indicate it’s time to start the timer to turn on the LED.

void ARDUINO_ISR_ATTR motionISR() {
  lastTrigger = millis();
  startTimer = true;
}

We’ll then handle these variables in the loop() to do the tasks we want.

setup()

In the setup(), set the motion sensor as an interrupt on RISING mode (when motion is detected, the sensor sets its output pin to HIGH).

pinMode(motionSensor, INPUT_PULLUP);
attachInterrupt(motionSensor, motionISR, RISING);

And set the LED as an OUTPUT and set it to LOW.

// Set LED to LOW
pinMode(led, OUTPUT);
digitalWrite(led, LOW);

loop()

In the loop(), we’re constantly getting the current time and saving it in the now variable.

now = millis();

Then, we check whether the LED timer has started and if the motion message has not already been printed. If these conditions are met, we turn the LED on, print a message to the Serial Monitor, and set the printMotion variable to true, because we have now printed the Motion Detected message to the Serial Monitor.

if (startTimer && !printMotion) {
  digitalWrite(led, HIGH);
  Serial.println("MOTION DETECTED!!!");
  printMotion = true;
}

loop()

In the loop(), we’re also checking if, since the startTimer has started, 20 seconds have passed since the last trigger. If 20 seconds have passed since the last trigger, we turn off the LED, and set the startTimer and printMotion variables to false.

if (startTimer && (now - lastTrigger > timeSeconds)) {
  Serial.println("Motion stopped...");
  digitalWrite(led, LOW);
  startTimer = false;
  printMotion = false;
}

Testing the Example

Upload the code to your ESP32 board. Open the Serial Monitor at a baud rate of 115200 and press the ESP32 RST button so that it starts running the code.

Move your hand in front of the PIR sensor.

Moving my hand in front of the PIR motion sensor connected to the ESP32

The LED should turn on, and a message is printed in the Serial Monitor saying “MOTION DETECTED!!!”.

ESP32 PIR Motion Sensor with Interrupts - Messages on Serial Monitor

After 20 seconds, the LED should turn off (if motion was not detected meanwhile).

ESP32 on a breadboard connected to an LED and a PIR motion sensor

Now that you understand how to use interrupts to detect motion with a PIR motion sensor, you can easily adjust the code to do any useful tasks instead of controlling an LED. You can, for example, send notifications to your email or smartphone to indicate that motion was detected.

We have a tutorial with seven different ways to send notifications with the ESP32 that you can explore:

Wrapping Up

In this tutorial, we covered how to use interrupts with the ESP32 to detect changes on its GPIOs. Instead of having to constantly poll the state of a GPIO, we can use interrupts. Our code will run normally, and when an interrupt is detected, a callback function (ISR) will run.

We covered an example using a pushbutton, and another one using a PIR motion sensor, but this can be applied to many other sensors that have an output pin that changes its state when reaching a certain threshold. For an in-depth guide on using a PIR motion sensor with the ESP32, you can check this tutorial:

For tutorials and guides about other sensors, you can check out our compilation of guides:

If you also want to learn how to use interrupts with the ESP32 using MicroPython firmware, check these guides instead:

We hope you’ve found this guide useful. To learn more about the ESP32, check out our resources:



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

1 thought on “ESP32 GPIO Interrupts with Arduino IDE”

  1. It would be nice to have a more in-depth look at interrupts and how to assign priorities (1-3 with C++ code and 4-7 in assembler) because there aren’t many explanations available.
    To read a keystroke (which is the case with the example in this article), these default routines are fine, but to read the frequency response of an LC circuit, for example, you need maximum precision.
    I’m about to finish an inductance reader based on the Colpitt oscillator, and I’ve noticed that using GPIO D4 gives me better results… perhaps because it has a higher default priority than other GPIOs?
    D4 is enabled by default for touch reading.
    Bye

    Reply

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.