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 | 仅输入模式 | 没有内部上拉/下拉 |
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 知识体系总览
下面这张图把本章的核心知识点串起来了,你可以对照着梳理思路。
避坑指南:我踩过的 GPIO 坑
做嵌入式这些年,GPIO 相关的坑我踩过不少。挑几个典型的说说:
- 上电时序问题:GPIO0 和 GPIO2 在芯片启动时有特殊要求。我曾经把按键接在 GPIO0 上,结果每次上电都进下载模式。后来加了 RC 延时电路才解决。
- 电流驱动能力:ESP32 的 GPIO 最大输出电流约 12mA,直接驱动继电器或电机肯定不行。我见过有人直接拿 GPIO 驱动 5V 继电器,结果芯片烧了。记住:驱动大负载必须加三极管或 MOSFET。
- 未使用引脚处理:悬空的输入引脚容易受干扰,导致系统功耗异常。我习惯把不用的引脚配置成输出模式并拉低,或者启用内部上拉/下拉。
- 中断服务函数别偷懒:中断里做复杂操作是大忌。我早期一个项目,在中断里调了
vTaskDelay(),结果系统直接死机。中断里只设标志位,处理逻辑放外面。
我的调试习惯: 遇到 GPIO 不工作,我一般按这个顺序排查:
- 万用表测引脚电压,确认硬件连接没问题
- 检查引脚号是否写错(尤其是 ESP32 引脚编号从 0 开始)
- 确认引脚没有被其他外设占用(比如 UART、SPI)
- 检查模式配置是否正确(输出/输入/上拉/下拉)
这套流程帮我省了不少时间,你也可以试试。
好了,GPIO 基础就聊到这儿。点灯和按键检测是嵌入式开发的基本功,看似简单,但背后涉及的模式配置、中断机制、消抖处理,都是后面做复杂项目的基础。把这些搞扎实了,后面学 I2C、SPI 这些总线协议会轻松很多。