STM32 GPIO 输入 — 按键检测(轮询方式)

野火 EBF STM32F103 Mini HAL 库:把 GPIO 配置为输入模式,用轮询扫描方式检测按键,并控制 LED 翻转。

STM32 GPIO 输入 — 按键检测(轮询方式)

开发板:野火 EBF STM32F103 Mini
固件库:HAL 库
对应例程10-GPIO输入—按键检测
参考源码User/main.cUser/key/bsp_key.cUser/key/bsp_key.hUser/led/bsp_led.h

上篇我们学会了让引脚输出电平来驱动 LED,本篇反过来——让引脚读取外部电平,最常见的用途就是按键检测。按键是嵌入式系统最基本的人机交互输入。


一、功能简介

实验板上有两颗用户按键 KEY1、KEY2。程序在 while(1) 里不断轮询(主动查询)按键引脚电平:一旦检测到某颗按键被按下,就翻转对应的 LED。

  • KEY1 按下 → LED1 翻转(亮变灭 / 灭变亮)
  • KEY2 按下 → LED2 翻转

核心知识点:

  1. 把 GPIO 配置为输入模式(浮空/上拉/下拉);
  2. HAL_GPIO_ReadPin() 读取引脚电平;
  3. 理解机械按键的抖动,并用"等待释放"做简单消抖。

二、硬件准备

项目 说明
开发板 野火 EBF STM32F103 Mini
按键引脚 KEY1 → PA0(按下为高电平),KEY2 → PC13(按下为高电平)
LED 引脚 LED1 → PC2,LED2 → PC3(低电平点亮)
调试器 ST-Link V2

硬件原理:本板按键一端接地、一端接 GPIO,并外接上拉电阻。按键未按时 GPIO 被上拉为高;按下时 GPIO 直接接地变为低——但野火 Mini 的 KEY 例程里 KEY_ON 定义为 1(高电平有效),说明该板按键是"按下输出高"的接法(按键另一端接 3.3V)。接线以你的原理图为准,代码里的 KEY_ON 宏用于适配这种差异。


三、完整代码(带中文注释)

1. main.c(主程序)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "main.h"
#include "stm32f1xx.h"
#include "./led/bsp_led.h"
#include ".\key\bsp_key.h"

int main(void)
{
  /* 系统时钟初始化成 72 MHz */
  SystemClock_Config();

  /* LED 端口初始化 */
  LED_GPIO_Config();

  /* 按键端口初始化 */
  Key_GPIO_Config();

  /* 轮询按键状态,若按键按下则翻转对应的 LED */
  while(1)
  {
    /* 检测 KEY1,按下则 LED1 翻转 */
    if( Key_Scan(KEY1_GPIO_PORT, KEY1_PIN) == KEY_ON )
    {
      LED1_TOGGLE;   // 翻转 LED1(亮<->灭)
    }

    /* 检测 KEY2,按下则 LED2 翻转 */
    if( Key_Scan(KEY2_GPIO_PORT, KEY2_PIN) == KEY_ON )
    {
      LED2_TOGGLE;
    }
  }
}

/* SystemClock_Config() 同 GPIO 输出章节,此处省略,保持 72MHz 系统时钟 */

2. bsp_key.h(按键引脚定义)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef __KEY_H
#define	__KEY_H

#include "stm32f1xx.h"
#include "main.h"

/* 引脚定义 */
#define KEY1_PIN                  GPIO_PIN_0
#define KEY1_GPIO_PORT            GPIOA
#define KEY1_GPIO_CLK_ENABLE()    __HAL_RCC_GPIOA_CLK_ENABLE()

#define KEY2_PIN                  GPIO_PIN_13
#define KEY2_GPIO_PORT            GPIOC
#define KEY2_GPIO_CLK_ENABLE()    __HAL_RCC_GPIOC_CLK_ENABLE()

/* 按键按下标志宏:按下为高电平,故 KEY_ON=1、KEY_OFF=0
 * 若你的板子是"按下为低电平",把这里改成 KEY_ON=0、KEY_OFF=1 即可 */
#define KEY_ON  1
#define KEY_OFF 0

void Key_GPIO_Config(void);
uint8_t Key_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

#endif /* __KEY_H */

3. bsp_key.c(按键初始化 + 扫描)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include ".\key\bsp_key.h"

/**
  * @brief  配置按键用到的 I/O 口
  */
