多按键实例讲解状态机

状态机是编程中很常用的一种思想,对于解决很多问题都有着很不错的·效果,因此学习状态机是很有必要的,下面我先简单介绍一下状态机,然后用一个多按键的例子来讲解状态机。

想象一下一个自动门:

  1. 平时它是关着的
  2. 有人走近(事件),它就打开
  3. 开门状态下,如果一段时间没人通过(事件),门就自动关上
  4. 在关门过程中,如果有人突然走近(事件),门就重新打开

这就是一个典型的有限状态机

状态机的核心思想是:​

  1. 状态:​​ 系统在某个时刻所处的特定模式或情形​(如“门关着”、“门开着”)。
  2. 事件:​​ 发生的事情或输入​(如“有人走近”、“定时器超时”),它会触发状态机做出反应。
  3. 转移:​​ 当某个事件发生时,系统从当前状态切换到另一个状态
  4. 动作:​​ 在执行状态转移时(或在进入/离开某个状态时),系统可能执行相应的操作​(如“启动马达开门”、“停止马达”)。

状态机(尤其是有限状态机 - FSM)的特点和优势:​

  • 有限性:​​ 只有预先定义好的、有限个状态
  • 事件驱动:​​ 行为由接收到的事件决定下一步怎么做。
  • 明确的行为:​​ 对于任何一个状态和接收到的事件,下一个状态或要执行的动作是明确且唯一定义的。这使得逻辑非常清晰。
  • 模型化复杂行为:​​ 它能有效且清晰地建模和实现那些行为依赖于其当前状态、并且对事件反应复杂的系统。
  • 可视化:​​ 通常用状态图​(带圆圈的状态和有箭头的事件/转移)来表示,非常直观。

两种主要类型(都属FSM):​

  1. Moore 状态机:​​ ​动作/输出只与当前状态有关。当进入某个状态,它就执行固定的动作。
  2. Mealy 状态机:​​ ​动作/输出取决于当前状态触发转移的输入事件

为什么有用?​

  • 简化逻辑设计:​​ 把复杂的、依赖于多种条件的逻辑分解成定义良好的状态和转移,思路更清晰。
  • 提高可维护性:​​ 状态图让系统行为一目了然,更容易理解和修改。
  • 避免错误:​​ 明确的状态转移规则有助于确保所有情况都被处理到。
  • 广泛应用:​
    • 软件工程:​​ UI流程控制(如登录状态)、游戏角色AI、订单处理系统、协议解析(如TCP状态机)、编译器词法分析。
    • 硬件设计:​​ 数字电路设计(如序列检测器、控制器)。
    • 业务建模:​​ 工作流引擎。
    • 嵌入式系统:​​ 设备控制器。

总结来说:​

状态机是管理和设计任何“其行为随其当前情形(状态)和发生事件而变化”的系统的强大工具。它像一个明确的流程图,定义了系统所有可能的状态、状态之间如何转换(在什么事件下)、以及在转换时或处于状态时要做什么。

好了简单的介绍完了,接下来是具体的多按键状态机示例。

假如说现在要你用一个按键来同时可以实现单击,双击,长按,长按保持这四个功能,你要怎么设计?我来讲讲我的思路,我会先把每个状态都列出来,按键一共有七种状态,首先是最基础的单击,双击,长按,长按保持四种要实现的状态,其次就是未按下,第一次按下,以及第一次按下后的等侯态。仔细想想以后我们可以画出这样一副图出来,最后所有状态都会回归到未按下的状态。

​编辑

现在来仔细讲讲这张图。对于这个多功能按键来说,它要进入各个状态的前提条件是什么?当然就是一个按下的动作,因此我们可以明确未按下的下一个状态是第一次按下。第一次按下以后,我们需要进行等待,因为这个时候可以有两种状态,一个是单击,一个是长按,我们需要通过按下维持的时间来判断下一个状态到底是什么。显而易见的,长按的结算状态就应该是长按与长按保持两种,单击就不一样,它除了结算成单击和进入长按状态以外还可以进入双击状态。通过松开时间和再次按下的时间差来判断是否进入双击,进入双击以后就也是跟之前一样,利用按下时间来判断是否进入长按。这样就可以简单地先连出一个状态机的图出来。

接下来是代码部分,我们先建立几个时间阈值还有键值与按键状态的枚举,枚举是状态机的好伙伴,可以帮助把有限的状态更好地囊括起来。建立完两个枚举就要开始建立按键的结构体,首先是名字与状态,为了获取按键按下维持的时间,我们需要它按时间,松开时间,有长按保持就添加一个长按保持计数,然后就是链表的下一个。接着就是注册两个回调函数,用来获取时间以及键值。

