3、按键输入与中断:GPIO输入模式、按键消抖(硬件与软件)、外部中断配置、中断服务函数编写
好,咱们进入第三章。这一章讲的是按键,看似简单,但坑不少。我刚开始做嵌入式那会儿,觉得按键不就是读个电平嘛,结果被现实狠狠教育了一顿——按键按下弹起那几下抖动,能把你的逻辑搞得一团糟。所以这一章,咱们把按键这件事彻底讲透。
3.1 GPIO输入模式
ESP32的GPIO作为输入,其实就两个关键点:上拉/下拉和电平读取。
你想想看,按键一端接GND,另一端接GPIO。按键没按下时,GPIO悬空,电平不确定。这时候就需要内部上拉电阻,把电平拉到高。按下时,GPIO被拉到GND,读到低电平。
配置代码很简单:
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE, // 先不用中断
.mode = GPIO_MODE_INPUT, // 输入模式
.pin_bit_mask = (1ULL < GPIO_NUM_0), // 选择GPIO0
.pull_up_en = 1, // 使能上拉
.pull_down_en = 0 // 下拉关闭
};
gpio_config(&io_conf);
嗯,这里要注意:GPIO0、GPIO2、GPIO12这几个引脚在ESP32上有特殊功能。GPIO0是BOOT模式选择引脚,GPIO12是电压选择引脚。我建议你尽量避开它们做按键,否则调试时容易出幺蛾子。
3.2 按键消抖——硬件方案
为什么需要消抖?机械按键内部是金属弹片,按下时不会瞬间稳定接触,而是会弹跳几次。这个过程大概持续5-20ms。如果不处理,一次按下可能被识别成多次。
硬件消抖,说白了就是加个RC低通滤波器。一个电阻串联,一个电容对地并联。时间常数τ = R × C,一般取10ms左右。
举个例子:R=10kΩ,C=1μF,τ=10ms。这个组合能把抖动的高频分量滤掉,让电平变化变得平滑。
我个人习惯是:如果板子空间允许,优先用硬件消抖。因为硬件消抖不占用CPU,而且可靠性高。我在一个工业控制项目里,所有按键都加了RC滤波,三年没出过一次误触发。
3.3 按键消抖——软件方案
但很多时候,硬件上没法加电容(比如空间受限、成本敏感)。这时候就得靠软件了。
软件消抖的核心思想就一句话:延迟判断,确认稳定。
最常用的方法是:检测到电平变化后,延时10-20ms,再读一次。如果两次一致,才认为是有效按键。
uint8_t key_scan(void) {
static uint8_t last_state = 1; // 上次状态,默认高电平
uint8_t current_state = gpio_get_level(KEY_GPIO);
if (current_state != last_state) {
// 状态变化,启动延时消抖
vTaskDelay(pdMS_TO_TICKS(20));
current_state = gpio_get_level(KEY_GPIO);
if (current_state == last_state) {
// 抖动,忽略
return 0;
}
last_state = current_state;
if (current_state == 0) {
// 按键按下(低电平有效)
return 1;
}
}
return 0;
}
我曾经犯过一个错误:在中断里直接调用vTaskDelay。结果系统直接崩溃。记住,中断服务函数里不能调用阻塞型函数。软件消抖最好放在任务循环里做。
3.4 外部中断配置
轮询方式虽然简单,但浪费CPU。更好的做法是用中断——按键按下时,CPU自动跳转到中断服务函数处理。
ESP32的GPIO中断支持四种触发方式:
| 触发方式 | 宏定义 | 说明 |
|---|---|---|
| 上升沿触发 | GPIO_INTR_POSEDGE | 电平从低变高时触发 |
| 下降沿触发 | GPIO_INTR_NEGEDGE | 电平从高变低时触发 |
| 任意沿触发 | GPIO_INTR_ANYEDGE | 电平变化就触发 |
| 低电平触发 | GPIO_INTR_LOW_LEVEL | 低电平持续触发 |
按键一般用下降沿触发(按下瞬间触发一次)或者低电平触发(按住时持续触发)。我个人习惯用下降沿,因为一次按下只触发一次,逻辑更清晰。
配置中断的代码:
// 配置GPIO为输入,并使能中断
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_NEGEDGE, // 下降沿触发
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL < GPIO_NUM_4),
.pull_up_en = 1,
.pull_down_en = 0
};
gpio_config(&io_conf);
// 安装GPIO中断服务
gpio_install_isr_service(0);
// 注册中断回调函数
gpio_isr_handler_add(KEY_GPIO, key_isr_handler, (void*) KEY_GPIO);
3.5 中断服务函数编写
中断服务函数(ISR)有严格的限制。我总结了几条铁律:
- 不能调用printf——printf内部用了互斥锁,中断里用会死锁
- 不能调用vTaskDelay——阻塞函数会挂起整个系统
- 不能申请动态内存——malloc不是线程安全的
- 执行时间尽量短——超过100μs就可能影响其他中断
那ISR里能做什么?说白了就两件事:读寄存器、写标志位。
// 中断服务函数
static void IRAM_ATTR key_isr_handler(void* arg) {
uint32_t gpio_num = (uint32_t) arg;
// 清除中断标志位
gpio_intr_clear(gpio_num);
// 设置标志位,通知主循环处理
key_pressed_flag = 1;
key_pressed_gpio = gpio_num;
}
// 主循环中处理
void app_main() {
// ... 初始化代码 ...
while (1) {
if (key_pressed_flag) {
key_pressed_flag = 0;
// 这里做消抖处理(延时再读一次)
vTaskDelay(pdMS_TO_TICKS(20));
if (gpio_get_level(key_pressed_gpio) == 0) {
// 确认按键按下,执行动作
handle_key_press(key_pressed_gpio);
}
}
// 其他任务...
}
}
你可能会问:为什么不在ISR里直接消抖?因为ISR里不能延时。那怎么办?我常用的做法是:ISR只负责通知,消抖逻辑放在任务里。这样既利用了中断的实时性,又避免了ISR的限制。
volatile 关键字。
3.6 综合示例:一个完整的按键中断程序
把上面所有知识点串起来,一个完整的按键中断程序大概长这样:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define KEY_GPIO 4
#define LED_GPIO 2
static volatile int key_pressed_flag = 0;
static void IRAM_ATTR key_isr_handler(void* arg) {
gpio_intr_clear(KEY_GPIO);
key_pressed_flag = 1;
}
void app_main() {
// 配置LED
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
// 配置按键
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);
// 安装中断服务
gpio_install_isr_service(0);
gpio_isr_handler_add(KEY_GPIO, key_isr_handler, (void*) KEY_GPIO);
int led_state = 0;
while (1) {
if (key_pressed_flag) {
key_pressed_flag = 0;
// 软件消抖
vTaskDelay(pdMS_TO_TICKS(20));
if (gpio_get_level(KEY_GPIO) == 0) {
// 按键有效,翻转LED
led_state = !led_state;
gpio_set_level(LED_GPIO, led_state);
printf("按键按下,LED状态:%d\n", led_state);
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
这个程序实现了:按下按键,LED翻转,串口打印状态。代码里包含了中断配置、ISR编写、软件消抖三个核心知识点。
嗯,到这里,按键输入与中断的内容就差不多了。总结一下:硬件消抖保底,软件消抖兜底,中断通知,任务处理。这套组合拳打好了,按键这块基本不会出问题。
下一章咱们开始讲PWM和LED呼吸灯效果,到时候你会看到中断和定时器是怎么配合的。