状态机初识
Posted 浅浅念
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了状态机初识相关的知识,希望对你有一定的参考价值。
状态机可以说是一组状态的集合,是协调相关信号动作,完成特定操作的控制中心,传统应用程序的控制流程基本是顺序的,遵循事先设定的逻辑,
从头到尾地执行。很少有事件能改变标准执行流程,而且这些事件主要涉及异常情况
另一类应用程序由外部发生的事件来驱动--换言之,事件在应用程序之外生成,无法由应用程序或程序员来控制。具体需要执行的代码取决于接收到的事件,
或者它相对于其他事件的抵达时间。所以,控制流程既不能是顺序的,也不能是事先设定好的,因为它要依赖外部事件。
状态机可归纳为4个要素,即当前状态,条件,动作,下个状态。这样的归纳主要出于对状态机的内在因果关系的考虑,
当前状态和条件是因,动作和下个状态是果。对于复杂些的逻辑,用状态机会有助于代码比较清晰,容易维护和Debug,而且效率也不错
有限状态机FSM思想广泛应用于硬件控制电路设计,也是软件上常用的一种处理方法(软件上称为FMM消息机)。它把复杂的控制逻辑分解成有限个稳定状态,在每个状态上判断事件,变连续处理为离散数字处理,符合计算机的工作特点。同时,因为有限状态机具有有限个状态,所以可以在实际的工程上实现。但这并不意味着其只能进行有限次的处理,相反,有限状态机是闭环系统,有限但是无穷,可以用有限的状态,处理无穷的事件。
有限状态机的工作原理如1所示,发生事件(event)后,根据当前状态(curState),决定执行的动作(action)并设置下一个状态号(nextState)。
图2为一个状态机实例的状态转移图,它的含义是:
- 在s0状态,如果发生e0事件,那么就执行a0动作,并保持状态不变;
- 如果发生e1事件,那么就执行a1动作,并将状态转移到s1态;
- 如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
- 在s1状态,如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
- 在s2状态,如果发生e0事件,那么就执行a0动作,并将状态转移到s0态。
有限状态机不仅能够用状态转移图表示,还可以用二维的表格代表。一般将当前状态号写在横行上,将事件写在纵列上,如表1所示。其中“--”表示空(不执行动作,也不进行状态转移),“an/sn”表示执行动作an,同时将下一状态设置为sn。表1和图2表示的含义是完全相同的。
观察表1可知,状态机可以用两种方法实现:竖着写(在状态中判断事件)和横着写(在事件中判断状态)。这两种实现在本质上是完全等效的,但在实际操作中,效果却截然不同。
竖着写(在状态中判断事件)C代码片段:
1 cur_state = nxt_state; 2 switch(cur_state) //在当前状态中判断事件 3 { 4 case s0: //在s0状态 5 if(e0_event) //如果发生e0事件,那么就执行a0动作,并保持状态不变; 6 { 7 //执行a0动作; 8 //nxt_state = s0; //因为状态号是自身,所以可以删除此句,以提高运行速度。 9 } 10 else if(e1_event) //如果发生e1事件,那么就执行a1动作,并将状态转移到s1态; 11 { 12 //执行a1动作; 13 nxt_state = s1; 14 } 15 else if(e2_event) //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态; 16 { 17 //执行a2动作; 18 nxt_state = s2; 19 } 20 else 21 { 22 break; 23 } 24 25 case s1: //在s1状态 26 if(e2_event) //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态; 27 { 28 //执行a2动作; 29 nxt_state = s2; 30 } 31 else 32 { 33 break; 34 } 35 36 case s2: //在s2状态 37 if(e0_event) //如果发生e0事件,那么就执行a0动作,并将状态转移到s0态; 38 { 39 //执行a0动作; 40 nxt_state = s0; 41 } 42 }
横着写(在事件中判断状态)C代码片段:
1 //e0事件发生时,执行的函数 2 void e0_event_function(int * nxt_state) 3 { 4 int cur_state; 5 cur_state = *nxt_state; 6 switch(cur_state) 7 { 8 case s0: //观察表1,在e0事件发生时,s1处为空 9 case s2: //执行a0动作; 10 *nxt_state = s0; 11 } 12 } 13 14 //e1事件发生时,执行的函数 15 void e1_event_function(int * nxt_state) 16 { 17 int cur_state; 18 cur_state = *nxt_state; 19 switch(cur_state) 20 { 21 case s0: //观察表1,在e1事件发生时,s1和s2处为空 22 //执行a1动作; 23 *nxt_state = s1; 24 } 25 } 26 27 //e2事件发生时,执行的函数 28 void e2_event_function(int * nxt_state) 29 { 30 int cur_state; 31 cur_state = *nxt_state; 32 switch(cur_state) 33 { 34 case s0: //观察表1,在e2事件发生时,s2处为空 35 case s1: 36 //执行a2动作; 37 *nxt_state = s2; 38 } 39 }
上面横竖两种写法的代码片段,实现的功能完全相同,但是,横着写的效果明显好于竖着写的效果。理由如下:
1. 竖着写隐含了优先级排序(其实各个事件是同优先级的),排在前面的事件判断将毫无疑问地优先于排在后面的事件判断,这种if/else if写法上的限制将破坏事件间原有的关系。而横着写不存在此问题。
2. 由于处在每个状态时的事件数目不一致,而且事件发生的时间是随机的,无法预先确定,导致竖着写沦落为顺序查询方式,结构上的缺陷使得大量时间被浪费。对于横着写,在某个时间点,状态是唯一确定的,在事件里查找状态只要使用switch语句,能一步定位到相应的状态,延迟时间可以预先准确估算。而且在事件发生时,调用事件函数,在函数里查找唯一确定的状态,并根据其执行动作和状态转移的思路清晰简洁,效率高,富有美感。
总之,我个人认为,在软件里写状态机使用横着写的方法比较妥帖。
竖着写的方法也不是完全不能使用,在一些小项目里,逻辑不太复杂,功能精简,同时为了节约内存耗费,竖着写的方法也不失为一种合适的选择。
在FPGA类硬件设计中,以状态为中心实现控制电路状态机(竖着写)似乎是唯一的选择,因为硬件不太可能靠事件驱动(横着写)。不过在FPGA里有一个全局时钟,在每次上升沿时进行状态切换,使得竖着写的效率并不低。虽然在硬件里竖着写也要使用if/else if这类查询语句,但他们映射到硬件上是组合逻辑,查询只会引起门级延迟(ns量级),而且硬件是真正并行工作的,这样竖着写在硬件里就没有负面影响。因此,在硬件设计里,使用竖着写的方式成为必然的选择。这也是为什么很多搞硬件的工程师在设计软件状态机时下意识的只使用竖着写方式的原因,思维定式罢了。
以上是关于状态机初识的主要内容,如果未能解决你的问题,请参考以下文章
初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段
初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段