3. 按键输入与中断:数字输入、内部上拉/下拉电阻、外部中断、消抖处理
按键,是嵌入式系统里最基础、也最容易出问题的外设之一。你想想看,一个简单的开关动作,背后却牵扯到电平逻辑、电阻配置、中断响应,还有那个让人头疼的「抖动」问题。这一章,我们就把它彻底讲透。
3.1 数字输入的本质:读引脚电平
ESP32 的 GPIO 引脚,既可以做输出(点灯),也可以做输入(读按键)。做输入时,我们实际上是在读取引脚上的电压:高于某个阈值就是 HIGH (1),低于阈值就是 LOW (0)。
ESP32 的阈值电压一般在 0.75V 左右(低电平)和 2.5V 左右(高电平),具体值可以参考芯片手册。但实际项目中,我们很少去抠这个精确值,只要保证输入信号「够高」或「够低」就行。
3.2 上拉与下拉电阻:让引脚有个「确定状态」
按键没按下时,引脚是悬空的。悬空的引脚会像一根天线,感应周围的电磁干扰,读到的电平随机跳变。这就是所谓的「浮空状态」。
解决办法很简单:给引脚一个确定的电平。用电阻把它拉到 VCC(上拉)或 GND(下拉)。
| 类型 | 连接方式 | 按键按下时 | 按键松开时 |
|---|---|---|---|
| 上拉电阻 | 电阻接 VCC 与引脚之间 | 引脚 → GND,读 LOW | 引脚 → VCC,读 HIGH |
| 下拉电阻 | 电阻接 GND 与引脚之间 | 引脚 → VCC,读 HIGH | 引脚 → GND,读 LOW |
我个人习惯用上拉 + 按下为低的方案。为什么?因为大多数单片机复位时引脚默认输入,内部上拉默认开启,这样按键没按下时引脚就是高电平,逻辑上更安全。
3.2.1 ESP32 内部上拉/下拉电阻
ESP32 内部集成了可编程的上拉和下拉电阻,省去了外部加电阻的麻烦。你只需要在代码里配置一下就行。
// 启用内部上拉电阻
gpio_set_pull_mode(GPIO_NUM_0, GPIO_PULLUP_ONLY);
// 启用内部下拉电阻
gpio_set_pull_mode(GPIO_NUM_0, GPIO_PULLDOWN_ONLY);
// 同时禁用上拉和下拉(浮空)
gpio_set_pull_mode(GPIO_NUM_0, GPIO_FLOATING);
3.3 外部中断:让 CPU 别傻等
轮询方式读取按键,CPU 要一直循环检查引脚电平,浪费算力。中断方式则让 CPU 该干嘛干嘛,按键按下时自动跳转到中断服务函数。
ESP32 的每个 GPIO 都可以配置为外部中断,支持以下触发方式:
- 上升沿触发: 电平从 LOW 变 HIGH 时触发
- 下降沿触发: 电平从 HIGH 变 LOW 时触发
- 低电平触发: 电平为 LOW 时持续触发
- 高电平触发: 电平为 HIGH 时持续触发
按键场景下,我们通常用下降沿触发(按下时触发一次)或上升沿触发(松开时触发一次)。
// 配置 GPIO0 为下降沿触发的外部中断
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_NEGEDGE, // 下降沿触发
.mode = GPIO_MODE_INPUT, // 输入模式
.pin_bit_mask = (1ULL << GPIO_NUM_0),
.pull_up_en = GPIO_PULLUP_ENABLE, // 启用内部上拉
.pull_down_en = GPIO_PULLDOWN_DISABLE
};
gpio_config(&io_conf);
// 注册中断服务
gpio_install_isr_service(0);
gpio_isr_handler_add(GPIO_NUM_0, button_isr_handler, NULL);
// 中断服务函数
static void IRAM_ATTR button_isr_handler(void* arg) {
// 注意:中断里不要做复杂操作,只设标志位
g_button_pressed = true;
}
3.4 按键消抖:别让一次按下变成十次
机械按键在按下和松开的瞬间,触点会弹跳几次,产生一连串的电平抖动。如果不处理,一次按键可能会触发多次中断。
抖动时间一般在 5ms ~ 20ms 之间。消抖的核心思路就是:等抖动过去再确认电平状态。
3.4.1 硬件消抖
在按键两端并联一个 0.1μF 的电容,利用电容的充放电特性平滑电平变化。这个方法简单粗暴,但会增加 BOM 成本。
3.4.2 软件消抖(推荐)
软件消抖更灵活,不需要额外硬件。常用方法有两种:
- 延时消抖: 检测到电平变化后,延时 10~20ms,再次读取电平确认。
- 状态机消抖: 通过连续多次采样,判断电平是否稳定。
我个人更推荐状态机消抖,因为它不阻塞程序运行。
// 简单的延时消抖示例
void button_task(void *pvParameter) {
while (1) {
if (gpio_get_level(GPIO_NUM_0) == 0) { // 检测到按下
vTaskDelay(pdMS_TO_TICKS(20)); // 延时 20ms 消抖
if (gpio_get_level(GPIO_NUM_0) == 0) { // 再次确认
// 按键有效,执行操作
printf("Button pressed!\n");
// 等待按键松开
while (gpio_get_level(GPIO_NUM_0) == 0);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
3.5 本章知识结构图
3.6 综合示例:带中断和消抖的按键控制
下面是一个完整的示例,把中断、消抖、内部上拉都整合在一起。按下按键,LED 翻转状态。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define BUTTON_GPIO 0
#define LED_GPIO 2
static volatile bool g_button_pressed = false;
// 中断服务函数
static void IRAM_ATTR button_isr_handler(void* arg) {
g_button_pressed = true;
}
// 按键处理任务
void button_task(void *pvParameter) {
bool led_state = false;
while (1) {
if (g_button_pressed) {
g_button_pressed = false;
// 延时消抖
vTaskDelay(pdMS_TO_TICKS(20));
// 确认按键是否真的按下
if (gpio_get_level(BUTTON_GPIO) == 0) {
led_state = !led_state;
gpio_set_level(LED_GPIO, led_state);
printf("LED state: %s\n", led_state ? "ON" : "OFF");
// 等待按键松开
while (gpio_get_level(BUTTON_GPIO) == 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void app_main(void) {
// 配置 LED 引脚
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(LED_GPIO, 0);
// 配置按键引脚(内部上拉 + 下降沿中断)
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_NEGEDGE,
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL << BUTTON_GPIO),
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE
};
gpio_config(&io_conf);
// 安装中断服务
gpio_install_isr_service(0);
gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, NULL);
// 创建按键处理任务
xTaskCreate(button_task, "button_task", 2048, NULL, 10, NULL);
}
嗯,到这里,按键输入与中断的核心内容就讲完了。从数字输入的基本原理,到上拉/下拉电阻的配置,再到中断的注册和消抖处理,每一步都是实际项目中绕不开的坎。记住:好的按键设计,能让用户感觉不到它的存在;差的设计,会让用户每按一次都骂一次娘。