原创 状态机与函数指针

2011-3-9 11:13 7148 8 8 分类: 测试测量

最近由于项目需要,开始研究状态机。其实原来就做过状态机相关的东西,毕竟在手机实时系统上状态是个很常见也很重要的东西。但是状态机的设计和实现的好坏直接影响整个系统的性能及可维护性。从设计上讲,无非是画状态图,理清各个状态之间的迁移关系;从编码上讲,对于非层次式有限状态自动机(FSM)一般有以下几种实现方法:
 
1、最简单的switch-case方法,设若干个变量保存当前状态,针对每个状态switch-case各种signal输入进行判断分别处理。说起来很容易理解,但是实际做起来却远不是这么简单。这种实现将所有状态的处理全部杂糅在一起,如果系统稍微复杂一些,那么维护起来将相当困难,我原来接触过的一个项目其中的主服务层的内部就有10多个变量标示各种不同类别的状态,十几层switch-case和if/else错综复杂,经常是一大段处理找不到他的输入signal在哪里,在什么情况下进行该处理。曾经安排我来维护这个,那个吐血...
 
2、状态表。以输入signal作索引,相应操作的函数指针和状态迁移作表元素,建立一个状态表。这样做的好处首先是速度快,完全避免了判断分支;所有状态迁移时的处理以独立函数的形式分离,在逻辑上比较清晰。但是该方法的缺点是即使可能某些状态并不接受某些signal输入,还是需要针对所有的signal输入和所有的状态列出所有可能的处理情况。并且由于c中没有现成的hash表,所以构造出的状态表通常是稀疏的,空间浪费比较大。另外由于将所有情况线性列举,如果对原状态图进行修改,在维护上也相对困难(需要在一个大的方阵中找出修改所影响的行列)。
 
3、状态模式。这个是基于面向对象的解决方法,基类中定义所有事件的处理接口,对于新状态通过派生基类的子类来扩展,在子类中重写该状态所涉及到的事件处理方法。这种方法可以很方便的扩展新状态,将不同状态处理独立开来,逻辑清晰。但是由于需要最大化基类接口数量,似乎不是很符合OO的设计原则,而且也需要枚举所有的signal输入,设计粒度太细。
 
4、不太好归纳该方法的名字,姑且叫综合方法吧。该方法综合了以上几种方法的优点,也是我比较欣赏的一种方法。该方法将着眼点从transition转移到state本身上来,以每个state作为粒度单位来设计。
定义一个状态管理变量currentState,保存当前状态处理的函数指针,同时定义init,destroy,trans等接口。对于新状态,只需新建一个状态处理函数,遵循接口
ret_value State1Handle( Signal )
{
    signal2:
        action2;
        trans(State2Handle);
    signal3:
        action3;
        trans(State3Handle);
    ....
}
trans中仅需要更改currentState的函数指针取值即可。
对于状态内的signal分发可以采用状态表(signal需可枚举)或简单的switch-case形式。
该方法避免了简单switch-case和状态表的处理堆叠的情况,状态转换非常高效(重新赋值状态处理指针),可以很方便的添加/删除/修改状态及处理(添加删除相应的状态处理函数及相关状态的处理即可),并且避免了signal处理接口的最大化,每个状态只处理所关心的signal输入。可以说这种方法最大限度的发挥了函数指针的灵活性。
 
以上所说的几种方法都针对非层次式状态机,也就是说对于某些共同的处理不能在宏观上加以综合,而只能在最小粒度上设计每个状态。而层次式状态机则允许有复合状态的出现,对于共通处理可以在父状态上进行而不必迁移到每一个子状态上。当子状态接收到一个共同处理时,可以将该处理throw给父状态,如果还不能处理就再次向上层throw。这种处理方法抽象程度更高,设计更加简洁。有一种量子模型是很好的层次式状态机的实现方式,不过由于此次项目尚未涉及层次式状态机,所以仅作了解。
 
越来越深的体会到了函数指针的强大了...

