有没有典型的状态机实施模式?


118

我们需要在C中实现一个简单的状态机。
标准的switch语句是最好的方法吗?
我们有一个当前状态(状态)和一个转换触发器。


switch(state)
{
  case STATE_1:
     state = DoState1(transition);
     break;
  case STATE_2:
     state = DoState2(transition);
     break;
}
...
DoState2(int transition)
{
   // Do State Work
   ...
   if(transition == FROM_STATE_2) {
     // New state when doing STATE 2 -> STATE 2
   }
   if(transition == FROM_STATE_1) {
    // New State when moving STATE 1 -> STATE 2
   }
   return new_state;
}

对于简单的状态机,是否有更好的方法

编辑:对于C ++,我认为Boost Statechart库可能是要走的路。但是,它对C 没有帮助。让我们集中精力讨论C用例。


Answers:


134

我更喜欢对大多数状态机使用表驱动的方法:

typedef enum { STATE_INITIAL, STATE_FOO, STATE_BAR, NUM_STATES } state_t;
typedef struct instance_data instance_data_t;
typedef state_t state_func_t( instance_data_t *data );

state_t do_state_initial( instance_data_t *data );
state_t do_state_foo( instance_data_t *data );
state_t do_state_bar( instance_data_t *data );

state_func_t* const state_table[ NUM_STATES ] = {
    do_state_initial, do_state_foo, do_state_bar
};

state_t run_state( state_t cur_state, instance_data_t *data ) {
    return state_table[ cur_state ]( data );
};

int main( void ) {
    state_t cur_state = STATE_INITIAL;
    instance_data_t data;

    while ( 1 ) {
        cur_state = run_state( cur_state, &data );

        // do other program logic, run other state machines, etc
    }
}

当然,这可以扩展为支持多个状态机等。还可以适应转换动作:

typedef void transition_func_t( instance_data_t *data );

void do_initial_to_foo( instance_data_t *data );
void do_foo_to_bar( instance_data_t *data );
void do_bar_to_initial( instance_data_t *data );
void do_bar_to_foo( instance_data_t *data );
void do_bar_to_bar( instance_data_t *data );

transition_func_t * const transition_table[ NUM_STATES ][ NUM_STATES ] = {
    { NULL,              do_initial_to_foo, NULL },
    { NULL,              NULL,              do_foo_to_bar },
    { do_bar_to_initial, do_bar_to_foo,     do_bar_to_bar }
};

state_t run_state( state_t cur_state, instance_data_t *data ) {
    state_t new_state = state_table[ cur_state ]( data );
    transition_func_t *transition =
               transition_table[ cur_state ][ new_state ];

    if ( transition ) {
        transition( data );
    }

    return new_state;
};

表驱动的方法更易于维护和扩展,更易于映射到状态图。


很好的入门方式,至少对我来说是起点。请注意,run_state()的第一行有一个顽皮的“”。那不应该在那里。
Atilla Filiz 2010年

2
如果此答案也至少对其他两种方法说两个字,那就更好了:一种具有大切换情况的“全局”方法,并使用“ 状态设计模式”分离状态并让每个状态自行处理其转换。
erikbwork 2013年

嗨,我知道这篇文章很旧,但我希望我能得到答案:) instance_data_t变量中肯定应该包含什么?我想知道如何更改中断状态...这是在此变量中存储有关已处理中断信息的好方法吗?例如,存储有关该按钮已按下的信息,因此应更改状态。
grongor 2013年

@GRoNGoR在我看来,您正在处理事件驱动的状态机。我认为您可以使用它来存储事件数据。
Zimano 2015年

3
非常好的触摸方式,如何定义NUM_STATES。
Albin Stigo

25

您可能已经看到我对另一个提到FSM的C问题的回答!这是我的方法:

FSM {
  STATE(x) {
    ...
    NEXTSTATE(y);
  }

  STATE(y) {
    ...
    if (x == 0) 
      NEXTSTATE(y);
    else 
      NEXTSTATE(x);
  }
}

定义以下宏