复制代码
 1 #define SHORT_PRESS_TIME 50   // 单击时间阈值,单位为毫秒
 2 #define MULTI_CLICK_TIME 300  // 多击时间间隔,单位为毫秒
 3 #define LONG_PRESS_TIME 800   // 长按时间阈值,单位为毫秒
 4 #define LONG_HOLD_TIME_MAX 2000  // 长按保持时间阈值,单位为毫秒
 5 #define LONG_PRESS_RELEASE_TIME 500  // 长按与长按保持的时间间隔
 6 #define LONG_HOLD_INTERVAL 200
 7 #define MAX_CLICK_COUNT 2  // 最大点击次数
 8 
 9 // 按键值定义
10 enum key_value {
11     KEY_ON = 1,  // 按键按下
12     KEY_OFF = 0,  // 按键松开
13 };
14 
15 // 按键状态定义
16 enum key_status {
17     OFF = -1,      // 无效状态
18     WAITING,       // 等待按键按下
19     CLICK,         // 单击
20     MULTI_CLICK,   // 多击(双击、三击等)
21     LONG_PRESS,    // 长按
22     LONG_HOLD,     // 长按保持
23 };
24 
25 // 按键事件结构体
26 typedef struct key_event_t {
27     int id;                    // 按键ID
28     enum key_status status;    // 按键状态
29     uint32_t press_time;       // 按下时间
30     uint32_t release_time;     // 松开时间
31     uint16_t click_count;      // 点击次数
32     uint16_t hold_count;       // 长按保持次数
33     struct key_event_t *next;  // 下一个按键事件
34 } key_event_t;
35 
36 // 回调函数类型定义
37 typedef uint32_t (*time_get_callback)(void);
38 typedef enum key_value (*key_value_get_callback)(int id);
复制代码

这里我也简单介绍一下链表以及链表的操作吧。链表顾名思义就是像链子一样的表,一个接着一个,像这样,每一节的末尾都是一个指向下一节的指针,一个连一个直到NULL结束。​编辑

添加操作也很好理解,就是如果链表头是NULL,就添加到链表头,然后把它的下一个变为NULL;如果链表头不是NULL,就一个个摸到链表尾的NULL,把它替换成新添加的,然后新添加的下一个就改为NULL。删除操作就是把想要删除的跳过,把它的前一个的next换成它的next也就是直接跳过它指向了下一个,用下面的来例子来讲,就是把1-next的值修改成2-next的值,让它可以直接指向3,这样就实现了跳过2,也就是删除的效果了。

复制代码
static key_event_t *head = NULL;
static time_get_callback global_time_get = NULL;
static key_value_get_callback global_key_value_get = NULL;


int key_event_init(time_get_callback time_get, key_value_get_callback key_value_get) {
    if (time_get == NULL || key_value_get == NULL) {
        return -1;
    }
    global_time_get = time_get;
    global_key_value_get = key_value_get;
    return 0;
}

static void deleteMyKey(uint32_t id){
    struct key_event_t *p = head;
    struct key_event_t *pre = NULL;
    while(p != NULL){
        if(p->id == id){
            if(pre == NULL){
                head = p->next;
            }else{
                pre->next = p->next;
            }
            free(p);
            return;
        }
        pre = p;
        p = p->next;
    }
}

static void addMyKey(uint32_t id){
    struct key_event_t *p = head;
    struct key_event_t *new = (struct key_event_t *)malloc(sizeof(struct key_event_t));
    if(new == NULL){
        return;
    }
    new->id = id;
    new->press_time = 0;
    new->release_time = 0;
    new->hold_count = 0;  // 初始按键保持次数为0
    new->next = NULL;
    if(p == NULL){
        head = new;
    }else{
        while(p->next != NULL){
            p = p->next;
        }
        p->next = new;
    }
}

key_event_t* findMyKey(int id)
{
    key_event_t *current = head;
    while(current != NULL && current->id != id)
    {
        current = current->next;  // 移动到下一个节点
    }
    if(current == NULL) {
        return NULL;  // 如果没有找到,返回NULL
    }
    return current;  // 返回找到的节点指针
}

uint16_t Key_GetClickCount(int id)
{
    key_event_t*p=findMyKey(id);
    if(p == NULL) {
        return 0; // 如果没有找到按键事件,返回0
    }
    else{
        return p->click_count; // 返回点击次数
    }
}
复制代码

 

接下来就是具体的状态变化:

复制代码
  1 /* 按键状态转换函数
  2  * 功能:根据按键ID和当前状态,计算并返回新的按键状态
  3  * 参数:id - 按键标识符
  4  * 返回值:enum key_status - 当前按键的最新状态
  5  * 状态机说明:
  6  *   OFF         : 初始状态/释放状态
  7  *   WAITING     : 按键按下但未达长按阈值
  8  *   CLICK       : 单击完成(按下后释放)
  9  *   MULTI_CLICK : 多击进行中(双击/三击等)
 10  *   LONG_PRESS  : 长按触发(超过长按时间阈值)
 11  *   LONG_HOLD   : 长按保持(持续按住时周期性触发)
 12  * 全局依赖:
 13  *   global_time_get      : 获取系统时间戳(毫秒级)
 14  *   global_key_value_get : 获取按键物理状态(KEY_ON/KEY_OFF)
 15  * 时间常量说明:
 16  *   LONG_PRESS_TIME     : 长按触发阈值(如500ms)
 17  *   MULTI_CLICK_TIME    : 多击时间窗口(如300ms)
 18  *   LONG_HOLD_INTERVAL  : 长按保持触发间隔(如1000ms)
 19  */
 20 enum key_status Key_StatusChange(int id) {
 21     // 全局函数指针校验(防止未初始化导致崩溃)
 22     if (global_time_get == NULL || global_key_value_get == NULL) {
 23         return OFF;
 24     }
 25     
 26     // 按键事件对象管理
 27     key_event_t *p = findMyKey(id);       // 查找按键状态记录
 28     if (p == NULL) {                       // 首次检测到此按键
 29         addMyKey(id);                      // 创建新记录
 30         p = findMyKey(id);                 // 重新获取指针
 31         if (p == NULL) {                   // 创建失败处理
 32             return OFF;
 33         }
 34     }
 35     
 36     // 当前状态快照(避免多次调用全局函数)
 37     uint32_t current_time = global_time_get();
 38     enum key_value current_value = global_key_value_get(id);
 39     enum key_status status = p->status;    // 上次保存的状态
 40     uint32_t elapsed_time;                 // 时间差临时计算
 41     uint32_t press_elapsed = current_time - p->press_time; // 当前按下持续时间
 42     
 43     // 状态跃迁检测:从OFF到WAITING(立即响应新按下事件)
 44     if (current_value == KEY_ON && status == OFF) {
 45         p->press_time = current_time;      // 记录按下时刻
 46         p->click_count = 0;                // 重置点击计数
 47         p->release_time = 0;               // 清除释放时间
 48         p->status = WAITING;               // 进入等待状态
 49         return WAITING;                    // 立即返回新状态
 50     }
 51     
 52     // 状态机核心处理(基于当前状态分支)
 53     switch (status) {
 54         case OFF:
 55             // OFF状态下检测到按下:初始化参数并进入等待状态
 56             if (current_value == KEY_ON) {
 57                 p->press_time = current_time;
 58                 p->click_count = 0;
 59                 p->release_time = 0;
 60                 p->status = WAITING;
 61                 return WAITING;
 62             }
 63             break;
 64             
 65         case WAITING:
 66             /* 等待状态处理逻辑:
 67              * 1. 若释放:记录释放时间,标记为单击(click_count=1)
 68              * 2. 若持续按下且超长按阈值:进入长按状态
 69              * 注意:未满足条件时保持WAITING状态
 70              */
 71             if (current_value == KEY_OFF) {
 72                 p->release_time = current_time;
 73                 p->click_count = 1;        // 首次单击计数
 74                 p->status = CLICK;
 75                 return CLICK;
 76             } 
 77             else if (press_elapsed >= LONG_PRESS_TIME) {
 78                 p->status = LONG_PRESS;     // 触发长按
 79                 p->hold_count = 0;          // 初始化长按保持计数
 80                 return LONG_PRESS;
 81             }
 82             return WAITING;                 // 未达条件,保持等待
 83             break;
 84             
 85         case CLICK:
 86             /* 单击完成后的处理:
 87              * 1. 若再次按下:检测是否在多击时间窗内
 88              *   是 → 进入多击状态(增加点击计数)
 89              *   否 → 返回单击状态并重置为OFF
 90              * 2. 若超多击时间窗:自动重置为OFF状态
 91              */
 92             if (current_value == KEY_ON) {
 93                 elapsed_time = current_time - p->release_time;
 94                 if (elapsed_time <= MULTI_CLICK_TIME) {
 95                     p->status = MULTI_CLICK;
 96                     p->click_count++;       // 增加点击计数(双击/三击)
 97                     p->press_time = current_time;
 98                     p->release_time = 0;    // 重置释放时间(因再次按下)
 99                     p->hold_count = 0;
100                     return MULTI_CLICK;
101                 } 
102                 else {                      // 超过多击时间窗
103                     p->status = OFF;        // 结束单击周期
104                     return CLICK;           // 仍返回本次单击
105                 }
106             } 
107             else if (current_time - p->release_time > MULTI_CLICK_TIME) {
108                 p->status = OFF;            // 超时自动重置
109                 return CLICK;
110             }
111             return CLICK;                   // 保持单击状态
112             break;
113             
114         case MULTI_CLICK:
115             /* 多击状态处理:
116              * 释放时:
117              *   - 若按下时间超窗:退出多击状态(判定为无效多击)
118              * 按下时:
119              *   - 若超长按阈值:转长按状态
120              *   - 若在时间窗内:增加点击计数(等待下一次释放)
121              */
122             if (current_value == KEY_OFF) {
123                 p->release_time = current_time;
124                 // 释放时检测:若本次按下时间过长,判定多击失败
125                 if (current_time - p->press_time > MULTI_CLICK_TIME) {
126                     p->status = OFF;        // 退出多击状态
127                     p->click_count = 0;     // 重置计数
128                     p->hold_count = 0;
129                     return OFF;
130                 }
131                 return MULTI_CLICK;
132             } 
133             else {
134                 // 按下持续期间检测长按
135                 if (press_elapsed >= LONG_PRESS_TIME) {
136                     p->status = LONG_PRESS; // 转长按状态(覆盖多击)
137                     p->hold_count = 0;
138                     return LONG_PRESS;
139                 }
140                 // 快速连按检测:在上次释放后规定时间内再次按下
141                 if (p->release_time > 0) {  // 存在前次释放记录
142                     elapsed_time = current_time - p->release_time;
143                     if (elapsed_time <= MULTI_CLICK_TIME && p->click_count < MAX_CLICK_COUNT) {
144                         p->click_count++;   // 增加有效点击计数
145                         p->press_time = current_time;
146                         p->release_time = 0; // 清除释放标记(因再次按下)
147                     }
148                 }
149                 return MULTI_CLICK;
150             }
151             break;
152             
153         case LONG_PRESS:
154             /* 长按状态处理:
155              * 释放 → 立即返回OFF
156              * 持续按下 → 检测是否达到保持触发间隔
157              */
158             if (current_value == KEY_OFF) {
159                 p->status = OFF;            // 释放即重置
160                 return OFF;
161             } 
162             else {
163                 elapsed_time = press_elapsed - LONG_PRESS_TIME;
164                 // 达到长按保持间隔:升级为LONG_HOLD状态
165                 if (elapsed_time >= LONG_HOLD_INTERVAL) {
166                     p->status = LONG_HOLD;
167                     p->hold_count = 1;      // 初始化保持计数
168                     return LONG_HOLD;
169                 }
170             }
171             return LONG_PRESS;              // 保持长按状态
172             break;
173             
174         case LONG_HOLD:
175             /* 长按保持状态:
176              * 释放 → 重置为OFF
177              * 持续按下 → 按保持间隔周期性触发
178              */
179             if (current_value == KEY_OFF) {
180                 p->status = OFF;
181             } 
182             else {
183                 elapsed_time = press_elapsed - LONG_PRESS_TIME;
184                 // 周期性触发:每达到整数倍间隔时更新计数
185                 if (elapsed_time >= (p->hold_count + 1) * LONG_HOLD_INTERVAL) {
186                     p->hold_count++;        // 增加保持计数
187                     return LONG_HOLD;        // 触发新事件
188                 }
189             }
190             return LONG_HOLD;               // 无事件时保持状态
191             break;
192     }
193     
194     return OFF; // 默认返回OFF状态(异常处理)
195 }
复制代码

 

 

