2、GPIO外设驱动:从点灯到中断实战

GPIO,说白了就是芯片的「手脚」——用来跟外界打交道的最基本方式。我刚开始用ESP-IDF时,觉得GPIO太简单了,不就是拉高拉低嘛。结果第一次做按键消抖就翻了车……嗯,这章咱们就把GPIO彻底讲透。

2.1 GPIO基础概念

ESP32的GPIO模块,每个引脚都可以独立配置。我个人习惯先搞清楚三个核心属性:方向、上下拉、中断触发方式。

属性 可选值 说明
方向 输入 / 输出 决定引脚是读还是写
上下拉 上拉 / 下拉 / 浮空 输入模式下防抖动、防悬空
中断触发 上升沿 / 下降沿 / 低电平 / 高电平 输入模式下的事件响应
小提示:ESP32的GPIO6~11默认接Flash,别乱动。我见过有人把这两个引脚当普通IO用,结果程序直接跑飞。

2.2 LED灯控制实战

先来个最简单的——点灯。这是嵌入式界的「Hello World」。你想想看,一个灯能亮,说明你的GPIO配置、时钟、电源都没问题。

硬件连接

  • LED正极 → GPIO2(ESP32开发板通常板载这个)
  • LED负极 → GND(串一个220Ω电阻限流)

代码实现

#include "driver/gpio.h"

#define LED_GPIO   GPIO_NUM_2

void app_main(void)
{
    // 配置GPIO为推挽输出
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << LED_GPIO),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE
    };
    gpio_config(&io_conf);

    while (1) {
        gpio_set_level(LED_GPIO, 1);   // 亮
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        gpio_set_level(LED_GPIO, 0);   // 灭
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}
关键点:gpio_config()一次性搞定所有配置,比逐个调用gpio_set_direction()、gpio_set_pull_mode()更高效。我项目里统一用这种方式,代码整洁很多。

2.3 按键输入检测

按键比LED麻烦一点。为什么?因为机械按键有抖动——按下弹起瞬间会跳变好几次。如果不处理,一次按下可能被误判成十几次。

硬件消抖 vs 软件消抖

方式 原理 优缺点
硬件消抖 RC滤波电路 可靠但增加BOM成本
软件消抖 延时采样+状态机 零成本,灵活

我个人偏爱软件消抖。来看代码:

#define BUTTON_GPIO  GPIO_NUM_0   // BOOT按键

void app_main(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_DISABLE
    };
    gpio_config(&io_conf);

    int prev_level = 1;  // 默认高电平(上拉)
    while (1) {
        int cur_level = gpio_get_level(BUTTON_GPIO);
        if (cur_level != prev_level) {
            vTaskDelay(20 / portTICK_PERIOD_MS);  // 消抖延时
            cur_level = gpio_get_level(BUTTON_GPIO);
            if (cur_level != prev_level) {
                prev_level = cur_level;
                if (cur_level == 0) {
                    printf("按键按下\n");
                }
            }
        }
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}
避坑指南:我曾经在消抖延时里用了vTaskDelay(50),结果按键响应特别迟钝。后来发现20ms就够——机械抖动通常不超过10ms。别盲目加大延时。

2.4 中断编程实战

轮询按键太浪费CPU了——你想想看,while循环里一直在读GPIO,啥也干不了。中断才是正经做法:按键按下时,CPU自动跳进中断服务函数,处理完再回去干正事。

中断配置流程

  1. 配置GPIO为输入模式,使能中断
  2. 注册中断服务函数(ISR)
  3. 在ISR里用队列或标志位通知任务
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"

static QueueHandle_t gpio_evt_queue = NULL;

// 中断服务函数
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
    uint32_t gpio_num = (uint32_t) arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

// 任务:处理中断事件
static void gpio_task_example(void* arg)
{
    uint32_t io_num;
    while (1) {
        if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
            printf("GPIO %d 触发中断\n", io_num);
        }
    }
}

void app_main(void)
{
    // 配置GPIO
    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_evt_queue = xQueueCreate(10, sizeof(uint32_t));

    // 启动任务
    xTaskCreate(gpio_task_example, "gpio_task", 2048, NULL, 10, NULL);

    // 安装GPIO中断服务
    gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
    gpio_isr_handler_add(BUTTON_GPIO, gpio_isr_handler, (void*) BUTTON_GPIO);
}
核心思想:ISR里不能做printf、不能延时、不能调用任何阻塞函数。正确的做法是「ISR只发信号,任务来处理」。我用队列传递GPIO编号,干净利落。

2.5 GPIO知识体系总览

下面这张图把本章的核心逻辑串起来了——从配置到中断,一条线走到底。

GPIO驱动知识体系 GPIO配置 输出模式 输入模式 中断模式 gpio_set_level() gpio_get_level() ISR + 队列 软件消抖 LED闪烁 / PWM调光 按键检测 / 状态机 事件驱动响应 核心原则:配置→方向→操作→应用,中断模式下ISR只发信号,任务来处理 软件消抖延时20ms,中断触发用下降沿,队列深度建议10以上

2.6 常见问题与避坑

  • GPIO不输出?检查gpio_config()里pin_bit_mask是否写对了。我习惯用(1ULL << GPIO_NUM_X)防止移位溢出。
  • 中断不触发?先确认gpio_install_isr_service()有没有调用。这个函数只调用一次,重复调用会报错。
  • 按键误触发?硬件上拉+软件消抖20ms基本能解决。如果还不行,检查PCB走线是不是太长,加个100nF电容到GND。
  • ISR里死机?别在ISR里调用printf、malloc、vTaskDelay。ISR里只能调用带FromISR后缀的FreeRTOS函数。
调试技巧:用逻辑分析仪抓GPIO波形,一眼就能看出抖动、中断触发时机对不对。比printf靠谱一百倍。