状态机学习
我们这里指的状态机是有限状态机
状态机(State Machine) 是一种描述事物状态及状态间转换规则的数学模型。它抽象表示一个系统在有限个状态之间如何根据输入事件进行转移。
我们用按键消抖作为例子:

这就是一个状态机的转换。那代码怎么写呢?
状态机中间在使用的时候,也会涉及到很多变量,比如状态机当前的状态,或者处理状态机的时候所使用的一些变量。我们会把这些变量都整理到一个结构体中。
我们一般使用的是表驱动法,可以把每个状态的转换和每个状态的输入都列在一个表里面。这样会更加清晰。表驱动法是一种使用表格结构来定义状态的转换规则和相应动作的状态机实现方法,便于代码的维护和拓展。

第一列是当前状态,第二列是当前状态接收到的事件,第三列是当前状态接收到事件以后会切换到的下一个状态是什么。
以第一行为例:
当前状态是按键空闲状态,当接收到按下事件的时候,会切换到按下消抖状态,不需要切换回调函数。
再看第二行:
切换到按下消抖状态以后,收到消抖时间达到这一事件以后,会切换到按键按下状态,然后执行按键按下回调函数。.
看代码部分:可以大体分为三个部分,第一个部分是获取原始输入,第二部分:把原始输入翻译成事件,第三部分:执行状态转换。
// 按键状态机处理函数
void key_sm_proc(key_sm_t *sm)
{
// ========== 第一部分:获取原始输入 ==========
// 调用回调函数获取当前按键状态(0=按下,1=释放)
uint8_t key_input = sm->key_input_func();
// 调用回调函数获取当前系统时间(用于计时和超时判断)
uint32_t now = sm->time_input_func();
// 初始化事件变量为无事件
key_event_t event = EVT_NONE;
// ========== 第二部分:将原始输入翻译成事件 ==========
// 根据当前状态机状态进行不同的处理
switch(sm->state)
{
// 状态1:按键空闲状态(初始状态)
case KEY_IDLE:
// 如果检测到按键被按下(输入为0)
if(key_input == 0)
{
// 生成按键按下事件
event = EVT_PRESS;
// 记录按下发生的时间,用于后续消抖计时
sm->last_time = now;
}
break;
// 状态2:按键按下消抖等待状态
case KEY_DOWN_DELAY:
// 如果检测到按键被释放(输入变为1)
if(key_input == 1) {
// 生成按键释放事件(可能是抖动导致的误触发)
event = EVT_RELEASE;
// 如果按键保持按下,并且消抖时间已到
} else if(now - sm->last_time > KEY_DEBOUNCE_MS) {
// 生成消抖超时事件,确认按键有效按下
event = EVT_TIMEOUT;
// 更新时间戳,用于后续长按计时
sm->last_time = now;
}
break;
// 状态3:按键确认按下状态
case KEY_DOWN:
// 如果检测到按键被释放
if(key_input == 1) {
// 生成按键释放事件
event = EVT_RELEASE;
// 记录释放时间(可能用于后续处理)
sm->last_time = now;
// 如果按键保持按下,并且长按超时时间已到
} else if(now - sm->last_time > KEY_LONGPRESS_TIMEOUT_MS) {
// 生成长按事件
event = EVT_LONGPRESS;
}
break;
// 这里可能还有其他状态处理,如长按保持状态等
......... // 省略,同理
}
相关的状态会定义成:
typedef enum {
KEY_UP,
KEY_DOWN_DELAY,
KEY_DOWN,
KEY_UP_DELAY,
KEY_LONGPRESS,
KEY_LONGPRESS_UP_DELAY,
KEY_DOUBLE_DOWN_DELAY,
KEY_DOUBLE_DOWN,
KEY_DOUBLE_UP_DELAY,
KEY_IDLE,
} key_state_t;
执行状态转换:就是查找当前的状态和当前接收到的事件能够同时匹配到这个表格里面的某一项。就可以进行状态转换,如果有回调函数的话,并且执行回调函数
// 遍历状态转移表,查找匹配的状态-事件组合
for(size_t i = 0; i < KEY_TABLE_SIZE; ++i) {
// 条件判断:查找当前状态和事件匹配的条目
// key_table[i].cur_state: 表中的当前状态
// sm->state: 状态机当前的实际状态
// key_table[i].event: 表中的事件
// event: 当前检测到的事件
if(key_table[i].cur_state == sm->state && key_table[i].event == event) {
// 执行状态转移:将状态机的状态更新为表中定义的下一状态
sm->state = key_table[i].next_state;
// 如果该状态转移条目定义了动作函数,则执行该动作
// key_table[i].action: 状态转移对应的回调函数指针
if(key_table[i].action)
key_table[i].action(sm); // 调用动作函数,传入状态机指针
// 找到匹配项后,跳出循环,不再继续查找
break;
}
// 如果未找到匹配项,则继续循环查找下一个条目
}
为了实现高内聚,低耦合,我们会用面向对象的思想封装一下状态机:
void key_sm_init(key_sm_t *sm,
void (*up_func)(void), // 按键释放回调
void (*down_func)(void), // 按键按下回调
void (*click_func)(void), // 单击回调
void (*long_func)(void), // 长按回调
void (*double_click_func)(void), // 双击回调
void (*idle_func)(void), // 空闲回调
uint8_t (*input_func)(void),// 输入检测函数
uint32_t (*time_func)(void) // 时间获取函数
)
使用的时候,创建两个不同的按键实例,互不干扰:
// 按键0:精简配置
key_sm_t key0_sm;
key_sm_init(&key0_sm,
NULL, // 不处理释放事件
NULL, // 不处理按下事件
key0_click_callback, // 只处理单击
key0_long_callback, // 处理长按
key0_double_click_callback, // 处理双击
NULL, // 不处理空闲
key_input_func, // 使用通用输入检测
time_input_func // 使用通用时间获取
);
// 按键1:完整配置
key_sm_t key1_sm;
key_sm_init(&key1_sm,
key1_up_callback, // 处理释放
key1_down_callback, // 处理按下
key1_click_callback, // 处理单击
key1_long_callback, // 处理长按
key1_double_click_callback, // 处理双击
key1_idle_callback, // 处理空闲
key1_input_func, // 使用专用输入检测
time_input_func // 使用通用时间获取
);
浙公网安备 33010602011771号