为什么要写这篇
学习状态机的目的就是了解的同时自己可以使用代码实现,最好还能想出一个应用场景来,那么掌握的效果会好很多。
这篇文章是在关于状态机介绍,跳转表介绍,回调函数介绍 以及常量名打印的基础上写的。状态机的使用是在同一状态下发生不同的事件会执行不同的动作且下一个状态有多种可能性。本来想着用按键来实现的,模拟全自动洗衣机,但是后面想了想不太好,因为不了解,实现起来难度就不止是状态机实现的难度了。而且隐约觉得执行的动作和下一个状态的可能性有点儿少,用起来有点儿勉强。
想要的效果是多个动作多种状态,而且还要比较直观易理解,就想到了一个正方体(不是魔方),执行的动作有四种:往前翻,往后翻,往左翻和往右翻。处在最上面的那个面是不同的(这就是状态)。然后就以这个为原型设计程序。
假设每个面有一种颜色,六个方位对应六种颜色:上下 左右 前后 分别为 白绿 黄橙 红蓝
最开始状态的命名就是state_white , state_green这种,颜色代表的是处在最上面的面的颜色。后来测试中发现:这样不足以准确描述正方体的状态,执行几次动作后 输出的状态与实际状态不对应。看来是我想简单了,后来分析了一下,六个面占据三个坐标系,本该三个面确定一个当前的状态,其实只需要两个(朝上的面 和 正对自己的面),垂直于平面的剩余三个面可以通过这两个面确定出来。共96种状态。
相关数据结构定义
最主要的是定义状态机的数据结构,所有的代码都是围着它转的。
/* 状态机数据结构 */
typedef struct
{
State currentState; ///< 当前状态
StateInfo *stateTable; ///< 状态表指针
uint8_t stateTableSize; ///< 状态表大小(元素个数)
} StateMachine;
关于State的定义
typedef enum
{
/* for example: state_white_red indicate UP: white, FRONT: red */
state_white_red = 0,
state_white_yellow,
state_white_blue,
state_white_orange,
state_green_red,
state_green_yellow,
state_green_blue,
state_green_orange,
state_yellow_white,
state_yellow_green,
state_yellow_red,
state_yellow_blue,
state_orange_white,
state_orange_green,
state_orange_red,
state_orange_blue,
state_red_white,
state_red_green,
state_red_yellow,
state_red_orange,
state_blue_white,
state_blue_green,
state_blue_yellow,
state_blue_orange
} State;
关于 结构体数组,先介绍结构体类型的定义
typedef struct
{
State curState; ///< 当前所处状态
Event event; ///< 发生事件
Action_Func action; ///< 执行动作
State nextState; ///< 下一个状态
} StateInfo;
其中Event 和 Action_Func数据类型的定义如下
typedef enum
{
event_turn_forward = 1, ///< 事件:需要前翻
event_turn_back, ///< 事件:需要后翻
event_turn_left, ///< 事件:需要左翻
event_turn_right ///< 事件:需要右翻
} Event;
typedef void (*Action_Func)(void);
void action_turn_forward(void);
void action_turn_back(void);
void action_turn_left(void);
void action_turn_right(void);
结构体数组的定义(由于元素个数太多,共96个,太占地方,此处粘贴16个)
static StateInfo state_table[] =
{
/* 六个方位对应六种颜色:上下 左右 前后 分别为 白绿 =黄橙= 红蓝 */
{state_white_red, event_turn_forward, action_turn_forward, state_blue_white},
{state_white_red, event_turn_back, action_turn_back, state_red_green},
{state_white_red, event_turn_left, action_turn_left, state_orange_red},
{state_white_red, event_turn_right, action_turn_right, state_yellow_red},
{state_white_yellow, event_turn_forward, action_turn_forward, state_orange_white},
{state_white_yellow, event_turn_back, action_turn_back, state_yellow_green},
{state_white_yellow, event_turn_left, action_turn_left, state_red_yellow},
{state_white_yellow, event_turn_right, action_turn_right, state_blue_yellow},
{state_white_blue, event_turn_forward, action_turn_forward, state_red_white},
{state_white_blue, event_turn_back, action_turn_back, state_blue_green},
{state_white_blue, event_turn_left, action_turn_left, state_yellow_blue},
{state_white_blue, event_turn_right, action_turn_right, state_orange_blue},
{state_white_orange, event_turn_forward, action_turn_forward, state_yellow_white},
{state_white_orange, event_turn_back, action_turn_back, state_orange_green},
{state_white_orange, event_turn_left, action_turn_left, state_blue_orange},
{state_white_orange, event_turn_right, action_turn_right, state_red_orange},
}
关于状态机的三个API接口,功能分别是:
- 注册状态机:将状态表的地址存入到状态机的状态表指针中
- 查找状态表:根据状态机中的当前状态和发生的事件,从状态表中找到对应的结构体,返回指针
- 运行状态机:执行相应的动作,将结构体中存的下一个状态赋值给状态机的当前状态
void state_machine_regist(StateMachine *pSM, StateInfo *pStateTable);
StateInfo *find_state_table(StateMachine *pSM, Event evt);
void runStateMachine(StateMachine *pSM, Event evt);
状态机测试的main函数
int main(void)
{
StateMachine SM_Type;
state_machine_regist(&SM_Type, state_table);
SM_Type.currentState = state_white_red;
SM_Type.stateTableSize = sizeof(state_table) / sizeof(state_table[0]);
printf("init state: %s \n\n", comparison_table[SM_Type.currentState].str);
runStateMachine(&SM_Type, event_turn_forward);
runStateMachine(&SM_Type, event_turn_left);
runStateMachine(&SM_Type, event_turn_left);
printf("final state: %s \n\n", comparison_table[SM_Type.currentState].str);
return 0;
}
程序运行结果
状态机的三个API接口实现
/* 状态机注册,给它一个状态表 */
void state_machine_regist(StateMachine *pSM, StateInfo *pStateTable)
{
if((pSM == NULL) || (pStateTable == NULL))
{
return;
}
pSM->stateTable = pStateTable;
}
/* 查找状态表 */
StateInfo *find_state_table(StateMachine *pSM, Event evt)
{
if(pSM == NULL)
{
return NULL;
}
for (uint8_t i = 0; i < pSM->stateTableSize; i++)
{
if ((pSM->stateTable[i].curState == pSM->currentState) && (pSM->stateTable[i].event == evt))
{
return &pSM->stateTable[i];
}
}
}
/* 运行状态机 */
void runStateMachine(StateMachine *pSM, Event evt)
{
StateInfo *pStateInfo;
if(pSM == NULL)
{
return;
}
pStateInfo = find_state_table(pSM, evt);
if (pStateInfo == NULL)
{
return;
}
Action_Func act = pStateInfo->action;
if (act == NULL)
{
return;
}
act();
pSM->currentState = pStateInfo->nextState;
}
关于编译运行
我将其分成了三个文件,编译时就需要些规则了,我的方式是使用Makefile, 后面想着将生成的文件和源文件放在一起太乱了,就又再RunCode.bat中加入了几条语句,编译完成后在当前目录下创建一个output文件夹,将生成的文件移动到这个文件夹下,进入这个文件下执行可执行程序,防止窗口闪退。
RunCode.bat 文件中代码
Makefile 文件中代码