技术系列之“状态机”
一、状态机描述
状态机理论最初的发展在数字电路设计领域。在数字电路方面,根据输出是否与输入信号有关,状态机可以划分为Mealy型和Moore型状态机;根据输出是否与输入信号同步,状态机可以划分为异步和同步状态机。而在软件设计领域,状态机设计的理论俨然已经自成一体。Moore型状态机的输出只和当前状态有关,和输入无关,如果在软件设计领域设计出这种类型的状态机,则该状态机接受的事件都是无内蕴信息的事件(输入)。Mealy型状态机的输入是由当前状态和输入共同决定,对应到软件设计领域,则该状态机接收的事件含有内蕴信息,并且影响状态机的输出。显然,这种划分在软件设计领域毫无意义。虽然软件设计领域的状态机也有同步和异步的划分,但和数字电路方面的同步异步已经不同。
除了《数字电路》,涉及到状态机的课程就是《编译原理》了(本人属计算机专业,其它专业是否涉及到状态机就不清楚了)。下面简单回顾一下《编译原理》里有关有限状态机的描述。在编译原理课程里面,对有限状态机的描述仅限在编译领域,特定状态,针对输入字符,发生状态改变,没有额外的行为,另编译原理里有限状态机的构成要素,还包含唯一的初始状态和一个终态集。数学语言描述如下:一个有限状态机M是一个五元组,M=(K,E,T,S,Z)。其中(1)K是一个有穷集,其中的每个元素称为状态;(2)E是一个有穷字母表,它的每个元素称为一个输入字符;(3)T是转换函数,是K×E->K上的映射;(4)S是K中的元素,是唯一的一个初态;(5)Z是K的一个子集,是一个终态集,或者叫结束集。很明显,状态机在编译原理里的讲解已经特化,输入被定位为字符集,状态改变的时候没有额外动作发生。
与编译原理中的状态机不同,软件设计领域中通用状态机的输入不是字符集,而是被称作事件的结构(可以是结构体,也可以是类对象),并且特定的状态下,针对发生的事件,不仅发生状态改变,而且产生动作。借鉴编译原理中状态机的初始状态和终态,通用状态机的数学语言描述如下:一个通用有限状态机M是一个七元组,M={K,E,T,M,F,S,Z}。其中(1)K是一个有穷集,其中的每个元素称为状态;(2)E是一个有穷集,它的每个元素称为一个事件;(3)T是转换函数,是K×E->K上的映射;(4)M是一个有穷集,它的每个元素称为动作;(5)F是动作映射函数,是K×E->M上的映射;(6)S是K中的元素,是唯一的一个初态;(7) Z是K的一个子集,是一个终态集,或者叫结束集。实用的状态机可以做进一步的优化,首先,可以把 (3)(5)整合在一起,做一个K×E->{K,M}的映射;其次从实用性的角度出发,禁止状态接收空事件(无输入的情况下,状态发生改变),作为弥补,为每个状态增加进入动作和离开动作;第三,鉴于定时器在系统中,尤其是在状态机中的重要性,可以为每个状态增加定时器以及超时后的状态转换。本文后面的讲述以及实现暂不考虑把定时器特化,如果需要,可以在状态的进入动作中初始化定时器(另:关于定时器,以后会写文章《系统设计之 定时器》)。
二、状态机分类(后文中如无特别说明,则状态机指软件设计领域的通用有限状态机)
依据状态之间是否有包含关系,分以下两种:
(1)常规状态机。状态机中的所有状态是不相交的、互斥的。
(2)层次状态机。状态机中的状态之间要么是互斥的,要么是真包含的,可以用树性结构来描述这些状态集,包含其它状态的状态称为枝节点,不包含其它状态的状态称为叶节点,为方便单树描述,总是设计一个状态包含所有的状态节点,称为根节点。状态机的状态只能停留在叶节点,而不能停留在枝节点,每个枝节点需要指定一个子节点为它的默认子节点,以便状态机进入枝节点的时候能够停留到叶节点。
三、状态机实现
- switch/case if/else方式实现。用于少量状态(3个及其以下)的时候,不需要引入专门的状态机模块。这种方式不能编写通用的状态机模块,不再多说。
- 面向过程方式:宏是实现面向过程方式的通用方式。虽然在状态机层面还是可以用面向对象的方式封装,这里还是把它称为面向过程的方式。
1. 常规状态机模块实现。这个状态机涉及到结构由上而下为:
- 顶层结构是状态机:当前状态id,缺省操作,状态表
- 状态表:状态数组
- 状态结构:状态id,状态名,进入操作,退出操作,缺省操作,状态事件表(数组)
- 状态事件结构:操作,事件,下一状态的id
- 状态机的算法是由状态机的结构决定的
实现如下:
#define SINGLE_STATE_MAX_EVENT 10 typedef int FSM_EVENT_ID; typedef struct event_param_st { FSM_EVENT_ID id; union{ int i; }data; }FSM_EVENT; typedef int FSM_STATE_ID; typedef void (*FSM_FUNC)(FSM_EVENT *); typedef struct state_event_st { FSM_FUNC func; FSM_EVENT_ID event; FSM_STATE_ID state; }FSM_STATE_EVENT; typedef struct state_st { FSM_STATE_ID id; char *name; FSM_FUNC enter_func; FSM_FUNC exit_func; FSM_FUNC default_func; FSM_STATE_EVENT event_table[SINGLE_STATE_MAX_EVENT]; }FSM_STATE; typedef FSM_STATE STATE_TABLE[]; typedef FSM_STATE * PTR_STATE_TABLE; #define END_EVENT_ID -1 #define END_STATE_ID -1 #define BEGIN_FSM_STATE_TABLE(state_stable) static STATE_TABLE state_stable={ #define BEGIN_STATE(id,name,enter_func,exit_func,default_func) {id,name,enter_func,exit_func,default_func,{ #define STATE_EVENT_ITEM(func,event,state) {func,event,state}, #define END_STATE(id) {NULL,END_EVENT_ID,END_STATE_ID}}}, #define END_FSM_STATE_TABLE(state_stable) {END_STATE_ID,NULL,NULL,NULL,NULL,NULL}}; typedef struct fsm_st { FSM_STATE_ID state_id; FSM_FUNC default_func; PTR_STATE_TABLE state_tables; }FSM; void fsm_do_event(FSM &fsm, FSM_EVENT &event) { FSM_STATE *state=&(fsm.state_tables[fsm.state_id]); int i=0; while(state->event_table[i].event!=END_EVENT_ID) { if(state->event_table[i].event==event.id) break; i++; } if(state->event_table[i].event!=END_EVENT_ID) { if(state->id!=state->event_table[i].state) { if(state->exit_func ) state->exit_func(&event); } if(state->event_table[i].func) state->event_table[i].func(&event); if(state->id!=state->event_table[i].state) { if(fsm.state_tables[state->event_table[i].state].enter_func) fsm.state_tables[state->event_table[i].state].enter_func(&event); fsm.state_id=state->event_table[i].state; } } else { if(state->default_func) state->default_func(&event); else { if(fsm.default_func) fsm.default_func(&event); } } }
以上说明实现原理,有特殊需要的话可以自己定制状态机,比如上面的状态事件表数组的上限取的是单个状态中事件项的最大值,也可以定义为所有事件的个数,这样的话事件也不需要查询,可以象状态样直接定位,只是状态事件表会浪费一些存储空间。上面的FSM_EVENT仅仅是个例子,实际开发根据需要定义不同的union。上面的算法也是假定状态表的状态定义是从0开始,顺序递增的。
对外部调用而言,最后的状态机结构和事件执行的方法可以封装为对象。下面举例说明状态机的定义(事件和状态都应该是enum类型,这里直接使用数字,仅为说明问题而已)。
BEGIN_FSM_STATE_TABLE(my_state_table) BEGIN_STATE(0,"first",enter_fsm,exit_fsm,defualt_fsm) STATE_EVENT_ITEM(func_fsm,1,1) STATE_EVENT_ITEM(func_fsm,2,2) END_STATE(0) BEGIN_STATE(1,"second",enter_fsm,exit_fsm,defualt_fsm) STATE_EVENT_ITEM(func_fsm,1,2) STATE_EVENT_ITEM(func_fsm,2,0) END_STATE(1) BEGIN_STATE(2,"third",enter_fsm,exit_fsm,defualt_fsm) STATE_EVENT_ITEM(func_fsm,1,0) STATE_EVENT_ITEM(func_fsm,2,1) END_STATE(2) END_FSM_STATE_TABLE(my_state_table)
void enter_fsm(FSM_EVENT * event) { printf("enter me\n"); } void exit_fsm(FSM_EVENT * event) { printf("exit me\n"); } void defualt_fsm(FSM_EVENT * event) { printf("i am defualt_fsm\n"); } void func_fsm(FSM_EVENT * event) { printf("i am func_fsm\n"); } int main() { printf("i am main\n"); FSM fsm={0,defualt_fsm,my_state_table}; printf("state[%d],name[%s]\n",fsm.state_id,fsm.state_tables[fsm.state_id].name); FSM_EVENT event; event.id=1; event.data.i=1; fsm_do_event(fsm,event); printf("state[%d],name[%s]\n",fsm.state_id,fsm.state_tables[fsm.state_id].name); }