谈谈单片机编程思想——状态机

    玩单片机还可以,各个外设也都会驱动,但是如果让你完整的写一套代码时,却无逻辑与框架可言。这说明编程还处于比较低的水平,你需要学会一种好的编程框架或者一种编程思想!比如模块化编程、状态机编程、分层思想等。

    本文来说一下状态机编程。

什么是状态机?

    状态机(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所示),但不需要事件触发。

    图中还有一个包含黑色实心圆点的圆圈,表示状态机生命周期的结束,这个例子中的状态机生生不息,所以没有状态指向该圆圈。

程序代码

    下面是根据上述状态转换图写成的代码:

void main(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  {  ; /*idle code*/  } }
}
void fsm_active(void)
{if(g_stFSM.u8KeyCnt > 3) /*击键是否满 5 次*/ { switch(g_stFSM.u8LedStat)  {  case LS_OFFOFF:   led_on(LED1); /*输出动作*/    g_stFSM.u8KeyCnt = 0;    g_stFSM.u8LedStat = LS_ONOFF; /*状态迁移*/    break;   case LS_ONOFF:    led_on(LED2); /*输出动作*/    g_stFSM.u8KeyCnt = 0;    g_stFSM.u8LedStat = LS_ONON; /*状态迁移*/    break;   case LS_ONON:    led_off(LED1); /*输出动作*/    g_stFSM.u8KeyCnt = 0;    g_stFSM.u8LedStat = LS_OFFON; /*状态迁移*/    break;   case LS_OFFON:    led_off(LED2); /*输出动作*/    g_stFSM.u8KeyCnt = 0;    g_stFSM.u8LedStat = LS_OFFOFF; /*状态迁移*/    break;   default: /*非法状态*/    led_off(LED1);    led_off(LED2);    g_stFSM.u8KeyCnt = 0;    g_stFSM.u8LedStat = LS_OFFOFF; /*恢复初始状态*/    break;  } } else {  g_stFSM.u8KeyCnt  ; /*状态不迁移,仅记录击键次数*/ }
}

    先看一下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。

声明:

本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/338397.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言结构体使用与指针的理解

以前总有一种疑惑。为什么结构体的指针有的需要用分配空间?有的不需要分配空间呢?现在总结一下思路:一:关于结构体的定义问题:使用结构体一般会使用变量或者定义指针typedef struct{ int a; int b; }data;使用这个结构…

elementui 进度条怎么做_小E,Excel中这样的进度条是怎么做出来的?

我的目标:让中国的大学生走出校门的那一刻就已经具备这些office技能,让职场人士能高效使用office为其服务。支持我,也为自己加油!前面我们分享过如何做进度条:《Excel进度条启示:专注与持续积累定会让人生出…

C语言结构体描述BMP的文件格式

BMP文件的结构其实非常简单,就是两个结构体+一个可选的调色板+位图数据。第一个结构体是BITMAPFILEHEADER,第二个结构体是BITMAPINFOHEADER。然后就是可选的调色板(RGBQUAD数组)。最后是位图数据。第一个结…

php mysql 常用语句_PHP mysql基本语句指令

1 /* 选择数据库 2 use test; 3 */ 4 5 /* 显示所有的数据库 6 show databases; 7 */ 8 9 /* 删除表/数据库 10 drop database test1; 11 delete from user1 where id4; 12 */ 13 14 /* 创建表 15 CREATE TABLE user1( 16 id int primary key auto_increment1 /*选择数据库2 us…

C语言Main函数到底有几种,你真的懂吗?

乍一看标题,感觉小编小题大做,但凡学过C语言的聚聚,都知道C程序入口就是main函数,且一套程序里面有且仅有一个。但是很多时候我们看到的main函数却并不是千篇一律,格式竟然会有差别,这究竟是为啥&#xff1…

python大型项目经验_图像分类:13个Kaggle项目的经验总结

来源:数据派THU任何领域的成功都可以归结为一套小规则和基本原则,当它们结合在一起时会产生伟大的结果。机器学习和图像分类也不例外,工程师们可以通过参加像Kaggle这样的竞赛来展示最佳实践。在这篇文章中,我将给你很多资源来学习…

C语言程序main入口函数

一.main()函数是什么样的我们先要搞清楚main()函数有哪几种?查阅C89/C99/C11标准文档,里面明确固定了两种写法:int main(void) { /* ... */ }int main(int argc, char *argv[]) { /* ... */ }除此之外,其他写法应该都是不规范的写…

spring可用于数据层吗_Spring XD用于数据提取

