[学习资料]

什么是状态机?怎么设计MCU状态机?

[复制链接]
324|8
手机看帖
扫描二维码
随时随地手机跟帖
dffzh|  楼主 | 2025-5-29 15:17 | 显示全部楼层 |阅读模式
本帖最后由 dffzh 于 2025-5-29 15:25 编辑

#申请原创#
@21小跑堂

1、什么是状态机?
状态机(State Machine, SM),也称为有限状态机(Finite State Machine, FSM),是嵌入式系统软件设计中一种非常重要的用来描述一个系统的行为模型和编程模式。
状态机由一组状态、一组输入事件和一组转换规则组成,系统在任何时刻都处于一种状态,当输入事件发生时,根据当前状态和转换规则,系统可能会转移到另一种状态。
因此,状态机主要包括以下几个基本要素:
状态(State): 系统所处的不同模式或阶段,一个状态机至少要包含两个状态;
事件(Event): 触发状态转换的条件或输入;
转换(Transition): 从一个状态到另一个状态的变化;
动作(Action): 状态转换时执行的操作。
状态机广泛应用于嵌入式系统软件设计中,特别是在需要明确不同状态行为和事件处理的应用场景,包括协议开发、系统界面启动和按键操作等等。
所以,通过使用状态机,开发者可以更容易地构建出可靠、易维护且高效的嵌入式软件系统。

2、怎么设计MCU状态机?
状态机的特性非常适合用于MCU(微控制器)的程序设计,因此接下来作者就通过实际代码向大家阐述一下如何设计MCU状态机。
状态机设计的一般步骤如下所示:
先明确系统状态:
你首先需要分析和明确系统需要哪些状态,常见的但不仅限于以下状态:
初始化状态(INIT)
空闲状态(IDLE)
等待状态(WAIT)
运行状态(RUN)
错误状态(ERROR)
休眠状态(SLEEP)
写状态(WRITE)
读状态(READ)
……
再定义状态转换条件:
当触发什么事件时进行状态切换,常见的但不仅限于以下事件:
定时器超时
外部中断
数据接收完成
等待超时
延时时间到
计数器计数值到
错误、报警和故障发生
……
最后选择状态机实现方式:
常见的MCU状态机实现方式主要包括两种:
第一种方式:switch-case结构
这种方式应该是大家比较熟悉的一种吧,一般在头文件里面通过宏定义或者枚举方式定义好需要的状态机值,然后在源文件里用switch-case结构实现状态机的代码逻辑,示例代码如下:
//header file
/*types of master commands*/
#define FALLBACK              0x5A
#define MASTERIDENT          0x95
#define DEVICEIDENT           0x96
#define DEVICE_STARTUP        0x97
#define PD_OUTOUT_OPERATE   0x98
#define DEVICE_OPERATE        0x99
#define DEVICE_PREOPERATE    0x9A

//source file
switch(slave_state)
        {
                case FALLBACK:
                {
                        //to add your code
                        break;
                }
                case MASTERIDENT:
                {
                        break;
                }
                case DEVICEIDENT:
                {
                        break;
                }
                case DEVICE_STARTUP:
                {
                        break;
                }
                case PD_OUTOUT_OPERATE:
                {
                        break;
                }
                case DEVICE_PREOPERATE:
                {
                        break;
                }
                case DEVICE_OPERATE:
                {

                        break;
                }
                default:
                {
                        break;
                }
        }
这种结构实现的状态机代码看上去比较简单明了易懂,但是如果状态机数量比较多的话,代码看上去就比较臃肿和繁琐。此时,你就可以考虑使用第二种方式来实现状态机。
第二种方式:状态表驱动结构
表驱动方法是一种让你可以在表中查找信息,而不必使用很多的逻辑语句(if-else或switch-case)来把它们找出来的方法。在简单的情况下,逻辑语句确实更简单更直接,但是随着逻辑链的复杂,表驱动法就变得越来越富有吸引力了。21ic论坛上也有专门介绍表驱动法的比较好的文章,比如链接https://bbs.21ic.com/icview-3441826-1-1.html。
那到底怎么用表驱动法实现状态机代码呢?请看代码结构:
//define communication command
#define COMM_SERVER_SCAN 0x0001
#define COMM_SERVER_INFO 0x0002
#define COMM_SERVER_DATA 0x0003
#define COMM_SERVER_DONE 0x0004

