打印
[学习资料]

也谈状态机和状态机的实现:状态机模式

[复制链接]
186|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
keer_zu|  楼主 | 2025-6-24 09:08 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 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)复杂度。

缺点:
  • 构建和初始化表可能稍显复杂。如果动作逻辑复杂,直接在表中表示可能不如在状态模式中封装在方法里灵活(通常需要结合函数指针、命令对象等)。对于非常小的状态机可能显得“杀鸡用牛刀”。
  • 示例:上文给出的状态转移表例子不是一个状态机的例子,之前文章:https://bbs.21ic.com/icview-3441826-1-1.html 给出了状态转移表的非常详细的描述,也给出了实例,感兴趣可以详细关注一下。


今天我主要将的是另外一种状态机的实现方式:状态机模式

从一个实例开始,例子很简单,按道理没有必要使用这种方式,只是为了用做状态机模式的例子。基于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那样让人一头雾水。
  • 可维护性好: 状态逻辑分散在各个状态类中,结构清晰,程序容易看懂。


缺点: 需要创建多个状态类,对于状态数量极多的情况可能会引入较多的类。状态间的转换逻辑分散在各个状态类中,有时全局视图不如查表法清晰,但是如果一旦模式实现,有上述很好的“开闭原则”,我们不需要再关注这些过多的类,所以缺点在这种方式实现之后基本不存在了。


感谢大家,欢迎反馈,一起交流状态机的实现。

使用特权

评论回复
沙发
ocon| | 2025-6-24 11:14 | 只看该作者
实用贴,学习一下~

使用特权

评论回复
板凳
keer_zu|  楼主 | 2025-6-24 11:26 | 只看该作者
ocon 发表于 2025-6-24 11:14
实用贴,学习一下~

状态机模式耦合小,代码可扩展性好,逻辑很清晰。只是一开始看着稍微有点绕,不过习惯就知道它的妙处了。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:qq群:49734243 Email:zukeqiang@gmail.com

1462

主题

12857

帖子

53

粉丝