Configuring & Handling ESP32 GPIO Interrupts In Arduino IDE

Often in a project you need the ESP32 to execute its normal program while continuously monitoring for some kind of event. A solution widely adopted is the use of an Interrupt.

Interrupts In ESP32

The ESP32 offers up to 32 interrupt slots for each core. Each interrupt has a certain priority level and can be categorized into two types.

Hardware Interrupts – These occur in response to an external event. For example, GPIO Interrupt(when a key is pressed down) or a Touch Interrupt(when touch is detected)

Software Interrupts – These occur in response to a software instruction. For example, a Simple Timer Interrupt or Watchdog Timer Interrupt(when timer times out)

ESP32 GPIO Interrupt

In ESP32, we can define an interrupt service routine function that will be called when a GPIO pin changes its signal value.

With an ESP32 board, all the GPIO pins can be configured to function as interrupt request inputs.

ESP32 GPIO Interrupt Pins

Attaching Interrupt to a GPIO Pin

In Arduino IDE, we use a function called attachInterrupt() to set an interrupt on a pin by pin basis. The recommended syntax looks like below.

attachInterrupt(GPIOPin, ISR, Mode);

This function takes three parameters:

GPIOPin – Sets the GPIO pin as an interrupt pin, which tells the ESP32 which pin to monitor.

ISR – Is the name of the function that will be called every time the interrupt is triggered.

Mode – Defines when the interrupt should be triggered. Five constants are predefined as valid values:

LOWTriggers interrupt whenever the pin is LOW
HIGHTriggers interrupt whenever the pin is HIGH
CHANGETriggers interrupt whenever the pin changes value, from HIGH to LOW or LOW to HIGH
FALLINGTriggers interrupt when the pin goes from HIGH to LOW
RISINGTriggers interrupt when the pin goes from LOW to HIGH

Detaching Interrupt from a GPIO Pin

You can optionally call detachInterrupt() function when you no longer want ESP32 to monitor a pin. The recommended syntax looks like below.

detachInterrupt(GPIOPin);

Interrupt Service Routine

Interrupt Service Routine is invoked when an interrupt occurs on any GPIO pin. Its syntax looks like below.

void IRAM_ATTR ISR() {
    Statements;
}

ISRs in ESP32 are special kinds of functions that have some unique rules most other functions do not have.

  • The interrupt service routine must have an execution time as short as possible, because it blocks the normal program execution.
  • Interrupt service routines should have the IRAM_ATTR attribute, according to the ESP32 documentation

What is IRAM_ATTR?

By flagging a piece of code with the IRAM_ATTR attribute we are declaring that the compiled code will be placed in the Internal RAM (IRAM) of the ESP32.

Otherwise the code is placed in the Flash. And flash on the ESP32 is much slower than internal RAM.

If the code we want to run is an interrupt service routine (ISR), we generally want to execute it as quickly as possible. If we had to ‘wait’ for an ISR to load from flash, things would go horribly wrong.

Hardware Hookup

Enough of theory! let’s see practical example.

Let’s hook one push button to GPIO pin 18 (D18) on the ESP32. You don’t require any pullup to this pin as we will pull the pin up internally.

Wiring Push Buttons to ESP32 For GPIO Interrupt
Wiring Push Buttons to ESP32 For GPIO Interrupt

Example: Simple Interrupt

The following sketch demonstrates the use of the interrupts and the correct way to write an interrupt service routine.

struct Button {
  const uint8_t PIN;
  uint32_t numberKeyPresses;
  bool pressed;
};

Button button1 = {18, 0, false};

void IRAM_ATTR isr() {
  button1.numberKeyPresses += 1;
  button1.pressed = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(button1.PIN, INPUT_PULLUP);
  attachInterrupt(button1.PIN, isr, FALLING);
}

void loop() {
  if (button1.pressed) {
      Serial.printf("Button 1 has been pressed %u times\n", button1.numberKeyPresses);
      button1.pressed = false;
  }

  //Detach Interrupt after 1 Minute
  static uint32_t lastMillis = 0;
  if (millis() - lastMillis > 60000) {
    lastMillis = millis();
    detachInterrupt(button1.PIN);
	Serial.println("Interrupt Detached!");
  }
}

Once you upload the sketch, press EN button on the ESP32 and open serial monitor at baud rate 115200. Now we will get the output as shown below, when you press the button.

ESP32 GPIO Interrupt ISR Output On Serial Monitor

Code Explanation

At the very start of the sketch we create a structure named Button. It has three members viz. pin number, number of key presses and pressed state. If you don’t know, Structure is the collection of variables of different types (but logically related to each other) under a single name.

struct Button {
  const uint8_t PIN;
  uint32_t numberKeyPresses;
  bool pressed;
};

Next we create an instance of the Button structure and initialize pin number to 18, number of key presses to 0 and default pressed state to false.

Button button1 = {18, 0, false};

The following piece of code is an Interrupt Service Routine. As mentioned earlier, ISR in ESP32 should have the IRAM_ATTR attribute.

In ISR we simply increment the KeyPresses counter by 1 & set button pressed state to True.

void IRAM_ATTR isr() {
  button1.numberKeyPresses += 1;
  button1.pressed = true;
}

In Setup section of code, we first initialize the serial communication with PC. Then set input pullup the D18 pin.

Next we tell the ESP32 to monitor the D18 pin and call the interrupt service routine isr when the pin goes from HIGH to LOW i.e. FALLING state.

Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);

In Loop section of the code, we simply check if the button pressed state returns to be true. When it does, we simply print the number of key pressed till now and set the button pressed state LOW so that we can continue getting next interrupts.

if (button1.pressed) {
      Serial.printf("Button 1 has been pressed %u times\n", button1.numberKeyPresses);
      button1.pressed = false;
}

In loop section we also check the number of milliseconds that have passed since the program first started using millis() function. When this time is more than 60,000 milliseconds or 1 Minute, we simply tell ESP32 to not to monitor D18 pin using detachInterrupt() function.

//Detach Interrupt after 1 Minute
static uint32_t lastMillis = 0;
if (millis() - lastMillis > 60000) {
  lastMillis = millis();
  detachInterrupt(button1.PIN);
  Serial.println("Interrupt Detached!");
}