9.1 有限状态机

 

          有限状态机(FSM)与流程图很相似,具有一组按照一定路径排列的状态,依据于状态中的事件和动作,一个状态可以转移到其他状态。

         状态是时间中的一个点,例如,当你等火车的时候,你在等待状态。一种状态在一个状态机中,只能出现一次。

       事件是某时发生的事情,例如火车到达,火车运行。

       动作是当事件出现时,实现的任务,例如,火车到达后,上车

       转移是两个状态之间的联系,可以从一个状态移动到另外一个状态。

       状态图就是对一个事物在某个事件发生后从一个源状态到另外一个目的状态转移的图形描述。
           状态图中,使用圆圈表示状态,圆圈中的文字或数字表示该状态的名字或是编码,状态转移方向用箭头表示,在箭头旁写的文字是转移条件。对于梅里状态图,在箭头旁用“输入/输出”的格式表示转移条件与满足该转移条件下的输出;而对于摩尔状态机,常将输出放在状态圆圈中。

由图可知,当k=0时,状态从a0转移到a1,若是k0=1,从状态a1转移到a2,等等,若是reset=0,则无论在什么状态,都将转移到a0状态。

FSM的实现方式:
1) switch/case或者if/else
这无意是最直观的方式,使用一堆条件判断,会编程的人都可以做到,对简单小巧的状态机来说最合适,但是毫无疑问,这样的方式比较原始,对庞大的状态机难以维护。

2) 状态表
维护一个二维状态表,横坐标表示当前状态,纵坐标表示输入,表中一个元素存储下一个状态和对应的操作。这一招易于维护,但是运行时间和存储空间的代价较大。

3) 使用State Pattern
使用State Pattern使得代码的维护比switch/case方式稍好,性能上也不会有很多的影响,但是也不是100%完美。不过Robert C. Martin做了两个自动产生FSM代码的工具,for java和for C++各一个,在http://www.objectmentor.com/resources/index上有免费下载,这个工具的输入是纯文本的状态机描述,自动产生符合State Pattern的代码,这样developer的工作只需要维护状态机的文本描述,每必要冒引入bug的风险去维护code。

4) 使用宏定义描述状态机
一般来说,C++编程中应该避免使用#define,但是这主要是因为如果用宏来定义函数的话,很容易产生这样那样的问题,但是巧妙的使用,还是能够产生奇妙的效果。MFC就是使用宏定义来实现大的架构的。
在实现FSM的时候,可以把一些繁琐无比的if/else还有花括号的组合放在宏中,这样,在代码中可以3)中状态机描述文本一样写,通过编译器的预编译处理产生1)一样的效果,我见过产生C代码的宏,如果要产生C++代码,己软MFC可以,那么理论上也是可行的。

 c.h  
 
typedef enum{  
STATE1 = 1,  
STATE2,  
STATE3,  
STATE4,  
STATE5,//password pass  
//...ADD here  
}STATE;  
 
typedef enum{  
INPUT1 = '2',  
INPUT2 = '4',  
INPUT3 = '7',  
INPUT4 = '9',  
}INPUT;  
 
typedef struct 
{  
STATE cur_state;  
INPUT input;  
STATE next_state;  
}STATE_TRANS;

c.c  

 #include   <stdio.h>    

#include "c.h"   

  

STATE_TRANS state_trans_arry[] =    

{   

{STATE1,INPUT1,STATE2},   

{STATE2,INPUT2,STATE3},   

{STATE3,INPUT3,STATE4},   

{STATE4,INPUT4,STATE5},   

};  

#define STATE_TRANS_CNT (sizeof(state_trans_arry)/sizeof(state_trans_arry[0]))

