当前位置:首页 > 公众号精选 > wenzi嵌入式软件
[导读]笔者能力有限,如果文中出现错误的地方,欢迎大家给我指出来,我将不胜感激,谢谢~同时如果各位朋友对于状态机还有不同的想法,笔者也很希望能够互相交流,微信二维码在公众号底部获取。 状态机的概念 有限状态机又称有限状态自动机,简称状态机,是表示有限

笔者能力有限,如果文中出现错误的地方,欢迎大家给我指出来,我将不胜感激,谢谢~同时如果各位朋友对于状态机还有不同的想法,笔者也很希望能够互相交流,微信二维码在公众号底部获取。

状态机的概念

有限状态机又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型,用英文缩写也被简称为 FSM。 FSM 会响应“事件”而改变状态,当事件发生时,就会调用一个函数,而且 FSM 会执行动作产生输出,所执行的动作会因为当前系统的状态和输入的事件不同而不同。

问题背景

为了更好地描述状态机的应用,这里用一个地铁站的闸机为背景,简单叙述一下闸机的工作流程:通常闸机默认是关闭的,当闸机检测到有效的卡片信息后,打开闸机,当乘客通过后,关闭闸机;如果有人非法通过,那么闸机就会产生报警,如果闸机已经打开,而乘客仍然在刷卡,那么闸机将会显示票价和余额,并在屏幕输出“请通过,谢谢”。在了解了闸机的工作流程之后,我们就可以画出闸机的状态图,状态图如下:在上图中,线条上面的字表示的是:闸机输入事件/闸机执行动作,方框内表示的是闸机的状态。 除了使用状态图来表示系统的工作流程外,我们也可以采用状态表的方式来表示系统的工作流程,状态表如下所示:

通过上述我们已经知道闸机的工作流程了,接下来我们来看具体的实现。

代码实现

嵌套的 switch 语句

使用嵌套的 switch 语句是最为直接的办法,也是最容易想的方法,第一层 switch 用于状态管理,第二层 switch 用于管理各个状态下的各个事件。代码实现可以用下述伪代码来实现:

   
  1. switch(当前状态)

  2. {

  3. case LOCKED 状态:

  4. switch(事件):

  5. {

  6. case card 事件:

  7. 切换至 UNLOCKED 状态;

  8. 执行 unlock 动作;

  9. break;

  10. case pass 事件:

  11. 执行 alarm 动作;

  12. break;

  13. }

  14. break;

  15. case UNLOCKED 状态:

  16. switch(事件):

  17. {

  18. case card 事件:

  19. 执行 thankyou 动作;

  20. break;

  21. case pass 事件:

  22. 切换至 LOCKED 状态;

  23. 执行 lock 动作;

  24. break;

  25. }

  26. break;

  27. }

上述代码虽然很直观,但是状态和事件都出现在一个处理函数中,对于一个大型的 FSM 中,可能存在大量的状态和事件,那么代码量将是非常冗长的。为了解决这个问题,可以采用状态转移表的方法来处理。

状态转移表

为了减少代码的长度,可以使用查表法,将各个信息存放于一个表中,根据事件和状态查找表项,找到需要执行的动作以及即将转换的状态。

   
  1. typedef struct _transition_t

  2. {

  3. 状态;

  4. 事件;

  5. 转换为新的状态;

  6. 执行的动作;

  7. }transition_t;


  8. transition_t transitions[] = {

  9. {LOCKED 状态,card 事件,状态转换为UNLOCKED,unlock动作},

  10. {LOCKED 状态,pass 事件,状态保持为LOCKED,alarm 动作},

  11. {UNLOCKED 状态,card 事件,状态转换为 UNLOCKEDthankyou动作},

  12. {UNLOCKED 状态,pass 事件,状态转换为 LOCKED,lock 动作}

  13. };


  14. for (int i = 0;i < sizeof(transition)/sizeof(transition[0]);i++)

  15. {

  16. if (当前状态 == transition[i].状态 && 事件 == transition[i].事件)

  17. {

  18. 切换状态为:transition[i].转换为新的状态;

  19. 执行动作:transition[i].执行的动作;

  20. break;

  21. }

  22. }

