摘要:不知道大家有没有这样一种感觉,可以玩单片机,各个功能模块也可以驱动,但是如果让你写一套完整的代码,没有逻辑和框架可以说,上来就开始写!东抄抄抄抄抄。说明编程还处于比较低的水平,那么如何才能提高自己的编程水平呢?学习一个好的编程框架或者一个编程思想可能会受用一生!比如模块化编程、框架编程、状态机编程等等,都是不错的框架。今天要讲的是状态机编程。由于文章篇幅较长,请大家慢慢欣赏。那么状态机是什么样的呢?状态机(statemachine)有五个要素,即状态(state)、转换(transition)、事件(event)、动作(action)、条件(guard)。什么是状态机?状态机是这样的:状态机有五个元素,分别是state、transition、event、action和guard。状态:系统在某一时刻的稳定工作状态,系统在整个工作周期中可能有多种状态。例如,电机有正转、反转和停止三种状态。状态机需要在状态集中选择一个状态作为初始状态。迁移:系统从一种状态迁移到另一种状态的过程称为迁移。迁移不会自动发生,需要对系统进行外部影响。失速的电机不会自行启动,因此必须通电才能使其启动。事件:在特定时刻发生的对系统有意义的事情。状态机之所以会发生状态转移,是因为某个事件的发生。对于电机来说,加正电压、加负电压、断电都是事件。Action:在状态机的迁移过程中,状态机会进行一些其他的行为,这些行为就是动作,动作就是状态机对事件的响应。给堵转电机施加正电压,电机由堵转状态转为正转状态,同时电机启动。这个启动过程可以看作是一个动作,即对开机事件的响应。条件:状态机不响应事件。对于事件,状态机必须满足特定条件才能发生状态转换。仍以电机处于堵转状态为例。虽然打开了开关并通电了,但是如果供电线有问题,电机还是不能转动。只谈概念太空了。举个小例子:一个MCU,一个按键,两个LED灯(记为L1和L2),一个人就够了!规则说明:1.L1L2状态转换顺序OFF/OFF--->ON/OFF--->ON/ON--->OFF/ON--->OFF/OFF2.通过按键控制L1L2的状态按钮,每次状态转换需要连续按下5次。3.L1L2OFF/OFF的初始状态图1根据功能需求编写如下程序。程序清单List1:voidmain(void){sys_init();led_off(LED1);led_off(LED2);g_stFSM.u8LedStat=LS_OFFOFF;g_stFSM.u8KeyCnt=0;while(1){if(test_key()==TRUE){fsm_active();}else{;/*idlecode*/}}}voidfsm_active(void){if(g_stFSM.u8KeyCnt>3)/*按键是否满5次*/{switch(g_stFSM.u8LedStat){caseLS_OFFOFF:led_on(LED1);/*输出动作*/g_stFSM.u8KeyCnt=0;g_stFSM.u8LedStat=LS_ONOFF;/*状态转换*/break;caseLS_ONOFF:led_on(LED2);/*输出动作*/g_stFSM.u8KeyCnt=0;g_stFSM.u8LedStat=LS_ONON;/*状态转换*/break;caseLS_ONON:led_off(LED1);/*输出动作*/g_stFSM.u8KeyCnt=0;g_stFSM.u8LedStat=LS_OFFON;/*状态转换*/break;caseLS_OFFON:led_off(LED2);/*输出动作*/g_stFSM.u8KeyCnt=0;g_stFSM.u8LedStat=LS_OFFOFF;/*状态转换*/break;默认:/*非法状态*/led_off(LED1);led_off(LED2);g_stFSM.u8KeyCnt=0;g_stFSM.u8LedStat=LS_OFFOFF;/*恢复初始状态*/break;}}else{g_stFSM.u8KeyCnt++;/*状态不迁移,只记录击键次数*/}}其实在状态机编程中,正确的顺序应该是状态转移图在前,程序在后,程序应该按照设计好的状态图来写。不过考虑到有的童鞋会觉得代码比转换图更友好,所以我还是先放程序吧。此状态转换图是使用UML(统一建模语言)语法元素绘制的。语法不是很标准,但足以说明问题。图2按键控制流水灯状态转换图圆角矩形代表状态机的各个状态,里面标出了状态的名称。带箭头的直线或弧线表示状态转换,从初始状态开始到下一个状态结束。图中文字内容为迁移说明,格式为:事件[条件]/动作列表(后两项可选)。“事件[条件]/动作列表”的含义是:如果某个状态发生了“事件”,并且状态机满足“[条件]”,则必须执行这个状态转移,同时响应事件的一系列“动作”。在此示例中,我将“KEY”用于击键事件。图中有一个黑色实心点,表示状态机在工作之前处于未知状态。必须强制状态机从这个状态迁移到初始状态才能运行。此迁移可以有一个操作列表(如图1所示),但不需要触发事件。图中还有一个包含实心黑点的圆圈,表示状态机生命周期结束。在这个例子中,状态机是无穷无尽的,所以没有状态指向这个圆圈。这张状态转换图就不多说了,相信大家通过上面的代码就可以轻松看懂了。现在我们来谈谈程序列表List1。先看fsm_active()函数,g_stFSM.u8KeyCnt=0;这条语句在switch-case中出现了5次,前4次作为每次状态转换的动作出现。从代码简化和效率提升的角度来看,我们可以在switch-case语句之前将这5次合并为1次。两者的效果是完全一样的。之所以代码这么冗长,是为了清晰地展示每一次状态转移中的所有动作细节,这种方式完全符合图2中状态转移图所表达的意图。再看一下状态机结构体变量g_stFSM,它有两个成员:u8LedStat和u8KeyCnt。用这个结构做状态机好像有点啰嗦。我们可以只使用像u8LedStat这样的整数变量来制作状态机吗?当然!我们把图2中的4个状态拆分成5个小状态,这样这个状态机也可以用20个状态来实现,只需要一个unsignedchar类型的变量就够了,每次按键都会触发状态转换,LED的状态每5次转换就可以换光,从外面看这两种方法的效果是完全一样的。假设我改变功能需求,通过连续击键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这样的状态机被称为扩展状态机。不知道行业里正式的中文术语怎么说,只好把英文短语搬过来了。2、状态机编程的优点说了这么多,大家大概明白了什么是状态机,也知道怎么写状态机程序了。那么用状态机写单片机程序有什么好处呢??(1)提高CPU使用效率。换句话说,只要我看到一个充满了delay_ms()的程序,我就会很痛苦。十几ms、几十ms的软件延迟是对CPU资源的巨大浪费。宝贵的CPU机器时间浪费在NOP指令上。一直停留在等待一个pin电平转换或者一个串口数据的程序也让我很纠结。如果事件永远不会发生,您是否必须等到世界末日?通过把程序变成状态机,这种情况就会发生明显的改变。程序只需要用全局变量记录工作状态,就可以转身去做其他的工作。不用找了。只要目标事件(定时未到,电平未跳变,串口数据被没收)未发生,工作状态就不会发生变化,程序会一直重复“query-dosomethingelse”-query-dosomethingelse”循环,这样CPU就不会空闲了。程序列表List3中,if{}else{}语句中else下面的内容(代码没有加,只是加了一个/*idlecode*/的注释)就是上面说的“其他工作”。这种处理方式的本质是在程序等待事件的过程中,每隔一段时间插入一些有意义的工作,让CPU不会一直无谓地等待。(2)逻辑完备性我觉得逻辑完备性是状态机编程最大的优势。不知道大家有没有用C语言写过计算器的小程序。我很久以前写的。写出来测试之后,太恐怖了!当我有规律地输入计算公式时,程序可以得到正确的计算结果,但如果我故意输入数字和运算符号的随机组合,程序总是会得到莫名其妙的结果。后来,我尝试模拟程序的工作过程。正确的公式,思路清晰,流畅,但遇到不规则的公式,走路就头晕。这么多的flags,那么多的Variables,来来回回变,最后直接分析不出来。时间长了,当我对状态机有所了解的时候,才恍然大悟,原来当时的程序是有逻辑漏洞的。如果把计算器程序看成一个反应系统,那么一个数字或运算符就可以看成一个事件,一个计算公式就是一组事件的组合。对于一个逻辑完整的反应式系统,无论是何种事件组合,系统都能正确处理事件,系统自身的工作状态始终处于可知可控状态。反之,如果一个系统的逻辑功能不完整,在某种特定的事件组合的驱动下,系统就会进入不可知、不可控的状态,这有悖于设计者的本意。状态机可以解决逻辑完整性问题。状态机是一种以系统状态和事件为变量的设计方法。它侧重于每个状态的特征和状态之间的关系。状态的转变恰恰是由一个事件引起的,所以在研究一个具体的状态时,我们自然会考虑任何一个事件对这个状态的影响。这样一来,每个州发生的每个事件都会在我们的考虑范围之内,不会留下任何逻辑漏洞。这么一说,可能大家会觉得是空话,真知是实践出来的。有一天,如果你真的想设计一个逻辑复杂的程序,我保证你会说:哇!状态机真的好用!(3)清晰的程序结构用状态机编写的程序结构非常清晰。程序员最痛苦的事情就是看别人写的代码。如果代码不是很规范,手上又没有流程图,读代码会让人头晕目眩。只有一遍又一遍地跟着程序走,多次之后才能隐约明白程序的大致工作流程。有流程图就更好了,但是如果程序比较大,流程图就不??会画的很详细了,流程的很多细节还是需要从代码中去理解。相比之下,用状态机编写的程序要好得多。拿一张标准的UML状态转换图,配上一些简洁的文字描述,程序中的每一个元素都能一目了然。程序中有哪些状态,会发生什么事件,状态机如何响应,响应后跳转到哪个状态,这些都非常清楚,甚至很多动作细节都可以从状态转换图中找到。毫不夸张的说,有了UML状态转换图,就不用再写程序流程图了。
