【架构】状态机-单片机的万能框架

avatar
作者
猴君
阅读量:4

目录

1 什么是状态机

1.1 状态机的概念

1.2 状态机具体化

2 状态机编程的优点

2.1 提高CPU使用效率

2.2 逻辑完备性

2.3 程序结构清晰

3 状态机在单片机上的应用

3.1 状态转换图

3.2 代码实现


1 什么是状态机

1.1 状态机的概念

状态机就是把一件事情分为几个过程来实现,每个过程对应一个状态,状态与状态之间的转换需要通过事件进行驱动。

以一个人每天的生活来举一个例子,比如一个人的生活包括吃饭、睡觉、正常搬砖、娱乐、加班等五个方面,这些状态可以通过一个状态图来表示。

1.2 状态机具体化

经过上一节,小伙伴们应该对状态机有了简单了解,下面对状态机做进一步说明。

状态机(state machine)有 5 个要素,分别是状态(state)、迁移(transition)、事件(event)、动作(action)、条件(guard)。

状态: 一个系统在某一时刻所存在的稳定的工作情况,系统在整个工作周期中可能有多个状态。例如一部电动机共有正转、反转、停转这 3 种状态。

一个状态机需要在状态集合中选取一个状态作为初始状态。

迁移:系统从一个状态转移到另一个状态的过程称作迁移,迁移不是自动发生的,需要外界对系统施加影响。停转的电动机自己不会转起来,让它转起来必须上电。

事件:某一时刻发生的对系统有意义的事情,状态机之所以发生状态迁移,就是因为出现了事件。对电动机来讲,加正电压、加负电压、断电就是事件。

动作:在状态机的迁移过程中,状态机会做出一些其它的行为,这些行为就是动作,动作是状态机对事件的响应。给停转的电动机加正电压,电动机由停转状态迁移到正转状态,同时会启动电机,这个启动过程可以看做是动作,也就是对上电事件的响应。

条件:状态机对事件并不是有求必应的,有了事件,状态机还要满足一定的条件才能发生状态迁移。还是以停转状态的电动机为例,虽然合闸上电了,但是如果供电线路有问题的话,电动机还是不能转起来。

2 状态机编程的优点

2.1 提高CPU使用效率

话说我只要见到满篇都是delay_ms()的程序就会头疼,因为这些软件延时是对CPU资源的巨大浪费,宝贵的CPU机时都浪费在了NOP指令上。那种为了等待一个管脚电平跳变或者一个串口数据而岿然不动的程序也让我非常纠结,如果事件一直不发生,你要等到世界末日么?

把程序状态机化,这种情况就会明显改观,程序只需要用全局变量记录下工作状态,就可以转头去干别的工作了,当然忙完那些活儿之后要再看看工作状态有没有变化。只要目标事件(定时未到、电平没跳变、串口数据没收完)还没发生,工作状态就不会改变,程序就一直重复着“查询—做任务—查询—做任务”这样的循环,这样CPU就闲不下来了。

这种处理方法的实质就是在程序等待事件的过程中间隔性地插入一些有意义的工作,好让CPU不是一直无谓地等待。

2.2 逻辑完备性

对于一个逻辑完备的反应式系统,不管什么样的事件组合,系统都能正确处理事件,而且系统自身的工作状态也一直处在可知可控的状态中。反过来,如果一个系统的逻辑功能不完备,在某些特定事件组合的驱动下,系统就会进入一个不可知不可控的状态,与设计者的意图相悖。使用状态机编程可以使程序的逻辑更加完善,从而减少BUG。

2.3 程序结构清晰

使用状态机编程可以使程序的结构更加清晰。

程序员最痛苦的事儿莫过于读别人写的代码。如果代码不是很规范,而且手里还没有流程图,读代码会让人晕了又晕,只有顺着程序一遍又一遍的看,很多遍之后才能隐约地明白程序大体的工作过程。有流程图会好一点,但是如果程序比较大,流程图也不会画得多详细,很多细节上的过程还是要从代码中理解。

相比之下,用状态机写的程序要好很多,拿一张标准的UML状态转换图,再配上一些简明的文字说明,程序中的各个要素一览无余。程序中有哪些状态,会发生哪些事件,状态机如何响应,响应之后跳转到哪个状态,这些都十分明朗,甚至许多动作细节都能从状态转换图中找到。可以毫不夸张的说,有了UML状态转换图,程序流程图写都不用写。

3 状态机在单片机上的应用

这一章我们来一个实战的练习,以片外ADC为例子。通常来讲,片外单芯片ADC采集的大致过程为:空闲状态->设置ADC开始采集->等待ADC采集完成->读取ADC采集数据,这里需要说明,片外这些ADC不会采集的特别快,采集一次需要的时间远比片上ADC的时间要长,如果单片机单纯的等待ADC采集完成,那么单片机的效率会变极低。所以采用状态机的方式,将ADC作为1个模块来完成。