从上述我们可以看到如果要往状态机中添加新的流程,那么只需要往状态表中添加东西就可以了,也就是说整个状态机的维护及管理只需要把重心放到状态转移表的维护中就可以了,从代码量也可以看出来,采用状态转移表的方法相比于第一种方法也大大地缩减了代码量,而且也更容易维护。但是对于状态转移表来说,缺点也是显而易见的,对于大型的 FSM 来说,遍历状态转移表需要花费大量的时间,从而影响代码的执行效率。 那要怎样设计代码量少,又不需要以遍历状态转移表的形式从而花费大量时间的状态机呢?这个时候就需要以面向对象的思想来设计有限状态机。

面向对象法设计状态机

面向对象基本概念

以面向对象的思想实现的状态机,大量涉及了对于函数指针的用法,必须对这个概念比较熟悉

上述所提到了两个设计方法都是基于面向过程的一种设计思想,面向过程编程(POP)是一种以过程为中心的编程思想,以正在发生的事件为主要目标,指导开发者利用算法作为基本构建块构建复杂系统。 即将所要介绍的面向对象编程(OOP)是利用类和对象作为基本构建块,因此分解系统时,可以从算法开始,也可以从对象开始,然后利用所得到的结构作为框架构建系统。 提到面向对象编程,那自然绕不开面向对象的三个基本特征:

  • 封装:隐藏对象的属性和实现细节,仅仅对外公开接口

  • 继承:使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展,C 语言使用 struct 的特性实现继承

  • 多态性:使用相同的方法,根据对象的类型调用不同的处理函数。

上述对于面向对象的三个基本特征做了一个简单的介绍,封装和继承的概念都都比较清晰,多态性这个特点可能会有所迷惑,在这里笔者用在书中看到一个例子来解释多态性,例子是这样的: 要求画一个形状,这个形状是可能是圆形,矩形,星形,无论是什么图形,其共性都是需要调用一个画的方法来进行绘制,绘制的形状可以通过函数指针调用各自的绘图代码绘制,这就是多态的意义,根据对象的类型调用不同的处理函数。 在介绍了上述很基本的概念之后,我们来看状态机的设计。

实现细节

我们由浅入深地来思考这个问题,首先我们可以想到把闸机当做一个对象,那么这个这个对象的职责就是处理 card 事件(刷卡)和 pass 事件(通过闸机),闸机会根据当前的状态执行不同的动作,也就有了如下的代码:

   
  1. enum {LOCKED,UNLOCKED};/*枚举各个状态*/


  2. /*定义闸机类*/

  3. typedef struct _turnstile

  4. {

  5. int state;

  6. void (*card)(struct _turnstile *p_this);

  7. void (*pass)(struct _turnstile *p_this);

  8. }turnstile_t;


  9. /* 闸机 card 事件 */

  10. void turnstile_card(turnstile_t *p_this)

  11. {

  12. if (p_this->state == LOCKED)

  13. {

  14. /* 切换至解锁状态 */

  15. /* 执行unlock动作,调用 unlock 函数 */

  16. }

  17. else

  18. {

  19. /* 执行 thank you 动作,调用 thank you 函数 */

  20. }

  21. }


  22. /* 闸机 pass 事件*/

  23. void turnstile_pass(turnstile_t *p_this)

  24. {

  25. if (p_this->state == LOCKED)

  26. {

  27. /* 执行 alarm 动作,调用 alarm 函数*/

  28. }

  29. else

  30. {

  31. /* 状态切换至锁闭状态 */

  32. /* 执行 lock 动作,调用 lock 函数 */

  33. }

  34. }

