2
\$\begingroup\$

In my quest to search or develop the 'perfect' state machine I have built the following class. With the class you can make a state machine object which comes with timing and transition methods. Being an embedded developer who hobbies a lot, this class is compliant to the HAL of the Arduino platform.

My aim was to code relative complex tasks while causing as few bugs as possible. That is why I tried to make a painfully clear syntax and an easy to use class. I'll start with the usage.

enter image description here

As part of my bug prevention, I start with simple sphere diagrams. They are used to generate the state machine skeleton. I already filled mine in and it demonstrates all methods of the class.

I recommend you start from the bottom where the state machine is to be found

// HEADER FILES
#include <Arduino.h>
#include "demoStatemachine.h"
#include "src/macros.h"
#include "src/stateMachineClass.h"


static StateMachine sm ;                                                        // static state machine object, you can have more than 1 state machine per source file
                                                                                // but I rarely use more than 1 state machine in a source file

#define beginState testRepeat                                                   // by default this line is commented... 
#ifndef beginState
#error beginState not defined                                                   // ...so a programmer cannot forget to set the begin state
#endif

// VARIABLES
uint8_t timeoutFlag ;

extern uint8 serialByte ;                                                       // global variable which contains the last received serial byte

// FUNCTIONS
extern void demoStatemachineInit(void)
{
    sm.nextState( beginState, 0 ) ;
}


// STATE FUNCTIONS
StateFunction( testRepeat )     // = bool testRepeatF() 
{
    if( sm.entryState() )                                                       // runs once upon entering this state
    {
        Serial.println( "entering test repeat ") ;
    }
    if( sm.onState() )                                                          // keeps running until sm.exit() is called
    {
        if( sm.repeat( 5000 ) )
        {
            Serial.println ("I say helle every 5000ms" ) ;
        }

        if( serialByte == 'S') sm.exit() ;
    }
    if( sm.exitState() )                                                        // runs once upon the last call to this state machine function
    {
        Serial.println( "leaving test repeat ") ;
    }
    return sm.endState() ;                                                      // is true when exitState has run, signales the state machine to pick the next state
}


StateFunction( testReboot )
{
    if( sm.entryState() )
    {
        Serial.println( "entering test Reboot  ") ;
    }
    if( sm.onState() )
    {
        if( serialByte == 'R' ) { sm.reboot( 500 ) ; }                          // lets entry state run again in 500ms 
        if( serialByte == 'S' ) { sm.exit() ; }
    }
    if( sm.exitState() )
    {
        Serial.println( "leaving test Reboot  ") ;
    }
    return sm.endState() ;
}


StateFunction( testTimeout )
{
    if( sm.entryState() )
    {
        Serial.println( "entering test Timeout  ") ;

        timeoutFlag = false ;
        sm.setTimeout( 10000 ) ;
    }
    if( sm.onState() )
    {
        if( sm.timeout() ) 
        {
            timeoutFlag = true ;
            sm.exit() ;
        }
        else if( serialByte == 'S' )
        {
            sm.exit() ;
        }
    }
    if( sm.exitState() )
    {
        if( timeoutFlag ) Serial.println("10 seconds passed, clock beat you") ;
        else              Serial.println("you beat the clock") ;        
    }
    return sm.endState() ;
}


#define entryState  if( sm.entryState() )                                       // this usually on top of the file, cannot use both macro and normal style in 1 file
#define onState     if( sm.onState() )
#define exitState   if( sm.exitState() == 0 ) return 0 ; \
                    else

StateFunction( testIdleState )
{
    entryState                                                                  // if( sm.entryState() )
    {
        Serial.println( "entering test idle, press any key to stop program") ;
    }
    onState                                                                     // if( sm.onState() )
    {
        if( serialByte == 'S')
        {
            sm.exit() ;
        }
    }
    exitState                                                                   // if( sm.exitState() == 0 ) return 0 ; else
    {
        Serial.println( "leaving the test idle state ") ;

        return 1 ;
    }
}


// STATE MACHINE
extern uint8_t demoStatemachine()
{
    STATE_MACHINE_BEGIN( sm ) //if() sm.run() ) switch( sm.getState() ) {       // the 'hidden' sm.run() is what handles the delay between 2 following states

    State(testRepeat) {                                                         // corresponds 101 with diagram
        sm.nextState( testReboot, 1000 ) ; }                                    // entering a non-zero time here, will cause a non-blocking delay before testReboot is run
                                                                                // this is my equivalent of the so-called off state
    State(testReboot) {
        sm.nextState( testTimeout, 2000 ) ; }

    State(testTimeout) {                                                        // = break ; case testTimeout: if( testTimeoutF() )
        if( timeoutFlag ) sm.nextState( testIdleState, 30000 ) ;
        else              sm.nextState( testRepeat,    0 ) ; }

    State(testIdleState) {
        sm.nextState( demoStatemachineIDLE, 0 ) ; }                             // this state does not actually exists
                                                                                // states without outgoing arrows default to idle state
    STATE_MACHINE_END( sm )   //     break ; }   return sm.getState() ;
}

