本篇接上一篇事件驱动框架之后,介绍状态机的原理相关的,以及事件驱动框架下事件处理状态机的实现。因为代码大多还是参照QP源码,所以仅供学习使用。
有限状态机,(英语:Finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
引用百度百科:http://baike.baidu.com/link?url=H4dYy2lfIm5yiGVQ0ExlYjEKCHFwByR1iBaySgXcQke9K7C4xPF05Ji8mYyvTzrTlzKBg9UJDXLhtbe_DGdtbWlcMUK-u3XdTpzlNODiLkR8vfU2YfqJxBy1L0YnurXcjBzG2HXC0R1Dkks3wkQz2a
状态机的概念在很多地方都有用到。我在学数字电路的时候和编程的时候都有接触过这种数学模型:其实说白了就是一种数学模型,它的原理都是相似的。这里主要在嵌入式编程中的引用。
网上应该有状态机最简单的点小灯的例子来借助理解状态机怎么编程,这里就直接放传送门了。
传送门空缺中。。。。
当然还有种层次状态机(HSM),这种直接把有限状态机(FSM)给包括进去了。结构上具有层次性,而且设计和实现都比较复杂。可以记住UML图来进行设计。
该部分参考了一部分的QP框架。QP的设计主要实现层次为实现层次状态机(HSM)的方式:规定每次事件处理完后的状态返回值(这个主要是根据UML图的模型,也就是必须开发者之前设计过的来实现的),状态转换过程中会自动根据返回值,判断实现是否需要调用进入和退出的状态回调。但是该种方式有一个缺点:它需要开发者对QP非常了解这个框架,函数回调是采用列举事件(switch)的方式,并且需要自己逻辑上使用状态机的编程思想,并且需要严格对照着UML图的状态转换方式进行编程。
下再介绍几种常用的状态机处理方式:但这些都不外乎提供2个接口:dispatch和initial。initial为初始化函数,即负责这个状态机控制的对象的初始化,及其设置状态机的初始状态。dispatch接口用来给状态机派发一个事件。
其实QP在这个功能上有点隐藏状态的意思。缺点就是需要把所有状态和时间都罗列,代码的重用性较低,内容容易膨胀。
该种方式是将事件和状态绑定到一张二维表上,然后根据不同的状态和时间进行规律的映射到这张表上,进行事件处理的回调。
缺点:这是一张静态的状态表,需要列举出所有的事件和状态,但状态和事件的类型增多时,维护就变得比较困难。
这种方式是把状态机作为一个对象,将状态的原型作为一个抽象类,然后子状态(具体状态)继承于抽象类,完成它的具体实现。个人感觉这种方式和上一种差不多,不过上一种相对来说多出了一张状态表。
缺点:多态的实现用C语言比较麻烦。如果继承每个状态还是需要列举出所有状态的实现的,如果对应的状态的事件为空,还是需要建立某个响应,比较浪费内存。如果是继承dispatch,则也需要在单个函数体内列举出事件响应。
PT协程会在后面专门章中介绍。
由于这个框架主要还是为了来做GUI的。整个框架采用事件驱动的结构来实现,主要形式采用状态表的形式。
这里讨论一点:就是HSM和FSM的关系。HSM是一种覆盖面很广的模型,包括了各种状态机的模型。但他的层次特征主要体现在对相同的状态类进行抽象,把同样的动作进行归并,比如说一个层次包含了它内包含多个具体状态的进入和退出的相同的操作。这样就减少了代码量和逻辑上的重复性。但如果不用抽象的概念,HSM依然是可以展开成拓展型FSM的(对于这种状态机有个专业的名字,忘了)。而QP的主要的状态机还是针对于HSM模型的,但HSM不容易理解,这也可能是限制了QP推广的原因。
因此在形式上,之前使用过的MATLAB的GUIDE和VB甚至是C#的界面设计时,他的动作响应callback回调函数令我印象深刻。比如:我希望鼠标按下图形化按钮后进行一个动作。这里我只需要在那个动作响应的函数里编程就可以了,剩下的事整个框架则会帮我自动完成,我无需关心它是如何被调用的。利用这个想法,因此我更偏向于使用状态表的方式。但是为了兼容一些类型的状态机,我仿照了QP的一些做法,并可选择性的保留了entry和exit的回调。当然,如果当前对象如果对此并没有要求,则可以省去。
除了SGUI以外,对于其他的对象来说,也可以采取这种策略或者其他几种常见的处理方式。
事件作为驱动整个框架的关键,对象之间通过接受事件,进行响应处理,状态的转化。事件驱动的结构如下:
typedef struct AEvtTag AEvt;
struct AEvtTag
{
ASignal sig; /* 事件信号量 */
uint16_t poolID; /* 内存池编号,对应的是事件块编号 */
};
ASignal 为型号量的类型,信号的多少可以在头文件中定义,选择不超出范围的大小。poolID是对应的事件编号。动态事件是从内存池中分配出来的,可以动态生成和释放,而静态事件则不能。
状态机的机构如下:
struct AFsmTag
{
const AStateHandler * state_table; /* 指向状态表 */
ASignal n_signals; /* 信号量总数 */
uint8_t n_states; /* 状态总数 */
uint8_t state; /* 当前状态编号 */
uint8_t method ; /* 方式: 提供2种:是否省略进入退出动作,默认忽略。
这里设置这个主要是为了省内存,如果找到更好的映射方式时改进*/
AStateHandler initial; /* 初始化 转换 */
};
其中大部分都是状态表设计的原型,用来辅助查表的实现的。这里我增加了一个method的变量,用来标志着个是否是基础的FSM带进入entry和exit的事件的。如果对于状态较多的对象,如果不需要用到进入和退出的动作不必要,则可以省去一大笔空间。
dipatch函数主要是实现事件的派送交给响应函数处理,另外如果有entry和exit,则会自动调用。不过这里有个比较麻烦的问题:用户在每次处理函数过后还是需要返回一个状态值(改变状态或者不改变)。
状态机初始化代码如下:显示调用初始化接口
/*! 预留信号状态 */
enum
{
A_ENTRY_SIG = 0, /* 进入状态动作 */
A_EXIT_SIG, /*退出状态动作 */
A_DEFAULT_SIG /*用户预留信号 */
};
void AFsm_init(AFsm *me)
{
AStateHandler t;
(me->initial)(me, (AEvt *)0); //调用对象初始化函数
assert((me->state) < (me->n_state));
if ((me->method == FSM_DEFAULT_METHOD))
{
t = *(me->state_table + me->state * me->n_signals + A_ENTRY_SIG); //默认进入预留
(*t)(me, (void *)0);
}
else
{
t = *(me->state_table + me->state * me->n_signals); //默认进入第一个状态
(*t)(me, (void *)0);
}
}
前面的枚举量为预留进入退出的信号标识。如果设置为FSM_DEFAULT_METHOD模式,则需要在该对象的第一个信号枚举=A_DEFAULT_SIG。那SGUI的自定义的一个响应举例:
/*! 自定义按键 */
enum KeyCode
{
TICK_SIG = A_DEFAULT_SIG,
UP_KEY_SIG,
DOWN_KEY_SIG,
LEFT_KEY_SIG,
RIGHT_KEY_SIG,
CONFIRM_KEY_SIG,
GUI_MAX_SIG
};
这样就默认将进入和退出的信号无形中加入到自定义的信号钱
初始化接口会自动调用对象的初始化函数,再将状态转换到默认状态:如果有设置状态的退出进入则会默认进入entry状态进行执行,否则进入第一个状态的第一个响应进行执行。(这里想着是否能改进一下,制定一下第一次进入的状态和时间)。
状态机调度代码如下:
void AFsm_dispatch(AFsm * me, AEvt const * e)
{
AStateHandler t;
uint8_t sta;
assert(e);//事件合法性
assert(state);//状态合法性
sta = me->state;
t = *(me->state_table + me->state * me->n_signals + e->sig); //获取状态表中的函数指针
if ((*t)(me, e) == A_RET_TRANS && (me->method == FSM_DEFAULT_METHOD)) //得到执行结果
{
/* 调用退出 */
t = *(me->state_table + sta * me->n_signals + A_EXIT_SIG);
(*t)(me, (void *)0);
/* 调用进入 */
t = *(me->state_table + me->state * me->n_signals + A_ENTRY_SIG);
(*t)(me, (void *)0);
}
}
如果该对象没有设置进入或者退出状态,则不执行进入退出段响应的代码。如果有设置FSM_DEFAULT_METHOD模式则在状态发生改变时自动调用进出状态的函数。
状态空响应的代码用来填充到状态表中对应状态和事件什么都不做的位置。这部分代码如下:
void AFsm_empty(AFsm * me, AEvt const * const e)
{
(void)me;
(void)e;
}
事件处理的状态机大概就这样了,原型还是用了状态表的原理。后来有听别人说过PT协程的方法来封装状态机,大概记得的印象中主要就是把while-case的结构隐藏到范式中,但实际上还是状态机的结构,但整体上看起来就和顺序编程没什么差别。因为看过很久了,而且正好要思考怎么拓展框架的灵活性,所以会单独拉一张来介绍下PT协程。