4、GPIO输入驱动——按键检测
按键检测,是嵌入式开发里最基础也最容易翻车的环节。你想想看,一个按键按下去,单片机怎么知道它被按了?怎么区分是「真的按了」还是「干扰抖动」?这些问题,我在刚做项目时都踩过坑。
这一章,我们就从 GPIO 输入模式配置开始,把按键检测的软硬件细节彻底讲透。
4.1 GPIO 输入模式配置
STM32 的 GPIO 做输入时,有几个关键参数要设置。我习惯用 LL 库的 LL_GPIO_Init() 函数,直接填结构体,清晰明了。
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = LL_GPIO_PIN_0;
GPIO_InitStruct.Mode = LL_GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = LL_GPIO_PULL_UP; // 上拉
LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
这里要注意 Mode 字段。输入模式下,GPIO 的施密特触发器是使能的,这能提高抗干扰能力。说白了,就是输入信号不会在阈值附近来回跳变。
4.2 上拉/下拉电阻选择
按键电路通常有两种接法:
- 按下为低电平:按键一端接 GND,另一端接 GPIO。此时 GPIO 内部要配成上拉。
- 按下为高电平:按键一端接 VCC,另一端接 GPIO。此时 GPIO 内部要配成下拉。
我建议优先用「按下为低电平 + 上拉」的方案。为什么?因为 STM32 内部上拉电阻默认是弱上拉(约 40kΩ),外部干扰不容易把高电平拉低。而如果按下为高电平,外部噪声可能把低电平误判成高电平。
| 配置方式 | 内部电阻 | 按下电平 | 抗干扰能力 |
|---|---|---|---|
| 上拉输入 | 40kΩ 上拉 | 低电平 | 强 |
| 下拉输入 | 40kΩ 下拉 | 高电平 | 中等 |
| 浮空输入 | 无 | 不确定 | 弱(不推荐) |
4.3 软件消抖与硬件消抖
按键按下和释放的瞬间,金属触点会弹跳。这个弹跳时间通常持续 5~20ms。如果不做处理,一次按键可能被识别成多次。
4.3.1 软件消抖
软件消抖的核心思路很简单:检测到电平变化后,延时一段时间再读一次。如果电平稳定,才确认按键动作。
uint8_t Key_Scan(void)
{
if (LL_GPIO_IsInputPinSet(BTN_PORT, BTN_PIN) == 0) // 检测到按下
{
Delay_ms(10); // 延时 10ms 避开抖动
if (LL_GPIO_IsInputPinSet(BTN_PORT, BTN_PIN) == 0) // 再次确认
{
while (LL_GPIO_IsInputPinSet(BTN_PORT, BTN_PIN) == 0); // 等待释放
return 1; // 有效按键
}
}
return 0;
}
这段代码里,我用了 Delay_ms(10)。这个延时时间怎么定?我一般取 10~20ms,具体看按键的 datasheet。有些便宜的按键弹跳时间可能到 30ms,那就得适当加长。
HAL_Delay() 那种阻塞方式,否则会拖慢整个系统。建议用定时器或系统滴答计时。
4.3.2 硬件消抖
硬件消抖是在按键引脚上加一个 RC 低通滤波器。电容充电/放电的惯性会平滑掉毛刺。
典型电路:
- 电阻 R:10kΩ
- 电容 C:0.1μF
- 时间常数 τ = R × C = 1ms
这个时间常数决定了滤波效果。τ 太小,滤不掉抖动;τ 太大,按键响应变慢。我一般取 1~5ms 的 τ 值。
硬件消抖的好处是不占用 CPU 时间,适合对实时性要求高的场景。但缺点是多两个元件,成本略增。
| 消抖方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 软件消抖 | 零成本,灵活 | 占用 CPU 时间 | 按键数量少,实时性要求不高 |
| 硬件消抖 | 不占 CPU,响应快 | 增加元件成本 | 按键数量多,或对响应有严格要求 |
4.4 中断方式检测按键(EXTI)
轮询方式检测按键,CPU 要一直去读 GPIO 电平,效率低。更好的做法是用外部中断(EXTI)。按键按下时触发中断,CPU 只在需要时才处理。
4.4.1 EXTI 配置步骤
- 使能 SYSCFG 时钟:EXTI 的中断线选择需要 SYSCFG 参与。
- 配置 GPIO 为输入:和前面一样。
- 连接 EXTI 线到 GPIO:通过
LL_SYSCFG_SetEXTISource()选择。 - 配置 EXTI 触发方式:上升沿、下降沿或双边沿。
- 使能 EXTI 中断:在 NVIC 中使能对应的中断通道。
// 1. 使能 SYSCFG 时钟
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SYSCFG);
// 2. GPIO 配置(略)
// 3. 连接 EXTI 线到 PA0
LL_SYSCFG_SetEXTISource(LL_SYSCFG_EXTI_PORTA, LL_SYSCFG_EXTI_LINE0);
// 4. 配置 EXTI 下降沿触发(按下为低电平)
LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_0);
LL_EXTI_EnableFallingTrig_0_31(LL_EXTI_LINE_0);
// 5. 使能 NVIC
NVIC_SetPriority(EXTI0_IRQn, 0);
NVIC_EnableIRQ(EXTI0_IRQn);
4.4.2 中断服务函数
中断触发后,在 ISR 里清除中断标志位,然后执行按键处理逻辑。注意,ISR 里不要做耗时操作,最好只设置一个标志位,在主循环里处理。
void EXTI0_IRQHandler(void)
{
if (LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_0))
{
LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_0);
// 设置按键标志位,主循环处理
key_pressed = 1;
}
}
Delay_ms() 做软件消抖,结果系统卡死。因为中断里延时,其他中断进不来,系统直接挂掉。正确的做法是在 ISR 里只清标志,消抖放在主循环或定时器里做。
4.5 知识结构总览
下面这张图把 GPIO 输入驱动按键检测的完整流程串起来了。你可以对照着看,每一步都对应我们刚才讲的内容。
4.6 小结
这一章我们讲了 GPIO 输入驱动的完整链路:从电路设计、模式配置,到上拉/下拉选择,再到消抖处理和中断检测。每个环节都有坑,但只要你理解了原理,按步骤来,就不会出大问题。
我个人建议,新手先从软件消抖的轮询方式入手,跑通了再切换到 EXTI 中断。这样你能更清楚地看到按键检测的整个流程。等你对中断熟悉了,再用硬件消抖也不迟。