int main()    
{  
int i;  
char ch;  
STATE state_machine = STATE1;  
 
while(ch != 'e')  
{  
   ch = getchar();  
   if((ch >= '0') && (ch <= '9'))//for digit password input only  
   {  
    for(i = 0;i < STATE_TRANS_CNT;i++)  
    {  
     if((ch == state_trans_arry.input) && (state_machine == state_trans_arry.cur_state))  
     {  
      state_machine = state_trans_arry.next_state;  
      continue;  
     }  
     else if(i == (STATE_TRANS_CNT - 1))//no transfer match,reset state  
     {  
      state_machine = STATE1;  
     }  
    }  
    if(state_machine == STATE5)  
     printf("Password correct,state transfer machine pass!\n");  
   }  
}  
return 0;  

状态图--一个图的数据结构!
1.while + switch;
2.状态机:就是指定系统的所有可能的状态及状态间跳转的条件,然后设一个初始状态输入给这台机器,机器就会自动运转,或最后处于终止状态,或在某一个状态不断循环。
游戏中状态切换是很频繁的。 可能以前要切换状态就是if~else,或者设标志,但这些都不太结构化, 如果把它严格的设为一种标准的状态机,会清楚的多。

比如控制一扇门的运动, 初始时门是关的, 当有力作用在门上时, 门开始慢慢打开,力的作用完后,门渐渐停止不动, 当有反向的力时,门又渐渐关上, 知道回到初始关的状态。 这个你会怎么来编程实现呢。 似乎很麻烦, 的确,没有状态机的思想时会很烦,设很多标志,一堆if条件。
用状态机的话,不只是代码更清晰, 关键是更符合逻辑和自然规律, 不同状态不同处理, 满足条件则跳转到相关状态。

伪码如下:
enum 
{  
 CLOSED, // 关上状态  
 CLOSING, // 正在关状态  
 OPENED, // 打开状态  
 OPENING, // 正在开的状态  
}doorState = CLOSED; // 初始为关  
 
Update()  
{  
     switch(doorState)  
     case CLOSED:  
          if (有人推门)  
               doorState = OPENING; // 跳转到正在开状态  
     break;  
     case OPENING:  
          door.Rotation += DeltaAngle; // 门的旋转量递增  
          if (门的速度为零) / / 力的作用已去  
               doorState = OPENED; // 跳转到开状态  
    break;  
    case OPENED:  
         if (有人关门)  
              doorState = CLOSING;  
   break;  
   case CLOSING:  
         door.Rotation -= DeltaAngle; // 门的旋转量递减  
         if (门的旋转角度减为零)  
              doorState = CLOSED; // 门已关上  
    break;   
}  
 
// 而绘制代码几乎不用怎么变, 门就是会严格按照状态机的指示去运动, 该停就会停  
Render()  
{  
     RotateView(door.Rotation);  
     DrawDoor(door.Position);  

就说一个基本游戏的运转: (用到了一个状态然后还有子状态)
UpdateGame()  
BEGIN;  
   switch(gameState)  
   case 等待选择菜单: //它有三个子状态。  
   if (选择菜单项 == 开始)  
   {  
       游戏初始;  
      gameState = 开始游戏  
   }  
   if (选择菜单项 == 选项)  
      gameState = 设置  
   if (选择菜单项 == 退出)  
       gameState = 退出  
 
 case 开始:  
   
 游戏运行;  
 if (用户按退出键)  
 gameState = 等待选择菜单 ;  
 ...其他的状态跳转处理;  
 case 退出:  
 释放资源;  
 退出;  
 case 设置:  
 分别处理不同的选项, 跳转不同的子状态;  
 case .... // 其他状态的处理  
   
END; 

某一个状态可以包含更多的子状态, 这样最好是同一层次的状态设为一个枚举, 并分到另一个switch处例如 enum STATES{state1, state2, state3}; state2又包含若干状态
则再定义enum SUB_STATE2{sub_state2_1, sub_state2_2, sub_state2_3,};想很多基本的渲染效果, 如淡入淡出, 闪烁等等, 用状态的思想会事半功倍, 思路也更清晰。其实像Opengl, Direct3D这样的渲染引擎本身就是状态机, 当你设置渲染的状态, 这台机器就保持这个状态进行渲染工作,如保持光照位置,保持片元颜色, 直到你再次改变它的状态。

总之, 用状态机的角度去看待问题, 往往能把比较复杂的系统分解为能单独处理的众多子状态, 处理也会得心应手。希望大家多用它, 很好的东西。

 用C语言实现一个状态机,很简单,和大家分享
用nRF24L01组建了一个简单的网络,做的一个小的状态机,网络中三个节点,开始拓扑为网状,后来为星型。

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h>  
 
 
//Finite state machine declaration  
//state declaration 
#define IDLE 0 //idle state in rx mode 
#define M_BROADCAST 1 //broadcast state in tx mode,broadcast to be a master point 
#define M_WAIT_BROADCAST_ACK 2 //wait for broadcast ack state in rx mode,wait for the point ack in a specific time window 
#define M_WAIT_COMMAND 3 //wait for command state,wait for PC command via UART 
#define M_BROADCAST_CANCEL 4 //broadcast cancel state,broadcast to cancel master point 
 
#define S_BROADCAST_ACK 5 //slave mode,send back self physical address 
#define S_WAIT_COMMAND 6 //slave mode, wait for command from the master point  
 
//state transition trig  
//used in master mode  
int isReqBeMaster = 0;//Is PC request the point to be master?  
int isTimeout = 0;//Is time out?  
int isReqCancelMaster = 0;//Is request to cancel master?  
//used in slave mode  
int isRxBroadcast = 0;//Is there a point broadcast to be master?  
int isRxBroadcastCancel = 0;//Is receive broadcast cancel master?  
 
typedef struct fsmtag  
{  
    int state; //state  
    int timeouttime; //time out time in milliseconds  
}fsm;  
 
//function prototype  
 
int main()  
{  
    fsm f;  
 
    f.state = IDLE;  
    f.timeouttime = 0;  
 
    while(1)  
    {  
        switch(f.state)  
        {  
        case IDLE:  
            puts("IDLE\nWait for isReqBeMaster(1/0) isRxBroadcast(1/0):");  
            scanf("%d %d",&isReqBeMaster,&isRxBroadcast);  
            if(isReqBeMaster)  
            {  
                f.state = M_BROADCAST;  
                break;  
            }  
            else if(isRxBroadcast)  
            {  
                f.state = S_BROADCAST_ACK;  
                break;  
            }  
            else 
                break;  
        case M_BROADCAST:  
            puts("M_BROADCAST\nBroadcasting...\n");  
            f.state = M_WAIT_BROADCAST_ACK;  
        case M_WAIT_BROADCAST_ACK:  
            puts("M_WAIT_BROADCAST_ACK\nWaiting for isTimeout(1/0):");  
            scanf("%d",&isTimeout);  
            if(isTimeout)  
            {  
                f.state = M_WAIT_COMMAND;  
                break;  
            }  
            else 
                break;  
        case M_WAIT_COMMAND:  
            puts("M_WAIT_COMMAND\nWaiting for isReqCancelMaster(1/0):");  
            scanf("%d",&isReqCancelMaster);  
            if(isReqCancelMaster)  
            {  
                f.state = IDLE;  
                break;  
            }  
            else 
                break;  
        //Slave mode routine  
        case S_BROADCAST_ACK:  
            puts("S_BROADCAST_ACK\nAcking...\n");  
            f.state = S_WAIT_COMMAND;  
            break;  
        case S_WAIT_COMMAND:  
            puts("S_WAIT_COMMAND\nWaiting for isRxBroadcastCancel(1/0):");  
            scanf("%d",&isRxBroadcastCancel);  
            if(isRxBroadcastCancel)  
            {  
                f.state = IDLE;  
                break;  
            }  
            else 
                break;  
        default:  
            puts("default");  
            printf("%d\n",rand());  
            f.state = IDLE;  
        }  
    }  
      
    return 0;  

 --------------------------------------------
              当前状态   s0        s1        s2     | 事件
              --------------------------------------------
                       a0/s0      --       a0/s0   |  e0
              --------------------------------------------
                       a1/s1      --        --     |  e1
              --------------------------------------------
                       a2/s2     a2/s2      --     |  e2
              --------------------------------------------

               表1 图2状态机实例的二维表格表示(动作/下一状态)

    图2为一个状态机实例的状态转移图,它的含义是:
        在s0状态,如果发生e0事件,那么就执行a0动作,并保持状态不变;
                 如果发生e1事件,那么就执行a1动作,并将状态转移到s1态;
                 如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
        在s1状态,如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
        在s2状态,如果发生e0事件,那么就执行a0动作,并将状态转移到s0态;
    有限状态机不仅能够用状态转移图表示,还可以用二维的表格代表。一般将当前状态号写在横行上,将事件写在纵列上,如表1所示。其中“--”表示空(不执行动作,也不进行状态转移),“an/sn”表示执行动作an,同时将下一状态设置为sn。表1和图2表示的含义是完全相同的。
    观察表1可知,状态机可以用两种方法实现:竖着写(在状态中判断事件)和横着写(在事件中判断状态)。这两种实现在本质上是完全等效的,但在实际操作中,效果却截然不同。

==================================
竖着写(在状态中判断事件)C代码片段
==================================
    cur_state = nxt_state;
    switch(cur_state){                  //在当前状态中判断事件
        case s0:                        //在s0状态
            if(e0_event){               //如果发生e0事件,那么就执行a0动作,并保持状态不变;
                执行a0动作;
                //nxt_state = s0;       //因为状态号是自身,所以可以删除此句,以提高运行速度。
            }
            else if(e1_event){          //如果发生e1事件,那么就执行a1动作,并将状态转移到s1态;
                执行a1动作;
                nxt_state = s1;
            }
            else if(e2_event){          //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
                执行a2动作;
                nxt_state = s2;
            }
            break;
        case s1:                        //在s1状态
            if(e2_event){               //如果发生e2事件,那么就执行a2动作,并将状态转移到s2态;
                执行a2动作;
                nxt_state = s2;
            }
            break;
        case s2:                        //在s2状态
            if(e0_event){               //如果发生e0事件,那么就执行a0动作,并将状态转移到s0态;
                执行a0动作;
                nxt_state = s0;
            }
    }

 

 #include <stdio.h>   
#include <stdlib.h>   
#include <string.h>    
typedef enum{     
       STATE0 = 0,     
       STATE1,     
       STATE2,     
       STATE3,  
       STATE4,//password pass     
       //...ADD here    
}STATE;    
typedef enum{     
       INPUT1 = '2',   
       INPUT2 = '4',   
       INPUT3 = '7',  
         INPUT4 = '9',    
}INPUT;    
int main()    
{          char ch;     
       STATE current_state = STATE0;     
    while(1){     
    printf("请输入数字进行解码:");    
    while((ch = getchar()) != '\n'){   
            if((ch < '0') || (ch > '9')){    
                   printf("非数字,请重新输入!\n");   
                   break;   
            }     
                     switch(current_state){     
                            case STATE0:     
                                   if(ch == '2')   current_state = STATE1;  
                        break;     
                 case STATE1:   
                        if(ch == '4')   current_state = STATE2;  
                        break;     
                 case STATE2:    
                        if(ch == '7')   current_state = STATE3;    
                        break;   
                 case STATE3:     
                        if(ch == '9')   current_state = STATE4;    
                        break;     
                 default:     
                        current_state = STATE0;     
                        break;   
                     }  
 
              }  
 
              if(current_state == STATE4){     
                     printf("Correct, lock is open!\n");  
                     break;   
              }else    
                     printf("Wrong, unlocked!\n");   
       }      
    return 0;  
 

PARTNER CONTENT

文章评论0条评论)

登录后参与讨论
EE直播间
更多
我要评论
0
8
关闭 站长推荐上一条 /3 下一条