#define FSM
#define STATE(x)      s_##x :
#define NEXTSTATE(x)  goto s_##x

可以对其进行修改以适合特定情况。例如,您可能具有FSMFILE要驱动FSM的文件,因此您可以将读取下一个字符的操作合并到宏本身中:

#define FSM
#define STATE(x)         s_##x : FSMCHR = fgetc(FSMFILE); sn_##x :
#define NEXTSTATE(x)     goto s_##x
#define NEXTSTATE_NR(x)  goto sn_##x

现在,您有两种类型的过渡:一种进入状态并读取新字符,另一种进入状态而不消耗任何输入。

您还可以使用以下方式自动处理EOF:

#define STATE(x)  s_##x  : if ((FSMCHR = fgetc(FSMFILE) == EOF)\
                             goto sx_endfsm;\
                  sn_##x :

#define ENDFSM    sx_endfsm:

这种方法的好处是,您可以将绘制的状态图直接转换为工作代码,相反,可以轻松地从代码中绘制状态图。

在其他用于实现FSM的技术中,转换的结构埋在控制结构中(如果,则为...),并由变量值(通常是state变量)控制,将漂亮的图与对象关联起来可能是一项复杂的任务。复杂的代码。

我从著名的《计算机语言》杂志上发表的一篇文章中学到了这种技术,不幸的是,该文章不再发表。


1
从根本上说,好的FSM就是关于可读性的。这提供了一个很好的接口,并且实现效果也很好。可惜的是,语言中没有本地的FSM结构。我现在可以看到它是C1X的较晚版本!
Kelden Cowan

3
我喜欢嵌入式应用程序的这种方法。是否可以在事件驱动的状态机中使用这种方法?
ARF

13

我也使用了表格方法。但是,有开销。为什么要存储第二个指针列表?C中不带()的函数是const指针。因此,您可以执行以下操作:

struct state;
typedef void (*state_func_t)( struct state* );

typedef struct state
{
  state_func_t function;

  // other stateful data

} state_t;

void do_state_initial( state_t* );
void do_state_foo( state_t* );
void do_state_bar( state_t* );

void run_state( state_t* i ) {
    i->function(i);
};

int main( void ) {
    state_t state = { do_state_initial };

    while ( 1 ) {
        run_state( state );

        // do other program logic, run other state machines, etc
    }
}

当然,根据您的恐惧因素(即安全性与速度),您可能需要检查有效的指针。对于大于三个左右状态的状态机,与等效的开关或表方法相比,以上方法应少用指令。您甚至可以将宏宏化为:

#define RUN_STATE(state_ptr_) ((state_ptr_)->function(state_ptr_))

同样,从OP的示例中我感觉到,在考虑/设计状态机时应该做一个简化。我认为过渡状态不应该用于逻辑。每个状态功能都应该能够执行其给定的角色,而无需明确了解过去的状态。基本上,您设计如何从所处的状态过渡到另一状态。

最后,不要基于“功能”边界开始设计状态机,而应使用子功能。取而代之的是根据必须等待什么时间才能继续的状态来划分状态。这将有助于最大程度地减少获得结果之前必须运行状态机的次数。在编写I / O函数或中断处理程序时,这可能很重要。

另外,经典switch语句的一些优缺点:

优点:

  • 它是用语言编写的,因此记录在案且清晰
  • 状态定义在调用它们的位置
  • 可以在一个函数调用中执行多个状态
  • 所有状态通用的代码可以在switch语句之前和之后执行

缺点:

  • 可以在一个函数调用中执行多个状态
  • 所有状态通用的代码可以在switch语句之前和之后执行
  • 切换执行速度可能很慢

请注意,这两个属性都是pro和con。我认为这种转换为国家之间共享过多提供了机会,并且国家之间的相互依赖性可能变得难以管理。但是,对于少数状态,它可能是最易读和可维护的。


10

对于简单的状态机,只需对状态使用switch语句和枚举类型。根据您的输入在switch语句中进行转换。在实际程序中,您显然会更改“ if(input)”以检查您的过渡点。希望这可以帮助。

