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 的施密特触发器是使能的,这能提高抗干扰能力。说白了,就是输入信号不会在阈值附近来回跳变。

个人习惯:我一般把不用的 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 配置步骤

  1. 使能 SYSCFG 时钟:EXTI 的中断线选择需要 SYSCFG 参与。
  2. 配置 GPIO 为输入:和前面一样。
  3. 连接 EXTI 线到 GPIO:通过 LL_SYSCFG_SetEXTISource() 选择。
  4. 配置 EXTI 触发方式:上升沿、下降沿或双边沿。
  5. 使能 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;
    }
}
我曾经踩过的坑:在 ISR 里直接调用 Delay_ms() 做软件消抖,结果系统卡死。因为中断里延时,其他中断进不来,系统直接挂掉。正确的做法是在 ISR 里只清标志,消抖放在主循环或定时器里做。

4.5 知识结构总览

下面这张图把 GPIO 输入驱动按键检测的完整流程串起来了。你可以对照着看,每一步都对应我们刚才讲的内容。

GPIO 输入驱动按键检测知识结构 按键电路设计 GPIO 输入模式配置 上拉/下拉电阻选择 软件消抖 vs 硬件消抖 EXTI 中断方式检测 按下为低电平 + 上拉 按下为高电平 + 下拉 浮空输入不推荐 软件:延时 10~20ms 再读 硬件:RC 低通滤波器 τ = R × C SYSCFG + EXTI 配置 下降沿/上升沿触发 ISR 只设标志位 稳定可靠的按键检测

4.6 小结

这一章我们讲了 GPIO 输入驱动的完整链路:从电路设计、模式配置,到上拉/下拉选择,再到消抖处理和中断检测。每个环节都有坑,但只要你理解了原理,按步骤来,就不会出大问题。

我个人建议,新手先从软件消抖的轮询方式入手,跑通了再切换到 EXTI 中断。这样你能更清楚地看到按键检测的整个流程。等你对中断熟悉了,再用硬件消抖也不迟。