从零开始,漫谈状态机
ID:逻辑思维
作者:GorgonMeducer
【说在前面的话】
(图片来源:https://en.wikipedia.org/wiki/Finite-state_machine)
【正文】
怎么理解状态?如何才算一个状态
extern bool serial_out(uint8_t chByte);
函数serial_out()可以用来向某个串行外设发送一个字符,比如UART。如果成功了就返回true,如果设备正忙导致本次发送失败则立即返回false。由于外设的发送速度相对CPU的运行频率来说差了好几个数量级——在CPU眼中外设慢得跟蜗牛一样,所以每次通过serial_out() 发送字符不一定是成功——很可能外设还在努力“消化”上一次的字符。这种情况下,如果我们要在状态机中描述发送字符这样的行为,就值得为其单独分配一个状态,因为它满足了我们前面说的条件:1)一件事情你要不停的尝试才有可能成功,而且2)每次做都可能会产生2个以上的结果。习惯上,我们会用图示的方法来描述状态,以发送字符'H'为例:
从图中很容易注意到:
我们用圆圈来表示一个状态;
圆圈中心我们会写一些注释性质的内容用来帮助人们理解这个状态是做什么的;
图中有三个箭头,最左上角单纯“指向”状态的箭头表示从别的什么地方“跃迁”到了当前状态——我们称为“扇入”;下方从当前状态指向别的什么地方的箭头表示从当前状态离开;——我们成为“扇出”;右上角从当前状态“扇出”后又“返回到”当前状态的情况,我们称之为“自返”——也就是返回自己的意思。是不是特别简单。
实际使用的时候,如果单凭一个状态圆圈里面的注释文字,我们仍然不能理解这个状态实际做了什么事情;或者说我们非常好奇这个状态实际尝试做了什么动作,就可以通过以下的标注方法追加更多的信息,比如:
你看,是不是更加清晰了?同样的情况还可以推广到“调用一个函数而函数有多个不同的返回值”的情况;或者是“我们通过调用函数做了一件事情,虽然函数没有返回值,但是我们可以通过多种其它手段来获得这件事情的多个不同结果”的情况等等——领会精神,以此类推。
【第二种情况】:假设我们只是单纯的在等待某一个事情发生;或者等待某个一结果——这个结果由2个以上的返回值组成等等,那么这个等待行为就需要分配一个独立的状态。举个例子:
int32_t get_sensor_voltage(void);
函数get_sensor_volatage()可以返回某个传感器的电压值;我们设置了上下两个门限,一旦电压超过了任何一个门限,我们就切换到其它状态,对应的状态图示如下:
在这里,HIGH_THRESHOLD和LOW_THRESHOLD是两个宏表示上下两个门限。可以看到,这个状态表示:如果传感器的电压值在两个门限之间,我们就留在当前状态(通过自返回);如果任意门限被超过,我们就相应的跳转到别的状态去。
所有的神奇都在状态跃迁上
在这个例子中,我们注意到:
虽然左上角扇入Delay状态的跃迁条件我们并不知道,但在此时复位计数器s_wCounter是再好不过了。所以我们空出了跃迁条件,并在横线的下方写下了计数器的初始化代码;
右上角的跃迁条件是:“如果计数器的值小于延时1s所需的最大值”,那么对应的动作就是让计数器自增;
右下角跃迁的条件是:“计数器的值超过了规定的最大值”,因此直接跳到目标状态而无需做其它动作。
状态机的起点和终点
容易看出,这里 start 不仅是整个状态机的起点,还兼任了扇入Delay状态的跃迁的条件——从图上来看,很容易理解成:“当状态机开始时复位计数器s_wCounter”——可谓一目了然。
状态机有多简单
“不要问,问就是子状态机”
如果状态机不能调用子状态机,那它跟咸鱼有什么两样?那么如何用图示表示子状态机呢?废话少说,直接上图:
如图所示:
子状态机是被圆角矩形包裹的
子状态机的右上角有一个自反的状态迁移,条件是“on going”意味子状态机正在执行,还未得出一个结果;
子状态机的右下角(或者别的什么位置)需要有一个标记有cpl条件的状态迁移,表示当子状态机内部达到了终点cpl以后,子状态机从这里退出并跃迁到指定的状态;
子状态机有一个标题栏,里面分别列举了状态机的名称以及传递给当前子状态机的形参列表。(状态机的返回值只能是类似cpl, on-going这样的状态,所以不需要特别标记)
通过子状态机调用,我们很容易用已有的状态机实现搭积木的功能,比如假设我们将此前Delay的状态机也做成子状态机,配合这个已有的print_hello子状态机,就可以轻松实现一个“打印hello然后延时1秒”的状态机:
(这里需要注意,当子状态机被调用时,它使用圆角矩形替代了普通状态的圆圈。)
怎么样,是不是很简单?
【后记】