STM32 GPIO 输入 — 按键检测(轮询方式)
开发板:野火 EBF STM32F103 Mini
固件库:HAL 库
对应例程:10-GPIO输入—按键检测
参考源码:User/main.c、User/key/bsp_key.c、User/key/bsp_key.h、User/led/bsp_led.h
上篇我们学会了让引脚输出电平来驱动 LED,本篇反过来——让引脚读取外部电平,最常见的用途就是按键检测。按键是嵌入式系统最基本的人机交互输入。
一、功能简介
实验板上有两颗用户按键 KEY1、KEY2。程序在 while(1) 里不断轮询(主动查询)按键引脚电平:一旦检测到某颗按键被按下,就翻转对应的 LED。
- KEY1 按下 → LED1 翻转(亮变灭 / 灭变亮)
- KEY2 按下 → LED2 翻转
核心知识点:
- 把 GPIO 配置为输入模式(浮空/上拉/下拉);
- 用
HAL_GPIO_ReadPin() 读取引脚电平;
- 理解机械按键的抖动,并用"等待释放"做简单消抖。
二、硬件准备
| 项目 |
说明 |
| 开发板 |
野火 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(检测到还是按下); 这一句做了两件事:
- 消抖:松手后再返回,避开抖动区;
- 单次触发:按住不放也只生效一次,直到松开。
这是最朴素的"轮询消抖"。更专业的做法是定时器定时扫描 + 状态机,或外部中断 + 延时消抖(见 EXTI 章节)。
4. LEDx_TOGGLE 是怎么实现的?
它定义在 bsp_led.h 里,直接操作寄存器 ODR 异或:
1
|
#define digitalToggle(p,i) {p->ODR ^= i;} // 输出反转状态
|
比先读再写更快,也无需关心当前是亮是灭。
五、实验现象
- 下载程序后,反复按下 KEY1,LED1 会在亮、灭之间切换;
- 按下 KEY2,LED2 同步切换;
- 长按按键不会让 LED 连续翻转(因为等待释放逻辑),必须松手再按。
六、常见问题
Q1:按键没反应?
- 确认
KEY_ON 宏的电平方向与实际硬件一致(按下到底是高是低);
- 确认按键 GPIO 时钟已开启;
- 用万用表/示波器量一下按下时引脚电平是否真的变了。
Q2:按一次 LED 闪了好几下?
- 说明没有消抖或消抖失效。检查
while(等待释放) 是否写对;若按键是低电平有效却配成了高电平判断,会一直进消抖等待。
Q3:轮询方式有什么缺点?
- 主循环被按键扫描占用,CPU 一直在空转查询,效率低下;且如果主循环里有长延时(如
HAL_Delay(1000)),按键会"漏检"。实际项目多用外部中断 + 定时器消抖。
七、小结
- 输入模式让 GPIO 从"发号施令"变成"收听外界";
- 按键检测 = 读引脚电平 + 消抖处理,轮询是最易理解的入门方式;
KEY_ON 这类宏把"硬件电平约定"隔离在头文件,方便移植。
八、拓展应用
- 组合键 / 长按:在
Key_Scan 里累计按下时间,区分"短按"“长按"实现不同功能。
- 外部中断法:把按键引脚配置为
GPIO_MODE_IT_FALLING,在 HAL_GPIO_EXTI_Callback() 里翻转 LED,彻底解放主循环(参考 13-EXTI—外部中断)。
- 矩阵键盘:用行扫描+列读取,4 个 IO 就能识别 16 个按键。
- 旋转编码器:把按键扫描思路迁移到正交编码信号,配合 TIM 编码器接口读取旋钮方向。