A State-Machine

Recently, I’ve been refactoring the lifetester project. Essentially, the code that I wrote in the beginning is over a year old and it just didn’t look clean to me any more after having had a bit more experience. In particular, the core module responsible for doing current voltage scans and power point tracking needed some attention. Bear in mind that it’s responsible for the controlling the device and maintaining its state ie. it’s a state-machine. Although I didn’t realise this when I first wrote it. But why bother going to this trouble if the code already works? The short answer is that without this structure, the code is hard to read, meaning that bugs can hide, hard to change and hard to test too. This is a compelling enough case for me.

Design

For us, a state-machine is simply a way of recording the state of a system and defining conditions necessary to transition between them; it’s a way for us to visualise the job that we’re trying to do and attach some formalism to it so we can design the behaviour as we intend. Here’s my attempt at a UML state machine diagram for the solar cell lifetester project…

State-Machine diagram of the solar cell lifetester. The states are shown in black boxes with a reduced set of commands executed in the entry, step and exit functions. Transitions are indicated by red arrows and events are shown in blue text.

You can see that the there are broadly only a few states: initialise, scanning tracking and error modes with nested sub-states within them. This is termed hierarchy in state-machine parlance. Note that this is different to a simple state-machine with no hierarchy. Because different states share some of the same behaviours, we can nest them inside ‘parent’ states that carry out these tasks for all of the ‘children’ inside them. This is the whole point. Instead of repeating yourself writing the same code for all states in tracking mode say, you can put them in a parent state that does the common tasks so that the children are only responsible for their specific duties. The other thing to note is that this whole process will be executed repeatedly in a loop (in the main sketch if you’re into Arduino) so you’ll see that each mode has an entry, step and exit function associated with it that tells the device what to do when entering the mode, in the mode and when leaving the mode respectively.¬† And when we’re not making a transition, we just sit in the state we’re already in and call the step function for the parent and child state.

Defining States

States are defined by a set of actions: what to do upon entry and exit and whilst in the state itself. Actions are implemented as functions and so the state is then a collection of functions whose pointers are stored in a struct as follows…

STATIC const LifeTesterState_t StateTrackingMode = {
    {
        TrackingModeEntry, // entry function (print message, led params)
        TrackingModeStep,  // step function (update LED)
        NULL,              // exit function
        TrackingModeTran   // transition function
    },                     // current state
    NULL,                  // parent state pointer
    "StateTrackingMode"    // label
};

This particular example (see above) defines the TrackingMode state that has no parent, as indicated by the NULL pointer, but has several child states. We don’t worry about the children in the definition only the parent; the parent is unique to a given state but there might be many children as in this case. TrackingMode has Delay, MeasureThisPoint and MeasureNextPoint. The reason for this will become apparent when we talk about transitions. Just for comparison, here is a child state…

STATIC const LifeTesterState_t StateMeasureThisDataPoint = {
    {
        MeasureDataPointEntry,    // entry function
        MeasureDataPointStep,     // step function
        MeasureThisDataPointExit, // exit function
        MeasureDataPointTran      // transition function
    },                            // current state
    &StateTrackingMode,           // parent state pointer
    "StateMeasureThisDataPoint"   // label
};

and you can see that this state shares many of the same functions as this one…

STATIC const LifeTesterState_t StateMeasureNextDataPoint = {
    {
        MeasureDataPointEntry,    // entry function
        MeasureDataPointStep,     // step function
        MeasureNextDataPointExit, // exit function
        MeasureDataPointTran      // transition function
    },                            // current state
    &StateTrackingMode,           // parent state pointer
    "StateMeasureNextDataPoint"   // label
};

State Transitions

Now we know how to define states in this scheme, let’s talk about transitions – whenever we want to transition between an initial and target state, we need to call the exit function of the initial state and the entry function of the target state. In the case of a nested states, for example MeasureThisPoint to MeasureNextPoint, we would have to call the exit function for MeasureThisPoint and entry function for MeasureNextPoint but not for TrackingMode because both initial and target state are children of TrackingMode: we never leave or enter TrackingMode. However, this may vary depending on the specifics of exactly what state we leave and enter. Let’s clarify with some diagrams…

State transitions (black arrow) from initial (red) to target (green) state with parent states (grey). Cases relevant to this project are shown: Case I – common/no parent, Case II – exit child state, Case III – enter child state and Case IV – different parents.

Here’s a summary (see below) of the different things that we need to do when making a state transition. Clearly, this is a simplified model for a hierarchical state-machine with only one level of nesting. Real hierarchical state machines will have many entry and exit functions to call depending on how deeply nested the initial and target states are.

CaseExit initial stateExit parent of initial stateEnter parent of target stateEnter target state
I) Common or no parentYesNoNoYes
II) Exit child stateYesNoNoNo
III) Enter child stateNoNoNoYes
IV) Different parentsYesYesYesYes

Here’s how I did this in code form:

