ESP32 GPIO 基础:从点灯到按键检测

GPIO,说白了就是芯片和外界打交道的最基本方式。我刚开始学嵌入式那会儿,觉得 GPIO 不就是高低电平嘛,有啥好学的?后来做项目才发现,就这简单的输入输出,坑还真不少。这一章咱们就把 ESP32 的 GPIO 彻底搞明白。

ESP32 GPIO 引脚概览

ESP32 芯片一共有 34 个 GPIO 引脚,编号从 GPIO0 到 GPIO39。但要注意,不是所有引脚都能随便用。我整理了一张常用引脚功能表,你写代码前最好对照着看。

GPIO 编号 可用功能 注意事项
GPIO0 数字IO、ADC2、触摸 上电时拉低会进入下载模式
GPIO1 数字IO、UART0_TX 默认用作串口输出,慎用
GPIO2 数字IO、ADC2、触摸 上电时拉高才能正常启动
GPIO3 数字IO、UART0_RX 默认用作串口输入,慎用
GPIO4-GPIO5 数字IO、ADC1、触摸 常规使用,比较安全
GPIO6-GPIO11 SPI Flash 相关 不建议用作普通IO
GPIO12-GPIO15 数字IO、ADC2、触摸 GPIO12 上电电压影响 Flash 电压
GPIO16-GPIO33 数字IO、ADC2 大部分可安全使用
GPIO34-GPIO39 仅输入模式 没有内部上拉/下拉
我曾经踩过的坑: 第一次用 ESP32 做项目,我直接把 LED 接在 GPIO6 上,结果怎么都点不亮。查了半天手册才发现,GPIO6-11 是 SPI Flash 的专用引脚,系统启动时就被占用了。所以啊,选引脚前一定先看数据手册。

GPIO 模式设置

ESP32 的每个 GPIO 都可以配置成多种模式。我个人习惯用 gpio_set_direction() 这个函数来设置,简单直接。

常用的 GPIO 模式有这几种:

  • GPIO_MODE_OUTPUT:输出模式,用来驱动 LED、蜂鸣器这类外设
  • GPIO_MODE_INPUT:输入模式,读取按键、传感器信号
  • GPIO_MODE_INPUT_OUTPUT:双向模式,开漏总线场景会用到
  • GPIO_MODE_DISABLE:禁用模式,省电时用

你想想看,如果只是点个灯,用输出模式就够了。但要是接 I2C 总线,就得用开漏输出加输入的双向模式。选错模式,轻则功能异常,重则烧引脚。

LED 闪烁实验

点灯是嵌入式的 Hello World。咱们用 GPIO2 接一个 LED,让它以 500ms 的间隔闪烁。

先看电路连接:

  • LED 正极 → 220Ω 电阻 → GPIO2
  • LED 负极 → GND

代码实现其实很简单:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define LED_GPIO  2

void app_main(void)
{
    // 配置 GPIO2 为输出模式
    gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);

    while (1) {
        // 点亮 LED
        gpio_set_level(LED_GPIO, 1);
        vTaskDelay(500 / portTICK_PERIOD_MS);

        // 熄灭 LED
        gpio_set_level(LED_GPIO, 0);
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
}
我的小技巧: 实际项目中,我习惯用 gpio_set_level() 而不是 gpio_write()。虽然功能一样,但 gpio_set_level() 的命名更直观,一看就知道是设置电平。代码可读性很重要,尤其是团队协作的时候。

按键输入检测

按键检测比点灯稍微复杂一点。最核心的问题就是:机械抖动。按键按下和释放的瞬间,电平会快速跳变几次,如果不做处理,一次按键会被误判成多次。

我常用的消抖方法有两种:

  • 硬件消抖:在按键两端并联一个 0.1μF 电容,简单粗暴
  • 软件消抖:检测到电平变化后,延时 10-20ms 再读一次

来看一个带软件消抖的按键检测代码:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define BUTTON_GPIO  0   // 开发板上的 BOOT 按键

void app_main(void)
{
    // 配置 GPIO0 为输入模式,启用内部上拉
    gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);
    gpio_set_pull_mode(BUTTON_GPIO, GPIO_PULLUP_ONLY);

    int last_state = 1;  // 上拉,默认高电平
    int stable_state = 1;

    while (1) {
        int current_state = gpio_get_level(BUTTON_GPIO);

        if (current_state != last_state) {
            // 检测到电平变化,延时消抖
            vTaskDelay(20 / portTICK_PERIOD_MS);
            stable_state = gpio_get_level(BUTTON_GPIO);

            if (stable_state == 0) {
                printf("按键按下!\n");
            } else {
                printf("按键释放!\n");
            }
        }

        last_state = current_state;
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}

