一、EFSM 框架
1.1 什么是有限状态机(FSM)
有限状态机(Finite State Machine)是计算机科学中最基础也最实用的模型之一。它描述的是:一个系统在任意时刻只能处于有限个状态中的某一个,当接收到一个外部事件后,根据当前状态和事件类型,执行特定的动作并转移到下一个状态。
用一句话概括:状态 + 事件 → 动作 + 新状态。
在嵌入式领域,FSM 的身影无处不在:通信协议的握手流程、设备的上下电管理、UI 界面的页面切换、电机控制的运行模式……只要系统存在"阶段性行为",就适合用 FSM 来建模。
EFSM(Extended Finite State Machine)是由 Cisco 工程师 Bo Berry 开发的一个轻量级、纯 C 语言实现的表驱动状态机框架。它的核心思想非常简洁:
将状态转移关系从"代码逻辑"抽象为"数据表格",由通用引擎统一驱动。
开发者只需要定义三样东西:状态有哪些、事件有哪些、每个状态下收到每个事件该调用什么函数并转移到什么状态。框架负责查表、调度、校验和历史记录。
1.1 EFSM框架的核心架构
EFSM 的架构可以用下面这张图来概括:
图1:EFSM 框架整体架构
图1:EFSM 框架整体架构。 绿色部分是用户需要定义的数据和函数,蓝色部分是框架提供的核心引擎,橙色部分是辅助调试功能。
这张图清楚地展示了 EFSM 的分层思想:用户只需关注"定义什么",框架负责"怎么运行"。这种关注点分离是工程实践中极为重要的设计原则。
1.2 核心数据结构
理解 EFSM 的关键在于理解它的四层数据结构,从底层到顶层依次为:
第一层:事件处理回调
typedef RC_FSM_t (*event_cb_t)(void *p2event, void *p2parm);
这是一个函数指针类型。每个事件处理函数接收两个 void* 参数——一个是事件数据(如配置信息),一个是上下文数据(如运行时状态),返回值表示处理结果。这种设计让框架完全不需要知道业务数据的具体类型,实现了彻底的解耦。
第二层:事件元组
typedef struct {
uint32_t eventID; // 归一化事件 ID
event_cb_t event_handler; // 事件处理函数(NULL 表示静默忽略)
uint32_t next_state; // 处理后的下一个状态
} event_tuple_t;
每个事件元组回答了一个问题:"收到这个事件,调用谁来处理,处理完跳到哪个状态?"
第三层:状态元组
typedef struct {
uint32_t state_id; // 归一化状态 ID
event_tuple_t *p2event_tuple; // 该状态的事件处理表
} state_tuple_t;
每个状态关联一张事件表,描述该状态下所有事件的处理方式。
第四层:状态机实例
typedef struct {
uint32_t tag; // 校验标签,防止野指针
uint32_t curr_state; // 当前状态
uint32_t number_states; // 状态总数
uint32_t number_events; // 事件总数
state_tuple_t *state_table; // 状态表指针
fsm_history_t *history; // 历史记录缓冲区
// ... 其他字段
} fsm_t;
1.3 核心 API
EFSM 对外暴露的 API 非常精简,只有五个核心函数:
| API | 功能 |
|---|---|
fsm_create() |
创建状态机实例,校验所有表的合法性 |
fsm_engine() |
驱动状态机:查表→调用处理函数→状态转移 |
fsm_destroy() |
销毁状态机,释放内存 |
fsm_get_state() |
查询当前状态 |
fsm_set_exception_state() |
在事件处理函数中动态改变目标状态 |
其中最核心的是 fsm_engine(),它的执行流程如下:
图2:fsm_engine() 执行流程
图2:fsm_engine() 执行流程。 这是整个框架的心脏,通过二维索引(当前状态 × 事件 ID)定位到具体的处理函数和目标状态。
注意其中的"异常状态"机制:正常情况下,状态转移的目标由事件表预定义;但如果事件处理函数在运行时发现了特殊情况(比如重试超限),可以调用 fsm_set_exception_state() 动态覆盖目标状态。这种"规则内有例外"的设计,在处理协议异常场景时非常实用。
1.4 设计亮点
EFSM 虽然代码量不大(核心代码不到 800 行),但蕴含了多个值得学习的工程设计思想:
表驱动:状态转移关系以数据表的形式存在,修改状态机行为只需修改表格,无需改动引擎代码。
归一化 ID:状态和事件都从 0 开始连续编号,使得查表操作变成简单的数组索引,时间复杂度 O(1)。
创建时校验:fsm_create() 会逐一检查状态表、事件表的完整性和 ID 的连续性,将错误拦截在初始化阶段而非运行时。
历史记录:每次状态转移都会记录到环形缓冲区中,最多保留 64 条记录,为故障排查提供了"黑匣子"。
TAG 校验:fsm_t 结构体的首字段是一个魔数
0xba5eba11("baseball"的谐音),用于防止野指针或内存越界导致的误操作。
二、实战 Demo:一个会话协议状态机
为了让你直观地感受 EFSM 的使用方式,我们来看一个完整的 Demo——一个简化的客户端-服务端会话协议。这个协议的生命周期是:建立连接 → 确认连接 → 数据交互 → 断开连接。
2.1 状态与事件定义
图3:Demo 会话协议状态转移图
图3:Demo 会话协议状态转移图。 展示了 4 个状态和关键事件驱动的转移路径。仅标注了有实际处理动作的转移,忽略事件未画出。
首先定义状态和事件的枚举:
// 归一化事件定义
typedefenum {
start_init_e, // 发起初始化
init_rcvd_e, // 收到初始化请求(服务端视角)
init_tmo_e, // 初始化确认超时
init_ack_e, // 收到初始化确认
start_term_e, // 发起终止
term_rcvd_e, // 收到终止请求
term_ack_e, // 收到终止确认
} session_events_e;
// 归一化状态定义
typedefenum {
idle_s = 0, // 空闲
wait_for_init_ack_s, // 等待初始化确认
established_s, // 会话已建立
wait_for_term_ack_s, // 等待终止确认
} demo_states_e;
然后定义描述表(用于调试时输出可读的名称):
static event_description_t normalized_event_table[] =
{{start_init_e, "Start Session Init"},
{init_rcvd_e, "Session Init"},
{init_tmo_e, "Session Init ACK TMO"},
{init_ack_e, "Session Init ACK"},
{start_term_e, "Start Session Termination"},
{term_rcvd_e, "Session Terminate"},
{term_ack_e, "Session Terminate ACK"},
{FSM_NULL_EVENT_ID, NULL} }; // 哨兵元素,标记表尾
staticstate_description_t normalized_state_table[] =
{{idle_s, "Idle State"},
{wait_for_init_ack_s, "Wait for Init Ack State"},
{established_s, "Established State"},
{wait_for_term_ack_s, "Wait for Terminate Ack State"},
{FSM_NULL_STATE_ID, NULL} }; // 哨兵元素
2.2 构建状态-事件表
这是 EFSM 最核心的部分——为每个状态定义一张事件表:
// 空闲状态的事件表
staticevent_tuple_t state_idle_events[] =
{{ start_init_e, event_start_init, wait_for_init_ack_s },
{ init_rcvd_e, event_init_rcvd, established_s },
{ init_tmo_e, event_ignore, idle_s },
{ init_ack_e, event_ignore, idle_s },
{ start_term_e, event_ignore, idle_s },
{ term_rcvd_e, event_ignore, idle_s },
{ term_ack_e, event_ignore, idle_s }};
// 等待初始化确认状态的事件表
staticevent_tuple_t state_wait_for_init_ack_events[] =
{{ start_init_e, event_ignore, wait_for_init_ack_s },
{ init_rcvd_e, event_ignore, wait_for_init_ack_s },
{ init_tmo_e, event_init_ack_tmo, wait_for_init_ack_s },
{ init_ack_e, event_init_ack_rcvd,established_s },
{ start_term_e, event_term_rcvd, wait_for_init_ack_s },
{ term_rcvd_e, event_term_rcvd, idle_s },
{ term_ack_e, event_ignore, wait_for_init_ack_s }};
// 会话已建立状态的事件表
staticevent_tuple_t state_established_events[] =
{{ start_init_e, event_ignore, established_s },
{ init_rcvd_e, event_ignore, established_s },
{ init_tmo_e, event_ignore, established_s },
{ init_ack_e, event_ignore, established_s },
{ start_term_e, event_start_term, wait_for_term_ack_s },
{ term_rcvd_e, event_term_rcvd, idle_s },
{ term_ack_e, event_ignore, established_s }};
// 等待终止确认状态的事件表
staticevent_tuple_t state_wait_for_term_ack_events[] =
{{ start_init_e, event_ignore, wait_for_term_ack_s },
{ init_rcvd_e, event_ignore, wait_for_term_ack_s },
{ init_tmo_e, event_ignore, wait_for_term_ack_s },
{ init_ack_e, event_ignore, wait_for_term_ack_s },
{ start_term_e, event_ignore, wait_for_term_ack_s },
{ term_rcvd_e, event_ignore, idle_s },
{ term_ack_e, event_term_ack_rcvd, idle_s }};
// 总状态表:将每个状态与其事件表关联
staticstate_tuple_t demo_state_table[] =
{{idle_s, state_idle_events},
{wait_for_init_ack_s, state_wait_for_init_ack_events},
{established_s, state_established_events},
{wait_for_term_ack_s, state_wait_for_term_ack_events},
{FSM_NULL_STATE_ID, NULL}};
阅读这张表,你可以清晰地回答任何一个"如果在 X 状态下收到 Y 事件会怎样"的问题。比如:在 wait_for_init_ack 状态下收到 init_ack_e 事件,会调用 event_init_ack_rcvd 函数,然后转移到 established_s 状态。这就是表驱动的威力——状态转移逻辑全部以声明式的数据存在。
2.3 事件处理函数
事件处理函数是真正的业务逻辑所在。以超时处理为例:
RC_FSM_t event_init_ack_tmo(void *p2event, void *p2parm)
{
demo_config_t *p2config = p2event;
demo_context_t *p2context = p2parm;
p2context->timeout_count++;
if (p2context->timeout_count >= p2config->tmo_threshold) {
printf("Error: init time outs exceededn");
// 可以调用 fsm_set_exception_state() 跳转到异常状态
return RC_FSM_OK;
}
printf("init timeout count=%u threshold=%un",
p2context->timeout_count, p2config->tmo_threshold);
return RC_FSM_OK;
}
注意两个 void* 参数如何被转换为具体的业务类型——p2event 是配置数据,p2parm 是上下文数据。这种通过 void* 传递的设计虽然牺牲了一点类型安全,但换来了框架与业务的彻底解耦,这在 C 语言中是经典的泛型技巧。
2.4 运行 Demo
int main(int argc, char **argv)
{
demo_config_t config;
demo_context_t context;
// 第一步:创建状态机
demo_fsm_create(&config, &context);
context.timeout_count = 0;
config.tmo_threshold = 3;
// 第二步:驱动状态机——模拟一次完整的会话生命周期
demo_fsm_engine(start_init_e, &config, &context); // 发起初始化
demo_fsm_engine(init_tmo_e, &config, &context); // 超时第1次
demo_fsm_engine(init_tmo_e, &config, &context); // 超时第2次
demo_fsm_engine(init_ack_e, &config, &context); // 收到确认
demo_fsm_engine(start_term_e, &config, &context); // 发起终止
demo_fsm_engine(term_ack_e, &config, &context); // 收到终止确认
// 第三步:查看历史记录
demo_fsm_show_history(&config, &context);
return0;
}
这段代码模拟了一个完整的会话过程:从空闲状态发起初始化,经历两次超时重试后收到确认进入已建立状态,最后主动发起终止并收到确认回到空闲。
运行后,框架自动记录的历史输出如下:
FSM: Demo State Machine History
Current State / Event / New State / rc
------------------------------------------------
Idle State / Start Session Init / Wait for Init Ack State / 0
Wait for Init Ack State / Session Init ACK TMO / Wait for Init Ack State / 0
Wait for Init Ack State / Session Init ACK TMO / Wait for Init Ack State / 0
Wait for Init Ack State / Session Init ACK / Established State / 0
Established State / Start Session Termination / Wait for Terminate Ack State / 0
Wait for Terminate Ack State / Session Terminate ACK / Idle State / 0
三、如何在自己的项目中使用 EFSM
将 EFSM 集成到你的项目中,只需要以下五步:
将 fsm.h 和 fsm.c 加入工程:框架只依赖标准 C 库(stdio.h、stdlib.h、string.h),无第三方依赖,适配任何编译器。
定义状态和事件枚举:从 0 开始连续编号,这是框架的硬性要求。
编写事件处理函数:每个函数签名统一为 RC_FSM_t func(void *p2event, void *p2parm),返回 RC_FSM_OK 表示正常转移。
填写状态-事件表:每个状态一张事件表,每个事件对应一个三元组 {事件ID, 处理函数, 目标状态}。不需要处理的事件,将处理函数设为 NULL 或一个空的 event_ignore 函数。
调用 API 驱动:fsm_create() 初始化,fsm_engine() 注入事件,fsm_destroy() 销毁。
四、EFSM 适用的典型场景
基于框架特性,EFSM 特别适用于以下嵌入式场景:
通信协议实现:TCP 连接管理、MQTT 会话、自定义串口协议握手等。状态转移图可以直接映射为代码中的事件表。
设备生命周期管理:上电自检 → 初始化 → 正常运行 → 低功耗 → 关机,每个阶段对事件的响应各不相同。
HMI / 菜单系统:页面之间的跳转关系本质上就是状态机,按键事件驱动页面切换。
电机控制:停止 → 加速 → 匀速 → 减速 → 停止,不同阶段对过流、过温等事件的处理策略不同。
OTA 升级流程:请求版本 → 下载固件 → 校验 → 写入 → 重启,需要严格的状态管理和异常回退。
五、总结
EFSM 不是什么革命性的新技术,但它用极少的代码量(不到 800 行)展示了一个工业级状态机框架该有的样子:表驱动的清晰架构、创建时的严格校验、运行时的历史记录、异常场景的灵活处理。
对于嵌入式开发者来说,掌握 EFSM 的意义不仅在于"多了一个可用的轮子",更在于理解它背后的设计思想——将变化的部分(业务逻辑)和不变的部分(引擎机制)分离,用数据驱动代替硬编码。这种思想可以迁移到你项目中的方方面面。
如果你正在为项目中越来越复杂的状态管理代码头疼,不妨试试把它重构成表驱动的形式。也许你会发现,代码一下子就"呼吸"起来了。
206