STM32按键去抖动软件实现详解

在 STM32 单片机开发中,按键去抖动是保证系统稳定性的关键基础技能。机械按键在按下或释放的瞬间,触点会因为弹性产生不稳定的抖动(通常持续 5ms~20ms)。如果 CPU 在抖动期间读取状态,会误判为多次按下。软件去抖动主要通过延时检测、状态机扫描等逻辑来规避这一问题。

以下是几种主流的软件去抖动实现方法,包含详细步骤、代码示例及原理解析。


一、 方法一:延时检测法(最基础,适合初学者)

这种方法利用"延时"来跳过抖动时间窗口。虽然简单,但会阻塞 CPU,仅适合逻辑简单的单任务程序。

实现步骤

  1. 检测触发:读取 GPIO 电平,判断是否达到触发条件(如低电平)。
  2. 延时消抖 :调用延时函数(如 HAL_Delay 或自写延时),等待 10ms~20ms,让机械抖动自然平息。
  3. 状态确认 :再次读取 GPIO 电平。如果电平依然符合触发条件,则判定为有效按下
  4. 动作执行:执行对应的按键功能代码。
  5. 松手等待 :通过 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 触发一次),在回调函数中扫描按键状态。这种方法通过计数器来判断状态稳定性。

实现步骤

  1. 初始化定时器:配置一个基础定时器(如 TIM6),设定中断周期为 10ms。
  2. 状态变量定义:定义变量记录按键当前状态、确认状态和计数器。
  3. 周期性扫描:在定时器中断服务函数中读取按键电平。
  4. 计数滤波
    • 如果读取到按下状态,计数器加 1。
    • 如果读取到未按下状态,计数器清 0。
    • 当计数器累加到设定值(如 2 次,即 20ms),判定为有效按下。
  5. 事件处理:在主循环中检查标志位并执行逻辑。

代码示例

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 初学者,建议先掌握延时检测法 理解原理,进阶后务必学习定时器扫描法,这是工程开发的标准做法。在编写代码时,务必注意引脚的上拉/下拉配置,确保按键未按下时有确定的电平 。

相关推荐
ghie90901 小时前
基于STM32的CAN通信完整例程(HAL库实现)
stm32·单片机·嵌入式硬件
lzj_pxxw1 小时前
W25Q64存储芯片 软件设计刚需常识
stm32·单片机·嵌入式硬件·mcu·学习
时空自由民.4 小时前
蓝牙协议栈介绍
linux·网络·单片机
蓝天居士5 小时前
M24C64芯片资料与程序代码(2)
嵌入式硬件·芯片资料
吃米饭7 小时前
HC32L021C8UB 移植 FreeRTOS
stm32·嵌入式·freertos·rtos
asjodnobfy7 小时前
开关电源尖峰电压计算
嵌入式硬件·硬件工程
振南的单片机世界7 小时前
开漏输出:只能拉低,不能拉高,高电平靠“外部”帮忙
stm32·单片机·嵌入式硬件
FFF团团员9098 小时前
CCS快速使用4(tim,pwm)
单片机·嵌入式硬件
某先森不吃鱼9 小时前
工程日志——离轴编码器矫正与磁场串扰解决
嵌入式硬件