总结

状态机,作为一种将复杂行为分解为清晰状态与可控转换的编程范式,为我们设计响应式系统提供了强大的理论支撑和实践工具。其核心构成:​状态、事件、转移和动作

而多功能按键检测的使用状态机将看似简单的“按键按下”过程分解为多个互斥状态(OFF、WAITING、CLICK、MULTI_CLICK、LONG_PRESS、LONG_HOLD),并通过严格定义的时间阈值​(SHORT_PRESS_TIME, MULTI_CLICK_TIME等)和按键释放/按下事件来驱动状态间的有序流转。

代码实现的关键在于:

  1. 精确定义状态枚举​:清晰描述所有可能的系统“模式”。
  2. 维护按键上下文结构体​:记录状态、时间戳、计数等关键信息,它们是状态判断的基础。
  3. 事件驱动的状态转移函数​:在核心的Key_StatusChange函数中,通过switch-case结构,根据当前状态新事件(按键值变化 + 时间推移)​,精确地判断下一状态并执行相应动作或返回结果。
  4. 时间管理​:通过global_time_get回调获取可靠的时间戳,计算按键时长和间隔是区分单击、双击、长按及长按保持的核心依据。

状态机的魅力就在于它能将散乱的条件判断(if...else)逻辑,转换成一张清晰可见、易于维护的“状态图”。显著提升代码的可读性、可维护性和健壮性,是一种解决“行为随状态而变”这类问题的通用编程利器。理解并实践它,将拥有设计和实现更优雅、更健壮软件系统的能力。

原创作者: cpd75 转载于: https://www.cnblogs.com/cpd75/p/18921361
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值