typedef enum
{
    STATE_1 = 0,
    STATE_2,
    STATE_3
} my_state_t;

my_state_t state = STATE_1;

void foo(char input)
{
    ...
    switch(state)
    {
        case STATE_1:
            if(input)
                state = STATE_2;
            break;
        case STATE_2:
            if(input)
                state = STATE_3;
            else
                state = STATE_1;
            break;
        case STATE_3:
            ...
            break;
    }
    ...
}

1
可能值得将“状态”放入函数内部,并将其变为静态。
史蒂夫·梅尔尼科夫

2
@Steve Melnikoff:仅当您只有一台状态机时。将其保留在函数之外,您可以拥有一系列具有自己状态的状态机。
Vicky 2010年

@Vicky:一个函数可以包含任意数量的状态机,如果需要的话,可以带有一组状态变量,如果不在其他地方使用它们,则可以将其作为函数存在于函数中(作为静态变量)。
Steve Melnikoff 2010年

10

Martin Fowler的UML Distilled中,他在第10章“状态机图”(强调我的意思)中指出(没有双关语):

状态图可以通过三种主要方式实现:嵌套开关状态模式状态表

让我们使用手机显示屏状态的简化示例:

在此处输入图片说明

嵌套开关

Fowler给出了一个C#代码示例,但我已经将其改编为我的示例。

public void HandleEvent(PhoneEvent anEvent) {
    switch (CurrentState) {
    case PhoneState.ScreenOff:
        switch (anEvent) {
        case PhoneEvent.PressButton:
            if (powerLow) { // guard condition
                DisplayLowPowerMessage(); // action
                // CurrentState = PhoneState.ScreenOff;
            } else {
                CurrentState = PhoneState.ScreenOn;
            }
            break;
        case PhoneEvent.PlugPower:
            CurrentState = PhoneState.ScreenCharging;
            break;
        }
        break;
    case PhoneState.ScreenOn:
        switch (anEvent) {
        case PhoneEvent.PressButton:
            CurrentState = PhoneState.ScreenOff;
            break;
        case PhoneEvent.PlugPower:
            CurrentState = PhoneState.ScreenCharging;
            break;
        }
        break;
    case PhoneState.ScreenCharging:
        switch (anEvent) {
        case PhoneEvent.UnplugPower:
            CurrentState = PhoneState.ScreenOff;
            break;
        }
        break;
    }
}

状态模式

这是我的示例的GoF状态模式的实现:

在此处输入图片说明

状态表

从福勒那里汲取灵感,以下是我的示例表格:

源状态目标状态事件警卫行动
-------------------------------------------------- ------------------------------------
屏幕关闭屏幕关闭按下按钮电源低显示低电源消息  
屏幕关闭屏幕打开按钮!powerLow
屏幕开启屏幕关闭按钮
屏幕关闭屏幕充电插头电源
屏幕上屏幕充电插头电源
屏幕充电ScreenOff拔下电源

比较方式

嵌套开关将所有逻辑保持在一个位置,但是当存在许多状态和转换时,可能很难阅读代码。比其他方法(没有多态性或解释),它可能更安全,更易于验证。

状态模式的实现可能会将逻辑分布在几个单独的类中,这可能会使整体上理解它成为一个问题。另一方面,小类易于理解。如果您通过添加或删除过渡来更改行为,则设计特别脆弱,因为过渡是层次结构中的方法,并且代码可能会有很多更改。如果您遵循小型接口的设计原则,那么您会发现这种模式的确不是那么好。但是,如果状态机是稳定的,则不需要进行此类更改。

状态表方法需要为内容编写某种解释器(如果您对所使用的语言有所了解,这可能会更容易),这可能需要做很多工作。正如Fowler指出的那样,如果您的表与代码分开,则可以在不重新编译的情况下修改软件的行为。但是,这有一些安全隐患。该软件的行为基于外部文件的内容。

编辑(不是真正的C语言)

也有一种流畅的界面(也称为内部领域特定语言)方法,具有一流功能的语言可能会促进这种方法。在无状态库是否存在,以及博客显示了一个简单的例子的代码。一个Java实现(预Java8)进行了讨论。GitHub上也向我展示了一个Python示例


