在嵌入式系统、游戏开发和协议解析等领域,状态机是一种常用且高效的设计模式。它通过将系统的行为划分为不同的状态,并根据输入事件在这些状态之间进行转换,简化了复杂系统的设计和维护。本文将介绍一个由 happyDom 开发的 C 语言状态机框架,并演示如何使用它来构建一个简单的状态机。
框架概述
happyDom/stateMachineC 是一个用 C 语言编写的轻量级状态机框架。它提供了定义状态、事件和状态转换的功能,支持有限状态机(FSM)的创建和管理。该框架的主要特点包括:
简洁易用:API 设计直观,易于集成和使用。
高性能:经过优化,适合资源受限的嵌入式环境。
可扩展性:支持动态添加状态和事件,灵活应对复杂场景。
快速入门
设计状态跳转图
作为演示,我们假设我们要实现的状态机如下👇
克隆仓库
首先,使用 Git 克隆状态机框架的源代码:
git clone git@gitee.com:DyyYq/stateMachineC.git
包含头文件
在您的项目中,包含状态机框架的头文件:
# include "stateMachine.h"
定义状态
定义一个枚举变量,来表示你的状态机的各个状态,此处我们有 开机状态 和 关机状态 两种状态;为了在开机和状态前处理一些预处理事件,我们增加了 预开机 和 预开机 两个状态;一共是 4 个状态。
enum myStates{ // 状态机状态值从 0 开始 stPreOff = 0, //预关机状态 stOff, //关机状态 stPreOn, //预开机状态 stOn, //开机状态 stCnt //这不是一个状态机的状态,但是这个值表征了状态机状态的的数量 };
定义跳转事件
状态机需要根据不同的事件进行状态的跳转,我们这个演示中,我们只有一个事件,就是用户的按键事件。我们假充用户按下按键 s 时,事件发生。则我们定义事件函数如下:
// 状态机跳转事件,这个事件用来检测是否发生了指定的事件,如果发生,返回go,否则返回 await smEventResult_t keyPressed(smUnit_t *pSt){ // 确保开机或者关机完成后,再响应用户的按键事件 if (pSt->roundCounter > 3 && _kbhit()) { if( 's' == _getch()){ return go; } } return aWait; }
为了检测 预关机 和 预开机 两个状态的动作是否完成,我们需要添加一个事件判定。如下👇:
// 判定预开机和预关机动作是否完成,此处我们简化处理,3s后我们直接判定动作完成 smEventResult_t preActionCmplt(smUnit_t *pSt){ if(pSt->roundCounter >= 3){ return go; } return aWait; }
定义状态动作
我们对每个状态下的动作,做如下的要求:
- 在 stOff 状态时,什么也不作。
- 在 stPreOn 状态时,打印显示读取用户配置的过程。
- 在 stPreOn 状态时,开机过程持续3s。
- 在 stOn 状态时,持续显示开机的时长。
- 在 stPreOn 状态时,打印显示保存用户配置的过程。
- 在 stPreOn 状态时,关机过程持续3s。
为满足以上设计要求,我们定义状态动作如下👇
//定义 preOff 状态下的 Entry 动作 void actionEntry_preOff(smUnit_t *pSt){ printf("保存用户配置中,请稍候!\n"); } //定义 preOff 状态下的 Do 动作 void actionDo_preOff(smUnit_t *pSt){ if(pSt->roundCounter < 3){ printf("%d\n", 3 - pSt->roundCounter); }else if(pSt->roundCounter == 3){ printf("用户配置保存完成\n"); } } // 定义 preOn 状态下的 entry 动作 void actionEntry_preOn(smUnit_t *pSt){ printf("加载用户配置中,请稍候!\n"); } // 定义 preOn 状态下的 do 动作 void actionDo_preOn(smUnit_t *pSt){ if(pSt->roundCounter < 3){ printf("%d\n", 3 - pSt->roundCounter); }else if(pSt->roundCounter == 3){ printf("用户配置加载完成\n"); } } // 定义 on 状态下的 Do 动作 void actionDo_on(smUnit_t *pSt){ printf("已经开机 %ds\n", pSt->roundCounter); }
定义状态机变量,并初始化之,然后将状态动作和事件注册到状态机变量中
以下代码,我们先定义了一个状态机变量,然后初始化该状态机,然后并我们定义的状态动作和跳转事件注册到状态机中。最后,再以1s为周期轮询运行状态机。
int main(int argc, char const *argv[]){ // 定义状态机变量 stateMachine_t demoSM; // 初始化状态机,指定状态数量,指定默认状态 fsm_init(&demoSM, stCnt, stOff); // 为各状态注册状态动作 demoSM.actionSignUp(&demoSM, stPreOff, actionEntry_preOff, actionDo_preOff, NULL); demoSM.actionSignUp(&demoSM, stPreOn, actionEntry_preOn, actionDo_preOn, NULL); demoSM.actionSignUp(&demoSM, stOn, NULL, actionDo_on, NULL); // 注册状态跳转事件 demoSM.eventSingUp(&demoSM, stOff, stPreOn, keyPressed); demoSM.eventSingUp(&demoSM, stPreOn, stOn, preActionCmplt); demoSM.eventSingUp(&demoSM, stOn, stPreOff, keyPressed); demoSM.eventSingUp(&demoSM, stPreOff, stOff, preActionCmplt); while (1){ demoSM.run(&demoSM); Sleep(1000); } }
运行效果
本状态机的运行效果如下👇