第四章:Key 模块 —— 让你的开发板“听得懂”你按了什么!
摘要:本文介绍两种STM32按键检测方案,适用于STM32G431RBT6开发板。方案一为简洁版,通过位运算识别按键按下/松开瞬间,适合简单应用;方案二采用状态机实现,具备去抖、长按、连按检测功能,适合复杂场景。文章详细讲解了硬件连接原理(上拉电阻+按键接地)、HAL库GPIO读取API,并提供完整代码示例。两种方案均以生活场景比喻(电梯按钮/手机闹钟),帮助初学者理解嵌入式按键处理的核心思想:周
让你的开发板“听得懂”你按了什么!
🎯 适用对象:零基础嵌入式小白
💡 开发环境:STM32G431RBT6 + STM32CubeMX + HAL 库
✅ 前提:你已在 CubeMX 中将按键引脚配置为 GPIO_Input + Pull-up
🌟 引子:为什么按键不是“一按就灵”?
想象你家的门铃:
- 如果你按一下,它“嘀——”响一次,很舒服。
- 但如果你按的时候手抖了一下,它“嘀嘀嘀嘀……”响十次,你会崩溃!
这就是机械按键的“抖动”问题。
而我们的任务,就是让单片机像一个聪明的管家:
“主人只按了一次?那我就执行一次动作,不管他手抖没抖!”
🔌 一、硬件原理图:按键是怎么连的?
在 STM32G431RBT6 开发板上,4 个独立按键通常这样连接:

VCC (3.3V)
│
[内部上拉电阻] ← CubeMX 启用!
│
├── PB0 ──┬── 按键1 ── GND
├── PB1 ──┬── 按键2 ── GND
├── PB2 ──┬── 按键3 ── GND
└── PA0 ──┬── 按键4 ── GND

🧠 三种 GPIO 输入模式(笔记中的精华!)
| 模式 | 原理 | 类比 | 适用场景 |
|---|---|---|---|
| 上拉(Pull-up) | 内部电阻接 VCC,无信号时为高电平 | 就像弹簧门:没人推时自动关(高电平) | 按键接地(最常见!) |
| 下拉(Pull-down) | 内部电阻接地,无信号时为低电平 | 就像重力门:没人拉时自动开(低电平) | 按键接 VCC(少见) |
| 浮空(Floating) | 无上下拉,电平不确定 | 就像风中旗帜:风吹哪边倒哪边 | 外部电路已提供确定电平 |

