[应用方案] 单片机编程思想——状态机

[复制链接]
2333|27
mollylawrence 发表于 2025-9-23 19:52 | 显示全部楼层 |阅读模式
玩单片机还可以,各个外设也都会驱动,但是如果让你完整的写一套代码时,却无逻辑与框架可言。这说明编程还处于比较低的水平,你需要学会一种好的编程框架或者一种编程思想!比如模块化编程、状态机编程、分层思想等。
本文来说一下状态机编程。
什么是状态机?
状态机(state machine)有5个要素:
  • 状态(state)
  • 迁移(transition)
  • 事件(event)
  • 动作(action)
  • 条件(guard)
状态:一个系统在某一时刻所存在的稳定的工作情况,系统在整个工作周期中可能有多个状态。例如一部电动机共有正转、反转、停转这 3 种状态。
一个状态机需要在状态集合中选取一个状态作为初始状态。
迁移:系统从一个状态转移到另一个状态的过程称作迁移,迁移不是自动发生的,需要外界对系统施加影响。停转的电动机自己不会转起来,让它转起来必须上电。
事件:某一时刻发生的对系统有意义的事情,状态机之所以发生状态迁移,就是因为出现了事件。对电动机来讲,加正电压、加负电压、断电就是事件。
动作:在状态机的迁移过程中,状态机会做出一些其它的行为,这些行为就是动作,动作是状态机对事件的响应。给停转的电动机加正电压,电动机由停转状态迁移到正转状态,同时会启动电机,这个启动过程可以看做是动作,也就是对上电事件的响应。
条件:状态机对事件并不是有求必应的,有了事件,状态机还要满足一定的条件才能发生状态迁移。还是以停转状态的电动机为例,虽然合闸上电了,但是如果供电线路有问题的话,电动机还是不能转起来。
举个例子
要解决的问题
电路如下图:
器件包括单片机MCU、一按键K0、LED灯L1和L2。
实现功能描述:
  • L1L2状态转换顺序OFF/OFF--->ON/OFF--->ON/ON--->OFF/ON--->OFF/OFF
  • 通过按键控制L1L2的状态,每次状态转换需连续按键5次
  • L1L2的初始状态OFF/OFF

状态转换图
在状态机编程中,正确的顺序应该是先有状态转换图,后有程序,程序应该是根据设计好的状态图写出来的。下面这张按键控制流水灯状态转换图,是用UML(统一建模语言)的语法元素画出来的,语法不是很标准,但拿来解释问题足够了。

上图中,圆角矩形代表状态机的各个状态,里面标注着状态的名称。
带箭头的直线或弧线代表状态迁移,起于初态,止于次态。
图中的文字内容是对迁移的说明,格式是:事件[条件]/动作列表(后两项可选)。
“事件[条件]/动作列表”要说明的意思是:如果在某个状态下发生了“事件”,并且状态机
满足“[条件]”,那么就要执行此次状态转移,同时要产生一系列“动作”,以响应事件。在这个例子里,我用“KEY”表示击键事件。
图中有一个黑色实心圆点,表示状态机在工作之前所处的一种不可知的状态,在运行之前状态机必须强制地由这个状态迁移到初始状态,这个迁移可以有动作列表(如图1所示),但不需要事件触发。
图中还有一个包含黑色实心圆点的圆圈,表示状态机生命周期的结束,这个例子中的状态机生生不息,所以没有状态指向该圆圈。
程序代码
下面是根据上述状态转换图写成的代码:
/*状态迁移*/
看一下fsm_active()这个函数,g_stFSM.u8KeyCnt = 0;这个语句在switch—case里共出现了 5 次,前 4 次是作为各个状态迁移的动作出现的。从代码简化提高效率的角度来看,我们完全可以把这 5 次合并为 1 次放在 switch—case 语句之前,两者的效果是完全一样的,代码里之所以这样啰嗦,是为了清晰地表明每次状态迁移中所有的动作细节,这种方式和上面状态转换图所要表达的意图是完全一致的。
再看一下g_stFSM这个状态机结构体变量,它有两个成员:u8LedStat和 u8KeyCnt。用这个结构体来做状态机好像有点儿啰嗦,我们能不能只用一个像 u8LedStat 这样的整型变量来做状态机呢?
当然可以!我们把上图中的这 4 个状态各自拆分成 5 个小状态,这样用 20 个状态同样能实现这个状态机,而且只需要一个 unsigned char 型的变量就足够了,每次击键都会引发状态迁移,每迁移 5 次就能改变一次 LED 灯的状态,从外面看两种方法的效果完全一样。
假设我把功能要求改一下,把连续击键5次改变L1L2的状态改为连续击键100次才能改变L1L2的状态。这样的话第二种方法需要4X100=400个状态!而且函数fsm_active()中的switch—case语句里要有400个case,这样的程序还有法儿写么?!
同样的功能改动,如果用g_stFSM这个结构体来实现状态机的话,函数fsm_active()只需要将if(g_stFSM.u8KeyCnt>3)改为if(g_stFSM.u8KeyCnt > 98)就可以了!
g_stFSM结构体的两个成员中,u8LedStat可以看作是质变因子,相当于主变量;u8KeyCnt可以看作是量变因子,相当于辅助变量。量变因子的逐步积累会引发质变因子的变化。
像g_stFSM这样的状态机被称作Extended State Machine。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
yorkbarney 发表于 2025-10-6 21:23 | 显示全部楼层
状态机包含五个基本要素,这些要素共同构成了状态机的工作机制:

