首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >搞懂 ESP32 GPIO 中断只需一篇!带你走进硬件的‘神经系统’

搞懂 ESP32 GPIO 中断只需一篇!带你走进硬件的‘神经系统’

原创
作者头像
Swift社区
发布2025-04-08 21:32:30
发布2025-04-08 21:32:30
1.9K0
举报
文章被收录于专栏:嵌入式嵌入式

前言

在用 ESP32 写项目的时候,很多人一开始都只会用 gpio_set_level 去控制 LED,或者用 gpio_get_level 轮询输入电平。但说实话,这种方式在真正项目里几乎是不够用的。你不可能一直在死循环里等一个按钮按下吧?不仅浪费资源,响应还慢。那怎么破?答案就是——GPIO 中断。

这篇文章就围绕官方例程,讲讲怎么用 ESP-IDF 来搞定 GPIO 的中断配置,以及怎么通过任务和队列去处理这些中断事件。

痛点在哪?

在做项目的过程中,我们经常会遇到这些问题:

  • 想实时响应一个按钮按下的动作,但又不想用轮询
  • 多个 GPIO 同时做输入,状态复杂,代码写得很乱
  • 想让 GPIO 和任务逻辑解耦,但不知道怎么做

这些问题的本质其实就是对“中断机制”掌握不够。所以今天就来通过一段实际代码,带你完整过一遍中断的配置 + 触发 + 响应 + 处理流程。

实际场景:按钮触发、传感器唤醒、外部信号感知

这段代码,其实模拟的就是一个“硬件触发 + 软件响应”的完整流程。

你可以想象成:

  • 有两个 GPIO 输出口,比如接了个 PWM 驱动器或者继电器
  • 有两个 GPIO 输入口,比如接了个按钮或者红外传感器
  • 当某个输入引脚电平变化(高变低、低变高),触发中断,系统马上处理这个信号,比如点亮一个灯、发个消息、启动一段业务逻辑

是不是很像一个物联网设备在处理各种外设数据的过程?比如家里的智能门锁:按下开锁键 -> 中断触发 -> MCU 判断身份 -> 开锁并反馈。

常规处理逻辑

使用 while(1) 一直轮询一个按键,如下面代码:

代码语言:c
复制
void app_main(void)
{
    //zero-initialize the config structure.
    gpio_config_t io_conf = {};
    //disable interrupt
    io_conf.intr_type = GPIO_INTR_DISABLE;
    //set as output mode
    io_conf.mode = GPIO_MODE_OUTPUT;
    //bit mask of the pins that you want to set,e.g.GPIO18/19
    io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
    //disable pull-down mode
    io_conf.pull_down_en = 0;
    //disable pull-up mode
    io_conf.pull_up_en = 0;
    //configure GPIO with the given settings
    gpio_config(&io_conf);

    //interrupt of rising edge
    io_conf.intr_type = GPIO_INTR_POSEDGE;
    //bit mask of the pins, use GPIO4/5 here
    io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
    //set as input mode
    io_conf.mode = GPIO_MODE_INPUT;
    //enable pull-up mode
    io_conf.pull_up_en = 1;
    gpio_config(&io_conf);

    //change gpio interrupt type for one pin
    gpio_set_intr_type(GPIO_INPUT_IO_0, GPIO_INTR_ANYEDGE);

    //create a queue to handle gpio event from isr
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
    //start gpio task
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

    //install gpio isr service
    gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
    //hook isr handler for specific gpio pin
    gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
    //hook isr handler for specific gpio pin
    gpio_isr_handler_add(GPIO_INPUT_IO_1, gpio_isr_handler, (void*) GPIO_INPUT_IO_1);

    //remove isr handler for gpio number.
    gpio_isr_handler_remove(GPIO_INPUT_IO_0);
    //hook isr handler for specific gpio pin again
    gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);

    printf("Minimum free heap size: %"PRIu32" bytes\n", esp_get_minimum_free_heap_size());

    int cnt = 0;
    while (1) {
        printf("cnt: %d\n", cnt++);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        gpio_set_level(GPIO_OUTPUT_IO_0, cnt % 2);
        gpio_set_level(GPIO_OUTPUT_IO_1, cnt % 2);
    }
}

