找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1113

积分

0

好友

163

主题
发表于 4 天前 | 查看: 15| 回复: 0

在嵌入式开发中,高效、可靠地处理按键输入是一个常见需求。许多传统的按键扫描程序(如一些开发板提供的例程)通常只支持单击事件,实现双击、长按等复杂逻辑时代码会变得臃肿。更重要的是,它们大多采用在main函数中循环扫描或使用delay函数的方式,这很容易阻塞主程序运行,导致系统响应迟钝。本文将介绍一种基于定时器中断驱动的状态机方法,实现按键单击、双击、长按、连按的识别,整个识别过程无延时、不阻塞,极大地提升了系统的实时性。

核心功能与设计思路

本程序的核心是一个可配置的按键状态机,具备以下特性:

  • 功能全面:支持单击、双击、长按、连按事件的识别。
  • 高度可配置:消抖时间、长按判定时间、双击间隔、连击频率等参数均可通过宏定义灵活调整。
  • 模式可选:通过配置结构体,可以自由选择使能或禁用长按、连按、双击功能。
  • 非阻塞:所有状态转移和计时均在定时器中断服务例程中完成,主循环仅需查询事件标志并执行相应操作。
  • 易于移植:只需适配KEY_ReadPin函数读取GPIO,并初始化按键配置数组即可。

关键设计逻辑

  1. 事件触发时机:状态机在定时器中断中判定并设置事件标志,主函数检测到标志后执行相应操作并清除标志,实现事件处理与状态判定的解耦。
  2. 单击:作为基础事件,在按键按下并经过消抖后,于释放时刻触发。
  3. 长按:若使能长按功能,当按键按下持续时间超过设定阈值并在释放时,触发长按事件;若未使能,则释放时仍触发单击事件。
  4. 连按:若使能连按功能,在按键长按不释放的情况下,达到长按时间后会触发第一次连按事件,之后以固定间隔持续触发,直到按键释放。释放时,根据是否使能长按来决定最终触发长按或单击事件。这本质上是利用状态机管理复杂时序的典型应用。
  5. 双击:若使能双击功能,在第一次单击释放后开始计时,若在设定时间窗口内再次按下并释放,则触发双击事件(第二次释放时)。若第二次按下行为变为长按,则逻辑会退化为:先触发第一次的单击事件,再根据长按使能情况触发相应事件。

代码实现详解

1. 头文件定义 (my_key.h)

头文件定义了状态机所需的所有枚举类型、宏和结构体。

#ifndef ___MY_KEY_H__
#define ___MY_KEY_H__
#include "main.h"

#define ARR_LEN(arr) ((sizeof(arr)) / (sizeof(arr[0]))) // 数组大小宏函数

// 时间参数配置(单位:ms,取决于定时器中断周期)
#define KEY_DEBOUNCE_TIME      10      // 消抖时间
#define KEY_LONG_PRESS_TIME    500     // 长按判定时间
#define KEY_QUICK_CLICK_TIME   100     // 连按时间间隔
#define KEY_DOUBLE_CLICK_TIME  200     // 双击判定时间
#define KEY_PRESSED_LEVEL      0       // 按键被按下时的电平

// 按键动作
typedef enum {
    KEY_Action_Press,   // 按住
    KEY_Action_Release, // 松开
} KEY_Action_TypeDef;

// 按键状态(状态机核心)
typedef enum {
    KEY_Status_Idle,             // 空闲
    KEY_Status_Debounce,         // 消抖
    KEY_Status_ConfirmPress,     // 确认按下
    KEY_Status_ConfirmPressLong, // 确认长按
    KEY_Status_WaitSecondPress,  // 等待再次按下(用于双击)
    KEY_Status_SecondDebounce,   // 再次消抖
    KEY_Status_SecondPress,      // 再次按下
} KEY_Status_TypeDef;

// 按键事件
typedef enum {
    KEY_Event_Null,        // 空事件
    KEY_Event_SingleClick, // 单击
    KEY_Event_LongPress,   // 长按
    KEY_Event_QuickClick,  // 连击
    KEY_Event_DoubleClick, // 双击
} KEY_Event_TypeDef;

