嵌入式状态机(二)
嵌入式状态机的进阶用法——状态表驱动
目录
前言
上一篇嵌入式状态机(一)中使用的switch方法驱动状态虽然简单快捷,但却有着高耦合、结构混乱等缺点。因此本篇文章在原先程序上做了优化调整,使其架构更加合理。
阅读本篇文章需要掌握以下知识:
一、c语言语法
结构体、结构体数组初始化、函数指针、指针运用
二、状态转移表
将状态转移的规则(方向)使用数组的形式存储,并作为状态机运行的依据
注:状态转移表是状态机设计中的重要工具,它通过表格形式系统化地定义了状态机的所有行为规则。
注:个人认为状态转移表的定义并没有那么严格,具体如何使用需要看个人的状态机如何设计。其核心思想是以表格的形式提前制定状态转移(事件发生后)的规则,大多数人习惯使用二维数组。
第一次代码优化
话不多说,我们先来开一下完整的代码
#include <stdio.h>
#include <stdbool.h>
//状态机状态
typedef enum {
state_A,
state_B,
state_C,
state_D,
state_end,
}state_enum;
//状态机事件
typedef enum{
event_1,
event_2,
event_3,
event_4,
event_5,
event_6,
event_end
}event_enum;
//状态机存储状态的实体
static state_enum s_buff= state_A;
//状态机存储事件的实体
static event_enum e_buff= event_end;
//宏定义,用于快速打印状态,在纯C环环境下模拟
#define PRINTF_STA(a) printf("-->当前状态"#a"\n")
//状态表
typedef struct state_conversion_table
{
void (*function)(event_enum*);
}state_conversion_table;
//提前声明
void S_A(event_enum *e);
void S_B(event_enum *e);
void S_C(event_enum *e);
void S_D(event_enum *e);
//状态表实体
state_conversion_table state_table[state_end]={
[state_A]
{
.function=S_A,
},
[state_B]
{
.function=S_B,
},
[state_C]
{
.function=S_C,
},
[state_D]
{
.function=S_D,
},
};
/**
* 状态A的处理函数
* @param e 指向事件枚举类型的指针,用于接收和传递事件信息
*/
void S_A(event_enum *e)
{
PRINTF_STA(state_A); // 打印当前状态为A的信息
if (*e == event_1)s_buff=state_C; // 如果事件为event_1,则切换状态到C
if (*e == event_2)s_buff=state_B; // 如果事件为event_2,则切换状态到B
*e= event_end; // 将事件重置为结束状态
}
void S_B(event_enum *e)
{
PRINTF_STA(state_B);
if (*e == event_3)s_buff=state_C;
if (*e == event_5)s_buff=state_D;
*e= event_end;
}
void S_C(event_enum *e)
{
PRINTF_STA(state_C);
if (*e == event_4)s_buff=state_D;
*e= event_end;
}
void S_D(event_enum *e)
{
PRINTF_STA(state_D);
if (*e == event_6)s_buff=state_A;
*e= event_end;
}
/**
* 状态机处理函数
* 该函数根据当前状态执行相应的状态处理函数
*/
void state_m(void)
{
// 检查状态缓冲区是否已超出状态表范围
if(s_buff>=state_end)return;
// 检查当前状态是否有对应的处理函数
if(state_table[s_buff].function!=NULL)
{
// 调用当前状态对应的处理函数,并传入事件缓冲区作为参数
state_table[s_buff].function(&e_buff);
}
}
#define e_num 8
//用于模拟事件的发生
event_enum e_[e_num]={
event_4,
event_1,
event_4,
event_6,
event_2,
event_3,
event_4,
event_6,
};
/**
* 主函数 - 执行状态机循环
* 该函数初始化并执行一个循环,共7次,每次调用状态机函数
*/
void main(void)
{
// 循环计数器i,从0到6,共执行7次
for (char i = 0; i < e_num; i++)
{
// 将数组e_中的第i个元素赋值给e_buff
e_buff=e_[i];
// 打印当前步骤的序号
printf("第%d步",i);
// 调用状态机函数
state_m();
}
}