For every Sphere there is a State(). For every arrow/transition there is a sm.nextState(). All code concerning when and why a next state is picked, goes anywhere between State() and nextState(). This is opposed to the transition table.

A 'State' is actually a case label which performs a function call to the corresponding state + F. So State( foo ); is actually break ; case foo: if( fooF() ) When foof() returns 1, the state function is finished and a next state will be picked.

Similarly, StateFunction() is also a macro StateFunction( foo ) becomes bool fooF(). There are several reasons why I use macros for this. First it allows me to use the same enumerated name in both State() and StateFunction() (DRY, don't repeat yourself) Secondly a StateFunction is IMO painfully clear syntax, it is a function which distinguishes itself from regular functions by being part of a state machine.

The matching header file contains the enumerated states and two function prototypes. The reason why these state names are in the header file, is that other code may compare the current state of state machine with one of it's state names. Take note that the first state demoStatemachineIDLE does not actually exist. A state which lacks an outgoing arrow or does not transition to another state (testIdleState in this case) automatically defaults to this idle state.

enum demoStatemachineStates
{
    demoStatemachineIDLE,
    testRepeat,
    testReboot,
    testTimeout,
    testIdleState
} ;

extern uint8_t demoStatemachine(void) ; 
extern void demoStatemachineInit(void) ; 

In this structure, you have the entryState, onState, exitState and offState. The offState is not an actual stretch of code, after all one does not do anything in the offState. If you enter a non-zero value in sm.nextState( bar, 500 ) ; barF will be called 500ms later. It is this 500ms delay which I deem the offState.

Working as embedded designer I wanted to be flexible. I can use Serial input, Wi-Fi input, sensor input and time input. That is why I favour this structure above lists with function points, events and state transitions tables. I keep code where it belongs.

To use the state machine you initialize it once, and you call the function

#include "src/macros.h"                                                         // just contains // #define DEBUG
#include "demoStatemachine.h"

uint8_t serialByte ;

void setup()
{
    initIO() ;
    Serial.begin( 115200 ) ;

    demoStatemachineInit() ;                                                    // initialize state machine
}

void loop()
{
    if( Serial.available() > 0 ) serialByte = Serial.read() ;
    else                         serialByte = 0 ;

    if( demoStatemachine() == demoStatemachineIDLE )                            // you can use the returned state for anything, usefull for nested state machine
    {
        Serial.println("program ended");

        bool PIGS_CAN_FLY = !false ;
        while( PIGS_CAN_FLY == true ) {;}                                       // loop forever
    }
}

The state machine returns its current state. When it reaches the IDLE state, it will return 0. You can make use of this to let state machines communicate to each other or call state machines from other state machines.

By keeping the state machine on the bottom of the file I can also navigate incredibly fast to the state functions.

That leaves us with the state machine class itself. The header file

#include <Arduino.h>
#include "macros.h"

#define StateFunction( x ) bool x##F()
#ifndef DEBUG
#define State(x) break; case x: if(x##F())
#else   
#define State(x) break; case x: if(sm.runOnce) Serial.println(#x); if(x##F())   // if debug is defined, all states are stringerized and printed when entry state is run
#endif
#define STATE_MACHINE_BEGIN(x) if( x.run() ) switch( x.getState() ) {
#define STATE_MACHINE_END(x) break ; } return x.getState() ;

class StateMachine {
public:
    StateMachine() ;
    
    void    setState( uint8_t ) ;
    uint8_t getState() ;
    void    nextState( uint8_t, uint32_t ) ;
    uint8_t entryState() ;
    uint8_t onState() ;
    uint8_t exitState() ;
    uint8_t run() ;
    void    setTimeout( uint32_t ) ;
    uint8_t timeout() ;
    void    exit() ;
    void    reboot( uint32_t ) ;
    uint8_t endState() ;
    uint8_t repeat( uint32_t ) ;
    #ifdef DEBUG
    uint8_t  runOnce ;  // if debug is active, this must be public in order to print the state names
    #endif
    
private:
    #ifndef DEBUG
    uint8_t  runOnce ;  // is usually private
    #endif
    uint8_t  enabled ;
    uint8_t  exitFlag ;
    uint32_t prevTime ;
    uint32_t interval ;
    uint8_t  state;
    uint8_t  timeOutSet ;
} ;

The #ifdef DEBUG directive is used to stringify the enumerated states with a different macro for State(). If DEBUG is defined, all enumerated states of all state machines will be printed to the monitor once when the entryState is run. This allows one to verify the integrity of the state machine even before the actual coding.

The source file of the class

#include "stateMachineClass.h"

/**
 * @brief creates an instance of the state machine class. Used to handle the flow
 */
StateMachine::StateMachine() 
{
    state = 0 ;
    runOnce = true ;
    exitFlag = false ;
}

/**
 * @brief manually set a state
 *
 * @param The state
 *
 * @return N/A
 */
void StateMachine::setState( uint8_t _state )
{
    state = _state ;
    runOnce = true ;
    exitFlag = false ;
}

/**
 * @brief Lets the entry state to run again in a certain amount of time. Can be usefull from time to time
 *
 * @param delay delayed execution if desired
 *
 * @return N/A
 */
void StateMachine::reboot( uint32_t _interval )
{
    runOnce = true ;
    if( _interval )
    {
        enabled = 0 ;
        prevTime = millis() ;
        interval = _interval ;
    }
}

/**
 * @brief  returns the current state variable
 *
 * @param N/A
 *
 * @return current state
 */
uint8_t StateMachine::getState()
{
    return state ;
}

/**
 * @brief function to handle the one time condition for the entry states
 *
 * @param N/A
 *
 * @return true or false
 */
uint8_t StateMachine::entryState()
{
    uint8_t retVal = runOnce ;
    runOnce = 0 ;
    return retVal ;
}

/**
 * @brief function to continously handle the on state, could be a macro
 *
 * @param N/A
 *
 * @return 1
 */
uint8_t StateMachine::onState()
{
    return 1 ;
}

/**
 * @brief function to handle the one time condition for the exit states
 *
 * @param N/A
 *
 * @return true or false
 */
uint8_t StateMachine::exitState()
{
    return exitFlag ;
}

/**
 * @brief Ensures the exit state of a state function will be executed
 *
 * @param N/A
 *
 * @return N/A
 */
void StateMachine::exit()
{
    exitFlag = true ;
}

/**
 * @brief sends signal to state machine if current state function is finished or not
 *
 * @param N/A
 *
 * @return exit flag
 */
uint8_t StateMachine::endState( )
{
    return exitFlag ;
}


/**
 * @brief sets a timestamp which can be used for timing within state functions
 *
 * @param timeout time in ms
 *
 * @return N/A
 */
void StateMachine::setTimeout( uint32_t time2run )
{
    prevTime = millis() ;
    interval = time2run ;
    timeOutSet = true ;
}

/**
 * @brief Monitors if the time in set by setTime() has passed 
 *
 * @param N/A
 *
 * @return true or false
 */
uint8_t StateMachine::timeout()
{
    if( (millis() - prevTime >= interval ) && timeOutSet == true )
    {
        timeOutSet = false ; 
        return 1 ;
    }
    
    return 0 ;
}

/**
 * @brief Transisition from current state to the next state
 *
 * @param _state new state to execute
 * @param _interval when to execute new state
 *
 * @return N/A
 */
void StateMachine::nextState( uint8_t _state, uint32_t _interval ) 
{
    exitFlag = 0 ;
    runOnce = 1 ;
    if( _interval )
    {
        enabled = 0 ;
        prevTime = millis() ;
        interval = _interval ;
    }
    state = _state ;
}

/**
 * @brief monitors the interval when there is an interval set between states
 *
 * @param N/A
 *
 * @return enabled flag
 */
uint8_t StateMachine::run()
{
    if( enabled == 0 )
    {
        if( millis() - prevTime >= interval )
        {
            enabled = 1 ;
        }
    }
    return enabled ;
}

/**
 * @brief Called from within an if-statement, this method can be used for
 * repeating parts of code with a certain interval
 *
 * @param N/A
 *
 * @return enabled flag
 */
uint8_t StateMachine::repeat( uint32_t _interval )
{
    if( millis() - prevTime >= _interval )
    {
        prevTime = millis() ;
        return true ;
    }
    return false ;
}

Generated code is compilable at start; the state machine will run even before coding a single word.

I am using this code extensively. I even added code-snippets to insert new state functions and states to save me the copy+paste trouble. So far it allows me to create code which sometimes even works on the first try. By keeping the code in the state functions as short as simple as possible, you get to prevent creating certain bugs. But I am guessing that this can be set for different state machine structures.

I am fully aware that most programmers are not particular fond of macros. I am asking for feedback; I am really interested in what others think of this code. But if your comment will only contain "cannot read through the macros" or something similar I ask you kindly not to comment at all.

EDIT: Real life example. For my work I program large machinery which use pneumatic cylinders. Not all cylinders come with sensors, so that is just a matter of setting an output and wait for half a second or so before taking the next step.

StateFunction( moveCylinder1 )
{
    if( sm.entryState() )
    {
        digitalWrite( cylinder1, HIGH ) ;
    }
    if( sm.onState() )
    {
        // nothing to do really
        sm.exit() ;
    }
    if( sm.exitState() )
    {
        // nothing to do really
    }
    return sm.endState() ;
}

...

State( moveCylinder1 ) {
    sm.nextState( moveCylinder2, 500 ) ; } // just wait 500ms before moving the 2nd pneumatic cylinder.

Giving the simplicity of the matter, you could drop the "sub states" and just type this instead. I use code snippets to insert a complete function, but stripping it is possible and is part of the flexibility

StateFunction( moveCylinder1 )
{
    digitalWrite( cylinder1, HIGH ) ;
    return 1 ;
}

However, some cylinders do have sensors on one or both ends because they can get stuck. In following example, I want to set a timeout for the movement. If the sensor is not made on time, I want to reset the cylinder print a message, let the warning light blink and I want to be able to make a new attempt with user input. This structure allow me to do this all in a single state function.

StateFunction( moveCylinder1 )
{
    if( sm.entryState() )
    {
        digitalWrite( cylinder1, HIGH ) ;               // set cylinder out

        sm.setTimeout( 1000 ) ;                         // set timeout
    }

    if( sm.onState() )
    {
        if( digitalRead( endSensor) ) sm.exit() ;       // if sensor is made before timeout, state machine will continu

        else if( sm.timeout() )                         // if timeout occurs
        {
            digitalWrite( cylinder1, LOW ) ;            // set cylinder back
            
            blinkWarningLight = true ;                  // draw attention of machine operator 

            Serial.println( "WARNING, cylinder 1 took too long, press restart switch" ) ;

            // sm.reboot( 1000 ) ;                      // you can automatically re attempt to set the cylinder again  and make a few attempts
        }
    }                                                   // OR

    if( resetButtonState == FALLING ) sm.reboot( 0 ) ;  // you wait on user input, this resets the state by letting the entry state run again

    if( sm.exitState() )
    {
        blinkWarningLight = false ;                     // clear the warning light
        Serial.println( "EVERYTHING OK" ) ;
    }
    return sm.endState() ;
}

...

    State( moveCylinder1 ) {
        sm.nextState( moveCylinder2, 0 ) ; } // we know the cylinder is in position, so no need to wait
\$\endgroup\$
5
  • 1
    \$\begingroup\$ if your comment will only contain "cannot read through the macros" or something similar I ask you kindly not to comment at all - that's not how this site works. By posting you agree to a social contract where all insightful observations in answers are on topic whether you asked specifically for them or not. \$\endgroup\$
    – Reinderien
    Commented Mar 1, 2022 at 13:00
  • \$\begingroup\$ "that's not how this site works." well it should. We all know that in the world of programming there are countless religions, how place your { }, snake_case <> cameCase. There are people who deny that gotos can have good uses and there are people who are blindsighted by macrophobia. I ask for constructive feedback about my code structure. I don't ask to receive complains about a minor detail of somebody who simply has a different coding religion than I do. Have you looked at my real life example yet? \$\endgroup\$
    – bask185
    Commented Mar 1, 2022 at 14:02
  • 1
    \$\begingroup\$ There are indeed countless religions, and even citing one of those in the context of an answer is fine, so long as it's called out as being opinion-based. Aversion of macro magic is not opinion-based, and is industry-accepted, conventional knowledge. The use of macros in your code is miles and miles away from being a minor detail. \$\endgroup\$
    – Reinderien
    Commented Mar 1, 2022 at 14:23
  • 1
    \$\begingroup\$ I agree with the other answers that you should really try to avoid the macros. Note that Arduino supports at least C++11 (minus the STL), so you should be able to write your code in such a way that you don't need macros at all. \$\endgroup\$
    – G. Sliepen
    Commented Mar 1, 2022 at 23:13
  • \$\begingroup\$ ok clear, one musn't use macro's.. anybody looked at the real life example yet? \$\endgroup\$
    – bask185
    Commented Mar 2, 2022 at 7:54

2 Answers 2

2
\$\begingroup\$

As mentioned in another review, this is needlessly complex and over-engineered.

I just wanted to add that one cardinal sin in C and C++, one of the worst things you can ever do, is to invent your own secret macro language. Local to your project and known only by you. This makes your code unreadable to C++ programmers who know C++ but do not know or want to know your invented macro language.

Everything along the lines of #define entryState if( sm.entryState() ) needs to go. I cannot stress this enough - if you write code like this in a professional setting, you'd get fired.


On the topic of state machines, they are often used to counter what is known as "flaghetti" - spaghetti programming using multiple flags/booleans. Code such as this example:

void state_do_stuff (void)
{
  if(flag_this && flag_that)
    do_this_and_that();
  else if(flag_that && flag_something else)
    do_something_else();
  ...
}

And so on, multiple flag checks all over the code, often various complex conditions. These are warning signs that this state might be too complex and you could break it up into several, getting rid of the flags in the process.


It's also good practice to centralize all state changes to one single place in the code. While doing so, you can also centralize all error handling to one place in the code. An idiomatic state machine for a bare metal embedded system usually looks like this:

void main()
{
  for(;;)
  {
    pet_wdog();
    result = state_machine(state);
    state = error_handler(result); // the only place where state changes
  }
}
\$\endgroup\$
5
  • \$\begingroup\$ sorry was busy, typing and hit enter too soon. If one claims he cannot comprehend entryState { and nor theorize what it would do given the surrounding syntax, he is propably more likely to get fired. Besides I deliberately requested not complains about macros. You said this earlier to me and you also told me that OOP is not well suited for 8 bit systems.. \$\endgroup\$
    – bask185
    Commented Mar 1, 2022 at 12:35
  • 1
    \$\begingroup\$ This answer has good content. Macro magic is the path to madness. Even your comment illustrates the problem: a programmer shouldn't need to theorize anything, and should be able to follow a programming pattern being able to trivially build a mental model of what the compiler will do without having to mentally unwind the convoluted implications of a macro. \$\endgroup\$
    – Reinderien
    Commented Mar 1, 2022 at 13:05
  • 2
    \$\begingroup\$ @bask185 Why ask for a review if you don't want one. I have over 20 years of experience of embedded microcontroller programming in C from a professional setting (industrial/automotive). Including extensive experience of salvaging failed and broken C projects that have gone haywire because of bad programmers - essentially I go in and take over the project once some quack has messed it up too bad. The things I remark about aren't subjective opinions, but problems that several times caused real C projects/products to fail in real-world applications and causing major monetary damage because of it. \$\endgroup\$
    – Lundin
    Commented Mar 1, 2022 at 15:18
  • \$\begingroup\$ "Why ask for a review if you don't want one". Why post a comment, if you know in that it will cause discussion and conflict. With out past, you must have known this would happen. That is why I feel trolled. We simply believe different things, we find eachother illogical and immutable and this won't change. I would have slept better if you simply laid of the keyboard. I really do wanted feedback I just dit not want a macro discussion. In fact, and I kid you not, the very reason why I 'kindly' asked not to post, was because of my past dealings with you. \$\endgroup\$
    – bask185
    Commented Mar 2, 2022 at 13:44
  • \$\begingroup\$ @bask185 I interact with hundreds of people on the SE network each week. I have no idea who you are and have no "past" with you. And regardless, stubbornly refusing to adopt what's widely considered good practice isn't going to do you any favours. \$\endgroup\$
    – Lundin
    Commented Mar 2, 2022 at 14:00
2
\$\begingroup\$

Particularly for embedded code this is overdesigned. A state machine can be as simple as a loop and an enum variable. In other words, given your described goal of

painfully clear

this is regretfully a swing and a miss. It's not just the usage that needs to be clear: it's the entire execution model, and currently it's just too complex. Even in state machine theory it's too complex, because a finite state machine should not distinguish between actions "on state entry", "on state" and "on state exit".

At the risk of over-generalising, I'm afraid there is not an embedded system design I've worked on that would benefit from this library, and I'll claim that your cylindre application does not either.

\$\endgroup\$
2
  • \$\begingroup\$ OP added a real life example. \$\endgroup\$
    – pacmaninbw
    Commented Mar 1, 2022 at 12:35
  • \$\begingroup\$ "because a finite state machine should not distinguish between actions "on state entry", "on state" and "on state exit". I have a serious query. Why precisely is this? if I would code a more 'conventional' state machine without the entry state, would my real life example not have to exist out of atleast three states? \$\endgroup\$
    – bask185
    Commented Mar 2, 2022 at 13:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.