// 按键模式使能选择(位掩码方式)
typedef enum {
    KEY_Mode_OnlySinge = 0x00,          // 只有单击
    KEY_Mode_Long = 0x01,               // 单击+长按
    KEY_Mode_Quick = 0x02,              // 单击+连按
    KEY_Mode_Long_Quick = 0x03,         // 单击+长按+连按
    KEY_Mode_Double = 0x04,             // 单击+双击
    KEY_Mode_Long_Double = 0x05,        // 单击+长按+双击
    KEY_Mode_Quick_Double = 0x06,       // 单击+连按+双击
    KEY_Mode_Long_Quick_Double = 0x07,  // 单击+长按+连按+双击
} KEY_Mode_TypeDef;

// 单个按键的配置与状态结构体
typedef struct {
    uint8_t KEY_Label;              // 按键标号,用于索引
    KEY_Mode_TypeDef KEY_Mode;      // 按键模式
    uint16_t KEY_Count;             // 按键按下计时器
    KEY_Action_TypeDef KEY_Action;  // 当前读取的按键动作
    KEY_Status_TypeDef KEY_Status;  // 当前按键状态
    KEY_Event_TypeDef KEY_Event;    // 触发的事件
} KEY_Configure_TypeDef;

extern KEY_Configure_TypeDef KeyConfig[];
extern KEY_Event_TypeDef key_event[];
void KEY_ReadStateMachine(KEY_Configure_TypeDef *KeyCfg);

#endif
2. 源文件实现 (my_key.c)

源文件包含状态机的核心逻辑和按键硬件读取接口。

#include "my_key.h"
#include <string.h> // 用于memset

// 硬件抽象层:读取指定按键的引脚电平(需根据实际硬件修改)
static uint8_t KEY_ReadPin(uint8_t key_label) {
    switch (key_label) {
        case 0: return (uint8_t)HAL_GPIO_ReadPin(K0_GPIO_Port, K0_Pin);
        case 1: return (uint8_t)HAL_GPIO_ReadPin(K1_GPIO_Port, K1_Pin);
        case 2: return (uint8_t)HAL_GPIO_ReadPin(K2_GPIO_Port, K2_Pin);
        case 3: return (uint8_t)HAL_GPIO_ReadPin(K3_GPIO_Port, K3_Pin);
        case 4: return (uint8_t)HAL_GPIO_ReadPin(K4_GPIO_Port, K4_Pin);
        // 可根据需要扩展更多按键
        // case X: return (uint8_t)HAL_GPIO_ReadPin(KX_GPIO_Port, KX_Pin);
    }
    return 1; // 默认返回未按下
}

