4、按键输入与中断:机械按键消抖原理、外部中断配置、使用按键控制 LED 开关
按键,是嵌入式系统里最基础的人机交互方式。
别看它简单,我刚开始做项目时,就在按键上栽过跟头。一个智能家居面板,按一下灯亮,再按一下灯灭——结果用户反映「按一下有时候闪两下」。排查了半天,就是没做消抖处理。
这节课,咱们就把按键这件事彻底讲透。
4.1 机械按键的物理特性与抖动
机械按键的本质,是两片金属簧片的接触与分离。
你想想看,金属片碰撞时会发生什么?
它会弹跳。就像你扔一个乒乓球到地上,它不会立刻停住,而是会弹几下才稳定。
按键按下时,电平信号不是干净地从高到低跳变,而是会在几十毫秒内反复跳变。这就是「抖动」。释放时也一样。
典型抖动时间:
- 按下抖动:5~20ms
- 释放抖动:5~10ms
- 稳定闭合时间:>50ms(取决于按键力度)
如果不处理抖动,MCU会认为你按了好几次。我在一个温控器项目里遇到过,用户明明只按了一次「+」键,温度却跳了3度。嗯,这就是没消抖的后果。
4.2 消抖的两种主流方法
消抖说白了就一个思路:等信号稳定了再读取。
4.2.1 硬件消抖
用RC低通滤波器。一个电阻加一个电容,把毛刺滤掉。
电路示意:
VCC —— 按键 —— R(10kΩ) —— IO口
|
C(0.1μF)
|
GND
RC时间常数 τ = R × C = 10kΩ × 0.1μF = 1ms。这个值能滤掉大部分抖动。
我个人习惯是:如果PCB空间允许,尽量加硬件消抖。它不占CPU,稳定可靠。
4.2.2 软件消抖
这是最常用的方法。核心代码就几句话:
// 伪代码:软件消抖
if (GPIO_ReadPin(KEY_PIN) == 0) { // 第一次检测到按下
Delay_ms(20); // 等待20ms,跳过抖动期
if (GPIO_ReadPin(KEY_PIN) == 0) { // 再次确认
// 按键确实被按下了
// 执行按键处理
}
}
为什么是20ms?
因为绝大多数机械按键的抖动不会超过20ms。我测试过十几种不同厂家的按键,最长的抖动也就18ms左右。所以20ms是个安全值。
小技巧:Delay_ms(20) 在裸机程序里没问题。但如果用了RTOS,千万别用阻塞延时。改用定时器或vTaskDelay(),否则整个系统都会被卡住。
4.3 ESP32的外部中断配置
用轮询方式读按键,CPU得一直盯着IO口,太浪费了。
中断才是正经做法。按键按下时,让CPU「被叫过来」处理,平时该干嘛干嘛。
ESP32的GPIO中断,配置起来其实很简单。我直接上代码:
#include <driver/gpio.h>
#define KEY_GPIO GPIO_NUM_0 // 开发板上的BOOT按键
#define LED_GPIO GPIO_NUM_2 // 板载LED
// 中断服务函数
static void IRAM_ATTR key_isr_handler(void* arg) {
// 注意:中断里不能做复杂操作
// 这里只设置一个标志位
uint32_t *flag = (uint32_t *)arg;
*flag = 1;
}
void app_main(void) {
uint32_t key_pressed = 0;
// 配置按键GPIO
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_NEGEDGE; // 下降沿触发(按下)
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << KEY_GPIO);
io_conf.pull_up_en = 1; // 内部上拉
gpio_config(&io_conf);
// 配置LED GPIO
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(LED_GPIO, 0);
// 安装中断服务
gpio_install_isr_service(0);
gpio_isr_handler_add(KEY_GPIO, key_isr_handler, (void*)&key_pressed);
while (1) {
if (key_pressed) {
key_pressed = 0; // 清除标志
// 注意:消抖不能在中断里做
// 所以我们在主循环里做延时消抖
vTaskDelay(pdMS_TO_TICKS(20));
if (gpio_get_level(KEY_GPIO) == 0) {
// 按键确实被按下了
int level = gpio_get_level(LED_GPIO);
gpio_set_level(LED_GPIO, !level);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
这里有几个关键点,我得强调一下:
| 配置项 | 说明 | 我的建议 |
|---|---|---|
| GPIO_INTR_NEGEDGE | 下降沿触发 | 按键按下时IO从高变低,用这个最合适 |
| IRAM_ATTR | 中断函数放在IRAM中 | 不加这个,中断响应会慢,极端情况会崩溃 |
| gpio_install_isr_service | 安装中断服务 | 整个程序只调用一次 |
避坑指南:我曾经在中断里直接调用gpio_set_level()来控制LED。结果发现有时候LED不响应,有时候程序跑飞。后来才意识到——中断里不能做耗时操作。GPIO操作虽然快,但如果有多个中断嵌套,就会出问题。正确做法是:中断里只设标志,主循环里处理逻辑。
4.4 完整示例:按键控制LED开关
把上面的代码整合一下,加上完整的消抖逻辑:
#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <driver/gpio.h>
#define KEY_GPIO GPIO_NUM_0
#define LED_GPIO GPIO_NUM_2
static volatile uint32_t g_key_flag = 0;
static void IRAM_ATTR key_isr(void* arg) {
g_key_flag = 1;
}
void app_main(void) {
// 配置按键
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_NEGEDGE,
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL << KEY_GPIO),
.pull_up_en = 1,
.pull_down_en = 0
};
gpio_config(&io_conf);
// 配置LED
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(LED_GPIO, 0);
// 中断初始化
gpio_install_isr_service(0);
gpio_isr_handler_add(KEY_GPIO, key_isr, NULL);
printf("按键控制LED示例已启动\n");
while (1) {
if (g_key_flag) {
g_key_flag = 0;
// 软件消抖:延时20ms后再次检测
vTaskDelay(pdMS_TO_TICKS(20));
if (gpio_get_level(KEY_GPIO) == 0) {
// 按键有效,翻转LED
int level = gpio_get_level(LED_GPIO);
gpio_set_level(LED_GPIO, !level);
printf("LED状态切换为: %s\n", level ? "OFF" : "ON");
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
这段代码跑在ESP32-DevKitC上,按下BOOT键,板载LED就会翻转状态。
4.5 进阶:长按与短按的区分
实际产品中,一个按键往往要承担多个功能。比如短按开关灯,长按调亮度。
怎么实现?
思路很简单:记录按键按下的时间。
// 伪代码:长按短按检测
uint32_t press_time = 0;
uint8_t key_state = 0; // 0:释放 1:按下
if (gpio_get_level(KEY_GPIO) == 0 && key_state == 0) {
// 刚按下
key_state = 1;
press_time = xTaskGetTickCount();
}
if (gpio_get_level(KEY_GPIO) == 1 && key_state == 1) {
// 刚释放
key_state = 0;
uint32_t duration = xTaskGetTickCount() - press_time;
if (duration < pdMS_TO_TICKS(1000)) {
// 短按(小于1秒)
short_press_handler();
} else {
// 长按(大于等于1秒)
long_press_handler();
}
}
我在一个智能插座项目里就用到了这个方案。短按控制通断,长按进入配网模式。用户反馈说操作很直观。
4.6 总结一下这节课的核心
- 机械按键一定会抖动,抖动时间5~20ms,必须消抖
- 软件消抖最常用:检测到按下→延时20ms→再次确认
- 中断比轮询高效:ESP32用gpio_isr_handler_add注册中断,中断里只设标志
- 消抖放在主循环,不要在中断里做延时
- 长按短按通过计时实现,灵活扩展功能
按键看似简单,但做好它,是嵌入式工程师的基本功。下次遇到按键不灵的情况,你知道该查哪里了吧?