STM32HAL 快速入门(十三):定时器消抖 —— 中断场景下的按键抖动处理

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
六、整体消抖流程(核心逻辑)
  1. 按键抖动阶段
    按键按下产生抖动,多次触发中断,每次中断调用mod_timer(&key_timer, 10)timeout被不断更新为 “当前时间 + 10ms”,定时器未超时。
  2. 按键稳定阶段
    抖动结束,不再触发中断。当HAL_GetTick()≥最后一次timeout(稳定 10ms 后),check_timer调用key_timeout_funccnt++(记录一次有效按键)。
  3. 重置阶段
    key_timeout_functimeout重置为~0,定时器回到 “未启动” 状态,等待下一次按键。
七、为什么timeout初始值设为~0
  • ~0是 32 位系统中的最大值(0xFFFFFFFF),远大于uwTick的递增速度,确保初始状态下check_timer不会误判超时;
  • 只有按键中断调用mod_timer后,timeout才被设为较小的 “当前时间 + 10ms”,才可能在 10ms 后触发超时。
总结

定时器消抖的核心是利用 SysTick 定时器的 1ms 中断,通过动态设置和重置超时时间,判断按键是否稳定。相比延时消抖,它避免了中断阻塞,提高了系统实时性。结构体的使用使代码更模块化,便于扩展(如支持多个按键消抖)。

结尾

本文详细讲解了定时器消抖的原理与实现,通过 SysTick 定时器和状态管理,高效解决了中断场景下的按键抖动问题。这种 “软件定时器” 思想可扩展到其他场景(如传感器超时检测、任务调度等)。
下一篇笔记,我们将学习 “环形缓冲区”—— 一种高效的数据存储结构,常用于处理串口、传感器等连续输入的数据流。Hello_Embed 继续带你探索 STM32 的实用编程技巧,敬请期待~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值