// 全局按键配置数组(支持多按键)
KEY_Configure_TypeDef KeyConfig[] = {
    // {按键标号, 工作模式, 计数初值, 初始动作, 初始状态, 初始事件}
    {0, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
    {1, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
    {2, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
    {3, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
    {4, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
    // {X, KEY_Mode_Long_Quick_Double, 0, KEY_Action_Release, KEY_Status_Idle, KEY_Event_Null},
};

// 全局按键事件数组,主循环查询此数组
KEY_Event_TypeDef key_event[ARR_LEN(KeyConfig)] = {KEY_Event_Null};

// 按键状态机处理函数(需在定时器中断中周期调用)
void KEY_ReadStateMachine(KEY_Configure_TypeDef *KeyCfg) {
    static uint16_t tmpcnt[ARR_LEN(KeyConfig)] = {0}; // 临时保存计时,用于双击逻辑

    // 1. 读取当前物理按键动作
    if (KEY_ReadPin(KeyCfg->KEY_Label) == KEY_PRESSED_LEVEL)
        KeyCfg->KEY_Action = KEY_Action_Press;
    else
        KeyCfg->KEY_Action = KEY_Action_Release;

    // 2. 状态机核心逻辑
    switch (KeyCfg->KEY_Status) {
        // 状态:空闲 ----------------------------------------------
        case KEY_Status_Idle:
            if (KeyCfg->KEY_Action == KEY_Action_Press) {
                KeyCfg->KEY_Status = KEY_Status_Debounce; // 进入消抖状态
            }
            KeyCfg->KEY_Event = KEY_Event_Null;
            break;

        // 状态:消抖 ----------------------------------------------
        case KEY_Status_Debounce:
            if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DEBOUNCE_TIME)) {
                // 消抖完成,确认按下
                KeyCfg->KEY_Count = 0;
                KeyCfg->KEY_Status = KEY_Status_ConfirmPress;
            } else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DEBOUNCE_TIME)) {
                // 消抖中,计时
                KeyCfg->KEY_Count++;
            } else {
                // 中途释放,视为抖动,回到空闲
                KeyCfg->KEY_Count = 0;
                KeyCfg->KEY_Status = KEY_Status_Idle;
            }
            KeyCfg->KEY_Event = KEY_Event_Null;
            break;

        // 状态:确认按下 ------------------------------------------
        case KEY_Status_ConfirmPress:
            if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_LONG_PRESS_TIME)) {
                // 达到长按时间
                KeyCfg->KEY_Count = KEY_QUICK_CLICK_TIME; // 准备第一次连按触发
                KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong;
            } else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_LONG_PRESS_TIME)) {
                // 未达到长按时间,持续计时
                KeyCfg->KEY_Count++;
            } else {
                // 在达到长按时间前释放
                if ((uint8_t)(KeyCfg->KEY_Mode) & 0x04) { // 如果使能了双击
                    KeyCfg->KEY_Count = 0;
                    KeyCfg->KEY_Status = KEY_Status_WaitSecondPress; // 进入等待第二次按下状态
                } else {
                    KeyCfg->KEY_Count = 0;
                    KeyCfg->KEY_Status = KEY_Status_Idle;
                    KeyCfg->KEY_Event = KEY_Event_SingleClick; // *** 触发单击事件 ***
                }
            }
            break;

        // 状态:确认长按 ------------------------------------------
        case KEY_Status_ConfirmPressLong:
            if (KeyCfg->KEY_Action == KEY_Action_Press) {
                // 持续按住
                if ((uint8_t)KeyCfg->KEY_Mode & 0x02) { // 如果使能了连按
                    if (KeyCfg->KEY_Count >= KEY_QUICK_CLICK_TIME) {
                        // 连按间隔时间到,触发连按事件
                        KeyCfg->KEY_Count = 0;
                        KeyCfg->KEY_Event = KEY_Event_QuickClick; // *** 触发连按事件 ***
                    } else {
                        KeyCfg->KEY_Count++;
                    }
                }
            } else {
                // 长按后释放
                if ((uint8_t)KeyCfg->KEY_Mode & 0x01) { // 如果使能了长按
                    KeyCfg->KEY_Count = 0;
                    KeyCfg->KEY_Status = KEY_Status_Idle;
                    KeyCfg->KEY_Event = KEY_Event_LongPress; // *** 触发长按事件 ***
                } else {
                    KeyCfg->KEY_Count = 0;
                    KeyCfg->KEY_Status = KEY_Status_Idle;
                    KeyCfg->KEY_Event = KEY_Event_SingleClick; // *** 未使能长按时,触发单击事件 ***
                }
            }
            break;

        // 状态:等待是否再次按下(用于双击判断)-------------------
        case KEY_Status_WaitSecondPress:
            if ((KeyCfg->KEY_Action != KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DOUBLE_CLICK_TIME)) {
                // 等待超时,无第二次按下,触发第一次的单击事件
                KeyCfg->KEY_Count = 0;
                KeyCfg->KEY_Status = KEY_Status_Idle;
                KeyCfg->KEY_Event = KEY_Event_SingleClick; // *** 触发单击事件 ***
            } else if ((KeyCfg->KEY_Action != KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DOUBLE_CLICK_TIME)) {
                // 等待中,计时
                KeyCfg->KEY_Count++;
            } else {
                // 在等待时间内再次按下
                tmpcnt[KeyCfg->KEY_Label] = KeyCfg->KEY_Count; // 保存已等待的时间
                KeyCfg->KEY_Count = 0;
                KeyCfg->KEY_Status = KEY_Status_SecondDebounce; // 进入第二次按下的消抖
            }
            KeyCfg->KEY_Event = KEY_Event_Null;
            break;

        // 状态:再次消抖 ------------------------------------------
        case KEY_Status_SecondDebounce:
            // 逻辑与第一次消抖类似
            if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_DEBOUNCE_TIME)) {
                KeyCfg->KEY_Count = 0;
                KeyCfg->KEY_Status = KEY_Status_SecondPress;
            } else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_DEBOUNCE_TIME)) {
                KeyCfg->KEY_Count++;
            } else {
                // 第二次按下消抖失败,恢复等待状态
                KeyCfg->KEY_Count = KeyCfg->KEY_Count + tmpcnt[KeyCfg->KEY_Label];
                KeyCfg->KEY_Status = KEY_Status_WaitSecondPress;
            }
            KeyCfg->KEY_Event = KEY_Event_Null;
            break;

        // 状态:再次按下 ------------------------------------------
        case KEY_Status_SecondPress:
            if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count >= KEY_LONG_PRESS_TIME)) {
                // 第二次按下变为长按,则本次行为不构成双击
                KeyCfg->KEY_Count = 0;
                KeyCfg->KEY_Status = KEY_Status_ConfirmPressLong;
                KeyCfg->KEY_Event = KEY_Event_SingleClick; // *** 先触发第一次的单击事件 ***
            } else if ((KeyCfg->KEY_Action == KEY_Action_Press) && (KeyCfg->KEY_Count < KEY_LONG_PRESS_TIME)) {
                KeyCfg->KEY_Count++;
            } else {
                // 第二次按下后释放,且未变成长按,成功构成双击
                KeyCfg->KEY_Count = 0;
                KeyCfg->KEY_Status = KEY_Status_Idle;
                KeyCfg->KEY_Event = KEY_Event_DoubleClick; // *** 触发双击事件 ***
            }
            break;
    }

    // 3. 将产生的事件记录到全局数组,供主循环查询
    if (KeyCfg->KEY_Event != KEY_Event_Null) {
        key_event[KeyCfg->KEY_Label] = KeyCfg->KEY_Event;
    }
}
3. 定时器中断与主函数调用示例

