States are represented by the ecu_fsm_state struct. It contains a set of handler functions that the user defines to describe the state’s behavior. This framework automatically executes the correct sequence of handler functions while the state machine is running:
entry() is an optional function that executes when the state is first entered. Set to ECU_FSM_STATE_ENTRY_UNUSED if unused.
exit() is an optional function that executes when the state is exited. Set to ECU_FSM_STATE_EXIT_UNUSED if unused.
handler() is a mandatory function that executes when the FSM is running in this state.
The contents are const-qualified, forcing every state to be created at compile-time via the ECU_FSM_STATE_CTOR() macro:
/* Defined by user. */staticvoidrunning_state_on_entry(structecu_fsm*fsm);staticvoidrunning_state_on_exit(structecu_fsm*fsm);staticvoidrunning_state_handler(structecu_fsm*fsm,constvoid*event);staticconststructecu_fsm_stateRUNNING_STATE=ECU_FSM_STATE_CTOR(&running_state_on_entry,&running_state_on_exit,&running_state_handler);
Or if the optional entry and exit handlers are unused:
/* Defined by user. */staticvoidrunning_state_handler(structecu_fsm*fsm,constvoid*event);staticconststructecu_fsm_stateRUNNING_STATE=ECU_FSM_STATE_CTOR(ECU_FSM_STATE_ENTRY_UNUSED,ECU_FSM_STATE_EXIT_UNUSED,&running_state_handler);
Representing states as objects allows them to be shared between multiple instances of the same FSM. No additional memory or overhead is required:
This framework has no knowledge of the application’s state machine type so it must only use ecu_fsm to remain portable. The ecu_fsm member acts as a common interface between the two mediums. Thus each state handler must take in an ecu_fsm pointer:
/* Defined by user. */staticvoidrunning_state_on_entry(structecu_fsm*fsm);staticvoidrunning_state_on_exit(structecu_fsm*fsm);staticvoidrunning_state_handler(structecu_fsm*fsm,constvoid*event);
The application’s state machine type can be retrieved within each handler’s definition via the ECU_FSM_GET_CONTEXT() macro:
This allows the framework to interact with the application through a common interface (the ecu_fsm struct), without inheritance. The macro takes in three parameters:
ecu_fsm_ptr_ = Pointer to intrusive ecu_fsm member. In this case, fsm.
type_ = User’s FSM type. In this case, structapp_fsm.
member_ = Name of ecu_fsm member within the user’s type. In this case, fsm_member.
The example above showcases a single state transition. When this occurs, the framework executes the current state’s exit handler then the new state’s entry handler. Thus the full execution order is:
ecu_fsm_dispatch(fsm, STOP_EVENT)
RUNNING_STATE::handler()
RUNNING_STATE::exit()
STOPPED_STATE::entry()
Warning
No state transitions are allowed in the exit handler. This is pointless since
when the exit handler runs, that state is already being exited:
staticvoidstate1_on_exit(structecu_fsm*fsm){ecu_fsm_change_state(fsm,&STATE2);/* NOT ALLOWED! */}
A consecutive state transition can occur by calling ecu_fsm_change_state() within a state’s entry handler. The following example transitions the state machine from the STOPPED_STATE to the PREOPERATIONAL_STATE then to the OPERATIONAL_STATE when a START_EVENT is received:
This framework automatically executes the proper state handlers, making the full execution order:
ecu_fsm_dispatch(fsm, START_EVENT)
STOPPED_STATE::handler()
STOPPED_STATE::exit()
PREOPERATIONAL_STATE::entry()
PREOPERATIONAL_STATE::exit()
OPERATIONAL_STATE::entry()
Warning
A self-state transition is not allowed in the entry handler as this
would cause an infinite loop:
staticvoidstate1_on_entry(structecu_fsm*fsm){ecu_fsm_change_state(fsm,&STATE1);/* NOT ALLOWED! */}
The final type of transition that can occur is the self-state transition. This is accomplished by supplying the current state to ecu_fsm_change_state() within the state’s main handler function:
staticvoidrunning_state_handler(structecu_fsm*fsm,constvoid*event){if(event==RESET_EVENT){/* Current state is RUNNING_STATE. Self-transition. */ecu_fsm_change_state(fsm,&RUNNING_STATE);}}
This framework automatically executes the proper state handlers, making the full execution order:
The application interacts with state machines generated by this framework through events. Events are objects that describe what happened and contain any relevant data:
When an event occurs, it is sent to the state machine via ecu_fsm_dispatch(). The application treats the state machine as a black-box and blindly dispatches events to it:
intmain(){if(requestedtostop){structeventstop_event={STOP_EVENT,0,0};ecu_fsm_dispatch(fsm,&stop_event);/* Don't care what state machine is currently doing. */}}
This pattern naturally decouples the FSM from the application as all forms of communication between the two are limited to event dispatching. Therefore the state machine’s implementation details (the state machine box) are fully encapsulated.
Compare this to the traditional polling state machine that is unfortunately most commonly used:
Polling state machines tightly couple themselves to the application as the two often communicate with each other through extensive use of global flags. The points below expand upon this and further explain why the event-driven approach is superior:
An event-driven state machine’s implementation is fully reusable and encapsulated. Porting the state machine to a new application simply involves dispatching events under different conditions. Applications also remain uneffected if the state machine’s internal details were to ever change. At a maximum, the changepoint is limited to creating new event structs:
Offloading the button press logic to the application does not fix the coupling. It just changes which global flags are used to facilitate communication between the two:
Porting this state machine to a new application is not trivial because it must carefully edit these global flags in a predefined fashion. Also if the state machine’s details change, every application that uses it would have to be refactored.
Multiple instances of the same event-driven state machine can be created since events are not shared objects. Each state machine instance operates independently from one another:
An event-driven state machine is far easier to test. The natural decoupling allows test code to simply create an event, dispatch it to the state machine, and verify its output. This encapsulates all implementation details from the test code, making it fully portable. The test state machine can also easily be reset by simply creating a new instance:
Polling state machines are extremely difficult to test since they are often implemented as free functions that are tightly coupled to the application. Test code must manually edit flags to reset and fully test the state machine. This is unportable since test code directly interacts with the state machine’s implementation details. All tests would have to be refactored whenever the state machine’s implementation changes:
staticvoidpolled_fsm_implementation(void){/* Good luck resetting this. */staticenum{ON_STATE,OFF_STATE}state;switch(state){caseON_STATE:/* Good luck resetting global flags and maintaining that in test code. */if(off_flag){off_flag=false;state=OFF_STATE;}break;// ...}}TEST(){setstate==ON_STATE;/* Somehow have to set local flag. */off_flag=true;polled_fsm_implementation();verify_output();}
Thread-safety is trivial for event-driven state machines. Events are not shared resources since they are represented as objects. Therefore each thread can create their own events, giving them exclusive access:
This makes the only shared resource the state machine. ecu_fsm_dispatch() must run to completion and cannot be pre-empted. This requirement is easily satisfied by delegating the state machine to its own RTOS thread that blocks on an event queue:
Other threads can only iteract with the state machine by posting events to its queue. Queues are a thread-synchronization primitive provided by the RTOS vendor so reads and writes are guaranteed to be thread-safe. The state machine performs work (processes events) in its own thread. A thread cannot pre-empt itself, so ecu_fsm_dispatch() is guaranteed to run to completion.
Also notice how the state machine is naturally decoupled from the RTOS. The implementation (state machine box) is simply wrapped in an RTOS thread. Porting to a different RTOS (or bare-metal) simply involves using the same state machine implementation code and wrapping it in RTOS-specific primitives.
Thread safety is not trivial for polling state machines. Every global flag is a shared resource that must be carefully guarded:
It is extremely difficult to analyze how different threads will effect the behavior of each global flag, and thus the behavior of the state machine. The state machine and application are also now both tightly coupled to the RTOS. Porting to a different RTOS requires changes everywhere (in this case using a new mutex API).
The above figures also showcase how much less efficient a polling state machine is. Blocking and CPU processing is required every time the state machine is polled (which is repeatedly). Blocking and CPU processing only occurs for event-driven state machines when they need to actually perform work (post an event or process an event).
The LED starts in the OFF_STATE. LED_ON_EVENTs and LED_OFF_EVENTs can be blindly dispatched by the application to turn the LED on and off respectively. The state machine’s implementation ensures the LED is always in the correct state. Its logic is contained within a reusable LED object, whose implementation is in led.h and led.c.
This LED module is then ported to two separate applications. The first on a microcontroller in main.c, where multiple LED objects are created and used. The second on a computer in tests.c, where the LED module is tested.
/*-------------------------- led.h --------------------------*/structled{void(*turn_on)(void*obj);/* Dependency injection. User-defined function that turns LED off. */void(*turn_off)(void*obj);/* User-defined function that turns LED on. */void*obj;/* Optional object passed to user-defined callbacks. */structecu_fsmfsm;/* State machine framework. */};enumled_event_id{LED_ON_EVENT,LED_OFF_EVENT};structled_event{enumled_event_idid;/* Wrap ID in struct in case more items have to be added to event in the future. */};externvoidled_ctor(structled*me,void(*turn_on)(void*obj),void(*turn_off)(void*obj),void*obj);externvoidled_start(structled*me);externvoidled_dispatch(structled*me,conststructled_event*event);
/*-------------------------- led.c --------------------------*/#include"led.h"staticvoidon_state_entry(structecu_fsm*led);staticvoidon_state_handler(structecu_fsm*led,constvoid*event);staticvoidoff_state_entry(structecu_fsm*led);staticvoidoff_state_handler(structecu_fsm*led,constvoid*event);/* States can be shared across any number of LED fsms. */staticconststructecu_fsm_stateON_STATE=ECU_FSM_STATE_CTOR(&on_state_entry,ECU_FSM_STATE_EXIT_UNUSED,&on_state_handler);staticconststructecu_fsm_stateOFF_STATE=ECU_FSM_STATE_CTOR(&off_state_entry,ECU_FSM_STATE_EXIT_UNUSED,&off_state_handler);staticvoidon_state_entry(structecu_fsm*led){structled*me=ECU_FSM_GET_CONTEXT(led,structled,fsm);(*me->turn_on)(me->obj);}staticvoidon_state_handler(structecu_fsm*led,constvoid*event){conststructled_event*e=(conststructled_event*)event;switch(e->id){caseLED_OFF_EVENT:{ecu_fsm_change_state(led,&OFF_STATE);break;}default:{/* Ignore all other events, including LED_ON_EVENT. */break;}}}staticvoidoff_state_entry(structecu_fsm*led){structled*me=ECU_FSM_GET_CONTEXT(led,structled,fsm);(*me->turn_off)(me->obj);}staticvoidoff_state_handler(structecu_fsm*led,constvoid*event){conststructled_event*e=(conststructled_event*)event;switch(e->id){caseLED_ON_EVENT:{ecu_fsm_change_state(led,&ON_STATE);break;}default:{/* Ignore all other events, including LED_OFF_EVENT. */break;}}}voidled_ctor(structled*me,void(*turn_on)(void*obj),void(*turn_off)(void*obj),void*obj){ecu_fsm_ctor(&me->fsm,&LED_OFF_STATE);me->turn_on=turn_on;me->turn_off=turn_off;me->obj=obj;}voidled_start(structled*me){ecu_fsm_start(&me->fsm);}voidled_dispatch(structled*me,conststructled_event*event){ecu_fsm_dispatch(&me->fsm,event);}
Converts intrusive ecu_fsm member back into the user’s state machine type. This should be used inside a state handler’s definition. See State Machine Representation Section for more details:
/* Defined by user. */staticvoidrunning_state_on_entry(structecu_fsm*fsm);staticvoidrunning_state_on_exit(structecu_fsm*fsm);staticvoidrunning_state_handler(structecu_fsm*fsm,constvoid*event);staticconststructecu_fsm_stateRUNNING_STATE=ECU_FSM_STATE_CTOR(&running_state_on_entry,&running_state_on_exit,&running_state_handler);
Constructor. Initializes the ecu_fsm data structure for use.
Warning
Must be called once on startup before the state machine is used. User is also responsible for allocating memory since ECU does not use dynamic memory allocation.
structecu_fsmfsm;/* User must allocate memory before constructor. */ecu_fsm_start(&fsm);/* ILLEGAL. Must construct before using. */ecu_fsm_ctor(&fsm,&INIT_STATE);ecu_fsm_start(&fsm);/* Ok. */
Relays event to the FSM where it is processed by the current state’s handler function. Manages all state transition logic if any state changes were signalled via ecu_fsm_change_state(). See Event-Driven Paradigm Section for more details.
Runs the initial state’s entry handler and manages all state transition logic if any state changes were signalled via ecu_fsm_change_state(). This function does nothing if the initial state’s entry handler is unused. See State Transitions Section for more details:
Warning
This function should only be called once on startup and must run to completion.