//define struct
typedef struct
{
    uint16_t uiCmd; //communication command
    void (*pHandler)(CommServerPack_t *pStr); //function pointer
} CommServerHandler_t;

//define struct array
const CommServerHandler_t g_strCommServerHandler[] =
{
        { COMM_SERVER_SCAN, fCommServerVendorInfo},
        {COMM_SERVER_INFO, fCommServerVendorInfo},
        {COMM_SERVER_DATA, fCommServerVendorInfo},
        {COMM_SERVER_DONE, fCommServerVendorInfo},
        {0, 0},
};

// define communication info handle
void fCommServerVendorInfo(CommServerPack_t *pStr)
{
    //add your code
}

//state machine application
for (i = 0;; i++)
    {
        if (g_strCommServerHandler[i].uiCmd == 0x00)
{
            break;
}
        else if (strRecvPack.uiCmd == g_strCommServerHandler[i].uiCmd)
        {
            g_strCommServerHandler[i].pHandler(&strRecvPack);
            break;
        }
    }
当然,以上的代码结构只是一个比较简单的框架,你可以在此基础上对控制状态机的结构体进行扩展,以满足你的应用需求。
因此,使用表驱动法能够得到代码简洁、执行效率高、扩展性好和可维护性好等好处。
如果系统有多个任务,需要注意以下几点:
为每个功能模块设计独立的状态机;
使用优先级调度确保关键状态及时处理;
注意状态机间的通信和同步。
文章最后,提供一些MCU状态机的设计技巧:
保持状态机简洁:每个状态只做必要的事情;
明确状态转换条件:避免模糊的转换逻辑;
添加默认处理:处理未预料到的事件;
考虑错误恢复:设计错误状态和恢复路径;
记录状态历史:有助于调试复杂问题;
使用状态图工具:如UML状态图辅助设计。
通过合理设计和使用MCU状态机,将会让你的MCU程序结构更加清晰、易于维护和扩展,同时也会提高整套系统的可靠性和响实时性。

使用特权

评论回复
评论
xch 2025-5-30 12:13 回复TA
炒图灵机90年的冷饭。 
dffzh 2025-5-29 15:21 回复TA
if-else if-else结构就不考虑在状态机的实现方式上了。 
xinxianshi| | 2025-5-29 15:50 | 显示全部楼层
是不是也可以用goto

使用特权

评论回复
dffzh|  楼主 | 2025-5-29 15:56 | 显示全部楼层
本帖最后由 dffzh 于 2025-5-29 16:02 编辑
xinxianshi 发表于 2025-5-29 15:50
是不是也可以用goto
goto语句主要用来实现控制流程的跳转,也可以用来跳转状态机;不过我一般很少用,主要是考虑到goto可能会降低代码的可读性和可维护性。

使用特权

评论回复
丙丁先生| | 2025-5-30 05:38 | 显示全部楼层
学习了。

使用特权

评论回复
zjsx8192| | 2025-5-30 08:14 | 显示全部楼层
有源码配合就更好了

使用特权

评论回复
dffzh|  楼主 | 2025-5-30 08:51 | 显示全部楼层
一起学习进步

使用特权

评论回复
dffzh|  楼主 | 2025-5-30 08:53 | 显示全部楼层
zjsx8192 发表于 2025-5-30 08:14
有源码配合就更好了
关于表驱动法的更详细的说明和源代码,可以看这篇非常优秀的文章:
https://bbs.21ic.com/icview-3441826-1-1.html

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

51

主题

604

帖子

5

粉丝