有限状态机编程

前言

接触了状态机编程算是比较长的事件了,一直被他的神奇的魔力吸引,但是一直没有搞明白它的工作原理。最近又试着尝试理解一下这个内容,感觉好像是明白了,写这篇笔记,算作记录。

状态机分析

状态机的工作核心

状态机的工作核心是:时钟。或者说是一个周期,每个周期保持一种状态,这种状态下执行某种任务。而下一个时钟周期到来的时候,通过判定条件,重新判定状态机的状态,进而确定这个周期内应该完成的任务是什么。所以,在使用状态机编程的时候,一定要记得这个。

状态机的使用

经过我一晚上的思考,我认为状态机编程其实应该分成两部分来完成,那就是状态机+事件,这两部分的内容应该是可以分开来完成的,这样应该就是比较好理解的。

到底是什么意思呢,就是这样的一个思路: 先把状态机实现出来,然后再把相应的事件和状态机的状态绑定到一起。 状态机负责判断当前的状态以及状态机的流转工作,而事件部分通过状态机的状态来决定当前执行什么事件或者说做什么工作。当一件事情完成的时候就退出事件部分,让状态机继续流转工作,改变状态做其他的事情,如此循环。下面我通过一个按键的例子来说明状态机是怎么工作的。

状态机实现多功能按键

先给定这样的要求:

按键按下的时候,程序执行任务A,按键松开的时候,执行任务B,当按键长按的时候,执行任务C,需要考虑按键的抖动。

这里,我们可以把按键的状态做成状态机,然后把任务A、B、C 和按键的按下状态、松开状态、长按状态绑定在一起。下面,我们先来实现状态机:

假设KEY为按键的电平,count为保持按下状态的周期次数,现有如下规则:

  • KEY=1,按键按下
  • KEY=0,按键弹起
  • count=5,代表长按,否则只算是按键按下而不算长按

状态机工作流程分析

我们上面说过,状态机的状态是有生命周期的,它的某一个状态只能维持一段时间。我们现在分析按键的消抖应该怎么做:

一般情况下,按键的初始状态都是弹起的, 当你按键按下的时候,按键会有大概 10us~20us 的抖动时间,这段时间内,按键 IO 的电平是不稳定的。我们可以这样子来消除抖动:我们以 15us 为周期对按键的电平进行检测,在按键弹起状态下,如果我检测到 KEY=1 ,说明此时按键被按下过,那么这时状态机的状态切换到抖动状态。至于这是抖动还是按键按下,我们通过下一个周期来判断。

按键进入了抖动状态,进入到下一个周期。在新的周期内,再次检测 IO 电平,如果说还是检测到 KEY=1 说明按键是真的被按下了(因为上一个周期已经经历了 15us 的时间,已经经过了按键抖动的那个时间),那么这是状态机切换到按下状态;如果说检测到 KEY=0 ,说明导致电平变化的原因确实是抖动而不是按键按下,那么这是我们把状态机切换会弹起状态。

按下弹起的过程也是如此分析。

那么,怎么判断是不是长按呢?

我们可以在按下状态下设置一个变量 count ,当上一个状态时按下并且当前状态也是按下的时候,进行 count++操作,当count>=5时,说明按键按下这个状态已经保持了5个周期了,也就是说按键长按了,这是就把把状态机切换到长按状态。

注意
在这里其实变量自增这个内容属于是事件的部分了,只不过这里它是驱动状态机工作的判定条件之一,所以应该也是属于状态机的内容

按下长按两个状态下如果 KEY 电平发生跳变,都应该跳到抖动状态,这里我设置抖动之后不能回到长按状态,所以长按状态下如果发生抖动,count 的值会归零。

下面是我的状态转换图,也就是状态机的工作流程图:

注意
(count++)表示进行的操作

事件分析

上面我们已经分析过按键这个任务的状态机工作流程了。下面我们来分析一下怎么把相关的事件绑定到状态机的状态上。

在一个状态机周期内,当状态机确定好当前的状态的时候。我们可以以状态机的当前状态来判定我们应该运行把鞋任务的代码,伪代码框架如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static state;
while(1)
{
    state=state_check();
    switch(state)
    {
        case 弹起:
            {
                A();
                break;
            }
        case 按下:
            {
                B();
                break;
            }
        case 长按:
            {
                C();
                break;
            }
        default:break;
    }
    delay_us(15);//状态机最小周期时间
}
注意
在上面的伪代码中,系统的实时性是得不到保障的,在执行相关任务的时候会占用CPU资源,此时即使按键状态变化系统也无法检测到。所以,以上的伪代码只能在相应状态的任务完成之后才能进入到下一轮状态判断。

总结分析

状态机编程我感觉是嵌入式编程的一个很好的编程思想,它使得你的代码逻辑更加清晰明了,而不是乱糟糟的(尤其是裸机开发的时候)。可以用作裸机的代码框架,也可以结合 RTOS 来使用,应用它来开发真的方便的不得了。

至于其他的实例只能以后有机会再进行了,现在有一个打算是使用状态机多功能菜单、AT设备的状态转换等等功能,具体什么时候实现,未知!

0%