大花科技
嵌入式开发原创教程嵌入式入门 02

GPIO 输入输出:按键控制 LED 与 20ms 消抖

从原理图确认有效电平,完成按键输入、LED 输出、非阻塞消抖和按键事件处理。

GPIO 输入输出:按键控制 LED 与 20ms 消抖

实验目标

  • 让一个 LED 受按键控制;
  • 理解上拉、下拉和有效电平;
  • 不用长时间 delay 实现消抖;
  • 区分“按键按住”和“按一下触发一次”。

一、先看硬件

常见接法是按键一端接 GND,另一端接 GPIO,并启用上拉电阻。这样:

  • 松开时读到高电平;
  • 按下时读到低电平;
  • 有效状态为低电平。

如果你的板子有外部上拉,CubeMX 中可以不开启内部上拉;如果没有,请开启 GPIO_PULLUP。

假设:

ccode
#define LED_GPIO_Port GPIOC
#define LED_Pin       GPIO_PIN_13
#define KEY_GPIO_Port GPIOA
#define KEY_Pin       GPIO_PIN_0

二、CubeMX 配置

  1. PC13 配置为 GPIO_Output、Push Pull、Low;
  2. PA0 配置为 GPIO_Input;
  3. PA0 的 Pull 设置为 Pull-up;
  4. 生成代码。

三、可用的非阻塞消抖

在 main.c 的 USER CODE 区域加入:

ccode
/* USER CODE BEGIN PV */
static GPIO_PinState key_last_raw = GPIO_PIN_SET;
static GPIO_PinState key_stable   = GPIO_PIN_SET;
static uint32_t key_changed_at    = 0;
static uint8_t key_pressed_event  = 0;

static void key_scan(void)
{
    GPIO_PinState raw = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);

    if (raw != key_last_raw)
    {
        key_last_raw = raw;
        key_changed_at = HAL_GetTick();
    }

    if ((HAL_GetTick() - key_changed_at) >= 20U &&
        raw != key_stable)
    {
        key_stable = raw;

        if (key_stable == GPIO_PIN_RESET)
        {
            key_pressed_event = 1U;
        }
    }
}
/* USER CODE END PV */

主循环:

ccode
while (1)
{
    key_scan();

    if (key_pressed_event)
    {
        key_pressed_event = 0U;
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }

    HAL_Delay(5);
}

这段代码的含义:

  • 原始电平变化时重新计时;
  • 连续稳定 20ms 才认可新状态;
  • 只有从松开变成按下时产生一次事件;
  • 主循环不会被 20ms 完全阻塞。

四、实验现象

每按一次按键,LED 翻转一次。按住按键不放,LED 不应持续快速闪烁。

五、常见问题

按键一直触发

先检查有效电平。如果按下时读到高电平,需要把消抖代码中的 GPIO_PIN_RESET 改成 GPIO_PIN_SET。

按键偶尔触发多次

可能是按键抖动时间较长或接线松动。把 20ms 改成 30-50ms 再测试。

串口打印影响按键

不要在中断中打印,也不要在主循环中使用很长的 HAL_Delay。先保证主循环每 5-10ms 运行一次按键扫描。

六、进阶练习

  1. 增加长按 2 秒触发另一个功能;
  2. 用两个按键分别控制 LED 开关;
  3. 把按键扫描封装成 button.c / button.h;
  4. 用串口输出按键事件,而不是在中断里打印。