Search code examples
cstatestate-machinestate-machine-workflow

Complicated state transitions: best practices


I work with embedded stuff, and I have some software module that manages hardware. This module has state, and state transitions are complicated: depending on events, the module could go from state A to state B or probably to C. But, when it exits some state, it should perform some actions with the hardware, in order to keep it in correct state too.

For rather simple modules, I just have a couple of functions like this:

enum state_e {
    MY_STATE__A,
    MY_STATE__B,
};

static enum state_e _cur_state;

void state_on_off(enum state_e state, bool on)
{
    switch (state){
        case MY_STATE__A:
            if (on){
                //-- entering the state A
                prepare_hardware_1(for_state_a);
                prepare_hardware_2(for_state_a);
            } else {
                //-- exiting the state A
                finalize_hardware_2(for_state_a);
                finalize_hardware_1(for_state_a);
            }
            break;
        case MY_STATE__B:
            if (on){
                //-- entering the state B
                prepare_hardware_1(for_state_b);
                prepare_hardware_2(for_state_b);
            } else {
                //-- exiting the state B
                finalize_hardware_2(for_state_b);
                finalize_hardware_1(for_state_b);
            }
            break;
    }
}

void state_set(enum state_e new_state)
{
    state_on_off(_cur_state, false);
    _cur_state = new_state;
    state_on_off(_cur_state, true);
}

Obviously, we need to keep all necessary actions for all states in the _state_on_off() function, and when we need to move to another state, we just call _state_set(new_state) and state transition goes smoothly independently of the direction: all needed actions are performed.

But it works for simple situations only. What if we have something in common between states MY_STATE__B and MY_STATE__C, so that when state is changed from MY_STATE__B to MY_STATE__C and back we should perform only shortened desctruction / construction? But when we go to some other state (say, to MY_STATE__A), we should perform full destruction.

What comes to mind is substates. So we have one state MY_STATE__BC, and substates like MY_BC_SUBSTATE__B and MY_BC_SUBSTATE__C; and of course we have its own function like _state_bc_on_off(). Even this is already a pain, but imagine something more complicated: it goes terrible.

So, what are the best practices for things like that?


Solution

  • A slightly more general state machine has

    • primitives -- subroutines that performs a specific action on a specific piece of hardware
    • sequences -- one or more primitives called in a specific order
    • transitions -- one or more sequences executed in a specific order

    The transitions are encoded in an array of structs. The sequences are selected by switch statement, and each sequences calls one or more primitives.

    #define stA    0x00000001  // bit mask for state A
    #define stB    0x00000002  // bit mask for state B
    #define stC    0x00000004  // bit mask for state C
    #define stAny  0xffffffff  // matches any state
    
    enum { seqXtoY, seqError, seqEnterA, seqExitA, seqEnterB, seqExitB, seqEnableC, seqDisableC, seqEnd };
    
    typedef struct
    {
        int oldState;     // bit mask that represents one or more states that we're transitioning from
        int newState;     // bit mask that represents one or more states that we're transitioning to
        int seqList[10];  // an array of sequences that need to be executed
    }
    stTransition;
    
    static stTransition transition[] =
    {
        // transitions from state A to B or C
        { stA, stB, { seqExitA, seqXtoY, seqEnterB, seqEnd } },
        { stA, stC, { seqExitA, seqXtoY, seqEnableC, seqEnterB, seqEnd } },
    
        // transitions from state B to A or C
        { stB, stA, { seqExitB, seqXtoY, seqEnterA, seqEnd } },
        { stB, stC, { seqXtoY, seqEnableC, seqEnd } },
    
        // transitions from states C to A or B
        { stC, stA, { seqDisableC, seqExitB, seqXtoY, seqEnterA, seqEnd } },
        { stC, stB, { seqDisableC, seqXtoY, seqEnd } },
    
        // any other transition (should never get here)
        { stAny, stAny, { seqError, seqEnd } }
    };
    
    static int currentState = stA;
    
    void executeSequence( int sequence )
    {
        switch ( sequence )
        {
            case seqEnterA:
                prepare_hardware_1(for_state_a);
                prepare_hardware_2(for_state_a);
                break;
    
            case seqExitA:
                finalize_hardware_2(for_state_a);
                finalize_hardware_1(for_state_a);
                break;
    
            case seqEnterB:
                prepare_hardware_1(for_state_b);
                prepare_hardware_2(for_state_b);
                break;
    
            case seqExitB:
                finalize_hardware_2(for_state_b);
                finalize_hardware_1(for_state_b);
                break;
    
            case seqEnableC:
                enable_hardware_3();
                break;
    
            case seqDisableC:
                disable_hardware_3();
                break;
        }
    }
    
    void executeTransition( int newState )
    {
        if ( newState == currentState )
            return;
    
        // search the transition table to find the entry that matches the old and new state
        stTransition *tptr;
        for ( tptr = transition; tptr->seqList[0] != seqError; tptr++ )
            if ( (tptr->oldState & currentState) && (tptr->newState & newState) )
                break;
    
        // execute the sequence list
        int *seqptr;
        for ( seqptr = tptr->seqList; *seqptr != seqEnd; seqptr++ )
        {
            if ( *seqptr == seqXtoY )
                currentState = newState;
            else if ( *seqptr == seqError )
                printf( "The state table is missing the transition from %d to %d\n", currentState, newState );
            else
                executeSequence( *seqptr );
        }
    
        // if the seqList doesn't have an explicit update, then we update at the end
        currentState = newState;
    }