您使用什么软件创建图片?
sjas

1
我怀疑它可能是通过PlantUML plantuml.com/state-diagram
Seidleroni


4

对于简单的情况,可以使用开关样式方法。我发现过去有效的方法是也处理过渡:

static int current_state;    // should always hold current state -- and probably be an enum or something

void state_leave(int new_state) {
    // do processing on what it means to enter the new state
    // which might be dependent on the current state
}

void state_enter(int new_state) {
    // do processing on what is means to leave the current atate
    // might be dependent on the new state

    current_state = new_state;
}

void state_process() {
    // switch statement to handle current state
}

我对boost库一无所知,但是这种方法非常简单,不需要任何外部依赖,并且易于实现。


4

switch()是在C中实现状态机的一种强大而标准的方法,但是如果您有很多状态,它会降低可维护性。另一种常见的方法是使用函数指针来存储下一个状态。这个简单的示例实现了一个设置/重置触发器:

/* Implement each state as a function with the same prototype */
void state_one(int set, int reset);
void state_two(int set, int reset);

/* Store a pointer to the next state */
void (*next_state)(int set, int reset) = state_one;

/* Users should call next_state(set, reset). This could
   also be wrapped by a real function that validated input
   and dealt with output rather than calling the function
   pointer directly. */

/* State one transitions to state one if set is true */
void state_one(int set, int reset) {
    if(set)
        next_state = state_two;
}

/* State two transitions to state one if reset is true */
void state_two(int set, int reset) {
    if(reset)
        next_state = state_one;
}

4

我在edx.org课程“嵌入式系统-塑造世界UTAustinX-UT.6.02x,第10章”(乔纳森·瓦尔瓦诺和拉梅什·耶拉巴利...)上找到了Moore FSM的一个非常不错的C实现。

struct State {
  unsigned long Out;  // 6-bit pattern to output
  unsigned long Time; // delay in 10ms units 
  unsigned long Next[4]; // next state for inputs 0,1,2,3
}; 

typedef const struct State STyp;

//this example has 4 states, defining constants/symbols using #define
#define goN   0
#define waitN 1
#define goE   2
#define waitE 3


//this is the full FSM logic coded into one large array of output values, delays, 
//and next states (indexed by values of the inputs)
STyp FSM[4]={
 {0x21,3000,{goN,waitN,goN,waitN}}, 
 {0x22, 500,{goE,goE,goE,goE}},
 {0x0C,3000,{goE,goE,waitE,waitE}},
 {0x14, 500,{goN,goN,goN,goN}}};
unsigned long currentState;  // index to the current state 

//super simple controller follows
int main(void){ volatile unsigned long delay;
//embedded micro-controller configuration omitteed [...]
  currentState = goN;  
  while(1){
    LIGHTS = FSM[currentState].Out;  // set outputs lines (from FSM table)
    SysTick_Wait10ms(FSM[currentState].Time);
    currentState = FSM[currentState].Next[INPUT_SENSORS];  
  }
}

2

您可能需要研究libero FSM生成器软件。通过状态描述语言和/或(Windows)状态图编辑器,您可以生成C,C ++,java和许多其他代码,以及精美的文档和图。来自iMatix的源代码和二进制文件



2

我最喜欢的模式之一是状态设计模式。对相同的给定输入集做出响应或表现不同。
在状态机上使用switch / case语句的问题之一是,随着创建更多状态,开关/ case变得更难/笨拙地读取/维护,升级了无组织的意大利面条式代码,并且在不破坏某些内容的情况下变得越来越难以更改。我发现使用设计模式可以帮助我更好地组织数据,这是抽象的重点。与其围绕状态来自来设计状态代码,不如构造代码,以便在您进入新状态时记录状态。这样,您可以有效地获取以前状态的记录。我喜欢@JoshPetit的答案,并直接从GoF书中获取了他的解决方案:

stateCtxt.h:

#define STATE (void *)
typedef enum fsmSignal
{
   eEnter =0,
   eNormal,
   eExit
}FsmSignalT;