上述代码的思想实现的有限状态机相比于前两种不需要进行大量的遍历,也不会导致代码量的冗长,看似已经比较完美了,但是我们再仔细想想,如果此时状态更改了,那 turnstilecard 函数和 turnstilepass 函数都要更改,也就是说事件和状态存在着耦合,这与“高内聚,低耦合”的思想所违背,也就是说如果我们要继续优化代码,那需要对事件和状态进行解耦。

状态和事件解耦

将事件与状态相分离,从而使得各个状态的事件处理函数非常的单一,因此在这里需要定义一个状态类:

   
  1. typedef struct _turnstile_state_t

  2. {

  3. void (*card)(void); /* card 事件处理函数 */

  4. void (*pass)(void); /* pass 事件处理函数 */

  5. }turnstile_state_t;

在定义了状态类之后,我们就可以使用状态类创建 lock 和 unlock 的实例并初始化。

   
  1. turnstile_state_t locked_state = {locked_card,locked_pass};

  2. turnstile_state_t unlocked_state = {unlocked_card,unlocked_pass};

在这里需要补充一下上述初始化项里函数里的具体实现。

   
  1. void locked_card(void)

  2. {

  3. /* 状态切换至解锁状态 */

  4. /* 执行 unlock 动作 ,调用 unlock 函数 */

  5. }


  6. void locked_pass(void)

  7. {

  8. /* 执行 alarm 动作,调用 alarm 函数 */

  9. }


  10. void unlocked_card(void)

  11. {

  12. /* 执行 thank you 动作,调用 thank you 函数 */

  13. }


  14. void unlocked_pass(void)

  15. {

  16. /* 状态切换至锁闭状态 */

  17. /* 执行 lock 动作,调用 lock 函数 */

  18. }

这样,也就实现了状态与事件的解耦,闸机不再需要判断当前的状态,而是直接调用不同状态提供的 card() 和 pass() 方法。定义了状态类之后,由于闸机是整个系统的中心,我们还需要定义闸机类,由于 turnstilestatet 中只存在方法,并不存在属性,那么我们可以这样来定义闸机类:

   
  1. typedef struct _turnstile_t

  2. {

  3. turnstile_state_t *p_state;

  4. }turnstile_t;

到这里,我们已经定义了闸机类,闸机状态类,以及闸机状态类实例,他们之间的关系如下图所示:通过图中我们也可以看到闸机类是继承于闸机状态类的,lockedstate 和 unlockedstate 实例是由闸机状态类派生而来的,那最底下的那个箭头是为什么呢?这是在后面需要讲到的对于闸机状态转换的处理,在获取输入事件调用具体的方法进行处理后,我们需要修改闸机类的pstate,所以也就有了这个箭头。 相比于最开始定义的闸机类,这个显得更加简洁了,同时 pstate 可以指向相应的状态对象,从而调用相应的事件处理函数。 在定义了一个闸机类之后,就可以通过闸机类定义一个闸机实例:

   
  1. turnstile_t turnstile;

然后通过函数进行初始化:

   
  1. void turnstile_init(turnstile_t *p_this)

  2. {

  3. p_this->p_state = &locked_state;

  4. }

整个系统闸机作为中心,进而需要定义闸机类的事件处理方法,定义方法如下:

   
  1. /* 闸机 card 事件*/

  2. void turnstile_card(turnstile_t *p_this)

  3. {

  4. p_this->p_state->card();

  5. }


  6. /* 闸机 pass 事件 */

  7. void turnstile_pass(turnstile_t *p_this)

  8. {

  9. p_this->p_state->pass();

  10. }

到这里,我们回顾前文所述,我们已经能够对闸机进行初始化并使得闸机根据不同的状态执行不同的处理函数了,再回顾整个闸机的工作流程,我们发现闸机在工作的时候会涉及到从 locked 状态到 unlocked 状态的相互变化,也就是状态的转移,因此状态转移函数可以这样实现:

   
  1. void turnstile_state_set(turnstile_t *p_this,turnstile_state_t *p_new_state)

  2. {

  3. p_this->p_state = p_new_state;

  4. }