假设定时器中断周期设置为1ms。

// 在定时器中断回调函数中调用状态机(1ms一次)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == htim1.Instance) { // 假设是TIM1
        // 遍历处理所有已配置的按键
        for (uint8_t i = 0; i < ARR_LEN(KeyConfig); i++) {
            KEY_ReadStateMachine(&KeyConfig[i]);
        }
    }
}

// 主函数中的事件处理逻辑
int main(void) {
    // 硬件初始化...
    while (1) {
        // 1. 执行其他后台任务...

        // 2. 检查并处理按键事件(非阻塞)
        if (key_event[0] == KEY_Event_SingleClick) {
            // 处理按键0的单击事件
            // function_for_key0_click();
        }
        if (key_event[1] == KEY_Event_LongPress) {
            // 处理按键1的长按事件
            // function_for_key1_longpress();
        }
        if (key_event[2] == KEY_Event_QuickClick) {
            // 处理按键2的连按事件,连按可视为快速重复的单击
            // function_for_key2_quickclick();
        }
        if (key_event[3] == KEY_Event_DoubleClick) {
            // 处理按键3的双击事件
            // function_for_key3_doubleclick();
        }

        // 3. 清除已处理的事件标志
        memset(key_event, KEY_Event_Null, sizeof(key_event));

        // 状态机的思想不仅适用于外设驱动,在复杂的网络协议解析或业务逻辑处理中也是核心的[软件设计模式](https://yunpan.plus/f/35-1)。
    }
}

总结与资源

通过上述基于定时器中断的状态机实现,我们成功构建了一个健壮、高效且功能丰富的按键处理模块。该方法将耗时的事件识别过程放入中断,确保了主循环的流畅性,特别适合在实时性要求较高的嵌入式应用中使用。

图




上一篇:苹果高管离职潮背后:芯片与AI核心人才外流的深层原因
下一篇:SteamTools开源工具箱实战:跨平台游戏管理、网络加速与账号切换指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 19:40 , Processed in 0.108672 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表