软件设计开发笔记1:基于状态机的程序设计
在编码实现的过程中,我们会经常使用到条件判断结构,而且使用起来很方便。但是在需要转移的状态比较多,或是条件比较复杂时,我们就可能需要很长的条件判断结构来处理。不过,过于复杂的条件判断结构会给代码的编写和维护带来很大的困扰,所以我们希望探索其他的方法来简化这类条件结构。
1、原理概述
条件判断在代码实现中非常有用,有时候甚至是必不可少的。但过于复杂的条件结构却会让程序逻辑变得冗长而繁琐,而在某些情况下我们希望采取方法避免这一情况出现。
1.1、问题提出
在项目开发中经常会遇到if/esle语句以及switch/case语句之类,或者是嵌套的多分支条件判断的结构。这类结构一旦过于复杂或冗长就会使程序的逻辑结构非常繁琐。所以很多时候我们希望避免使用过于复杂的条件结构。基于这一目的,我们希望探索一些方法来简化这类问题。
在我们实践的过程中,我们发现有些复杂的条件结构实际控制的是同一事物在不同状态下的转换。这就让我们想到了状态机,那么是否可以借用状态机的机制来解决这一类的问题呢?在这一篇中我们就来分析和实现这一议题。
1.2、什么是状态机
我们先来看看什么是状态机。一般来说,状态机(state machine)包含有5个要素,分别是状态(state)、迁移(transition)、事件(event)、动作(action)、条件(guard)。我们来具体看看这5个要素都是什么。
-
状态:一个系统在某一时刻所存在的稳定的工作情况,系统在整个工作周期中可能有多个状态。一个状态机需要在状态集合中选取一个状态作为初始状态。
-
迁移:系统从一个状态转移到另一个状态的过程称作迁移,迁移不是自动发生的,需要外界对系统施加影响。
-
事件:某一时刻发生的对系统有意义的事情,状态机之所以发生状态迁移,就是因为出现了事件。
-
动作:在状态机的迁移过程中,状态机会做出一些其它的行为,这些行为就是动作,动作是状态机对事件的响应。
-
条件:状态机对事件并不是有求必应的,状态机还要满足一定的条件才能发生状态迁移并实现对事件的响应。
对于状态机,我们的理解是,一个事物存在多个状态,这些状态可以相互转换,有些可能是双向的,有些可能是单向的。当一定的外部事件发生时,会促使事物的状态发生转变,这一过程就是发生了状态的迁移。当事物的状态发生转换后会执行一定的动作。但这些动作有可能是在状态转换进入时执行,也可能是状态持续过程中执行,这就要看动作执行的前提条件。
2、分析设计
接下来我们将以BLDC操作面板的实际操作项目来分析如何实现通过状态机机制简化条件结构。在这个BLDC操作面板项目中,我们使用按键操作来实现BLDC的操作控制以及LED显示菜单的切换。
首先我们来看一看BLDC的操作控制实现。对于BLDC的操作的操作,我们希望按下启动停止按钮时,BLDC启动并按设定的速度持续运行。在BLDC正常运行的过程中,如果长按启动停止按钮则进入全速状态,如果是短按启动停止按钮则停止。如果是在全速状态,如果长按启动停止按钮则停止,如果是短按启动停止按钮则回到常规速度状态。
对于这个需求如果我们使用条件结构则需要使用if/else语句或者switch/case语句来判断状态,然后在各个分支中通过条件判断按钮的动作以实现对应的操作。在这一方式下,我们需要使用条件结构的嵌套来实现这个过程,从逻辑结构上来说过于复杂而且不同功能模块的耦合比较紧密。
接下来,我们以状态机的机制来分析一下。我们注意到BLDC实际有3种状态,分别是停止状态、常速运行状态、全速运行状态。而这3种状态之间可以相互转化,但并无直接关联,在不同的状态下将执行不同的操作。它们之间将根据按钮的事件产生转换。对比前面我们对状态机的要素的表述,实际上已经完全具备了状态机的全部要素,所以我们将其状态转化过程表述如下图:
接下来我们看一看菜单的切换问题。菜单的切换更复杂一点,就是在不同的情况下,会有不同的显示。我们将其归为5类,也就是5个菜单,这些菜单根据按键的不同显示不同的菜单。我们将每个显示菜单定义为一种状态,那么其实就已经具备了状态机的全部5个要素,具体状态转换过程如下图所示:
我们将BLDC的控制以及显示菜单的切换抽象为状态机,以避免冗长的条件选择结构,简化程序逻辑结构,使得程序更为清晰。
3、软件实现
前述,我们已经分析了将BLDC的控制及显示菜单的切换使用状态机来实现的方法。接下来我们就来考虑其具体的实现方式。
首先,我们来分析BLDC的控制。我们已经知道BLDC的控制要求有3个状态:停止状态、常速状态、全速状态。通过按键事件来控制状态产生迁移并执行动作。我们需要一个变量来记录按键事件对BLDC产生的命令,这个命令变量取值0、1、2以对应3个状态的迁移命令。之所以去这样的3个值并没有什么特殊之处,仅仅只是为了我们在后续的处理中方便使用而已。同样我们需要变量来记录当前所处的状态,取值也用0、1、2对应3个状态。当然我们还需要定义每种状态下所对应的动作,为了操作方便我们将每种状态下的动作定义为一个单独的函数,也就是每种状态有一个响应函数。至于响应函数的实现则根据需求而定,函数中包括相应条件。具体如下:
void (*BldcControl[3])(void)={BldcStopHandler,
BldcNormalSpeedHandler,
BldcFullSpeedHandler};
BldcControl[aPara.phyPara.pumpStartStop]();
其中aPara.phyPara.pumpStartStop变量记录的是对按键事件的记录,状态机根据变量的值来调用状态响应函数来迁移并维持在指定的状态。三个函数对应三种状态下的响应函数。这样就实现了不同的事件迁移到不同的状态的状态机结构,相比于条件分支判断结构要简化很多。
接下来,我们再来看看菜单显示状态的实现。前面我们已经描述过菜单显示划分为5种状态,我们使用一个变量来记录状态及迁移。这个变量取值0、1、2、3、4分别对应当前速度显示状态、量程显示状态、全速设定显示状态、系数设定显示状态、速度设定显示状态。具体如下:
void (*LedDisPlay[5])(void)={SpeedCurrentDisplay,
SpeedUpperDisplay,
SpeedFullDisplay,
SpeedFactorDisplay,
SpeedSettingDisplay};
LedDisPlay[aPara.phyPara.menuIndex]();
同样的aPara.phyPara.menuIndex是状态迁移及状态记录变量,而5个函数则对应不同状态下的响应函数。
4、小结
在这一篇中,我们以一个BLDC驱动控制板的实例描述了使用状态机代替复杂的条件分支判断结构的过程及方法。我们实现了使用状态机机制编码BLDC的驱动控制和菜单显示切换的功能。这一实例已经应用于多个项目之中,效果良好。
这一方式其实适用于很多需要条件判断来切换控制的场合。事实上,我们在多个电机控制、流程控制等应用场合都是用了类似的方法,而且应用的结果都比较满意。当然,我们并不是建议读者使用此法,只是提供一种思路,我们认为所谓结构优化本就是见仁见智的事情。
欢迎关注:
如果阅读这篇文章让您略有所得,还请点击下方的【好文要顶】按钮。
当然,如果您想及时了解我的博客更新,不妨点击下方的【关注我】按钮。
如果您希望更方便且及时的阅读相关文章,也可以扫描上方二维码关注我的微信公众号【木南创智】