而状态的转移是在事件处理之后进行变化的。那么我们可以这样修改处理函数,这里用输出语句替代闸机动作执行函数:

   
  1. void locked_card(turnstile_t *p_turnstile)

  2. {

  3. turnstile_state_set(p_turnstile,&unlocked_state);

  4. printf("unlock\n"); /* 执行 unlock 动作 */

  5. }


  6. void locked_pass(turnstile_t *p_turnstile)

  7. {

  8. printf("alarm\n"); /* 执行 alarm 动作*/

  9. }


  10. void unlocked_card(turnstile_t *p_turnstile)

  11. {

  12. printf("thankyou\n"); /* 执行 thank you 动作*/

  13. }


  14. void unlocked_pass(turnstile_t *p_turnstile)

  15. {

  16. turnstile_state_set(p_turnstile,&locked_state);

  17. printf("lock\n"); /* 执行 lock 动作 */

  18. }

既然处理函数都发生了变化,那么闸机状态类也应该发生更改,更改如下:

   
  1. typedef struct _turnstile_state_t

  2. {

  3. void (*card)(turnstile_t *p_turnstile);

  4. void (*pass)(turnstile_t *p_turnstile);

  5. }turnstile_state_t;

但是回顾之前我们给出的闸机类和闸机状态类的关系,闸机类是继承于闸机状态类的,也就是说先有的闸机状态类后有的闸机类,但是这里却在闸机状态类的方法中使用了闸机类的参数,其实这样也是可行的,需要提前对闸机类进行处理,总的闸机类状态类定义如下:

   
  1. #ifndef __TURNSTILE_H__

  2. #define __TURNSTILE_H__


  3. struct _turnstile_t;

  4. typedef struct _turnstile_t turnstile_t;


  5. typedef struct _turnstile_state_t

  6. {

  7. void (*card)(turnstile_t *p_turnstile);

  8. void (*pass)(turnstile_t *p_turnstile);

  9. }turnstile_state_t;


  10. typedef struct _turnstile_t

  11. {

  12. turnstile_state_t *p_state;

  13. }turnstile_t;


  14. void turnstile_init(turnstile_t *p_this); /* 闸机初始化 */

  15. void turnstile_card(turnstile_t *p_this); /* 闸机 card 事件处理 */

  16. void turnstile_pass(turnstile_t *p_this); /* 闸机 pass 事件处理 */


  17. #endif

上述就是所有的关于状态机的相关定义了,下面通过上述的定义实现状态机的实现:

   
  1. #include <stdio.h>

  2. #include <turnstile.h>


  3. int main(void)

  4. {

  5. int event;

  6. turnstile_t turnstile; /* 闸机实例 */

  7. turnstile_init(&turnstile); /* 初始化闸机为锁闭状态 */


  8. while(1)

  9. {

  10. scanf("%d",&event);

  11. switch(event)

  12. {

  13. case 0:

  14. turnstile_card(&turnstile);

  15. break;

  16. case 1:

  17. turnstile_pass(&turnstile);

  18. break;

  19. default:

  20. exit(0);

  21. }

  22. }

上述代码运行结果如下:

结论

以上便是笔者关于状态机的全部总结,讲述了面向过程和面向对象两种实现方法,虽然从篇幅上看面向对象的方法要更为复杂,但是代码的执行效率以及长度都要优于面向过程的方法,所以了解面向对象的程序设计方法是很有必要的。

这篇文章是在笔者学习了《程序设计与数据结构》周立功版后的自己的理解,该书的PDF版可以从立功科技官网的周立功专栏中获取,也可以在公众号回复 程序设计与数据结构获取,同时关于面向对象状态机的代码汇总版也可以在公众号回复 FSM获取。

您的阅读是对我最大的鼓励,您的建议是对我最大的提升,欢迎点击下方图片进入小程序进行评论



免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