3. GPIO 与中断:GPIO 基本操作、外部中断配置、按键消抖实战

GPIO 是嵌入式开发最基础、也最绕不开的模块。说白了,就是芯片用来跟外界打交道的「手脚」。你让它输出高电平,它就亮灯;你读它的电平,就知道按键有没有被按下。但实际项目中,GPIO 的坑远比你想的多。我刚开始用 ESP32 时,就因为在中断里做了延时,结果整个系统卡死,排查了一整天。

这一节,我们就把 GPIO 的基本操作、外部中断配置,还有按键消抖这几个核心问题彻底讲透。

3.1 GPIO 基本操作:从点亮 LED 开始

ESP32 的 GPIO 功能很灵活。大部分引脚都可以配置为输入、输出、上拉、下拉、甚至复用为外设功能。先看一个最简单的例子——点亮 LED。

核心步骤:

  1. 调用 gpio_set_direction() 设置引脚方向
  2. 调用 gpio_set_level() 输出高/低电平
#include "driver/gpio.h"

#define LED_GPIO   GPIO_NUM_2

void app_main(void)
{
    gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
    
    while (1) {
        gpio_set_level(LED_GPIO, 1);  // 亮
        vTaskDelay(pdMS_TO_TICKS(500));
        gpio_set_level(LED_GPIO, 0);  // 灭
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

嗯,这里要注意:ESP32 的 GPIO2 在开发板上通常连接着板载 LED,但有些模组可能不一样。我建议你拿到板子后,先查一下原理图,别想当然。

3.2 输入模式与内部上拉/下拉

读取按键状态时,需要把 GPIO 配置为输入模式。但浮空输入很容易受干扰,所以一般会启用内部上拉或下拉电阻。

配置函数 作用
gpio_set_pull_mode() 设置上拉/下拉/浮空
gpio_get_level() 读取引脚电平
#define BUTTON_GPIO  GPIO_NUM_0

void button_init(void)
{
    gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);
    gpio_set_pull_mode(BUTTON_GPIO, GPIO_PULLUP_ONLY);  // 内部上拉
}

你想想看,如果按键一端接地,另一端接 GPIO,启用内部上拉后,没按下时读到高电平,按下时读到低电平。这个逻辑很常见,但很多人会搞反。

小技巧: 我习惯在初始化后先读一次电平,确认硬件连接是否正确。有时候焊接虚焊,读到的电平会一直飘。

3.3 外部中断配置:别在中断里做「重活」

轮询方式浪费 CPU,而且响应不及时。外部中断才是正经做法。ESP32 的每个 GPIO 都可以配置为中断源,支持上升沿、下降沿、低电平、高电平触发。

配置流程很简单:

  1. 调用 gpio_config() 设置引脚和中断类型
  2. 调用 gpio_install_isr_service() 安装中断服务
  3. 调用 gpio_isr_handler_add() 注册中断处理函数
#include "driver/gpio.h"

#define BUTTON_GPIO  GPIO_NUM_0

static void IRAM_ATTR button_isr_handler(void *arg)
{
    // 中断处理 —— 只做标记,不做耗时操作
    // 比如用队列或信号量通知任务
}

void interrupt_init(void)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BUTTON_GPIO),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .intr_type = GPIO_INTR_NEGEDGE,  // 下降沿触发
    };
    gpio_config(&io_conf);

    gpio_install_isr_service(0);
    gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, NULL);
}

避坑指南: 我曾经在中断处理函数里调用了 printf(),结果系统频繁崩溃。因为 printf() 不是 ISR-safe 的。中断里只做三件事:置标志位、发信号量、写队列。其他事情交给任务去干。

3.4 按键消抖实战:硬件 vs 软件

机械按键按下时,触点会弹跳,产生多个脉冲。如果不处理,一次按下可能触发多次中断。这就是「抖动」。

消抖有两种思路:

方式 优点 缺点
硬件消抖(RC 滤波) 不占 CPU,实时性好 增加 BOM 成本
软件消抖(延时/采样) 零成本,灵活 占用 CPU 时间

我个人习惯在低功耗项目里用硬件消抖,因为软件延时会让系统无法进入深度睡眠。但在普通项目中,软件消抖完全够用。

3.4.1 软件消抖:延时法

检测到电平变化后,延时 10-20ms,再读一次。如果电平一致,就认为是有效按下。

bool debounce_read(gpio_num_t gpio, int delay_ms)
{
    if (gpio_get_level(gpio) == 0) {
        vTaskDelay(pdMS_TO_TICKS(delay_ms));
        if (gpio_get_level(gpio) == 0) {
            return true;  // 确认按下
        }
    }
    return false;
}

3.4.2 软件消抖:状态机法

延时法会阻塞任务,不适合实时性要求高的场景。状态机法通过定时采样,不阻塞,更优雅。

typedef enum {
    STATE_IDLE,
    STATE_DEBOUNCE,
    STATE_PRESSED
} button_state_t;

button_state_t state = STATE_IDLE;

void button_task(void *arg)
{
    while (1) {
        int level = gpio_get_level(BUTTON_GPIO);
        switch (state) {
            case STATE_IDLE:
                if (level == 0) state = STATE_DEBOUNCE;
                break;
            case STATE_DEBOUNCE:
                if (level == 0) {
                    state = STATE_PRESSED;
                    // 触发按键事件
                } else {
                    state = STATE_IDLE;
                }
                break;
            case STATE_PRESSED:
                if (level == 1) state = STATE_IDLE;
                break;
        }
        vTaskDelay(pdMS_TO_TICKS(10));  // 10ms 采样一次
    }
}

经验之谈: 状态机消抖的采样周期很关键。10ms 是通用值,但有些劣质按键抖动长达 30ms。我建议你在项目初期用示波器抓一下按键波形,确认抖动时间,再定采样周期。

3.5 知识体系总览

下面这张图把 GPIO 和中断的核心知识点串了起来。你可以把它当作快速索引。

GPIO 与中断知识体系 GPIO 基本操作 输出模式:gpio_set_level() 输入模式:gpio_get_level() 上拉/下拉:gpio_set_pull_mode() 外部中断配置 gpio_config() 设置中断类型 gpio_install_isr_service() gpio_isr_handler_add() 按键消抖实战 硬件消抖:RC 滤波 软件消抖:延时法 软件消抖:状态机法 实战要点总结 1. 中断处理函数必须短小精悍,只做标记 2. 软件消抖采样周期建议 10-20ms 3. 低功耗项目优先用硬件消抖 4. 初始化后先读一次电平,确认硬件状态

3.6 避坑与经验总结

做 GPIO 和中断开发,有几个坑我踩过不止一次,列出来给你参考:

  • 中断里调用 FreeRTOS API: 很多 API 不能在 ISR 里用,比如 vTaskDelay()。要用带 FromISR 后缀的版本。
  • GPIO 复用冲突: ESP32 的某些引脚默认被其他外设占用(比如 JTAG、ADC)。配置前先查数据手册。
  • 中断优先级: 别把所有中断都设成最高优先级。否则低优先级任务永远得不到 CPU。
  • 按键电平逻辑: 上拉输入,按下为低;下拉输入,按下为高。别搞反了。

我曾经踩过的坑: 有一次量产时,发现部分板子按键不灵敏。排查到最后,发现是 PCB 走线太长,寄生电容导致信号上升沿变缓,触发了多次中断。后来在软件里加了 5ms 的去抖滤波才解决。所以,硬件设计也要考虑信号完整性。

好了,GPIO 和中断的内容就讲到这里。这些基础打牢了,后面做传感器驱动、低功耗唤醒才会顺手。记住:中断里别干重活,消抖要因地制宜,硬件设计别马虎。