状态:系统在某一时刻的稳定工作情况,例如电动机的正转、反转、停转状态。
迁移:系统从一个状态转移到另一个状态的过程,需要事件触发。
事件:触发状态迁移的外部或内部条件,例如按键按下、定时器超时。
动作:状态迁移过程中执行的操作,例如点亮LED灯。
条件:状态迁移的额外约束条件,例如供电线路正常。
robincotton 发表于 2025-10-8 15:41 | 显示全部楼层
每个状态只能通过特定事件转换到其他状态,避免歧义。
nomomy 发表于 2025-10-8 17:17 | 显示全部楼层
将复杂状态分解为多层状态机              
hearstnorman323 发表于 2025-10-8 18:45 | 显示全部楼层
每个状态迁移都应该有明确的触发条件。
sanfuzi 发表于 2025-10-8 22:36 | 显示全部楼层
状态数量过多会增加代码复杂度。              
mikewalpole 发表于 2025-10-10 10:25 | 显示全部楼层
可以增加 default分支处理未定义的事件或状态
jtracy3 发表于 2025-10-11 20:11 | 显示全部楼层
状态最好用 ​​枚举 ​​ 定义,而不是直接用数字,增强可读性和可维护性
uiint 发表于 2025-10-14 12:59 | 显示全部楼层
在 switch-case 中添加 default 分支
burgessmaggie 发表于 2025-10-14 14:42 | 显示全部楼层
避免出现​​状态机失控、意外进入未知状态、死循环或卡死​
earlmax 发表于 2025-10-14 16:31 | 显示全部楼层
新增状态或修改转换规则时,只需在对应case中修改,不影响其他逻辑。
timfordlare 发表于 2025-10-14 16:56 | 显示全部楼层
switch-case结构:直观但扩展性差,适合简单状态机。
函数指针表:高效且易扩展,适合复杂状态机。
kkzz 发表于 2025-10-14 18:39 | 显示全部楼层
状态与转换
状态(State):表示系统的某个稳定模式(如“空闲”“等待数据”“发送中”)。
事件(Event):触发状态转换的条件(如定时器超时、外部中断、数据接收完成)。
动作(Action):状态转换时执行的操作(如启动ADC采样、发送应答帧)。
fengm 发表于 2025-10-14 19:06 | 显示全部楼层
​系统在任何时刻都处于某个确定的状态,当某个事件发生时,根据当前状态和事件类型,决定下一步迁移到哪个状态,并可能执行相应的动作。​
ingramward 发表于 2025-10-14 19:43 | 显示全部楼层
初学者可能会把某个“程序动作”当作是一种“状态”来处理。这被称为“伪态”。正确的做法是,状态应该代表系统的一个稳定状态,而动作则是在状态迁移过程中执行的行为。
plsbackup 发表于 2025-10-17 10:55 | 显示全部楼层
每个状态都要有明确的​​含义和职责​​
状态数量不宜过多或过少,避免状态爆炸或逻辑模糊
bartonalfred 发表于 2025-10-17 11:14 | 显示全部楼层
使用状态迁移表定义状态与事件的对应关系,适合复杂系统。
xiaoyaodz 发表于 2025-10-17 12:09 | 显示全部楼层
用中断或定时器标志判断操作是否完成,而非轮询。
macpherson 发表于 2025-10-18 10:43 | 显示全部楼层
状态应代表系统的稳定工作阶段,而非瞬时动作。
youtome 发表于 2025-10-18 12:14 | 显示全部楼层
避免在状态机中动态分配内存,改用静态数组或位域。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

53

主题

2024

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部