3. GPIO 与中断:GPIO 基本操作、外部中断配置、按键消抖实战
GPIO 是嵌入式开发最基础、也最绕不开的模块。说白了,就是芯片用来跟外界打交道的「手脚」。你让它输出高电平,它就亮灯;你读它的电平,就知道按键有没有被按下。但实际项目中,GPIO 的坑远比你想的多。我刚开始用 ESP32 时,就因为在中断里做了延时,结果整个系统卡死,排查了一整天。
这一节,我们就把 GPIO 的基本操作、外部中断配置,还有按键消抖这几个核心问题彻底讲透。
3.1 GPIO 基本操作:从点亮 LED 开始
ESP32 的 GPIO 功能很灵活。大部分引脚都可以配置为输入、输出、上拉、下拉、甚至复用为外设功能。先看一个最简单的例子——点亮 LED。
核心步骤:
- 调用
gpio_set_direction()设置引脚方向 - 调用
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 都可以配置为中断源,支持上升沿、下降沿、低电平、高电平触发。
配置流程很简单:
- 调用
gpio_config()设置引脚和中断类型 - 调用
gpio_install_isr_service()安装中断服务 - 调用
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 和中断的核心知识点串了起来。你可以把它当作快速索引。
3.6 避坑与经验总结
做 GPIO 和中断开发,有几个坑我踩过不止一次,列出来给你参考:
- 中断里调用 FreeRTOS API: 很多 API 不能在 ISR 里用,比如
vTaskDelay()。要用带FromISR后缀的版本。 - GPIO 复用冲突: ESP32 的某些引脚默认被其他外设占用(比如 JTAG、ADC)。配置前先查数据手册。
- 中断优先级: 别把所有中断都设成最高优先级。否则低优先级任务永远得不到 CPU。
- 按键电平逻辑: 上拉输入,按下为低;下拉输入,按下为高。别搞反了。
我曾经踩过的坑: 有一次量产时,发现部分板子按键不灵敏。排查到最后,发现是 PCB 走线太长,寄生电容导致信号上升沿变缓,触发了多次中断。后来在软件里加了 5ms 的去抖滤波才解决。所以,硬件设计也要考虑信号完整性。
好了,GPIO 和中断的内容就讲到这里。这些基础打牢了,后面做传感器驱动、低功耗唤醒才会顺手。记住:中断里别干重活,消抖要因地制宜,硬件设计别马虎。