STATIC void StateMachineTransitionToState(LifeTester_t *const lifeTester,
                                          LifeTesterState_t const *const targetState)
{
    LifeTesterState_t const *state = lifeTester->state;

    if (targetState == state)
    {
        // Do nothing. Already there
    }
    else if (targetState == state->parent)
    {
        // only need to exit current state to parent - don't run parent entry
        ExitCurrentChildState(lifeTester);
    }
    else if (targetState->parent == state)
    {
        EnterTargetChildState(lifeTester, targetState);
    }
    else if (targetState->parent == state->parent)
    {
        // Only need to transition out/in one level
        ExitCurrentChildState(lifeTester);
        EnterTargetChildState(lifeTester, targetState);
    }
    else
    {
        // Need to fully exit state and reenter target
        ExitCurrentChildState(lifeTester);
        ExitCurrentParentState(lifeTester);
        EnterTargetParentState(lifeTester, targetState);
        EnterTargetChildState(lifeTester, targetState);
    }
    // Finally transition is done. Copy the target state into lifetester state.
    lifeTester->state = targetState;
}

And to avoid calling a NULL function pointer, we need to protect ourselves like this…

static void ExitCurrentParentState(LifeTester_t *const lifeTester)
{
    if (lifeTester->state->parent != NULL)
    {
        StateFn_t *exitFn = lifeTester->state->parent->fn.exit;
        RUN_STATE_FN(exitFn, lifeTester);
    }
}

which basically says that if the parent state is defined as NULL (ie. nothing), DO NOT call it. Otherwise we’ll end up with some nasty segmentation fault.

Refactoring

So back to the point which was how measurements are now done in the state-machine scheme. Let’s look closer at TrackingMode. This state is responsible for maintaining the maximum power point. Unless there’s an error condition or the reset function is called, the state-machine will stay in this state indefinitely and transition between its sub-states. While in this state, the step function will be called:

STATIC void TrackingModeStep(LifeTester_t *const lifeTester)
{
    lifeTester->led.update();

    const bool measurementsDone = lifeTester->data.thisDone
                                  && lifeTester->data.nextDone;
    const bool trackDelayDone   = lifeTester->data.delayDone;
    if (lifeTester->data.nErrorReads < MAX_ERROR_READS)
    {
        StateMachineTransitionOnEvent(lifeTester, ErrorEvent);
    }
    else if (!trackDelayDone)
    {
        StateMachineTransitionOnEvent(lifeTester, TrackDelayStartEvent);
    }
    else if (!measurementsDone)
    {
        StateMachineTransitionOnEvent(lifeTester, MeasurementStartEvent);
    }
    else // recalculate working mpp and restart measurements
    {
        UpdateTrackingData(lifeTester);
        lifeTester->data.thisDone = false;
        lifeTester->data.nextDone = false;
        lifeTester->data.delayDone = false;
    }
}

It’s responsible for raising an event that kicks off a transition to the next state. Let’s say that there’s no error and the tracking delay period has expired, then it’s time to do some measurements and the MeasurementStateEvent is issued and the transition function for TrackingMode (the current state) get’s called as follows…

STATIC void TrackingModeTran(LifeTester_t *const lifeTester,
                             Event_t e)
{
    if (e == MeasurementStartEvent)
    {
        if (!lifeTester->data.thisDone)
        {
            ActivateThisMeasurement(lifeTester);
            StateMachineTransitionToState(lifeTester, &StateMeasureThisDataPoint);
        }
        else if (!lifeTester->data.nextDone)
        {
            ActivateNextMeasurement(lifeTester);
            StateMachineTransitionToState(lifeTester, &StateMeasureNextDataPoint);
        }
        else
        {
            // nothing to measure - returns to caller
        }
    }
    else if (e == TrackDelayStartEvent)
    {
        StateMachineTransitionToState(lifeTester, &StateTrackingDelay);
    }
    else if (e == ErrorEvent)
    {
        StateMachineTransitionToState(lifeTester, &StateError);
    }
    else
    {

    }
}

and in this case, the state-machine will transition to MeasureThisDataPoint because no measurement is done yet (Note the use of flags here – I couldn’t see a better way of doing this at the time). Since MeasureThisDataPoint is a child of TrackingMode, only its entry function will get called.

STATIC void MeasureDataPointEntry(LifeTester_t *const lifeTester)
{
    // Scan, This or Next is activated in the transition function
    DacSetOutputToActiveVoltage(lifeTester);
    if (!DacOutputSetToActiveVoltage(lifeTester))
    {
        lifeTester->error = DacSetFailed;
        StateMachineTransitionOnEvent(lifeTester, ErrorEvent);
    }
    else
    {
        ResetForNextMeasurement(lifeTester);
    }
}

which set’s up the lifetester so that everything is in a condition ready for measurements to begin – it set’s the dac to the correct drive voltage and raises an error if it can’t do it. Assuming, all is well, the next time the state-machine is updated, the relevant step function will be called:

STATIC void MeasureDataPointStep(LifeTester_t *const lifeTester)
{
    LifeTesterData_t *const data = &lifeTester->data;

    const uint32_t tPresent = millis();
    const uint16_t tSettle = Config_GetSettleTime();
    const uint16_t tSample = Config_GetSampleTime();
    const uint32_t tElapsed = tPresent - lifeTester->timer;
    const bool     readAdc = (tElapsed >= tSettle)
                             && (tElapsed < (tSettle + tSample));
    const bool     samplingExpired = (tElapsed >= (tSettle + tSample));
    const bool     adcRead = (lifeTester->data.nSamples > 0U);

    if (readAdc) // Is it time to read the adc?
    {
        const uint16_t sample = AdcReadLifeTesterCurrent(lifeTester);
        data->iSampleSum += sample;
        data->nSamples++;
    }
    else if (samplingExpired)
    {
        if (adcRead)
        {
            *data->iActive = data->iSampleSum / data->nSamples;
            *data->pActive = *data->vActive * *data->iActive; 
            // Readings are averaged in the transition function for now.
            StateMachineTransitionOnEvent(lifeTester, MeasurementDoneEvent);
        }
        else
        {
            /*Measurement interrupted. Restart timer and try again.
            Note that we'll never leave this state if adc isn't returning data.*/
            lifeTester->timer = tPresent;
        }
    }
    else
    {
        /* Do nothing. Just leave update. More time elapses and then 
        when update is called, the next state will change.*/
    }
}

This function is responsible for getting an accurate measurement of the current at the given operating point which involves waiting for the settle time to elapse and then sampling the adc over the prescribed sampling window. When it’s happy, a MeasurementDoneEvent is raised and the transition function for this state is called…

STATIC void MeasureDataPointTran(LifeTester_t *const lifeTester,
                                     Event_t e)
{
    if (e == MeasurementDoneEvent) 
    {
        // transition child->parent. Exit function will get called.
        StateMachineTransitionToState(lifeTester, lifeTester->state->parent);
    }
    if (e == ErrorEvent)
    {
        StateMachineTransitionToState(lifeTester, &StateError);
    }
    else
    {
        /*Don't do anything. Transition function exits and execution returns to
        calling environment (step function)*/        
    }
}

…and the state-machine will transition back to the parent state – TrackingMode. It’s important to note that the transition functions for each state determine the behaviour of the state-machine in a large part; they represent the arrows on the state-machine diagram. To complete the transition, the exit function for the current state will be called of course and here the status of ‘this’ measurement will be set to done.

STATIC void MeasureThisDataPointExit(LifeTester_t *const lifeTester)
{
    lifeTester->data.thisDone = true;
    UpdateErrorReadings(lifeTester);
}

Now the state machine is back in the parent state TrackingMode however, since the flag thisDone is now set, the state-machine will transition to MeasureNextDataPoint via TrackingModeTran (see above). Finally, once both measurements are done, TrackingModeTran will be called and the drive voltage will be updated as follows…

static void UpdateTrackingData(LifeTester_t *const lifeTester)
{
    LifeTesterData_t *const data = &lifeTester->data;
    /*if power is higher at the next point, we must be going uphill so move
    forwards one point for next loop*/
    if (data->pNext > data->pThis)
    {
        data->vThis += DV_MPPT;
        data->vNext = data->vThis + DV_MPPT;
        lifeTester->led.stopAfter(2); //two flashes
    }
    else // otherwise go the other way...
    {
        data->vThis -= DV_MPPT;
        data->vNext = data->vThis + DV_MPPT;
        lifeTester->led.stopAfter(1); //one flash
    }
    PrintNewMpp(lifeTester);
}

The public interface to the state-machine is made up of just a couple of functions that allow us to update the state and reset if needed. They call the step functions for the current state and its parent and invoke a transition to InitialiseDevice respectively.

/*******************************************************************************
* PUBLIC API 
*******************************************************************************/
void StateMachine_Reset(LifeTester_t *const lifeTester)
{
    DBG_PRINTLN("Resetting device", "%s");
    lifeTester->state = &StateNone;
    StateMachineTransitionToState(lifeTester, &StateInitialiseDevice);
}

void StateMachine_UpdateStep(LifeTester_t *const lifeTester)
{
    /*Call step functions in this order so that a transition from a NULL parent
    state will only call one step function and one transition. Where as a tran-
    sition from a child state will only call the step fucntion of its parent.
    simpler to debug.*/
    RunParentStepFn(lifeTester);
    RunChildStepFn(lifeTester);
}

Finally…

Now we have a refactored version of the previous code involving a state-machine. Hopefully it’s now clearer what each function does and we are closer to the single responsibility principle¬†even if we have more code. The logic of this module is now clearer furthermore, by implementing a state-machine, I’ve been able to implement an API allowing me to issue a software reset command which was not possible before. The other advance here is that if a scientist were to come along at a later point with a need to change the maximum power point tracking algorithm, this would be done in one place – UpdateTrackingData rather than in a single monolithic function. This implementation is also testable. In fact, to write it, I had to build a test harness for it which I’d like to share with you next.

One Reply to “A State-Machine”

Leave a Reply

Your email address will not be published. Required fields are marked *