spring可用于数据层吗Spring XD是一个功能强大的工具,它是一组可安装的Spring Boot服务,可以独立运行,在YARN或EC2之上运行。 Spring XD还包括一个管理UI网站和一个用于作业和流管理的命令行工具。 Spring XD是一组功能强大的服务&#xff0c…

go语言mysql操作_使用Go语言操作MySQL数据库的思路与步骤

最近在做注册登录服务时,学习用Go语言操作MySQL数据库实现用户数据的增删改查,现将个人学习心得总结如下,另外附有代码仓库地址,欢迎各位有兴趣的fork。软件环境:Goland、Navicat for MySQL。一、实现思路1&#xff0c…

学习嵌入式C语言的6个层级,你在哪一层?

C语言可以说是一种经典的编程语言,没有C语言就没有今天的各种操作系统。C语言是基础,那么你掌握了多少?新手级别学习目的:过计算机二级,考证,应付期末考试。需要掌握的程度:掌握C语言的基本语法…

intellij idea_IntelliJ IDEA内部设计

intellij ideaIntelliJ IDEA的第一个版本于2001年1月发布,当时它是第一个集成了高级代码导航和代码重构功能的Java IDE之一。 2009年,JetBrains开源了其社区版本 。 从那时起,创建了许多基于它的IDE,例如Google的Android Studio。…

C语言 | 函数执行成功时,return 1 还是return 0?

今天分享的内容是关于函数执行成功,返回0还是1的讨论~基本上,没有人会将大段的C语言代码全部塞入 main() 函数,更好的做法是按照复用率高,耦合性低的原则,尽可能的将代码拆分不同的功能模块,并封装成函数。…

jcache_窥探JCache API(JSR 107)

jcache这篇文章从较高的层次介绍了JCache API,并提供了一个预告片–仅够您(希望)开始对此发痒了;-) 在这篇文章中……。 JCache概述 JCache API,实现 JCache API支持的(Java)平台 快速了解O…

redis 启动加载mysql_Redis分析系列:启动加载过程

从本篇文章开始(命名为Redis分析系列),将会通过分析Redis的源代码(以Redis 2.2.0 RC1为准),来对它的内部实现做一些探讨。本文主要介绍Redis启动加载过程,总体上可以分为如下几步:1. 初始化全局服务器配置2. 加载配置文件(如果指定…

c 文件怎么进行读取和写入操作?

C >>和<<读写文本文件&#xff1a;fstream 或者 ifstream 类负责实现对文件的读取&#xff0c;它们内部都对 >> 输出流运算符做了重载&#xff1b;同样&#xff0c;fstream 和 ofstream 类负责实现对文件的写入&#xff0c;它们的内部也都对 << 输出流…

mysql+误操作怎么恢复_Mysql误操作恢复流程

一、开启binlog。show variables like log_bin;#vim /etc/my.cnf在[mysqld]中加入log-bin mysql-binlog-bin /usr/local/mysql/log/mysql-bin.log重启mysql服务#service mysqld stop#service mysqld start二、数据写入建库create database …

drools6.5_Drools 6.2.0.Final发布

drools6.5我们很高兴地宣布最新&#xff0c;最出色的Drools 6.2.0.Final版本。 特别是此发行版更加注重改进的可用性和功能&#xff0c;这些功能使项目更易于使用&#xff08;和采用&#xff09;。 新功能包括对工作台UI的大量改进&#xff0c;对社交活动和插件管理的支持以及…

c程序编写x的y次方的方法

c程序怎么编写x的y次方?C语言pow()函数&#xff1a;求x的y次方&#xff08;次幂&#xff09;头文件&#xff1a;#include pow() 函数用来求 x 的 y 次幂&#xff08;次方&#xff09;&#xff0c;其原型为&#xff1a;double pow(double x, double y);pow()用来计算以x 为底的…

8条嵌入式C语言编程小知识总结

1. 流水线被指令填满时才能发挥最大效能&#xff0c;即每时钟周期完成一条指令的执行(仅指单周期指令)。如果程序发生跳转&#xff0c;流水线会被清空&#xff0c;这将需要几个时钟才能使流水线再次填满。因此&#xff0c;尽量少的使用跳转指令可以提高程序执行效率&#xff0c…

c语言函数的三种调用方式是什么?

函数的三种调用方式&#xff1a;1、函数作为表达式中的一项出现在表达式中&#xff0c;例“zmax(x,y)”&#xff1b;2、函数作为一个单独的语句&#xff0c;例“printf("%d",a)”&#xff1b;3、函数作为调用另一个函数时的实参&#xff0c;例“printf("%d"…