3. GPIO 输入检测:按键消抖与 LED 控制实战

各位同学,欢迎来到第三章。上一章我们点亮了 LED,那是输出。这一章我们反过来,让单片机“听”按键的动作——也就是 GPIO 输入检测。

按键看似简单,但实际项目中坑不少。我记得刚入行时,做一个手持设备,按键一按就触发好几次,用户投诉说“这机器是不是疯了”。后来才发现,是没做消抖处理。好,咱们今天就把这个坑填上。

3.1 按键为什么会抖动?

你按下按键,金属触点接触时并不是“啪”一下就稳定了。实际上,它会弹跳几次,持续几毫秒到十几毫秒。单片机跑得飞快,几微秒就能读一次,所以它会读到一连串的 0 和 1 交替变化。

核心概念:按键抖动是机械触点固有的物理现象,不是电路故障。消抖的目的就是滤掉这些毛刺,只认一次稳定的电平变化。

抖动的典型波形是这样的:

按键按下时的电平抖动波形 GND VCC 未按下 抖动区 稳定按下 释放 抖动时间通常持续 5~20ms,取决于按键机械结构

3.2 消抖的两种主流方法

消抖说白了就两个思路:硬件消抖和软件消抖。我个人习惯优先用软件消抖,因为不增加成本,改起来也方便。

方法 原理 优点 缺点
硬件消抖 RC 低通滤波 + 施密特触发器 不占 CPU,响应快 增加元件,PCB 面积变大
软件消抖 延时采样或连续多次采样 零成本,灵活可调 占用 CPU 时间(可用定时器优化)

我的建议:量产产品中,如果按键数量少(1~4 个),用软件消抖完全够用。如果按键矩阵很大,或者对响应速度要求极高,可以考虑硬件消抖。

3.3 软件消抖的经典实现

最常用的方法是“延时 10~20ms 后再次读取”。为什么是 10~20ms?因为绝大多数按键的抖动不会超过 20ms。我曾经遇到过一种劣质按键,抖了 30ms,那次我直接把延时改到 40ms 才搞定。

代码逻辑很简单:

  1. 检测到按键电平变化(比如从高变低)
  2. 延时 20ms
  3. 再次读取同一引脚
  4. 如果电平仍然为低,确认按键按下
  5. 否则视为干扰,忽略

用 HAL 库写出来就是这样:

// 按键消抖检测函数
// 返回值:1 表示按键确实按下,0 表示抖动或未按下
uint8_t Key_Read(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)  // 第一次检测到低电平
    {
        HAL_Delay(20);  // 延时 20ms 避开抖动
        if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)  // 再次确认
        {
            return 1;  // 按键确实按下
        }
    }
    return 0;  // 抖动或未按下
}

注意:HAL_Delay(20) 会阻塞 CPU 20ms。在简单系统中没问题,但在多任务或实时性要求高的系统中,建议改用定时器轮询或状态机方式。

3.4 HAL_GPIO_ReadPin 函数详解

这个函数是 HAL 库读取 GPIO 电平的标准接口。咱们拆开看看:

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
参数/返回值 说明
GPIOx GPIO 端口基地址,如 GPIOA、GPIOB、GPIOC
GPIO_Pin 引脚编号,如 GPIO_PIN_0、GPIO_PIN_1,可用 | 组合多个引脚
返回值 GPIO_PIN_RESET (0) 或 GPIO_PIN_SET (1)

底层原理其实很简单:它读取 GPIOx->IDR 寄存器的对应位。IDR 是输入数据寄存器,每一位对应一个引脚的电平状态。

避坑指南:我曾经在项目里直接用 (GPIOA->IDR & GPIO_PIN_0) 来读引脚,结果发现某些型号的 STM32 在低功耗模式下 IDR 会失效。用 HAL_GPIO_ReadPin 就安全得多,因为它内部做了时钟和模式检查。

3.5 CubeMX 配置 GPIO 输入

咱们以 STM32F103C8T6 为例,配置一个按键输入引脚:

  1. 打开 CubeMX,选择芯片型号
  2. 在 Pinout 视图中,点击 PC13 引脚(假设接按键)
  3. 选择 GPIO_Input 模式
  4. 在 GPIO Settings 标签页中配置:
配置项 推荐值 说明
GPIO mode Input mode 输入模式
Pull-up/Pull-down Pull-up 外部按键接 GND,内部上拉保证空闲时高电平
Maximum output speed Low 输入模式不关心速度,保持默认即可

小技巧:如果按键一端接 GND,另一端接 GPIO,就选内部上拉。如果按键一端接 VCC,另一端接 GPIO,就选内部下拉。这样外部只需要一个按键,省掉电阻。

3.6 实战:按键控制 LED 亮灭

好,理论讲完了,咱们直接上手。硬件连接如下:

  • 按键:一端接 PC13,另一端接 GND
  • LED:正极接 PB0(通过 220Ω 电阻),负极接 GND

完整代码:

// main.c 主循环部分
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();  // CubeMX 生成的 GPIO 初始化

    uint8_t key_pressed = 0;  // 按键按下标志

    while (1)
    {
        if (Key_Read(GPIOC, GPIO_PIN_13))  // 检测按键
        {
            // 防止一次按下多次触发
            if (!key_pressed)
            {
                key_pressed = 1;
                HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);  // 翻转 LED
            }
        }
        else
        {
            key_pressed = 0;  // 按键释放后清除标志
        }
    }
}

// 按键消抖函数(前面已定义)
uint8_t Key_Read(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)
    {
        HAL_Delay(20);
        if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET)
        {
            return 1;
        }
    }
    return 0;
}

关键点:为什么加 key_pressed 标志?因为按键按下期间,Key_Read 会一直返回 1,如果不加标志,LED 会在一次按下中反复翻转。这个坑我踩过,当时调试了半天,以为是消抖没做好。

3.7 本章知识体系总览

下面这张图把 GPIO 输入检测的完整流程串起来了:

GPIO 输入检测:按键控制 LED 知识体系 硬件连接 按键 → GND,内部上拉 CubeMX 配置 GPIO_Input + Pull-up HAL 库接口 HAL_GPIO_ReadPin() 软件消抖核心逻辑 检测电平变化 → 延时 20ms → 再次确认 按键按下检测 Key_Read() 返回 1 LED 翻转控制 HAL_GPIO_TogglePin() key_pressed 标志防止一次按下多次触发 从硬件配置到软件消抖,再到 LED 控制,每一步都环环相扣

嗯,这一章的内容就到这里。按键消抖看起来是个小问题,但处理不好会让整个产品体验大打折扣。记住:延时 20ms 再读一次,加个防抖标志,基本就能应对 90% 的场景了。