在 STM32 单片机开发中,按键去抖动是保证系统稳定性的关键基础技能。机械按键在按下或释放的瞬间,触点会因为弹性产生不稳定的抖动(通常持续 5ms~20ms)。如果 CPU 在抖动期间读取状态,会误判为多次按下。软件去抖动主要通过延时检测、状态机扫描等逻辑来规避这一问题。
以下是几种主流的软件去抖动实现方法,包含详细步骤、代码示例及原理解析。
一、 方法一:延时检测法(最基础,适合初学者)
这种方法利用"延时"来跳过抖动时间窗口。虽然简单,但会阻塞 CPU,仅适合逻辑简单的单任务程序。
实现步骤
- 检测触发:读取 GPIO 电平,判断是否达到触发条件(如低电平)。
- 延时消抖 :调用延时函数(如
HAL_Delay或自写延时),等待 10ms~20ms,让机械抖动自然平息。 - 状态确认 :再次读取 GPIO 电平。如果电平依然符合触发条件,则判定为有效按下。
- 动作执行:执行对应的按键功能代码。
- 松手等待 :通过
while循环检测按键是否松开,防止一次物理按下被重复执行。
代码示例 (基于 STM32 HAL 库)
c
#include "main.h"
// 定义按键引脚 (假设接在 PA0,低电平有效)
#define KEY_PIN GPIO_PIN_0
#define KEY_PORT GPIOA
#define KEY_PRESS 0 // 按下时的电平
void Key_Delay_Debounce(void) {
// 1. 检测是否按下
if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == KEY_PRESS) {
// 2. 延时去抖,等待 20ms 让抖动过去
HAL_Delay(20);
// 3. 再次检测确认是否真的按下
if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == KEY_PRESS) {
// 4. 执行功能,例如翻转 LED
// HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
// 5. 松手检测,防止长按连续触发
while (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == KEY_PRESS);
}
}
}
原理解析
HAL_Delay(20)是核心,它强制 CPU 暂停 20ms。在这 20ms 内,无论引脚电平如何剧烈波动(抖动),程序都不予理睬。- 松手检测 (
while循环)确保了只有当用户完全松开按键后,函数才返回,下一次按下才能再次触发。这实现了"按一次,执行一次"的逻辑。
二、 方法二:定时器扫描法(推荐,非阻塞)
在复杂系统中,我们不能使用 Delay 阻塞主循环。最佳实践是利用定时器中断(如每 10ms 触发一次),在回调函数中扫描按键状态。这种方法通过计数器来判断状态稳定性。
实现步骤
- 初始化定时器:配置一个基础定时器(如 TIM6),设定中断周期为 10ms。
- 状态变量定义:定义变量记录按键当前状态、确认状态和计数器。
- 周期性扫描:在定时器中断服务函数中读取按键电平。
- 计数滤波 :
- 如果读取到按下状态,计数器加 1。
- 如果读取到未按下状态,计数器清 0。
- 当计数器累加到设定值(如 2 次,即 20ms),判定为有效按下。
- 事件处理:在主循环中检查标志位并执行逻辑。
代码示例
1. 全局变量定义
c
// 按键状态结构体
typedef struct {
uint8_t current_state; // 当前读取到的状态 (0:释放, 1:按下)
uint8_t stable_state; // 经过消抖后的稳定状态
uint8_t press_flag; // 按键按下事件标志 (0:无事件, 1:有按下事件)
uint8_t counter; // 消抖计数器
} Key_t;
Key_t key1 = {0, 0, 0, 0};
2. 定时器中断回调函数 (每 10ms 调用一次)
c
// 假设使用 HAL 库的定时器回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM6) { // 确认是我们要用的定时器
uint8_t read_level = HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN); // 读取当前电平
// 将物理电平转换为逻辑状态 (假设低电平按下)
key1.current_state = (read_level == GPIO_PIN_RESET) ? 1 : 0;
// 消抖逻辑
if (key1.current_state == 1) {
// 检测到按下,计数器增加
key1.counter++;
// 如果连续检测到按下超过 2 次 (20ms),且之前是释放状态
if (key1.counter > 2 && key1.stable_state == 0) {
key1.stable_state = 1; // 锁定状态为按下
key1.press_flag = 1; // 触发按下事件标志
}
} else {
// 检测到释放,计数器清零,状态复位
key1.counter = 0;
key1.stable_state = 0;
}
}
}
3. 主循环处理
c
int main(void) {
// ... 系统初始化代码 ...
// HAL_TIM_Base_Start_IT(&htim6); // 启动定时器中断
while (1) {
// 检查按键按下标志
if (key1.press_flag == 1) {
key1.press_flag = 0; // 清除标志,避免重复执行
// 执行按键功能
// HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
}
// 其他任务...
}
}
原理解析
- 非阻塞 :主循环
while(1)可以随时处理其他任务,只有定时器中断会短暂打断 CPU 进行按键采样。 - 滤波原理:只有当电平连续保持稳定一段时间(计数器累加),才认为状态改变。瞬间的抖动会导致计数器在 0 和 1 之间跳变,无法达到阈值,从而被过滤掉 。
三、 方法三:状态机法(进阶,逻辑严谨)
状态机法将按键过程分解为"空闲"、"按下消抖"、"按下确认"、"释放消抖"等状态,逻辑非常清晰,适合处理单击、双击、长按等复杂功能。
代码示例 (逻辑片段)
c
typedef enum {
KEY_IDLE, // 空闲状态
KEY_DEBOUNCE, // 按下消抖中
KEY_PRESSED, // 按下已确认
KEY_RELEASE // 释放消抖中
} KeyState;
KeyState key_state = KEY_IDLE;
uint32_t key_tick = 0; // 记录时间戳
void Key_StateMachine_Handler(void) {
uint8_t current_level = HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN);
uint32_t now_tick = HAL_GetTick(); // 获取当前系统时间(ms)
switch (key_state) {
case KEY_IDLE:
if (current_level == GPIO_PIN_RESET) { // 检测到低电平
key_state = KEY_DEBOUNCE;
key_tick = now_tick; // 记录进入时间
}
break;
case KEY_DEBOUNCE:
// 如果 20ms 后依然是低电平,确认按下
if ((now_tick - key_tick > 20) && (current_level == GPIO_PIN_RESET)) {
key_state = KEY_PRESSED;
// 这里可以触发按下事件
} else if (current_level == GPIO_PIN_SET) {
// 抖动导致提前变高,回退到空闲
key_state = KEY_IDLE;
}
break;
case KEY_PRESSED:
// 等待松手
if (current_level == GPIO_PIN_SET) {
key_state = KEY_RELEASE;
key_tick = now_tick;
}
break;
case KEY_RELEASE:
// 松手消抖,防止松手瞬间的抖动误判为再次按下
if ((now_tick - key_tick > 20) && (current_level == GPIO_PIN_SET)) {
key_state = KEY_IDLE; // 回到初始状态
}
break;
}
}
四、 总结与对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 延时检测法 | 逻辑极简单,代码量少,容易理解 | 阻塞 CPU,浪费资源,响应慢 | 简单演示、只控制 LED 的单任务程序 |
| 定时器扫描法 | 非阻塞,CPU 效率高,稳定性好 | 需要占用一个定时器资源 | 需要同时处理屏幕、通信等多任务系统 |
| 状态机法 | 逻辑严谨,扩展性强(可加双击/长按) | 代码稍复杂,需维护状态流转 | 复杂交互界面、菜单系统 |
对于 STM32 初学者,建议先掌握延时检测法 理解原理,进阶后务必学习定时器扫描法,这是工程开发的标准做法。在编写代码时,务必注意引脚的上拉/下拉配置,确保按键未按下时有确定的电平 。