typedef struct fsm 
{
   FsmSignalT signal;
   // StateT is an enum that you can define any which way you want
   StateT currentState;
}FsmT;
extern int STATECTXT_Init(void);
/* optionally allow client context to set the target state */
extern STATECTXT_Set(StateT  stateID);
extern void STATECTXT_Handle(void *pvEvent);

stateCtxt.c:

#include "stateCtxt.h"
#include "statehandlers.h"

typedef STATE (*pfnStateT)(FsmSignalT signal, void *pvEvent);

static FsmT      fsm;
static pfnStateT UsbState ;

int STATECTXT_Init(void)
{    
    UsbState = State1;
    fsm.signal = eEnter;
    // use an enum for better maintainability
    fsm.currentState = '1';
    (*UsbState)( &fsm, pvEvent);
    return 0;
}

static void ChangeState( FsmT *pFsm, pfnStateT targetState )
{
    // Check to see if the state has changed
    if (targetState  != NULL)
    {
        // Call current state's exit event
        pFsm->signal = eExit;
        STATE dummyState = (*UsbState)( pFsm, pvEvent);

        // Update the State Machine structure
        UsbState = targetState ;

        // Call the new state's enter event
        pFsm->signal = eEnter;            
        dummyState = (*UsbState)( pFsm, pvEvent);
    }
}

void STATECTXT_Handle(void *pvEvent)
{
    pfnStateT newState;

    if (UsbState != NULL)
    {
         fsm.signal = eNormal;
         newState = (*UsbState)( &fsm, pvEvent );
         ChangeState( &fsm, newState );
    }        
}


void STATECTXT_Set(StateT  stateID)
{
     prevState = UsbState;
     switch (stateID) 
     {
         case '1':               
            ChangeState( State1 );
            break;
          case '2':
            ChangeState( State2);
            break;
          case '3':
            ChangeState( State3);
            break;
     }
}

statehandlers.h:

/* define state handlers */
extern STATE State1(void);
extern STATE State2(void);
extern STATE State3(void);

statehandlers.c:

#include "stateCtxt.h:"

/* Define behaviour to given set of inputs */
STATE State1(FsmT *fsm, void *pvEvent)
{   
    STATE nextState;
    /* do some state specific behaviours 
     * here
     */
    /* fsm->currentState currently contains the previous state
     * just before it gets updated, so you can implement behaviours 
     * which depend on previous state here
     */
    fsm->currentState = '1';
    /* Now, specify the next state
     * to transition to, or return null if you're still waiting for 
     * more stuff to process.  
     */
    switch (fsm->signal)
    {
        case eEnter:
            nextState = State2;
            break;
        case eNormal:
            nextState = null;
            break;
        case eExit:
            nextState = State2;
            break;
    }

    return nextState;
}

STATE  State3(FsmT *fsm, void *pvEvent)
{
    /* do some state specific behaviours 
     * here
     */
    fsm->currentState = '2';
    /* Now, specify the next state
     * to transition to
     */
     return State1;
}

STATE   State2(FsmT *fsm, void *pvEvent)
{   
    /* do some state specific behaviours 
     * here
     */
    fsm->currentState = '3';
    /* Now, specify the next state
     * to transition to
     */
     return State3;
}

对于大多数状态机,尤其是。有限状态机,每个状态将知道其下一个状态应该是什么,以及转换到其下一个状态的条件。对于宽松状态设计,情况可能并非如此,因此可以选择公开API来转换状态。如果需要更多抽象,可以将每个状态处理程序分离到自己的文件中,这些文件等效于GoF书中的具体状态处理程序。如果您的设计很简单,只有几个状态,那么为简单起见,可以将stateCtxt.c和statehandlers.c合并到一个文件中。


State3和State2即使声明为void也具有返回值。
2014年

1

以我的经验,使用“ switch”语句是处理多种可能状态的标准方法。尽管我很惊讶您正在向每个状态处理传递一个过渡值。我认为状态机的全部意义在于每个状态都执行一个动作。然后,下一个动作/输入确定要转换到的新状态。因此,我希望每个状态处理功能都能立即执行固定的进入状态的操作,然后再决定是否需要转换到另一个状态。