状态机流程图
第一次代码优化解析
一、解决高耦合
将原先switch中的状态单独提取出来,成为独立的状态函数。
void S_A(event_enum *e);
void S_B(event_enum *e);
void S_C(event_enum *e);
void S_D(event_enum *e);
/**
* 状态A的处理函数
* @param e 指向事件枚举类型的指针,用于接收和传递事件信息
*/
void S_A(event_enum *e)
{
PRINTF_STA(state_A); // 打印当前状态为A的信息
if (*e == event_1)s_buff=state_C; // 如果事件为event_1,则切换状态到C
if (*e == event_2)s_buff=state_B; // 如果事件为event_2,则切换状态到B
*e= event_end; // 将事件重置为结束状态
}
void S_B(event_enum *e)
{
PRINTF_STA(state_B);
if (*e == event_3)s_buff=state_C;
if (*e == event_5)s_buff=state_D;
*e= event_end;
}
void S_C(event_enum *e)
{
PRINTF_STA(state_C);
if (*e == event_4)s_buff=state_D;
*e= event_end;
}
void S_D(event_enum *e)
{
PRINTF_STA(state_D);
if (*e == event_6)s_buff=state_A;
*e= event_end;
}
通过函数指针设计统一的函数接口,连接各个状态函数来实现解耦合。
typedef struct state_conversion_table{
void (*function)(event_enum*);
}state_conversion_table;
注意:严格讲状态机中的状态应该只关心当前动作、当前事件、下一时刻状态,不应该更改当前的事件。但作为嵌入式编程需要结合实际情况考虑,需尽可能的减小状态机框架的开销。在中小型工程中没有必要为了只执行一次状态而单独开辟进入状态、退出状态的动作。
二、明晰状态转移
使用一维数组组成一个简易的状态转移表。
typedef struct state_conversion_table
{
void (*function)(event_enum*);
}state_conversion_table;
//状态表
state_conversion_table state_table[state_end]={
[state_A]
{
.function=S_A,
},
[state_B]
{
.function=S_B,
},
[state_C]
{
.function=S_C,
},
[state_D]
{
.function=S_D,
},
};
state_conversion_table是新建的结构体变量类型,其成员为可以指向状态函数的函数指针。
state_table状态表是state_conversion_table类型的数组,数组大小为state_end个(在c语言默认初始化的情况下,枚举最后一个成员的数值等于枚举成员的个数)。
这样一来state_table数组的个数与状态相等,那么只要对应好数组的下标值与对应的函数指针指向的状态函数,我们就可以通过下表访问数组方式来访问对应的状态(默认情况下c语言的枚举成员的本质是从0开始的数字)。
三、模拟使用
现在我们运行状态机是不在需要通过垄长switch分支语句,只需要访问对应状态的数组成员即可。
由于我们是用函数指针来连接状态函数的,所以我们需要注意函数指针的指向与数组的范围,避免出现野指针的情况
/**
* 状态机处理函数
* 该函数根据当前状态执行相应的状态处理函数
*/
void state_m(void)
{
// 检查状态缓冲区是否已超出状态表范围
if(s_buff>=state_end)return;
// 检查当前状态是否有对应的处理函数
if(state_table[s_buff].function!=NULL)
{
// 调用当前状态对应的处理函数,并传入事件缓冲区作为参数
state_table[s_buff].function(&e_buff);
}
}
#define e_num 8
//用于模拟事件的发生
event_enum e_[e_num]={
event_4,
event_1,
event_4,
event_6,
event_2,
event_3,
event_4,
event_6,
};
/**
* 主函数 - 执行状态机循环
* 该函数初始化并执行一个循环,共7次,每次调用状态机函数
*/
void main(void)
{
// 循环计数器i,从0到6,共执行7次
for (char i = 0; i < e_num; i++)
{
// 将数组e_中的第i个元素赋值给e_buff
e_buff=e_[i];
// 打印当前步骤的序号
printf("第%d步",i);
// 调用状态机函数
state_m();
}
}
四、总结
解决了高耦合与结构混乱的问题,但是状态转换的规则依旧不便修改。
第二次代码优化
完整代码
#include <stdio.h>
#include <stdbool.h>
//状态机状态
typedef enum {
state_A,
state_B,
state_C,
state_D,
state_end,
}state_enum;
//状态机事件
typedef enum{
event_1,
event_2,
event_3,
event_4,
event_5,
event_6,
event_end
}event_enum;
//宏定义,用于快速打印状态,在纯C环环境下模拟
#define PRINTF_STA(a) printf("-->当前状态"#a"\n")
//状态函数
typedef state_enum (*StateFunction)(event_enum *);
//提前声明
state_enum S_A_C(event_enum *e);
state_enum S_A_B(event_enum *e);
state_enum S_B_C(event_enum *e);
state_enum S_B_D(event_enum *e);
state_enum S_C_D(event_enum *e);
state_enum S_D_A(event_enum *e);
StateFunction stata[state_end][event_end]={
[state_A]={
[event_1]=S_A_C,
[event_2]=S_A_B,
},
[state_B]={
[event_3]=S_B_C,
[event_5]=S_B_D,
},
[state_C]={
[event_4]=S_C_D,
},
[state_D]={
[event_6]=S_D_A,
},
};
state_enum S_A_C(event_enum *e)
{
PRINTF_STA(state_A);
*e= event_end;
return state_C;
}
state_enum S_A_B(event_enum *e)
{
PRINTF_STA(state_A);
*e= event_end;
return state_B;
}
state_enum S_B_C(event_enum *e)
{
PRINTF_STA(state_B);
*e= event_end;
return state_C;
}
state_enum S_B_D(event_enum *e)
{
PRINTF_STA(state_B);
*e= event_end;
return state_D;
}
state_enum S_C_D(event_enum *e)
{
PRINTF_STA(state_C);
*e= event_end;
return state_D;
}
state_enum S_D_A(event_enum *e)
{
PRINTF_STA(state_D);
*e= event_end;
return state_A;
}
//状态机存储状态的实体
static state_enum s_buff= state_A;
//状态机存储事件的实体
static event_enum e_buff= event_end;
void stata_m(void)
{
//检测是否超出数组范围
if (s_buff >= state_end || e_buff >= event_end)
{
s_buff = state_A;
e_buff = event_end;
return;
}
StateFunction buf= stata[s_buff][e_buff];
//检测是否为空指针,防止野指针
if(buf!=NULL)s_buff=buf(&e_buff);
}
//用于模拟事件的发生
event_enum e_[7]={
event_1,
event_4,
event_6,
event_2,
event_3,
event_4,
event_6,
};
/**
* 主函数 - 执行状态机循环
* 该函数初始化并执行一个循环,共7次,每次调用状态机函数
*/
void main(void)
{
// 循环计数器i,从0到6,共执行7次
for (char i = 0; i < 7; i++)
{
// 将数组e_中的第i个元素赋值给e_buff
e_buff=e_[i];
// 打印当前步骤的序号
printf("第%d步",i);
// 调用状态机函数
stata_m();
}
}
第二次代码优化解析
一、状态转移函数
改变原有的状态函数结构,使下一时刻状态由函数返回值决定,当前动作、当前事件由状态转移表决定
state_enum S_A_C(event_enum *e);
//状态A转换状态B函数
state_enum S_A_C(event_enum *e)
{
//状态动作
PRINTF_STA(state_A);
//可以删除
*e= event_end;
//下一时刻状态
return state_C;
}
二、状态转移表
//状态函数
typedef state_enum (*StateFunction)(event_enum *);
StateFunction stata[state_end][event_end]={
[state_A]={
[event_1]=S_A_C,
[event_2]=S_A_B,
},
[state_B]={
[event_3]=S_B_C,
[event_5]=S_B_D,
},
[state_C]={
[event_4]=S_C_D,
},
[state_D]={
[event_6]=S_D_A,
},
};
直接使用函数指针充当state数组的类型,由state_end和event_end指定数组的大小,形成了一个行为状态列为事件的状态列表。
驱动原理与第一次代码优化相同,都是通下标锁定对应的函数。不过这个程序的重点是状态转换。
注意:使用 [state_A] 这种指定初始化器是 C99 标准,如果非C99 标准需要将其全部列出,并在没有转换规格处填上NULL防止野指针
三、总结
程序至此已经形成了基本的状态表转换雏形了,继续完善就要加入状态机上下文切换、状态转换条件、状态进入、退出动作等类容,小编能力有限就到此为止了。
除了将状态表转换与函数指针直接相连以外还有另外一种方法:
第三次代码优化
#include <stdio.h>
#include <stdbool.h>
// 状态机状态
typedef enum {
state_A,
state_B,
state_C,
state_D,
state_end,
} state_enum;
// 状态机事件
typedef enum {
event_1,
event_2,
event_3,
event_4,
event_5,
event_6,
event_end
} event_enum;
// 宏定义,用于快速打印状态
#define PRINTF_STA(a) printf("-->当前状态"#a"\n")
// 状态转换表:直接存储下一个状态
state_enum state_transition_table[state_end][event_end] = {
[state_A] = {
[event_1] = state_C,
[event_2] = state_B,
// 其他事件默认为当前状态(不转换)
},
[state_B] = {
[event_3] = state_C,
[event_5] = state_D,
},
[state_C] = {
[event_4] = state_D,
},
[state_D] = {
[event_6] = state_A,
},
};
// 状态进入函数类型定义
typedef void (*StateEntryFunction)(void);
// 状态进入函数声明
void enter_state_A(void);
void enter_state_B(void);
void enter_state_C(void);
void enter_state_D(void);
// 状态进入函数表
StateEntryFunction state_entry_table[state_end] = {
[state_A] = enter_state_A,
[state_B] = enter_state_B,
[state_C] = enter_state_C,
[state_D] = enter_state_D,
};
// 状态进入函数实现
void enter_state_A(void) {
PRINTF_STA(state_A);
}
void enter_state_B(void) {
PRINTF_STA(state_B);
}
void enter_state_C(void) {
PRINTF_STA(state_C);
}
void enter_state_D(void) {
PRINTF_STA(state_D);
}
// 状态机存储状态的实体
static state_enum s_buff = state_A;
// 状态机存储事件的实体
static event_enum e_buff = event_end;
// 状态机处理函数
void state_machine(void) {
// 边界检查
if (s_buff >= state_end || e_buff >= event_end) {
printf("错误:非法状态或事件!\n");
s_buff = state_A;
e_buff = event_end;
return;
}
// 获取下一个状态
state_enum next_state = state_transition_table[s_buff][e_buff];
// 如果没有定义转换,保持当前状态
if (next_state >= state_end) {
next_state = s_buff;
}
// 如果状态发生变化,调用状态进入函数
if (next_state != s_buff) {
s_buff = next_state;
state_entry_table[s_buff]();
} else {
printf("状态%d下不处理事件%d\n", s_buff, e_buff);
}
// 消费事件
e_buff = event_end;
}
// 提供给外部的接口函数
void feed_event(event_enum e) {
e_buff = e;
}
// 用于模拟事件的发生
event_enum event_sequence[7] = {
event_1,
event_4,
event_6,
event_2,
event_3,
event_4,
event_6,
};
// 主函数
int main(void) {
// 初始状态进入
state_entry_table[s_buff]();
for (int i = 0; i < 7; i++) {
printf("第%d步", i);
feed_event(event_sequence[i]);
state_machine();
}
return 0;
}
第三次代码优化解析
一、状态转变表
// 状态转换表:直接存储下一个状态
state_enum state_transition_table[state_end][event_end] = {
[state_A] = {
[event_1] = state_C,
[event_2] = state_B,
// 其他事件默认为当前状态(不转换)
},
[state_B] = {
[event_3] = state_C,
[event_5] = state_D,
},
[state_C] = {
[event_4] = state_D,
},
[state_D] = {
[event_6] = state_A,
},
};
state_transition_table状态转换表的类型是state_enum类型,只负责提供状态转换的方向(规则),具体的执行函数由另一个状态函数列表提供
// 状态进入函数声明
void enter_state_A(void);
void enter_state_B(void);
void enter_state_C(void);
void enter_state_D(void);
// 状态进入函数表
StateEntryFunction state_entry_table[state_end] = {
[state_A] = enter_state_A,
[state_B] = enter_state_B,
[state_C] = enter_state_C,
[state_D] = enter_state_D,
};
二、状态函数
// 状态进入函数实现
void enter_state_A(void) {
PRINTF_STA(state_A);
}
void enter_state_B(void) {
PRINTF_STA(state_B);
}
void enter_state_C(void) {
PRINTF_STA(state_C);
}
void enter_state_D(void) {
PRINTF_STA(state_D);
}
状态函数只负责状态内部的动作
三、状态机执行
// 状态机处理函数
void state_machine(void) {
// 边界检查
if (s_buff >= state_end || e_buff >= event_end) {
printf("错误:非法状态或事件!\n");
s_buff = state_A;
e_buff = event_end;
return;
}
// 获取下一个状态
state_enum next_state = state_transition_table[s_buff][e_buff];
// 如果没有定义转换,保持当前状态
if (next_state >= state_end) {
next_state = s_buff;
}
// 如果状态发生变化,调用状态进入函数
if (next_state != s_buff) {
s_buff = next_state;
state_entry_table[s_buff]();
} else {
printf("状态%d下不处理事件%d\n", s_buff, e_buff);
}
// 消费事件
e_buff = event_end;
}
通过先前的state_transition_table状态转换表获取当前状态发生的事件后,下一时刻的状态。
判断该状态是否越界。
s_buff存储上一次(初始状态)的状态,当当前状态next_state与上一次状态s_buff不同时执行state_entry_table中对应的状态。
结语
第二部分程序与第三部分程序是大体相同的思路类型,但实现的方法不同,其他的状态表的思想也是大同小异。我们一般称这种方法为Table-Driven,除此之外还有State Pattern的状态机实现方法。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)