5. External Interrupt
5.1 What is an Interrupt
In the world of microprocessors or microcontrollers, an interrupt is a special event that interrupts and temporarily suspends the currently executing program in order to handle a specific condition or event. Examples include pressing a button, a timer expiring, or receiving serial port data. An interrupt is a very important concept in computer systems; it responds asynchronously to these specific situations. Here is an example: suppose we are writing code, and suddenly a phone call comes in. At this point, we stop writing code and answer the phone, and after the call is finished, we continue writing code. The phone call here is equivalent to an interrupt, which interrupts what we are currently doing. Answering the phone and discussing things is equivalent to what the interrupt needs to execute — that is, the interrupt service routine. Interrupts can be divided into two types: hardware interrupts and software interrupts. Hardware interrupts are usually triggered by physical events from external devices, such as pressing a button, a timer expiring, or data arriving at a serial port. When these events occur, the microprocessor immediately suspends its current task and jumps to a predefined interrupt service routine (ISR) to respond to the event. Software interrupts are triggered by software instructions and are usually used for more complex processing tasks. For example, the system calls of an operating system use software interrupts.
5.2 External Interrupt
In the previous chapter, when we did the button experiment, although we could implement reading the GPIO input function, the code was constantly detecting changes in the IO input port. If we later add a large amount of code, it will take a long time to poll the button detection part, so the efficiency is not high. Especially in some specific situations, for example, a certain button may only be pressed once a day to perform related functions, so we waste a lot of time detecting the button status in real time. To solve this problem, we introduce the concept of external interrupts. As the name implies, the related functions are only executed when the button is pressed (an interrupt is generated). This greatly saves CPU resources, so interrupts are very commonly used in actual projects. An external interrupt is a type of hardware interrupt that is triggered by events external to the microcontroller. Certain pins of the microcontroller are designed to respond to the occurrence of specific events, such as a button press, a signal change from a sensor, etc. These designated pins are usually called "external interrupt pins". When an external interrupt event occurs, the current program execution is immediately stopped, and the program jumps to the corresponding interrupt service routine (ISR) for processing. After processing is complete, the program returns to where it was interrupted and continues execution. For embedded systems and real-time systems, the use of external interrupts is very important. It helps the system respond immediately to external events, greatly improving the efficiency and real-time performance of the system. The ESP32-S3 development board provides many pins as available external interrupt pins, and you can configure these pins to perform external interrupt experiments.
The external interrupts of the ESP32 have rising edge, falling edge, low level, and high level trigger modes. The rising edge and falling edge triggers are as follows:
After a pin is set as an external interrupt pin, when the configured trigger mode is detected on the pin, the program will enter the interrupt callback function to execute the corresponding routine;
5.3 Purpose and Advantages of External Interrupts
The external interrupt function of the ESP32-S3 has the following purposes and advantages in development:
- Real-time response to external events: The external interrupt function of the ESP32-S3 allows you to respond immediately when an external event trigger is detected. These external events can be from sensors, buttons, switches, received signals, etc. Through external interrupts, you can capture these events in real time and perform corresponding operations without frequent polling or waiting.
- Saving computing resources: External interrupts allow you to transfer the task of handling external events to the chip's hardware, thereby saving the processor's computing resources. Compared with software polling, external interrupts can reduce the burden on the processor, allowing it to more effectively use other resources for more complex tasks.
- Precise event capture: The external interrupt function of the ESP32-S3 can capture the trigger of external events in a very precise way. You can configure the interrupt trigger mode (such as rising edge, falling edge, any level, low-level hold, high-level hold, etc.) to adapt to different external events, and immediately interrupt the execution of the current program when the event occurs, and then execute the interrupt service function.
- High-priority processing: External interrupts can be set to high-priority processing, taking precedence over the currently executing program. This is very useful for important events that require an immediate response, such as emergency notifications, sensor detections, etc. When an external event is triggered, the processor will immediately transfer to the interrupt service function for execution, ensuring that the related operations are processed in a timely and accurate manner, avoiding delays in processing the program.
- Multi-channel interrupt processing: The ESP32-S3 supports multiple external interrupts. You can connect multiple external events to different interrupt pins, thereby achieving parallel processing of multiple events. This allows you to handle multiple external events such as sensors and switches, improving the flexibility and scalability of the system. In summary, the external interrupt function of the ESP32-S3 provides advantages such as real-time response, saving computing resources, precise event capture, high-priority processing, and multi-channel interrupt processing. It provides us with a more flexible and efficient way to handle external events, and helps build more powerful and reliable applications.
5.4 Process of Using External Interrupts
Using the external interrupts of the ESP32-S3, you can make the ESP32-S3 automatically interrupt the program when a specific event occurs and immediately handle that event. The following is the basic process of using external interrupts in the ESP-IDF environment:
- Configure the pin and interrupt In the code, import the corresponding header files so that you can use the GPIO functions provided by ESP-IDF. Typically, you need to import the
<driver/gpio.h>header file.
#include <driver/gpio.h>Set the pin on the ESP32-S3 as an external interrupt input so that it can detect the trigger of external events.
// Configure the button
gpio_config_t io_conf = {};
// Set as falling edge interrupt
io_conf.intr_type = GPIO_INTR_NEGEDGE;
// Set as GPIO0 pin
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
// Set as input mode
io_conf.mode = GPIO_MODE_INPUT;
// Set to enable pull-up resistor
io_conf.pull_up_en = 1;
// Set to disable pull-down resistor
io_conf.pull_down_en = 0;
// Apply the above configuration to the pin
gpio_config(&io_conf);2
3
4
5
6
7
8
9
10
11
12
13
14
- Set the interrupt handler Implement a callback function for the interrupt service routine to handle the interrupt response in the function. (The function name can be any name, but it must conform to the C language standard.) The interrupt handler needs to be declared as
IRAM_ATTRto ensure that it runs in the executable area in memory.
void IRAM_ATTR gpio_isr_handler(void* arg) {
// Handle the interrupt response
}2
3
- Configure the interrupt Use the
gpio_install_isr_service()function to install the GPIO interrupt service routine, and use thegpio_isr_handler_add()function to assign an interrupt handler to the specified GPIO pin:
gpio_install_isr_service(ESP_INTR_FLAG_EDGE); // Install the GPIO interrupt service routine
gpio_isr_handler_add(GPIO_NUM_21, gpio_isr_handler, NULL); // Assign the interrupt handler2
- Prototype of the
gpio_install_isr_service()function:
void gpio_install_isr_service(esp_intr_alloc_flag_t flags);This function installs the GPIO interrupt service routine and configures the behavior of the interrupt service routine according to the specified flag parameter flags.
flagscan use the following flags:- ESP_INTR_FLAG_LEVEL1: Use interrupt level 1. Disables interrupts of the same level during the execution of the interrupt service routine.
- ESP_INTR_FLAG_LEVEL2: Use interrupt level 2. Disables interrupts of the same level and level 1 during the execution of the interrupt service routine.
- ESP_INTR_FLAG_EDGE: Use edge-triggered mode. Enables GPIO edge-triggered interrupts.
- ESP_INTR_FLAG_LOWMED: Set the interrupt priority to low-medium.
- ESP_INTR_FLAG_HIGH: Set the interrupt priority to high. The example code uses the
ESP_INTR_FLAG_EDGEflag, which means that GPIO edge-triggered interrupts are enabled. You can choose the appropriate flag according to your actual needs.
What is edge triggering?
📌 Edge triggering is a way of triggering a signal, which triggers the corresponding operation based on the edge where the signal changes. The edge can be a rising edge or a falling edge. In rising-edge triggering, the operation is triggered when the signal changes from a low level to a high level. Rising-edge triggering is usually used to detect signal changes, indicating the start of an event. In falling-edge triggering, the operation is triggered when the signal changes from a high level to a low level. Falling-edge triggering is usually used to detect signal changes, indicating the end of an event or a state transition. Edge triggering can be used in applications such as digital circuits, interrupt control, and input/output interfaces. For example, the moment a switch changes from off to on, you can use rising-edge triggering to detect the switch state change and perform the corresponding operation. Similarly, when the pulse width of a pulse signal ends, you can use falling-edge triggering to detect the change of the pulse signal and perform the corresponding processing.
- Prototype of the
gpio_isr_handler_add()function:
esp_err_t gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void* args);This function accepts the following parameters:
gpio_num: GPIO pin number, specifying the GPIO pin to which the interrupt handler is assigned.isr_handler: Function pointer to the interrupt handler function. The interrupt handler is a user-defined callback function that will be executed when an interrupt occurs.args: Parameters passed to the interrupt handler. This is a pointer to user-specific data that can be used in the interrupt handler. Using thegpio_isr_handler_add()function, you can assign a custom interrupt handler to a specific GPIO pin. When the specified GPIO pin triggers an interrupt, the assigned interrupt handler will be called and the predefined operation will be executed. Note that before using this function, you need to callgpio_install_isr_service()to install the GPIO interrupt service routine.
- Enable the external interrupt
// Enable the GPIO module interrupt signal
gpio_intr_enable(KEY_PIN);2
gpio_intr_enable() is a function in the ESP32 related to GPIO interrupts, used to enable the interrupt of the IO port corresponding to the specified GPIO number. The function prototype is:
void gpio_intr_enable(gpio_num_t gpio_num)Where gpio_num indicates the GPIO number for which to enable the interrupt. Before using the gpio_intr_enable() function, you need to install and register the interrupt handler through the gpio_install_isr_service() function and the gpio_isr_handler_add() function.
5.5 Hardware Connection and Preparation
Take the onboard BOOT button on the development board as an example. The BOOT button is connected to GPIO0 of the ESP32-S3 chip, and the LED is connected to the GPIO48 pin. We need to configure the button pin GPIO0 as an external interrupt pin, and the LED pin GPIO48 as output mode. The button is used to control the LED on and off.
5.6 External Interrupt Verification
Write the following code in main.c: (For the button code bsp_key.h and the LED code bsp_led.h, please refer to the LED chapter and the Button-Controlled LED chapter)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "esp_system.h"
#include "esp_log.h"
#include "bsp_led.h"
#include "bsp_key.h"
// Interrupt service function
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
static int cnt = 0;
cnt++;
// When the interrupt is triggered, the LED state changes
gpio_set_level(LED_PIN, cnt % 2);
}
void app_main(void)
{
gpio_config_t io_conf = {};
// Configure the LED
LedGpioConfig();
// Configure the button
// Falling edge interrupt
io_conf.intr_type = GPIO_INTR_NEGEDGE;
// Set the input register of GPIO0
io_conf.pin_bit_mask = ( 1 << KEY_PIN);
// Input mode
io_conf.mode = GPIO_MODE_INPUT;
// Enable pull-up mode
io_conf.pull_up_en = 1;
io_conf.pull_down_en = 0;
gpio_config(&io_conf);
// Register the interrupt service
gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
// Set the GPIO interrupt service function
gpio_isr_handler_add(KEY_PIN, gpio_isr_handler, (void*)NULL);
// Enable the GPIO module interrupt signal
gpio_intr_enable(KEY_PIN);
int time = 0;
while(1)
{
printf("time: %d\n", time++);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
After downloading the above code, pressing the button you can see the LED state change. Press the button to turn on the LED, press again to turn it off, and so on.
However, there is a problem with using the above method. The code downloaded by ESP-IDF runs based on FreeRTOS. If you add functions such as printf and delay in the interrupt service function, a reset will occur.
The reason why it is best to avoid using delay functions (such as vTaskDelay()) or blocking calls in the interrupt handler is that the interrupt handler should execute as quickly as possible to ensure timely processing of interrupt events. Delay functions and blocking calls will cause the interrupt handler to occupy the CPU for a long time, affecting the execution of other tasks and the response performance of the system. In addition, interrupt handlers usually execute in the interrupt context, which is somewhat different from the task context, and some blocking functions may not be available in the interrupt context.
Avoiding standard output functions (such as printf()) in interrupt handlers is because standard output functions are usually blocking, which will cause the interrupt handler to block and prolong the execution time. The interrupt handler should be as short as possible and only complete the necessary operations to ensure the timely response of the system. In addition, interrupt handlers usually execute in the interrupt context, where standard output functions may not work correctly.
Based on the above problems, another way of external interrupt can be used. Create a FreeRTOS task that detects the button status through a queue. The code is as follows: After running, the LED blinks at 1-second intervals. When the button is pressed, an interrupt is triggered, and the interrupt sends a queue signal. In the button detection task,
will output.#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#define GPIO_OUTPUT_PIN_SEL (1ULL<<GPIO_NUM_48)
#define GPIO_INPUT_PIN_SEL (1ULL<<GPIO_NUM_0)
#define ESP_INTR_FLAG_DEFAULT 0
static QueueHandle_t gpio_evt_queue = NULL;
// Interrupt service function
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
uint32_t gpio_num = (uint32_t) arg;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}
// Button detection task
static void gpio_get_key_value_task(void* arg)
{
uint32_t io_num;
for(;;) {
// Read the latest gpio_evt_queue message
if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
printf("GPIO[%"PRIu32"] intr, val: %d\n", io_num, gpio_get_level(io_num));
}
}
}
void app_main(void)
{
gpio_config_t io_conf = {};
// Configure the LED
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
io_conf.pull_down_en = 0;
io_conf.pull_up_en = 0;
gpio_config(&io_conf);
// Configure the button
// Falling edge interrupt
io_conf.intr_type = GPIO_INTR_NEGEDGE;
// Set the input register of GPIO0
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
// Input mode
io_conf.mode = GPIO_MODE_INPUT;
// Enable pull-up mode
io_conf.pull_up_en = 1;
io_conf.pull_down_en = 0;
gpio_config(&io_conf);
// Create a queue to handle gpio events from isr
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
// Register the interrupt service
gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
// Set the GPIO interrupt service function
gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, (void*) GPIO_NUM_0);
// Enable the GPIO module interrupt signal
gpio_intr_enable(GPIO_NUM_0);
// Create a button detection task
xTaskCreate(gpio_get_key_value_task, // Task function
"gpio_get_key_value_task", // Task name
2048, // Task stack
NULL, // Parameter passed to the task function
10, // Task priority
NULL // Task handle
);
int cnt = 0;
while(1) {
printf("cnt: %d\n", cnt++);
vTaskDelay(1000 / portTICK_PERIOD_MS);
gpio_set_level(GPIO_NUM_48, cnt % 2);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
The above example uses some FreeRTOS knowledge. FreeRTOS is a small, lightweight real-time operating system (RTOS). Its design goal is simplicity and ease of use. It provides simple thread, mutex, semaphore, and timer functions, and supports a variety of different programming models, including pre-set priority scheduling and round-robin scheduling. Features:
- A small kernel, making it suitable for embedded systems and microcontrollers.
- Provides priority-based real-time multitasking capabilities.
- Rich kernel objects such as mutexes, semaphores, queues, and timers to help handle synchronization and communication between tasks.
- Has very detailed online documentation and a large amount of sample code to help you understand and get started quickly. The specific implementation details and usage can be found in its official documentation or related chapters in many popular embedded system development manuals. In summary, FreeRTOS is an excellent operating system for real-time task processing in embedded systems. It is small, easy to use, and very flexible. The above example code mainly uses the following FreeRTOS-related functions:
xQueueCreate(): Creates a new queue. The parameter specifies a certain amount of available storage space and the size (in bytes) of each item in the storage space.xQueueSendFromISR(): Sends an item to a queue from within an interrupt routine. It accepts the queue to send to, a pointer to the element, and a pointer used to determine whether a task switch is needed.xQueueReceive(): Receives an item from the queue. This function receivedio_numfrom thegpio_evt_queuequeue.xTaskCreate(): Creates a new task and adds it to the list of ready tasks.vTaskDelay(): Delays the calling task for a given amount of time. These are commonly used functions in FreeRTOS and their purposes. They are responsible for task scheduling, interrupt services, and memory management.