2
有不同的基础模型:Mealy机器和Moore机器。Mealy的行为取决于过渡,Moore的行为取决于国家。
xmjx

1

有一本书的标题为《C / C ++中的实用状态图》。但是,这样太重量级了我们所需要的。


2
我对这本书有完全相同的反应。为何需要700多页来描述和实现我认为非常直观和直接的内容?!?!?

1

对于支持的编译器__COUNTER__,您可以将它们用于简单(但较大)状态的mashines。

  #define START 0      
  #define END 1000

  int run = 1;
  state = START;    
  while(run)
  {
    switch (state)
    {
        case __COUNTER__:
            //do something
            state++;
            break;
        case __COUNTER__:
            //do something
            if (input)
               state = END;
            else
               state++;
            break;
            .
            .
            .
        case __COUNTER__:
            //do something
            if (input)
               state = START;
            else
               state++;
            break;
        case __COUNTER__:
            //do something
            state++;
            break;
        case END:
            //do something
            run = 0;
            state = START;
            break;
        default:
            state++;
            break;
     } 
  } 

使用__COUNTER__而不是硬编码数字的优点是,您可以在其他状态的中间添加状态,而不必每次都重新编号。如果编译器不支持__COUNTER__,则在一定程度上应谨慎使用__LINE__


您能否解释更多您的答案?
abarisone 2015年

在正常的“切换”状态mashine中,您有case 0,case 1,case 2 ... ... case100。如果现在想在5和6之间添加3个case,则必须将其余的数字重新编号为100,现在为103。使用__COUNTER__消除了重新编号的需要,因为预编译器在编译期间进行编号。
勒布

1

您可以在c中使用简约的UML状态机框架。https://github.com/kiishor/UML-State-Machine-in-C

它支持有限状态机和分层状态机。它只有3个API,2个结构和1个枚举。

状态机由state_machine_t结构表示。它是可以继承以创建状态机的抽象结构。

//! Abstract state machine structure
struct state_machine_t
{
   uint32_t Event;          //!< Pending Event for state machine
   const state_t* State;    //!< State of state machine.
};

状态由指向的指针表示 state_t框架中结构。

如果框架是为有限状态机配置的,则state_t包含

typedef struct finite_state_t state_t;

// finite state structure
typedef struct finite_state_t{
  state_handler Handler;        //!< State handler function (function pointer)
  state_handler Entry;          //!< Entry action for state (function pointer)
  state_handler Exit;           //!< Exit action for state (function pointer)
}finite_state_t;

该框架提供了一个dispatch_event用于将事件分发到状态机的API,以及两个用于状态遍历的​​API。

state_machine_result_t dispatch_event(state_machine_t* const pState_Machine[], uint32_t quantity);
state_machine_result_t switch_state(state_machine_t* const, const state_t*);

state_machine_result_t traverse_state(state_machine_t* const, const state_t*);

有关如何实现分层状态机的更多详细信息,请参阅GitHub存储库。

代码示例
https://github.com/kiishor/UML-State-Machine-in-C/blob/master/demo/simple_state_machine/readme.md
https://github.com/kiishor/UML-State-Machine-in -C / blob / master / demo / simple_state_machine_enhanced / readme.md


您还可以添加适合该问题的代码示例吗?
朱利奥·卡钦

存储库中的demo文件夹有一个示例。github.com/kiishor/UML-State-Machine-in-C/blob/master/demo/…。我目前正在处理另一个涉及键,led和计时器的嵌入式系统示例,但还没有完成。准备就绪后,将通知您。
Nandkishor Biradar


0

您的问题类似于“是否存在典型的数据库实施模式”?答案取决于您想要实现什么?如果要实现更大的确定性状态机,则可以使用模型和状态机生成器。可以在www.StateSoft.org-SM Gallery中查看示例。贾努斯·多布罗沃尔斯基(Janusz Dobrowolski)


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.