STM32HAL 快速入门(十三):定时器消抖 —— 中断场景下的按键抖动处理
前言
大家好,这里是 Hello_Embed。在之前的笔记中,我们用 “延时 20ms” 处理按键机械抖动,但这种方法在中断控制场景中存在明显缺陷 —— 中断服务函数需要快速响应,若加入延时会阻塞程序运行。本篇将介绍更优的解决方案:定时器消抖,利用 STM32 的 SysTick 定时器实现高效、非阻塞的按键抖动过滤,确保一次按键仅被识别为一次有效操作。下一篇笔记我们将学习 “环形缓冲区”,进一步优化数据处理逻辑。
一、为什么需要定时器消抖?
按键的金属触点在按下或松开时,会因机械振动产生 5~10ms 的电平波动(即 “抖动”),表现为多次触发中断。若在中断服务函数中直接使用HAL_Delay(20)
消抖,会导致:
- 中断服务函数被长时间阻塞,影响其他中断的响应(如定时器、串口);
- 降低系统实时性,甚至导致重要任务超时。
定时器消抖的优势:无需在中断中延时,通过定时器记录按键稳定后的时间,仅在抖动完全结束后才认定为有效操作,兼顾响应速度与准确性。
二、验证抖动:用 OLED 显示中断触发次数
为直观展示抖动现象,我们通过 OLED 显示按键中断的触发次数 —— 按下一次按键,若计数远超 1,说明存在抖动。
1. 实验代码
#include "driver_oled.h" // 需将OLED驱动路径添加至工程包含目录
int cnt = 0; // 记录中断触发次数
// 按键中断回调函数(PB14)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_14)
{
cnt++; // 每触发一次中断,计数+1
// 控制LED(按下亮,松开灭)
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_14) == GPIO_PIN_SET)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
else
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
}
}
// 主函数:初始化并显示计数
int main(void)
{
HAL_Init();
MX_GPIO_Init(); // 初始化GPIO(按键、LED)
MX_I2C1_Init(); // 初始化IIC(OLED通信)
OLED_Init(); // 初始化OLED
OLED_Clear(); // 清屏
while (1)
{
OLED_PrintSignedVal(0, 2, cnt); // 在OLED第2行显示计数
}
}
2. 实验现象
按下一次按键,OLED 显示的cnt
值远大于 1,证明抖动导致了多次中断触发,需通过消抖处理:
三、定时器消抖的核心:SysTick 定时器
STM32 的 SysTick 定时器(系统滴答定时器)是实现消抖的理想工具,它能提供稳定的 1ms 周期中断,用于记录时间戳判断按键是否稳定。
1. SysTick 定时器的工作原理
在启动文件startup_stm32f103xb.s
中,定义了 SysTick 中断服务函数入口:
DCD SysTick_Handler ; SysTick Handler
其 C 语言实现如下(每 1ms 触发一次):
void SysTick_Handler(void)
{
HAL_IncTick(); // 调用计数递增函数
}
HAL_IncTick
函数的作用是递增全局毫秒计数器uwTick
:
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq; // uwTick每1ms增加1(uwTickFreq=1)
}
通过HAL_GetTick
函数可获取当前uwTick
值(系统启动后的毫秒数):
__weak uint32_t HAL_GetTick(void)
{
return uwTick; // 返回当前毫秒时间戳
}
简言之:SysTick 每 1ms 触发一次中断,uwTick
持续递增,我们可利用这个 “时间戳” 判断按键是否稳定。
2. 定时器消抖的逻辑设计
结合按键抖动时间(5~10ms),消抖逻辑如下:
- 按键触发中断时,启动一个 10ms 的定时器(记录 “当前时间 + 10ms” 作为超时时间);
- 若 10ms 内无新的抖动中断(按键稳定),则认定为一次有效操作;
- 若 10ms 内有新的抖动中断,重置定时器(延长 10ms 等待时间)。
为管理 “超时时间、回调函数” 等相关数据,我们使用结构体打包信息,方便维护:
struct soft_timer{
uint32_t timeout; // 超时时间戳(uwTick达到此值时触发处理)
void *arg; // 传给回调函数的参数(可选)
void (*func)(void *); // 超时后执行的回调函数(如计数、点灯)
};
四、完整代码实现与解析
我们通过结构体、定时器检查函数、中断回调函数等,实现定时器消抖。
1. 核心结构体与变量
// main.c
struct soft_timer{
uint32_t timeout; // 超时时间戳
void *arg; // 回调函数参数
void (*func)(void *); // 超时回调函数
};
// 定义按键专用的软件定时器(初始状态:未启动)
struct soft_timer key_timer = {~0, NULL, key_timeout_func};
int cnt = 0; // 记录有效按键次数(验证消抖效果)
key_timer.timeout = ~0
:~0
在 32 位系统中为0xFFFFFFFF
(极大值),表示初始未启动;key_timeout_func
:超时后执行的函数(标记有效按键)。
2. 超时回调函数(key_timeout_func
)
void key_timeout_func(void *args)
{
cnt++; // 消抖成功,有效按键次数+1
key_timer.timeout = ~0; // 重置超时时间(关闭定时器,等待下一次按键)
}
3. 定时器设置函数(mod_timer
)
用于启动 / 重置定时器(计算超时时间戳):
void mod_timer(struct soft_timer *pTimer, uint32_t timeout)
{
// 超时时间 = 当前时间 + 延迟时间(如10ms)
pTimer->timeout = HAL_GetTick() + timeout;
}
4. 定时器检查函数(check_timer
)
每 1ms 在 SysTick 中断中调用,检查是否超时:
void check_timer(void)
{
// 若当前时间 >= 超时时间,执行回调函数
if (key_timer.timeout <= HAL_GetTick())
{
key_timer.func(key_timer.arg);
}
}
需在 SysTick 中断中注册此函数:
// stm32f1xx_it.c
void SysTick_Handler(void)
{
HAL_IncTick();
extern void check_timer(void); // 声明外部函数
check_timer(); // 每1ms检查一次
}
5. 按键中断回调函数
按键触发中断时,重置定时器(延长 10ms 等待):
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_14) // 按键中断
{
mod_timer(&key_timer, 10); // 重置定时器为“当前时间+10ms”
}
}
五、各部分函数作用详解
1. 核心结构体与变量
struct soft_timer
:打包 “超时时间、回调函数、参数”,使代码模块化;key_timer
:按键专用定时器,timeout
初始为~0
(未启动),绑定key_timeout_func
作为超时处理函数;cnt
:仅在消抖成功后递增,用于验证消抖效果。
2. 超时回调函数(key_timeout_func
)
- 功能:标记一次有效按键,
cnt++
; - 重置
timeout
为~0
:避免定时器在无按键操作时误触发。
3. 定时器设置函数(mod_timer
)
- 核心:通过
HAL_GetTick()
获取当前时间,计算 “当前时间 + 延迟时间” 作为新的timeout
; - 例:当前
HAL_GetTick()
为 1000ms,调用mod_timer(&key_timer, 10)
后,timeout
=1010ms(10ms 后超时)。
4. 定时器检查函数(check_timer
)
- 触发时机:每 1ms 在
SysTick_Handler
中调用; - 逻辑:若当前时间≥
timeout
,说明按键已稳定 10ms,调用key_timeout_func
处理。
5. 按键中断回调函数(HAL_GPIO_EXTI_Callback
)
- 作用:每次按键抖动触发中断时,调用
mod_timer
重置timeout
(延长等待); - 抖动期间:多次触发中断,
timeout
不断被更新为 “当前时间 + 10ms”,定时器始终 “未超时”; - 稳定后:10ms 内无新中断,
check_timer
检测到超时,执行key_timeout_func
。
六、整体消抖流程(核心逻辑)
- 按键抖动阶段:
按键按下产生抖动,多次触发中断,每次中断调用mod_timer(&key_timer, 10)
,timeout
被不断更新为 “当前时间 + 10ms”,定时器未超时。 - 按键稳定阶段:
抖动结束,不再触发中断。当HAL_GetTick()
≥最后一次timeout
(稳定 10ms 后),check_timer
调用key_timeout_func
,cnt++
(记录一次有效按键)。 - 重置阶段:
key_timeout_func
将timeout
重置为~0
,定时器回到 “未启动” 状态,等待下一次按键。
七、为什么timeout
初始值设为~0
?
~0
是 32 位系统中的最大值(0xFFFFFFFF
),远大于uwTick
的递增速度,确保初始状态下check_timer
不会误判超时;- 只有按键中断调用
mod_timer
后,timeout
才被设为较小的 “当前时间 + 10ms”,才可能在 10ms 后触发超时。
总结
定时器消抖的核心是利用 SysTick 定时器的 1ms 中断,通过动态设置和重置超时时间,判断按键是否稳定。相比延时消抖,它避免了中断阻塞,提高了系统实时性。结构体的使用使代码更模块化,便于扩展(如支持多个按键消抖)。
结尾
本文详细讲解了定时器消抖的原理与实现,通过 SysTick 定时器和状态管理,高效解决了中断场景下的按键抖动问题。这种 “软件定时器” 思想可扩展到其他场景(如传感器超时检测、任务调度等)。
下一篇笔记,我们将学习 “环形缓冲区”—— 一种高效的数据存储结构,常用于处理串口、传感器等连续输入的数据流。Hello_Embed 继续带你探索 STM32 的实用编程技巧,敬请期待~