终端执行指令

代码语言:bash
复制
idf.py -p PORT flash monitor

按照注释连好线(GPIO18 → GPIO4、GPIO19 → GPIO5),运行这段代码:

执行结果如下:

我们可以试试下面这个例程。让硬件响应更“聪明”,让你的代码更“省力”。

代码结构讲解(不啰嗦,重点来了)

1. 宏定义部分

代码语言:c
复制
#define GPIO_OUTPUT_IO_0    CONFIG_GPIO_OUTPUT_0
#define GPIO_OUTPUT_IO_1    CONFIG_GPIO_OUTPUT_1
#define GPIO_INPUT_IO_0     CONFIG_GPIO_INPUT_0
#define GPIO_INPUT_IO_1     CONFIG_GPIO_INPUT_1

这些是从配置里取出定义好的 GPIO 编号(你也可以在 menuconfig 里改)。

然后用位运算凑出 “输出引脚掩码” 和 “输入引脚掩码”,方便一口气配置多个引脚。

2. 配置 GPIO

代码语言:c
复制
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
gpio_config(&io_conf);

先配置输出引脚,不启用中断,然后再配置输入引脚,并开启上拉电阻 + 设置中断类型。

注意:这里 GPIO_INPUT_IO_0 是双边沿触发(即升沿、降沿都响应),而 GPIO_INPUT_IO_1 是只在上升沿触发。

3. 设置中断回调函数

代码语言:c
复制
static void IRAM_ATTR gpio_isr_handler(void* arg) {
    uint32_t gpio_num = (uint32_t) arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

这个函数非常关键。它会在中断触发时执行(注意是 ISR 环境,要快),然后把中断的引脚号送进一个队列,留给主任务慢慢处理。

4. 主任务处理逻辑

代码语言:c
复制
static void gpio_task_example(void* arg) {
    uint32_t io_num;
    for (;;) {
        if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
            printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num));
        }
    }
}

这就是一个后台任务,负责从队列里拿到中断事件,然后做处理。实际项目里你可以替换成业务逻辑,比如控制其他外设、发 MQTT 消息、存日志等等。

5. 在主函数里串起来

代码语言:c
复制
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
gpio_isr_handler_add(GPIO_INPUT_IO_1, gpio_isr_handler, (void*) GPIO_INPUT_IO_1);

这部分代码就是把中断服务、队列、任务全都注册起来,形成一个完整的响应链路。

可视化图示(中断响应流程)

代码语言:txt
复制
+-----------+       触发        +----------------+      事件送队列       +-------------------+
| 外部信号  | ----------------> | ISR 中断处理函数 | ------------------> | 后台任务逻辑处理  |
+-----------+                  +----------------+                        +-------------------+

总结

通过这段代码,我们可以搞清楚以下几个关键知识点:

  • GPIO 的输入输出如何配置
  • GPIO 中断如何设置成上升沿、下降沿、双边沿
  • 怎么通过 ISR + 队列 + 任务来异步处理中断
  • 怎么让中断响应不阻塞主线程、不影响整体逻辑

这些技巧不仅适用于按钮处理、传感器感知、低功耗唤醒等场景,也是你做嵌入式开发的必备能力。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 痛点在哪?
  • 实际场景:按钮触发、传感器唤醒、外部信号感知
  • 常规处理逻辑
  • 代码结构讲解(不啰嗦,重点来了)
    • 1. 宏定义部分
    • 2. 配置 GPIO
    • 3. 设置中断回调函数
    • 4. 主任务处理逻辑
    • 5. 在主函数里串起来
  • 可视化图示(中断响应流程)
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档