为什么延时 20ms? 机械按键的抖动时间一般在 5-15ms 之间,延时 20ms 基本能覆盖绝大多数按键。我试过用 5ms,偶尔还会误触发;用 50ms 又感觉按键反应太迟钝。20ms 是个比较折中的值。

GPIO 中断:更高效的检测方式

轮询方式有个缺点:CPU 得一直跑循环去读引脚状态,浪费资源。实际项目中,我更喜欢用 GPIO 中断。让硬件在电平变化时主动通知 CPU,CPU 该干嘛干嘛,不用一直盯着。

ESP32 的 GPIO 中断支持以下几种触发方式:

中断类型 触发条件 适用场景
GPIO_INTR_POSEDGE 上升沿触发 检测信号从低到高
GPIO_INTR_NEGEDGE 下降沿触发 检测按键按下(高→低)
GPIO_INTR_ANYEDGE 任意边沿触发 需要检测两种变化
GPIO_INTR_LOW_LEVEL 低电平触发 持续低电平告警
GPIO_INTR_HIGH_LEVEL 高电平触发 持续高电平告警

用中断实现按键检测,代码会清爽很多:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define BUTTON_GPIO  0

// 中断服务函数
static void IRAM_ATTR button_isr_handler(void* arg)
{
    // 注意:中断里不能调用 printf!
    // 一般只设置标志位,具体处理放到任务里
    int* press_count = (int*)arg;
    *press_count += 1;
}

void app_main(void)
{
    // 配置 GPIO
    gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);
    gpio_set_pull_mode(BUTTON_GPIO, GPIO_PULLUP_ONLY);

    // 安装 GPIO 中断服务
    gpio_install_isr_service(0);

    // 注册中断回调
    int press_count = 0;
    gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, &press_count);

    // 设置触发方式:下降沿触发
    gpio_set_intr_type(BUTTON_GPIO, GPIO_INTR_NEGEDGE);

    while (1) {
        if (press_count > 0) {
            printf("按键被按下 %d 次\n", press_count);
            press_count = 0;
        }
        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
}
注意: 中断服务函数里千万别做耗时操作,比如打印、延时、动态内存分配。我见过有人直接在中断里调 printf,结果系统直接崩溃。中断里只做两件事:设置标志位、记录数据。具体处理放到主循环或任务里。

GPIO 知识体系总览

下面这张图把本章的核心知识点串起来了,你可以对照着梳理思路。

ESP32 GPIO 知识体系 ESP32 GPIO 知识体系 GPIO 基础 引脚配置 模式设置 输出应用 输入应用 引脚编号 功能复用 输出 输入 LED 闪烁 PWM 轮询 中断 硬件消抖 软件消抖 核心原则:先查手册选引脚 → 配置模式 → 根据场景选轮询或中断 → 输入信号必须消抖

避坑指南:我踩过的 GPIO 坑

做嵌入式这些年,GPIO 相关的坑我踩过不少。挑几个典型的说说:

  • 上电时序问题:GPIO0 和 GPIO2 在芯片启动时有特殊要求。我曾经把按键接在 GPIO0 上,结果每次上电都进下载模式。后来加了 RC 延时电路才解决。
  • 电流驱动能力:ESP32 的 GPIO 最大输出电流约 12mA,直接驱动继电器或电机肯定不行。我见过有人直接拿 GPIO 驱动 5V 继电器,结果芯片烧了。记住:驱动大负载必须加三极管或 MOSFET。
  • 未使用引脚处理:悬空的输入引脚容易受干扰,导致系统功耗异常。我习惯把不用的引脚配置成输出模式并拉低,或者启用内部上拉/下拉。
  • 中断服务函数别偷懒:中断里做复杂操作是大忌。我早期一个项目,在中断里调了 vTaskDelay(),结果系统直接死机。中断里只设标志位,处理逻辑放外面。

我的调试习惯: 遇到 GPIO 不工作,我一般按这个顺序排查:

  1. 万用表测引脚电压,确认硬件连接没问题
  2. 检查引脚号是否写错(尤其是 ESP32 引脚编号从 0 开始)
  3. 确认引脚没有被其他外设占用(比如 UART、SPI)
  4. 检查模式配置是否正确(输出/输入/上拉/下拉)

这套流程帮我省了不少时间,你也可以试试。

好了,GPIO 基础就聊到这儿。点灯和按键检测是嵌入式开发的基本功,看似简单,但背后涉及的模式配置、中断机制、消抖处理,都是后面做复杂项目的基础。把这些搞扎实了,后面学 I2C、SPI 这些总线协议会轻松很多。