State Machines

Description of the structure and operation of state machines.

The MultiMediaCard Controller uses state machines to manage the interactions with the MultiMediaCard hardware.

State machines allows the controller to maintain the state of each submitted session – allowing it to schedule a second session when the first becomes blocked, for example.

To handle the complex sequence of bus operations involved, the controller implements a state machine stack, allowing a parent state machine function to invoke a child state machine function. The state machine stack allows nesting of children down to a depth of 10 levels.

Each session object has its own individual state machine stack because each session runs its own sequence of bus operations, with the controller managing these multiple sequences. This means that each session object has a state machine object, an instance of the TMMCStateMachine class.

The state machine remembers the next state and child function name, and moves to that state as soon as control returns to the session.

The stack chooses the next session to be handled and manages the state machine through the state machine dispatcher.

Structure of the state machine stack

The stack itself is represented as an array of TMMCMachineStackEntry objects contained within the state machine object TMMCStateMachine.

The state machine maintains a "pointer", TMMCStateMachine::iSP, to the current state entry. This is not a true pointer, but just a value that indexes into the array. However, for all practical purposes it has the effect of a pointer. Each state entry in the stack is thought of as having a parent-child relationship with the other.

Each state entry maintains three pieces of information:

  • A pointer to the state machine function.

  • A variable containing the current state; the value and meaning of the state is defined by the state machine function.

  • A bitmask of TMMCErr defined error conditions; these are set by the parent state machine function so that it gets the chance to trap those errors if the child state machine function returns those errors. Errors are propagated up the stack, and any error conditions not trapped at any level in the state machine hierarchy leads to the state machine terminating with that error value.

In general, the overall state of the state machine reflects the state of the current state entry; for example, TMMCStatemachine::State() returns the state held in the current state entry.

While the flow of control through a stack can be complex, the following diagram shows a short example snapshot. Here, the current state in the current stack entry is Sn and is handled by the function PF(). The code decides to pass control to the child function CF(), but ensures that, on completion, the next state in the current stack entry will be Sn+1.

For example, the DMMCStack::CIMUpdateAcqSM() function implements the CIM_UPDATE_ACQ macro as defined by the MultiMediaCard System Specification, and is a typical state machine function.

Most commands and macros involve one or more asynchronous operations and while such operations are outstanding, a session is blocked. While a session is blocked, its state machine dispatch function is not called. As soon an asynchronous event removes the blocking condition, control returns to the state machine.

Structure of a state machine function

State machine functions are defined and implemented in the DMMCStack class in pairs: one as a static function, and the other as a member function. The static variant takes a TAny* pointer that is cast to a DMMCStack pointer, and the DMMCStack member function is then called:

TMMCErr DMMCStack::FunctionNameSMST( TAny* aPtr )
    {
    return( STATIC_CAST(DMMCStack*,aPtr)->FunctionNameSM() );
    }
TMMCErr DMMCStack::FunctionNameSM()
    {
    enum states {EStBegin=0, EStNext,…, EStEnd };    // Enumeration of states for this state machine
    DMMCSession& s = Session();

    SMF_BEGIN
    // State EStBegin
    // Actions for state EStBegin

    SMF_STATE( EStNext )
    // State EStNext
    // Actions for state EStNext

    SMF_END
    }

A state machine function can release control and wait to be re-entered by returning zero. If its session is not blocked then that will happen immediately. If the state machine function returns non-zero, the session will be completed with that error code unless the parent state machine function has explicitly intercepted such an error by setting the trap mask.

Important points to note

Each state machine function must define a list of states that can exist. States are defined as an enumeration whose first and last values must be labelled EStBegin and EStEnd, respectively. EStBegin must have the value zero. Any other intermediate states are program defined.

To make the state machine functions more readable, a number of macros exist to help with the layout of the function code, and to control program flow. The most basic macros are SMF_BEGIN, SMF_STATE and SMF_END that expand into a switch statement. The above code expands to:

TMMCErr DMMCStack::FunctionNameSM()
{
enum states {EStBegin=0, EStNext,…, EStEnd };    // Enumeration of states for this state machine
DMMCSession& s = Session();

TMMCStateMachine& m = Machine();            //SMF_BEGIN
const TMMCErr err = m.SetExitCode( 0 );        //SMF_BEGIN
for(;;)                                        //SMF_BEGIN
    {                                        //SMF_BEGIN
    switch(m.State())                        //SMF_BEGIN
        {                                    //SMF_BEGIN
        case EStBegin:                        //SMF_BEGIN
            {                                //SMF_BEGIN
            if(err) (void)0;                //SMF_BEGIN 
            // State EStBegin
            // Actions for state EStBegin

            }                                //SMF_STATE
        case EStNext:                        //SMF_STATE
            {                                //SMF_STATE
            // State EStNext
            // Actions for state EStNext

        case EStEnd:                                                //SMF_END
            break;                                                    //SMF_END
            default:                                                //SMF_END
            DMMCController::Panic(DMMCController::EMMCMachineState);//SMF_END
        }                                                            //SMF_END
        break;                                                        //SMF_END
    }                                                                //SMF_END
return(m.Pop());                                                    //SMF_END
}

Notes:

  • be aware that SMF_BEGIN generates an open curly bracket while SMF_STATE generates a corresponding close bracket, and SMF_END closes the switch statement.

You need to be aware of the code that is generated by these macros. In particular, some such as SMF_BEGIN generate an opening curly brace, while others such as SMF_STATE generate a corresponding close curly brace. Also, you need to know whether a macro generates a break; or a return; statement. Lack of awareness here may cause code to be generated that flows from one case statement to another, which may not be your intent.

Blocking on an asynchronous request

The state machine can be made to block. This is done so that you can wait for an asynchronous operation to complete. When the operation completes, it unblocks the state machine. There are two stages to blocking a state machine:

  • Call DMMCStack::BlockCurrentSession() to indicate that the state machine associated with the current session is to be blocked

  • Execute one of the SMF_xxxWAIT macros to return from the current state machine function and wait.

Note that the state machine function must return by calling one of the SMF_xxxWAIT macros. It must not poll or sit in a loop! The state machines are not threaded, and this means that CPU time can only be given to other tasks if the function returns.

DMMCStack::BlockCurrentSession() takes an argument describing the type of block. The state machine will only unblock when an unblock request with the matching argument is called. In the platform specific layer of the MultiMediaCard controller, you should always set the KMMCBlockOnASSPFunction bit in the argument passed to BlockCurrentSession().

To unblock the state machine, call DMMCStack::UnBlockCurrentSession() setting the KMMCBlockOnASSPFunction bit in the flags argument passed to the function, and also an error code. This will invoke the state machine scheduler, which will re-start the state machine.

Note that further state machine processing must take place in a DFC rather than within the interrupt service routine (ISR), and this can be forced by ORing the session status, DMMCSession::iState, with the KMMCSessStateDoDFC bit before unblocking the session. The bit has the effect of queueing a DFC to resume the state machine at some later stage, before returning to the ISR.

The following code shows the idea:

TMMCErr DMMCStackAssp::DoSomethingSM()
    {
    enum states {EStBegin=0, EStDone,EStEnd };    // enumeration of states for this state machine

    SMF_BEGIN
    // start the asynchronous operation
    BlockCurrentSession( KMMCBlockOnASSPFunction );
    StartAsynchronousOp();

    // block and wait. Go to state EStDone when the asynchronous op is complete
    SMF_WAITS( EStDone );
    
    SMF_STATE( EStDone )
    // operation is complete, check for errors and return from state machine
    TInt errors = CheckForHardwareErrors();
    SMF_RETURN( errors );

    SMF_END
    }
void TMMCInterrupt::Service( TAny* aParam )
    {
    Session().iState |= KMMCSessStateDoDFC;
    UnBlockCurrentSession(KMMCBlockOnASSPFunction, KMMCErrNone);
    }

Example of a state machine calling sequence

This shows the state machine calling sequence that implements the reading of a single block on a MultiMediaCard, where the card is not yet selected. Note that the lowest level state machine function called is IssueMMCCommandSM(), which is implemented by the base port.