下面描述一下ADC需要实现功能。ADC需要1s采集一次,采集后的数值保存到变量中。当然,通篇文章中的程序代码都是功能示意性代码,并非完整代码。

3.1 状态转换图

3.2 代码实现

主函数main: 在主函数中循环执行AdcModule()函数;

实现状态机的重要函数AdcModule:使用switch分支语句来实现状态机,包含了“检测采集时间是否到达”、“设置ADC开始采集”、“检测ADC是否采集完成”、“读取ADC的数据寄存器”和默认状态等五个状态,正常工作时一般处于前四个状态中的一个,当发生状态转移条件发生时,状态会发生迁移;

void AdcStatCheckTime(AdcStrcutTypeDef *pAdcStruct):“检测采集时间是否到达”状态需要执行的函数;

void AdcStatStart(AdcStrcutTypeDef *pAdcStruct):“设置ADC开始采集”状态需要执行的函数;

void AdcStatCheckComplete(AdcStrcutTypeDef *pAdcStruct):“检测ADC是否采集完成”状态需要执行的函数;

void AdcStatCheckReadData(AdcStrcutTypeDef *pAdcStruct):“读取ADC的数据寄存器”状态需要执行的函数。

#define ADC_ACQUISITION_PERIOD_MS (unsigned short)(1000)/*ADC 采集周期*/ #define ADC_STAT_CHECK_TIME (unsigned char)(0x00) /*检查是否到达 ADC 采集时间*/ #define ADC_STAT_START (unsigned char)(0x01) /*启动 ADC 采集*/ #define ADC_STAT_CHECK_COMPLETE (unsigned char)(0x02) /*检查 ADC 是否采集完成*/ #define ADC_STAT_CHECK_READ_DATA (unsigned char)(0x03) /*读取 ADC 采集的数据*/   typedef struct {         unsigned short CountMs; /*ADC 采集周期计时*/         unsigned short AdcData; /*ADC 采集的数据*/         unsigned char stat; /*ADC 模块状态变量*/ }AdcStructTypeDef;   AdcStrcutTypeDef AdcStruct;   /*主函数*/ int main() {     while(1)     {          /*ADC 模块采集*/         AdcModule(&AdcStruct);     } }    /*定时器 1ms 中断函数,用于记录采集时间是否到达采集时间*/ void TimerInterruptMs(void) {     /*在定时器中, 改变 ADC 的计数变量*/     pAdcStruct->CountMs++; }   void AdcMoudle(AdcStrcutTypeDef *pAdcStruct) {     /*根据当前状态判断执行哪一个状态处理函数*/     switch(pAdcStrcut->stat)     {         case ADC_STAT_CHECK_TIME :              AdcStatCheckTime(pAdcStruct);             break;         case ADC_STAT_START :              AdcStatStart(pAdcStruct);             break;         case ADC_STAT_CHECK_COMPLETE :              AdcStatCheckComplete(pAdcStruct);             break;         case ADC_STAT_CHECK_READ_DATA :              AdcStatCheckReadData(pAdcStruct);             break;         /*默认情况下设置状态为 ADC 检查是否到采集时间*/         default :             pAdcStruct->stat = ADC_STAT_CHECK_TIME;             break;     } }   /* “检测采集时间是否到达”状态需要执行的函数 */  void AdcStatCheckTime(AdcStrcutTypeDef *pAdcStruct) {         if(pAdcStruct->CountMs < ADC_ACQUISITION_PERIOD_MS)         {              /*采集周期没有到达直接返回*/             return;         }                  /*清零计时器*/         pAdcStruct->CountMs = 0;         /*设置状态为 启动 ADC 采集*/         pAdcStruct->stat = ADC_STAT_START;         return; }  /* “设置ADC开始采集”状态需要执行的函数 */  void AdcStatStart(AdcStrcutTypeDef *pAdcStruct) {         /*设置 ADC 开始转换*/         AdcStartCmd();         /*设置状态为 检查 ADC 是否采集完成*/         pAdcStruct->stat = ADC_STAT_CHECK_COMPLETE;         return; }  /* “检测ADC是否采集完成”状态需要执行的函数 */        void AdcStatCheckComplete(AdcStrcutTypeDef *pAdcStruct) {          /*读取 ADC 状态,判断是否采集完成*/         if(AdcReadCompleteStat() == 0x00)         {             /*没有采集完成直接返回*/             return;         }          /*设置状态为:读取ADC的数据寄存器*/     pAdcStruct->stat = ADC_STAT_CHECK_READ_DATA; }  /* “读取ADC的数据寄存器”状态需要执行的函数 */  void AdcStatCheckReadData(AdcStrcutTypeDef *pAdcStruct) {         /*读取 ADC 采集的数据到变量*/        pAdcStruct->AdcData = AdcReadData();         /*设置状态为 检查是否到达 ADC 采集时间*/     pAdcStruct->stat = ADC_STAT_CHECK_TIME; }    

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!