目录

前言

一、c语言语法

二、状态转移表

第一次代码优化

第一次代码优化解析

一、解决高耦合

二、明晰状态转移

三、模拟使用

四、总结

第二次代码优化

第二次代码优化解析

一、状态转移函数

二、状态转移表

三、总结

第三次代码优化

第三次代码优化解析

一、状态转变表

二、状态函数

三、状态机执行

结语


前言

        上一篇嵌入式状态机(一)中使用的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的状态机实现方法。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