void Key_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* 开启按键 GPIO 口的时钟 */
    KEY1_GPIO_CLK_ENABLE();
    KEY2_GPIO_CLK_ENABLE();

    /* 选择按键的引脚 */
    GPIO_InitStructure.Pin = KEY1_PIN;

    /* 设置引脚为输入模式 */
    GPIO_InitStructure.Mode = GPIO_MODE_INPUT;

    /* 设置引脚不上拉也不下拉(外部已接上拉电阻) */
    GPIO_InitStructure.Pull = GPIO_NOPULL;

    /* 用上面的结构体初始化按键 */
    HAL_GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStructure);

    /* KEY2 同理 */
    GPIO_InitStructure.Pin = KEY2_PIN;
    HAL_GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure);
}

/**
  * @brief   检测是否有按键按下
  * @param   GPIOx    :端口(如 GPIOA)
  * @param   GPIO_Pin :引脚(如 GPIO_PIN_0)
  * @retval  KEY_ON  :按键按下; KEY_OFF:未按下
  */
uint8_t Key_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    /* 检测是否有按键按下 */
    if( HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == KEY_ON )
    {
        /* 等待按键释放(既是消抖,也防止一次按下触发多次) */
        while( HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == KEY_ON );
        return KEY_ON;
    }
    else
        return KEY_OFF;
}

四、代码分段讲解

1. 输入模式怎么选?

GPIO 输入有 4 种常见模式:浮空输入、上拉输入、下拉输入、模拟输入。

  • 如果外部已经有上拉/下拉电阻,可以用 GPIO_NOPULL(浮空);
  • 如果外部没电阻,就用 GPIO_PULLUP / GPIO_PULLDOWN 让 MCU 内部电阻帮忙,省掉外围元件。

本例按键外部已带上拉,所以用了 GPIO_NOPULL

2. HAL_GPIO_ReadPin() 读的是什么?

它返回对应引脚的当前电平:GPIO_PIN_SET(高)或 GPIO_PIN_RESET(低)。内部就是读 IDR 输入数据寄存器。

3. 为什么要"等待释放"?

机械按键按下/松开瞬间,金属触点会弹跳几毫秒,导致电平快速抖动,可能被误判成多次按下。while(检测到还是按下); 这一句做了两件事:

  1. 消抖:松手后再返回,避开抖动区;
  2. 单次触发:按住不放也只生效一次,直到松开。

这是最朴素的"轮询消抖"。更专业的做法是定时器定时扫描 + 状态机,或外部中断 + 延时消抖(见 EXTI 章节)。

4. LEDx_TOGGLE 是怎么实现的?

它定义在 bsp_led.h 里,直接操作寄存器 ODR 异或:

1
#define digitalToggle(p,i)  {p->ODR ^= i;}   // 输出反转状态

比先读再写更快,也无需关心当前是亮是灭。


五、实验现象

  1. 下载程序后,反复按下 KEY1,LED1 会在亮、灭之间切换;
  2. 按下 KEY2,LED2 同步切换;
  3. 长按按键不会让 LED 连续翻转(因为等待释放逻辑),必须松手再按。

六、常见问题

Q1:按键没反应?

  • 确认 KEY_ON 宏的电平方向与实际硬件一致(按下到底是高是低);
  • 确认按键 GPIO 时钟已开启;
  • 用万用表/示波器量一下按下时引脚电平是否真的变了。

Q2:按一次 LED 闪了好几下?

  • 说明没有消抖或消抖失效。检查 while(等待释放) 是否写对;若按键是低电平有效却配成了高电平判断,会一直进消抖等待。

Q3:轮询方式有什么缺点?

  • 主循环被按键扫描占用,CPU 一直在空转查询,效率低下;且如果主循环里有长延时(如 HAL_Delay(1000)),按键会"漏检"。实际项目多用外部中断 + 定时器消抖

七、小结

  • 输入模式让 GPIO 从"发号施令"变成"收听外界";
  • 按键检测 = 读引脚电平 + 消抖处理,轮询是最易理解的入门方式;
  • KEY_ON 这类宏把"硬件电平约定"隔离在头文件,方便移植。

八、拓展应用

  1. 组合键 / 长按:在 Key_Scan 里累计按下时间,区分"短按"“长按"实现不同功能。
  2. 外部中断法:把按键引脚配置为 GPIO_MODE_IT_FALLING,在 HAL_GPIO_EXTI_Callback() 里翻转 LED,彻底解放主循环(参考 13-EXTI—外部中断)。
  3. 矩阵键盘:用行扫描+列读取,4 个 IO 就能识别 16 个按键。
  4. 旋转编码器:把按键扫描思路迁移到正交编码信号,配合 TIM 编码器接口读取旋钮方向。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计