Interrupts

Interrupts are a way to achieve concurrency when working with microcontrollers.

An interrupt allows us to “interrupt” the current execution of a program in order to do a different task. This is usually achieved by instructing the microcontroller to look for level changes (From high to low or from low to high) on a GPIO pin and executing a function when that happens.

Interrupt Service Routines (ISR)

ISRs are callback functions that are executed when an interrupt is triggered. They should be made very fast and simple, because they block the execution of other parts of the system.

They are special in that they can’t block execution waiting for a lock and then resume when the lock is available. If we try to hold a mutex within an ISR, the program will crash. For this reason, many ESP-IDF functions (e.g. ESP_LOG functions) can’t be used inside an ISR.

Queues

Due to all restrictions on ISRs, it’s common for ISRs to simply put a message in a queue and let another task take care of processing the message.

We can create a queue like this:

1
2
3
4
5
static QueueHandle_t queue;

extern "C" void app_main() {
  queue = xQueueCreate(10, sizeof(char));
}

In this case, we are creating a queue that can hold up to 10 variables of type char.

We can then have a task that processes messages from the queue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void queue_task(void *params) {
  char c;
  while (true) {
    if (xQueueReceive(queue, &c, portMAX_DELAY)) {
      // In this scenario we are ignoring `c` and just logging a message. We
      // could do anything here
      ESP_LOGI(TAG, "Interrupt triggered");
    }
  }
}

extern "C" void app_main() {
  ...

  xTaskCreate(queue_task, "queue_task", 2048, NULL, 10, NULL);

  ...
}

Configuring interrupts

To configure an interrupt, we need to know which pin will listen for interrupts, and when we want interrupts to trigger: rising, falling, or both.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define INTERRUPT_PIN GPIO_NUM_19

static void interrupt_handler(void *args) {
  char c = 1;
  xQueueSendFromISR(queue, &c, nullptr);
}

extern "C" void app_main() {
  ...

  gpio_config_t interrupt_config = {
    .pin_bit_mask = 1ULL << INTERRUPT_PIN,
    .mode = GPIO_MODE_INPUT,
    .pull_up_en = GPIO_PULLUP_ENABLE,
    .pull_down_en = GPIO_PULLDOWN_DISABLE,
    .intr_type = GPIO_INTR_ANYEDGE,
  };
  gpio_config(&interrupt_config);

  gpio_install_isr_service(0);
  gpio_isr_handler_add(INTERRUPT_PIN, interrupt_handler, nullptr);

  ...
}

The code above, configures pin 19 to trigger interrupt_handler, every time it notices a level change (GPIO_INTR_ANYEDGE).

Conclusion

The trickiest part of interrupts is the restrictions inside the ISRs. Luckily, most of the problems around that can be mitigated by using a queue. Experimentation and memory constraints will need to be taken into account when choosing the size of the queue.

As usual, you can find a full working example in my examples’ repo.

[ esp32  programming  ]
Unit Testing Code for ESP32
Building a Stand-Alone Library for ESP-IDF
Configuring ESP32 to act as Access Point
ESP32 Non-Volatile Storage (NVS)
Making HTTP / HTTPS requests with ESP32