ESP32-S3开发教程五-按键中断1
中断,是嵌入式中的一个重要概念,是一种由硬件设备或软件程序主动触发的、异步的事件通知机制。app_main这个场景中,“电话响” 就是,“接电话” 就是,整个过程的核心就是那么,在ESP32-S3中,要如何完成这一过程呢?
中断,是嵌入式中的一个重要概念,是一种由硬件设备或软件程序主动触发的、异步的事件通知机制。
简单做一个类比:
- 你坐在办公室里按部就班写工作报告(对应:CPU 按顺序执行普通任务 / 程序,比如 ESP32 中
app_main里的循环、普通任务)。 - 突然,你的办公电话响了(对应:硬件 / 软件触发了中断,比如 ESP32 中 GPIO11 电平下降、串口收到数据)。
- 你立刻放下手中的报告,暂停当前的写作进度(对应:CPU 暂停当前正在执行的任务,保存当前执行状态)。
- 你拿起电话接打,处理这个紧急事情(对应:CPU 跳转到预先定义的「中断服务程序(ISR)」,执行中断处理逻辑)。
- 电话挂断后,你回到办公桌前,从刚才暂停的地方继续写报告(对应:ISR 执行完成,CPU 恢复之前暂停的任务状态,继续执行原程序)。
这个场景中,“电话响” 就是中断请求,“接电话” 就是中断处理,整个过程的核心就是「暂停当前任务→处理紧急事件→恢复原任务」。
那么,在ESP32-S3中,要如何完成这一过程呢?
一、新建工程
新建一个名为“Interrupt”的工程,这里不再赘述。
二、添加头文件
分别完善CMake.txt与main.c文件:
idf_component_register(SRCS "main.c"
PRIV_REQUIRES driver freertos
INCLUDE_DIRS ".")
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void app_main(void)
{
}
编译确认无误。
三、初始化中断引脚
在ESP32-S3中,中断是由ISR统一调配。由于大部分中断需要通过引脚的电平改变来触发,所以我们需要先初始化一个中断引脚:
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void gpio_init(void)
{
gpio_config_t io_config = {
.pin_bit_mask = (1ULL << GPIO_NUM_11),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&io_config);
gpio_install_isr_service(0);
}
void app_main(void)
{
gpio_init();
}
io_config中,我们设置了一个GPIO11为输入模式,同时使能了它的上拉电阻,禁用了它的下拉电阻,这部分与前面相同。
不同在最后一行,.inter_type此时不是DISABLE,而是GPIO_INTR_NEGEDGE(下降沿触发)。这一栏的可选参数有:
-
GPIO_INTR_DISABLE(禁用中断)
-
GPIO_INTR_POSEDGE(上升沿触发)
-
GPIO_INTR_NEGEDGE(下降沿触发)
-
GPIO_INTR_ANYEDGE(任意边沿触发)
-
GPIO_INTR_LOW_LEVEL(低电平触发)
-
GPIO_INTR_HIGH_LEVEL(高电平触发)
什么是上升沿和下降沿?

简单来说,电平的变化是有一个过程的,由低到高的过程被称为上升沿,由高到低的过程被成为下降沿。
这里,我们的按键是连接GPIO11和GND的,所以当按键按下时,将产生一个下降沿,按键松开时,将产生一个上升沿。
这里,我们选择下降沿触发。
gpio_install_isr_service函数是ESP32的全局中断初始化函数,只需要知道在整个程序中,要使用中断就必须执行一次这个代码即可。这里我们参数填0,代表使用默认配置。
四、编写业务代码
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
volatile bool gpio_interrupt_flag = false;
void IRAM_ATTR gpio_isr_handler(void* arg)
{
gpio_interrupt_flag = true;
}
void gpio_init(void)
{
gpio_config_t io_config = {
.pin_bit_mask = (1ULL << GPIO_NUM_11),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
gpio_config(&io_config);
gpio_install_isr_service(0);
gpio_isr_handler_add(GPIO_NUM_11, gpio_isr_handler, NULL);
}
void app_main(void)
{
gpio_init();
while(1)
{
if(gpio_interrupt_flag == true)
{
printf("基础中断触发!(GPIO%d)\n", GPIO_NUM_11);
gpio_interrupt_flag = false;
vTaskDelay(pdMS_TO_TICKS(200));
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
gpio_isr_handler_add函数,作用是将初始化好的GPIO中断引脚绑定一个中断处理函数,当满足中断条件时,CPU中断当前任务,进入中断处理函数进行处理。它需要三个参数:
- 已经被正确初始化的中断引脚,这里我们填GPIO11。
-
编写好的中断服务函数,函数格式必须符合void (*gpio_isr_t)(void*)(即无返回值、参数为void*),且建议用IRAM_ATTR修饰。
-
传递给 ISR 函数的自定义参数(可选),一般用来区分多个GPIO引脚触发同一个中断服务函数的,若无需传递参数,填NULL即可。
需要注意的是,该函数注册的是「专属 ISR」—— 一个 ISR 函数可以绑定多个 GPIO 引脚(共享),但一个 GPIO 引脚只能绑定一个 ISR 函数(专属)。
接下来,我们看终端服务函数。使用IRAM_ATTR进行修饰,强制编译到内部快速 RAM,保证中断微秒级快速响应,避免 Flash 读取延迟导致中断丢失。
在这里,我们定义了一个全局变量叫做gpio_interrupt_flag,用于标记中断是否被触发。使用volatile进行修饰,告诉编译器该变量可能在ISR 中被修改,禁止编译器优化该变量的读取(避免普通代码读取到缓存的旧值)。
在主函数中,我们通过轮询标志位,当检测到标志位发生改变,即输出一条信息。
下面是当前代码的程序执行流程图:

五、编译烧录,查看现象
编译烧录成功后,点击底部栏打开监视器,初次打开会输出一大堆绿色的东西,这是ESP32的初始化信息,此时我们按下按钮,可以看到正常输出了一句话:


六、一些小疑问
是的,正如我们最开始举的例子,我们希望在电话打进来之前,可以做一些自己的工作。但是目前的主函数,每循环一次就要读取一次标志位的值,这样会极大的浪费性能。能不能直接把输出信息的代码放进中断处理函数?这样做会导致什么样的后果?
且听下回分解。
更多推荐

所有评论(0)