本帖最后由 keer_zu 于 2025-6-24 09:44 编辑
#申请原创#
@21小跑堂 @21ic小管家
前面坛友讲到状态机的实现:[学习资料] 什么是状态机?怎么设计MCU状态机?
他首先描述了什么是状态机:它是一种抽象数学模型,用于描述一个系统在其生命周期内可能处于的有限个状态,以及触发系统在这些状态之间转移的条件(事件或输入)。
具备5个核心要素:状态, 事件,转移, 初始状态,动作。此处不再一一详述。
接下来给出状态机的设计方法,比较简略,只是列举了一些常见的状态(更准确是状态归类),接下来所谓状态转换条件其实就是“事件”,这里没有讨论状态转换的时机和动作执行的时机,其实状态机设计是非常值得详细探讨的地方。因为站在不同视角,同一个系统会得到不同的状态模型,好的状态定义会更贴合系统特征。其次,状态机的设计要遵从特定规范,规范大多来自上述5个核心要素在实践中的具体实现方法。状态机设计和实现过程,上述五个核心要素非常重要,在接下来的讨论中会很明确显现出来。
然后给出了两种实现:
1. 基于 switch-case / if-else 语句,(其实switch-case和if-else本质上是一样的)
原理:
- 在代码中使用一个switch-case(或嵌套的if-else)结构。外层switch根据当前状态变量选择分支,在每个状态分支内,再使用switch-case或if-else来处理不同的事件。
优点:
- 实现简单直观,对于小型状态机非常直接。不需要额外的数据结构或框架。
缺点:
- 状态和事件逻辑交织在一起,代码可读性和可维护性随着状态和事件数量的增加急剧下降(容易变成“面条代码”)。添加新状态或事件需要修改多处代码。状态和转移缺乏集中管理。
示例:如上面引文里的实例。
2. 状态转移表
原理: 使用数据结构(通常是二维数组、字典/映射)来显式地存储状态转移信息。
- 表的行通常代表当前状态。
- 表的列通常代表输入事件。
- 单元格的内容定义了:当处于该行状态并接收到该列事件时,应转移到哪个新状态(以及可能执行什么动作)。
实现方式:
- 查表法: 维护一个静态的转移表(数组、Map<State, Map<Event, Transition>>)。驱动引擎根据当前状态和事件查找表,获取对应的转移信息(目标状态+动作),然后执行动作并更新状态。
- 表驱动法: 更广义,数据(状态、事件、转移)存储在表结构中,由一个通用的解释引擎(循环)来读取表并根据当前输入执行转移。动作可能存储为函数指针、命令对象或直接在表中描述。
优点:
- 高度集中化: 所有状态转移逻辑都定义在一个地方(表中),清晰明了,易于查看和维护。
- 数据驱动: 修改状态机行为通常只需修改表数据,无需改动引擎代码,非常适合动态配置或由外部工具生成。
- 高效: 对于大型状态机,查表操作通常是O(1)复杂度。
缺点:
今天我主要将的是另外一种状态机的实现方式:状态机模式
从一个实例开始,例子很简单,按道理没有必要使用这种方式,只是为了用做状态机模式的例子。基于esp32,要做一个蓝牙通信的小项目,当蓝牙连接建立得到适合,系统根据上位机发出的指令工作,如果没有蓝牙连接,系统将按照自己的模式工作。就这两个状态:
// 定义事件类型(仅包含蓝牙相关事件)
typedef enum {
EVENT_BT_CONNECTED,
EVENT_BT_DISCONNECTED
} EventType;
1. 一个抽象的状态定义:(五要素之一:状态)
// 状态结构体
struct State {
void (*handle_event)(StateMachine *sm, EventType event); // 处理事件
void (*on_sys_process)(StateMachine *sm); // 系统处理函数
};
这里定义了一个抽象得到状态,类似C++里的类。其中handle_event()函数处理传入的事件:event。on_sys_process()函数是处在当前状态时候系统执行的代码,可以放在main或者task内的大循环里执行。
在当前状态下,系统处理当前状态的处理函数:
// 调用当前状态的系统处理函数
void call_sys_process(StateMachine *sm) {
sm->current_state->on_sys_process(sm);
}
2. 状态机:
// 状态机结构体
typedef struct {
State *current_state;
} StateMachine;
状态机是处理状态迁移的“总导演”,生成一个单例对象。
3. 初始化状态机:(五要素之一:初始状态)
// 初始化状态机
void init_state_machine(StateMachine *sm) {
transition_to_state(sm, &bt_disconnected_state);
}
可以认为是StateMachine的构造函数,初始状态为:EVENT_BT_DISCONNECTED,bt_disconnected_state是State的一个实例:
State bt_disconnected_state = {
.handle_event = bt_disconnected_state_handler,
.on_sys_process = bt_disconnected_on_sys_process
};
另一个状态是:
State bt_connected_state = {
.handle_event = bt_connected_state_handler,
.on_sys_process = bt_connected_on_sys_process
};
状态转换函数:
// 状态转换函数
void transition_to_state(StateMachine *sm, State *new_state) {
sm->current_state = new_state;
}
可以看作StateMachine的public成员函数(或者开放给State的友元函数),负责状态转移(五要素之一:状态转移)
4. 发送事件给状态机(五要素之一:事件)
// 发送事件到状态机
void send_event(StateMachine *sm, EventType event) {
sm->current_state->handle_event(sm, event);
}
在事件发生的地方调用send_event()发送事件给状态机。
5. 完整实现
以下是完整的实现,头文件:
// 定义事件类型(仅包含蓝牙相关事件)
typedef enum {
EVENT_BT_CONNECTED,
EVENT_BT_DISCONNECTED
} EventType;
// 前向声明状态结构体
typedef struct State State;
// 状态机结构体
typedef struct {
State *current_state;
} StateMachine;
// 状态结构体
struct State {
void (*handle_event)(StateMachine *sm, EventType event); // 处理事件
void (*on_sys_process)(StateMachine *sm); // 系统处理函数
};
void init_state_machine(StateMachine *sm);
void send_event(StateMachine *sm, EventType event);
.c文件:
#include <BluetoothSerial.h>
#include "bt_sm.h"
#include "led_functions.h"
// 函数声明
void transition_to_state(StateMachine *sm, State *new_state);
void bt_connected_state_handler(StateMachine *sm, EventType event);
void bt_disconnected_state_handler(StateMachine *sm, EventType event);
void bt_connected_on_sys_process(StateMachine *sm);
void bt_disconnected_on_sys_process(StateMachine *sm);
// 具体状态定义
State bt_connected_state = {
.handle_event = bt_connected_state_handler,
.on_sys_process = bt_connected_on_sys_process
};
State bt_disconnected_state = {
.handle_event = bt_disconnected_state_handler,
.on_sys_process = bt_disconnected_on_sys_process
};
// 状态转换函数
void transition_to_state(StateMachine *sm, State *new_state) {
sm->current_state = new_state;
}
// 已连接状态的事件处理
void bt_connected_state_handler(StateMachine *sm, EventType event) {
switch (event) {
case EVENT_BT_DISCONNECTED:
Serial.println("蓝牙已连接状态: 收到断开事件,转换到未连接状态\n");
transition_to_state(sm, &bt_disconnected_state);
break;
default:
Serial.printf("蓝牙已连接状态: 忽略事件 %d\n", event);
break;
}
}
// 未连接状态的事件处理
void bt_disconnected_state_handler(StateMachine *sm, EventType event) {
switch (event) {
case EVENT_BT_CONNECTED:
Serial.println("蓝牙未连接状态: 收到连接事件,转换到已连接状态\n");
transition_to_state(sm, &bt_connected_state);
break;
default:
Serial.printf("蓝牙未连接状态: 忽略事件 %d\n", event);
break;
}
}
// 已连接状态的系统处理函数
void bt_connected_on_sys_process(StateMachine *sm) {
Serial.println("蓝牙已连接状态: 处理系统进程\n");
}
// 未连接状态的系统处理函数
void bt_disconnected_on_sys_process(StateMachine *sm) {
Serial.println("蓝牙未连接状态: 处理系统进程\n");
}
}
// 初始化状态机
void init_state_machine(StateMachine *sm) {
transition_to_state(sm, &bt_disconnected_state);
}
// 发送事件到状态机
void send_event(StateMachine *sm, EventType event) {
sm->current_state->handle_event(sm, event);
}
// 调用当前状态的系统处理函数
void call_sys_process(StateMachine *sm) {
sm->current_state->on_sys_process(sm);
}
StateMachine g_sm;
int bt_sm_test() {
StateMachine sm;
// 初始化状态机,进入未连接状态
init_state_machine(&sm);
// 测试状态转换和系统处理函数
call_sys_process(&sm); // 未连接状态处理系统进程
send_event(&sm, EVENT_BT_CONNECTED); // 转换到已连接状态
call_sys_process(&sm); // 已连接状态处理系统进程
send_event(&sm, EVENT_BT_DISCONNECTED); // 转换回未连接状态
call_sys_process(&sm); // 未连接状态处理系统进程
return 0;
}
状态模式总结:
原理: 这是面向对象设计中经典的GoF设计模式之一。
- 定义一个抽象sate接口(或基类),声明处理事件的方法(如bt_connected_state_handler(StateMachine *sm, EventType event)和bt_disconnected_state_handler(StateMachine *sm, EventType event))。
- 为每个具体的状态创建一个实现了State接口的具体类。
- 上下文对象(Context)持有当前状态对象的引用。当事件发生时,上下文将事件委托给当前状态对象处理,这里的:StateMachine 。
- 状态对象在处理事件时,可以执行动作并决定上下文的下一个状态(通常通过调用上下文的方法来设置新状态)。
优点:
- 高内聚低耦合: 每个状态的行为封装在自己的类中,符合单一职责原则。
- 易于扩展: 添加新状态只需添加新的状态类,通常不需要修改现有状态类或上下文类(开闭原则),如果添加或者删除一个状态非常容易,不想前面两种方式要命的散弹式修改。
- 状态转换显式化: 状态转换逻辑清晰地写在状态类的方法里,不想switch-case或者if-else那样让人一头雾水。
- 可维护性好: 状态逻辑分散在各个状态类中,结构清晰,程序容易看懂。
缺点: 需要创建多个状态类,对于状态数量极多的情况可能会引入较多的类。状态间的转换逻辑分散在各个状态类中,有时全局视图不如查表法清晰,但是如果一旦模式实现,有上述很好的“开闭原则”,我们不需要再关注这些过多的类,所以缺点在这种方式实现之后基本不存在了。
感谢大家,欢迎反馈,一起交流状态机的实现。
|