状态机是编程中很常用的一种思想,对于解决很多问题都有着很不错的·效果,因此学习状态机是很有必要的,下面我先简单介绍一下状态机,然后用一个多按键的例子来讲解状态机。
想象一下一个自动门:
- 平时它是关着的。
- 有人走近(事件),它就打开。
- 开门状态下,如果一段时间没人通过(事件),门就自动关上。
- 在关门过程中,如果有人突然走近(事件),门就重新打开。
这就是一个典型的有限状态机。
状态机的核心思想是:
- 状态: 系统在某个时刻所处的特定模式或情形(如“门关着”、“门开着”)。
- 事件: 发生的事情或输入(如“有人走近”、“定时器超时”),它会触发状态机做出反应。
- 转移: 当某个事件发生时,系统从当前状态切换到另一个状态。
- 动作: 在执行状态转移时(或在进入/离开某个状态时),系统可能执行相应的操作(如“启动马达开门”、“停止马达”)。
状态机(尤其是有限状态机 - FSM)的特点和优势:
- 有限性: 只有预先定义好的、有限个状态。
- 事件驱动: 行为由接收到的事件决定下一步怎么做。
- 明确的行为: 对于任何一个状态和接收到的事件,下一个状态或要执行的动作是明确且唯一定义的。这使得逻辑非常清晰。
- 模型化复杂行为: 它能有效且清晰地建模和实现那些行为依赖于其当前状态、并且对事件反应复杂的系统。
- 可视化: 通常用状态图(带圆圈的状态和有箭头的事件/转移)来表示,非常直观。
两种主要类型(都属FSM):
- Moore 状态机: 动作/输出只与当前状态有关。当进入某个状态,它就执行固定的动作。
- 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等)和按键释放/按下事件来驱动状态间的有序流转。
代码实现的关键在于:
- 精确定义状态枚举:清晰描述所有可能的系统“模式”。
- 维护按键上下文结构体:记录状态、时间戳、计数等关键信息,它们是状态判断的基础。
- 事件驱动的状态转移函数:在核心的Key_StatusChange函数中,通过switch-case结构,根据当前状态和新事件(按键值变化 + 时间推移),精确地判断下一状态并执行相应动作或返回结果。
- 时间管理:通过global_time_get回调获取可靠的时间戳,计算按键时长和间隔是区分单击、双击、长按及长按保持的核心依据。
状态机的魅力就在于它能将散乱的条件判断(if...else)逻辑,转换成一张清晰可见、易于维护的“状态图”。显著提升代码的可读性、可维护性和健壮性,是一种解决“行为随状态而变”这类问题的通用编程利器。理解并实践它,将拥有设计和实现更优雅、更健壮软件系统的能力。
原创作者: cpd75 转载于: https://www.cnblogs.com/cpd75/p/18921361