✅ 设计用的是 上拉 + 按键接地,所以:
- 没按 → 高电平(
GPIO_PIN_SET)- 按下 → 低电平(
GPIO_PIN_RESET)
📚 二、HAL 库核心 API:HAL_GPIO_ReadPin()
这是你和硬件“对话”的桥梁!
函数原型:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
参数说明:
GPIOx:端口,如GPIOA、GPIOBGPIO_Pin:引脚,如GPIO_PIN_0
返回值:
GPIO_PIN_SET→ 高电平(3.3V)GPIO_PIN_RESET→ 低电平(0V)
使用场景(你笔记中的总结):
- ✅ 按键检测:判断是否被按下
- ✅ 传感器读取:如红外避障模块输出
- ✅ 通信监测:如检测 UART 的 CTS 信号
实现原理(底层揭秘):
- 读取 输入数据寄存器(IDR)
- 通过位操作提取指定引脚状态
就像从一排开关中,只看第3个是不是“关”了。
📦 三、方案一:简洁版 —— “瞬间识别按下/松开”
适合初学者快速上手,代码少、逻辑清!
🎯 生活比喻:电梯按钮
- 你按一下“3楼”,电梯记住:“有人要去3楼!”
- 即使你手抖按了5次,电梯也只执行一次动作。
1. 全局变量(像四个小信箱)
// key_app.h
#ifndef __KEY_APP_H
#define __KEY_APP_H
#include "stm32g4xx_hal.h"
extern uint8_t key_val; // 现在谁在按?(0=没人,1~4=按键号)
extern uint8_t key_old; // 上一秒谁在按?
extern uint8_t key_down; // 谁**刚刚开始按**?(只触发一次!)
extern uint8_t key_up; // 谁**刚刚松手**?
uint8_t key_read(void);
void key_proc(void);
#endif
2. 核心函数(完整保留的逻辑)
// key_app.c
#include "key_app.h"
uint8_t key_val = 0;
uint8_t key_old = 0;
uint8_t key_down = 0;
uint8_t key_up = 0;
/**
* @brief 读取按键状态
* @return 0: 无按键;1~4: 对应按键被按下
*/
uint8_t key_read(void)
{
uint8_t temp = 0;
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) temp = 1;
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) temp = 2;
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET) temp = 3;
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) temp = 4;
return temp;
}
/**
* @brief 按键处理函数
* 更新 key_down(刚按下)和 key_up(刚松开)标志
*/
void key_proc(void)
{
key_val = key_read(); // 读当前状态
key_down = key_val & (key_old ^ key_val); // 刚刚按下的键
key_up = (~key_val) & (key_old ^ key_val); // 刚刚松开的键
key_old = key_val; // 更新历史
}
🧠 位运算魔法(小白也能懂):
key_old ^ key_val→ 找出变化了的键key_val & (...)→ 只保留从“没按”变成“按了” 的键 → 按下事件(~key_val) & (...)→ 只保留从“按了”变成“没按” 的键 → 松开事件
✅ 效果:长按不重复触发,只在“按下瞬间”执行一次!
🧠 四、方案二:状态机版 —— “智能管家模式”
适合需要去抖、长按、连按的复杂场景!
🎯 生活比喻:手机闹钟 App(经典例子!)
你的手机闹钟有三个状态:
- 未设定 → 你还没设时间
- 已设定 → 时间设好了,等待响铃
- 响铃中 → 到点了!叮铃铃!
状态之间会转换:
- 设定闹钟 → 从未设定 → 已设定
- 时间到 → 从已设定 → 响铃中
- 关闭 → 从响铃中 → 未设定
按键状态机同理!
按键的三个状态:
- 空闲(没按)
- 按下(正在按)
- 释放(刚松手,判断是否连按)
1. 结构体定义(每个按键都是一个“小管家”)
// key_state.h
#ifndef __KEY_STATE_H
#define __KEY_STATE_H
#include "stm32g4xx_hal.h"
typedef struct {
GPIO_TypeDef *gpiox; // 控制哪个端口(如 GPIOB)
uint16_t pin; // 控制哪个引脚(如 PIN_0)
uint16_t ticks; // 计时器(单位:任务周期)
uint8_t level; // 当前电平(1=高,0=低)
uint8_t id; // 按键编号(0~3)
uint8_t state; // 状态:0=空闲,1=按下,2=释放
uint8_t debouce_cnt; // 去抖计数器
uint8_t repeat; // 连按次数
} button;
extern button btns[4];
void key_init(void);
void key_task(button *btn);
void key_state(void);
#endif
2. 初始化与状态机函数(完整版)
// key_state.c
#include "key_state.h"
button btns[4];
void key_init(void)
{
btns[0].gpiox = GPIOB; btns[0].pin = GPIO_PIN_0; btns[0].level = 1; btns[0].id = 0;
btns[1].gpiox = GPIOB; btns[1].pin = GPIO_PIN_1; btns[1].level = 1; btns[1].id = 1;
btns[2].gpiox = GPIOB; btns[2].pin = GPIO_PIN_2; btns[2].level = 1; btns[2].id = 2;
btns[3].gpiox = GPIOA; btns[3].pin = GPIO_PIN_0; btns[3].level = 1; btns[3].id = 3;
}
void key_task(button *btn)
{
// 读取当前电平(0=按下,1=释放)
uint8_t gpio_level = (HAL_GPIO_ReadPin(btn->gpiox, btn->pin) == GPIO_PIN_RESET) ? 0 : 1;
// 非空闲状态才计时
if (btn->state > 0) {
btn->ticks++;
}
// 去抖处理:连续3次读到相同变化才确认
if (btn->level != gpio_level) {
if (++(btn->debouce_cnt) >= 3) {
btn->level = gpio_level;
btn->debouce_cnt = 0;
}
} else {
btn->debouce_cnt = 0;
}
// 状态机
switch (btn->state) {
case 0: // 空闲状态
if (btn->level == 0) { // 按下了!
btn->ticks = 0;
btn->repeat = 1; // 第一次按
btn->state = 1; // 进入“按下”状态
}
break;
case 1: // 按下状态
if (btn->level == 1) { // 松开了!
if (btn->ticks >= 15) { // 长按(假设15*5ms=75ms)
btn->repeat = 0; // 防止释放时误触发单击
}
btn->ticks = 0;
btn->state = 2; // 进入“释放”状态
}
break;
case 2: // 释放状态
if (btn->ticks >= 15) {
btn->state = 0; // 真正结束,回到空闲
} else {
if (btn->level == 0) { // 快速连按!
btn->repeat++;
btn->ticks = 0;
btn->state = 1; // 直接回到“按下”
}
}
break;
}
}
void key_state(void)
{
for (uint8_t i = 0; i < 4; i++) {
key_task(&btns[i]);
}
}
🧪 五、如何使用?完整示例
方法一:简洁版(推荐新手)
// main.c
#include "key_app.h"
#include <stdio.h>
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
while (1) {
key_proc(); // 每10ms调用一次
if (key_down == 1) {
printf("✅ 按键1被按下!\n");
}
if (key_up == 2) {
printf("👋 按键2被松开了!\n");
}
HAL_Delay(10);
}
}
方法二:状态机版(高级玩法)
// main.c
#include "key_state.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
key_init();
while (1) {
key_state(); // 每5ms调用一次
// 检测按键0是否刚按下
if (btns[0].state == 1 && btns[0].ticks == 0) {
if (btns[0].repeat == 1) {
printf("🔘 按键1单击\n");
} else {
printf("🔁 按键1连按第 %d 次\n", btns[0].repeat);
}
}
HAL_Delay(5);
}
}
🧠 本章口诀(背下来!)
🔘 按键接地接上拉,
没按是高,按下是低!📏 HAL_GPIO_ReadPin,
RESET 就是按下了!📦 简洁版:down/up 分明,
电梯按钮一次灵!🧠 状态机:像闹钟一样聪明,
去抖长按连按全搞定!⏱️ 每 5~10ms 扫一次,
按键响应快又稳!
✅ 总结
提供的两套代码,分别代表了嵌入式按键处理的两种哲学:
- 简洁版:像“快递员”——快速送达,不啰嗦
- 状态机版:像“智能管家”——去抖、长按、连按,样样精通
现在,即使是第一次接触单片机的小白,也能根据项目需求,选择最适合的方案!
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)