The term ECU in this document refers to Embedded C Utilities, the shorthand name for this project.
Framework that creates and runs hierarchical state machines (HSMs). Applications use this framework by containing an intrusive ecu_hsm member.
This framework models HSMs as closely as possible to UML state machines with the following known deviations:
Actions associated with state transitions are performed before states are exited. UML mandates: exit->action->entry. This framework instead executes: action->exit->entry
Composite state: state that contains other states. In the figure above, ON_STATE is a composite state.
Leaf state: state that does not contain any other states. In the figure above, IDLE_STATE, RUNNING_STATE, and OFF_STATE are leaf states.
Least common ancestor (LCA): The deepest state in the hierarchy that has both A and B as descendants. For example:
LCA(IDLE_STATE, RUNNING_STATE) == ON_STATE
LCA(IDLE_STATE, ON_STATE) == ON_STATE
LCA(RUNNING_STATE, OFF_STATE) == TOP_STATE
Initial transition: mandatory state transition taken by a composite state(s) to eventually get into a leaf state. The red arrows in the figure above are initial transitions.
Source state: original state the HSM is in before an event is dispatched to it.
Target state: new state the HSM should be in after a transition. For example if the HSM in the figure above is in the RUNNING_STATE and an OFF_EVENT is dispatched:
The finite state machine (FSM) in the example above repeats code by handling the OFF_EVENT in both the IDLE_STATE and RUNNING_STATE. The HSM avoids this repetition by only handling the OFF_EVENT in the ON_STATE.
When an event is dispatched to an HSM, it is propagated up the state hierarchy until it is handled. For example the execution order for an OFF_EVENT while in the IDLE_STATE is:
IDLE_STATE::handler(OFF_EVENT)
ON_STATE::handler(OFF_EVENT)
And similarly for an OFF_EVENT while in the RUNNING_STATE:
RUNNING_STATE::handler(OFF_EVENT)
ON_STATE::handler(OFF_EVENT)
Obviously no such propagaton exists for the FSM.
The nested model of the HSM also requires state transitions to be handled differently from FSMs. Transitions in an FSM simply involves exiting the source state and entering the target state. For example, the OFF_EVENT in the RUNNING_STATE causes the following execution order in the FSM:
RUNNING_STATE::handler(OFF_EVENT)
RUNNING_STATE::exit()
OFF_STATE::entry()
However an HSM must exit and enter all necessary states in the hierarchy. The same OFF_EVENT in the HSM’s RUNNING_STATE causes:
RUNNING_STATE::handler(OFF_EVENT)
ON_STATE::handler(OFF_EVENT)
RUNNING_STATE::exit()
ON_STATE::exit()
OFF_STATE::enter()
HSM state transitions are explained in detail in the State Transitions Section. The basic algorithm involves:
Find LCA(Source state, Target state). I.e. LCA(RUNNING_STATE, OFF_STATE) == TOP_STATE.
Exit up from the source state until the LCA. I.e. exit up from RUNNING_STATE until TOP_STATE.
Enter from the LCA to the target state. I.e. enter from TOP_STATE to OFF_STATE.
States are represented by the ecu_hsm_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 this state is first entered. Set to ECU_HSM_STATE_ENTRY_UNUSED if unused.
exit() is an optional function that executes when this state is exited. Set to ECU_HSM_STATE_EXIT_UNUSED if unused.
initial() is a mandatory function for composite states. Otherwise this must be set to ECU_HSM_STATE_INITIAL_UNUSED for leaf states. Executes if this state is a composite and the HSM transitions into it. An initial transition to a state lower in the hierarchy is taken.
handler() is a mandatory function that executes when the HSM is running in this state. Processes dispatched events and returns true if the event was handled. Returns false if the event should be propagated up the state hierarchy.
parent is this state’s parent. All states must have its parent be another user-defined state or ECU_HSM_TOP_STATE.
The contents are const-qualified, forcing every state to be created at compile-time via the ECU_HSM_STATE_CTOR() macro:
/* Defined by user. */staticvoidon_state_on_entry(structecu_hsm*hsm);staticvoidon_state_on_exit(structecu_hsm*hsm);staticvoidon_state_initial(structecu_hsm*hsm);staticboolon_state_handler(structecu_hsm*hsm,constvoid*event);staticconststructecu_hsm_stateON_STATE=ECU_HSM_STATE_CTOR(&on_state_on_entry,&on_state_on_exit,&on_state_initial,&on_state_handler,&ECU_HSM_TOP_STATE);
Representing states as objects allows them to be shared between multiple instances of the same HSM. 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_hsm to remain portable. The ecu_hsm member acts as a common interface between the two mediums. Thus each state handler must take in an ecu_hsm pointer:
/* Defined by user. */staticvoidon_state_on_entry(structecu_hsm*hsm);staticvoidon_state_on_exit(structecu_hsm*hsm);staticvoidon_state_initial(structecu_hsm*hsm);staticboolon_state_handler(structecu_hsm*hsm,constvoid*event);
The application’s state machine type can be retrieved within each handler’s definition via the ECU_HSM_GET_CONTEXT() macro:
This allows the framework to interact with the application through a common interface (the ecu_hsm struct), without inheritance. The macro takes in three parameters:
ecu_hsm_ptr_ = Pointer to intrusive ecu_hsm member. In this case, hsm.
type_ = User’s HSM type. In this case, structapp_hsm.
member_ = Name of ecu_hsm member within the user’s type. In this case, hsm_member.
The application can only interact with HSMs created with this framework by dispatching events via ecu_hsm_dispatch().
Events are objects that describe what happened and contain any relevant data. This pattern naturally decouples the state machine from the application and is fully explained in the Event-Driven Paradigm Section of the FSM Framework. The exact same principles apply to HSMs.
When an event is dispatched to an HSM, it is processed in the current state. If the event is unhanded, it is propagated up the state hierarchy. Take this example HSM (initial transitions, etc not shown for conciseness):
If the HSM is in S11 and EVENT_A is dispatched, it is propgated up the state hierarchy and eventually handled in state S. The full handling order is:
S11:handler(EVENT_A)
S1::handler(EVENT_A)
S::handler(EVENT_A)
A state handles the event (stops propagating it) by returning true in its handler definition. A state propgates the event up the hierarchy by returning false in its handler definition. In the example HSM shown above S11, S1, and S handler definitions would look like:
staticboolS11_handler(structecu_hsm*hsm,constvoid*event){returnfalse;/* Propagate all events up state hierarchy. */}staticboolS1_handler(structecu_hsm*hsm,constvoid*event){returnfalse;/* Propagate all events up state hierarchy. */}staticboolS_handler(structecu_hsm*hsm,constvoid*event){/* Handle EVENT_A. All other events propagated up the state hierarchy. */if(event==EVENT_A){returntrue;}else{returnfalse;}}
Event propagation completes when the top state (root) is reached. This framework requires the top state to beECU_HSM_TOP_STATE. It is a dummy default state that handles all events by always returning true in its handler definition.
Warning
All HSMs are required to have their top states be ECU_HSM_TOP_STATE.
Note how this top state is also required to guarantee a cohesive state hierarchy. I.e. A tree must always have a root.
This framework automatically executes the proper exit and entry paths. The complexity of this operation is fully encapsulated in ecu_hsm_change_state().
Warning
The following rules MUST be satisfied when performing a state transition:
A state transition can never occur in a state’s entry and exit handlers.
All composite states must define an initial transition down the state hierarchy. This is because the HSM must be in a leaf state once all transitions are complete.
The keyboard stores which keys have been pressed in a standard USB HID report. It is sent to the computer when the application requests to send it, or when the report becomes full. Caps lock logic is also handled.
The complexity of these operations are simplified by modeling the keyboard’s behavior as an HSM, whose implementation is fully encapsulated within a reusable keyboard object defined in keyboard.h and keyboard.c. Therefore the application does not have to worry about any of the complexities explained above. It simply interacts with the keyboard by blindly dispatching events to it.
This module (keyboard object) is then ported to two separate applications. The first on a microcontroller in main.c, where a keyboard object is created and used. The second on a computer in tests.c, where the keyboard module is tested.
/*-------------------------- keyboard.h --------------------------*/structkeyboard{structecu_hsmhsm;uint8_tmodifier;/* HID report byte 0. Bitmask of modifier (shift, etc) keys pressed. Unused for this example. */uint8_tdata[6];/* HID report bytes 2 to 7. Stores keys that have been pressed. */size_tindex;/* Current index in data[]. */void(*send)(constuint8_t*report,size_tcount,void*obj);/* Dependency injection. Send HID report to computer. */void*obj;/* Optional object passed to user-defined callbacks. */};enumkeyboard_event_id{KEYBOARD_CONNECTED_EVENT,/* Keyboard disconnected from computer. */KEYBOARD_DISCONNECTED_EVENT,/* Keyboard reconnected to computer. */KEYBOARD_KEYPRESS_EVENT,/* Key pressed. */KEYBOARD_SEND_EVENT/* Application requests to send HID report. */};structkeyboard_event{enumkeyboard_event_idid;uint8_tkeycode;};externvoidkeyboard_ctor(structkeyboard*me,void(*send)(constuint8_t*,size_t,void*),void*obj);externvoidkeyboard_start(structkeyboard*me);externvoidkeyboard_dispatch(structkeyboard*me,constvoid*event);
/*-------------------------- keyboard.c --------------------------*/#include"keyboard.h"#define KEYCODE_CAPS_LOCK 0x39Ustaticvoidkeycode_report_clear(structkeyboard*me);/* Resets HID report buffer but does not send it. */staticvoidkeycode_report_send(structkeyboard*me);/* Sends current HID report to computer then resets it. */staticvoidconnected_state_on_entry(structecu_hsm*hsm);staticvoidconnected_state_initial(structecu_hsm*hsm);staticboolconnected_state_handler(structecu_hsm*hsm,constvoid*event);staticbooldefault_state_handler(structecu_hsm*hsm,constvoid*event);staticboolcaps_lock_state_handler(structecu_hsm*hsm,constvoid*event);staticbooldisconnected_state_handler(structecu_hsm*hsm,constvoid*event);staticconststructecu_hsm_stateCONNECTED_STATE=ECU_HSM_STATE_CTOR(&connected_state_on_entry,ECU_HSM_STATE_EXIT_UNUSED,&connected_state_initial,&connected_state_handler,&ECU_HSM_TOP_STATE);staticconststructecu_hsm_stateDEFAULT_STATE=ECU_HSM_STATE_CTOR(ECU_HSM_STATE_ENTRY_UNUSED,ECU_HSM_STATE_EXIT_UNUSED,ECU_HSM_STATE_INITIAL_UNUSED,&default_state_handler,&CONNECTED_STATE);staticconststructecu_hsm_stateCAPS_LOCK_STATE=ECU_HSM_STATE_CTOR(ECU_HSM_STATE_ENTRY_UNUSED,ECU_HSM_STATE_EXIT_UNUSED,ECU_HSM_STATE_INITIAL_UNUSED,&caps_lock_state_handler,&CONNECTED_STATE);staticconststructecu_hsm_stateDISCONNECTED_STATE=ECU_HSM_STATE_CTOR(ECU_HSM_STATE_ENTRY_UNUSED,ECU_HSM_STATE_EXIT_UNUSED,ECU_HSM_STATE_INITIAL_UNUSED,&disconnected_state_handler,&ECU_HSM_TOP_STATE);staticvoidkeycode_report_clear(structkeyboard*me){me->modifier=0;memset(&me->data[0],0,sizeof(me->data));me->index=0;}staticvoidkeycode_report_send(structkeyboard*me){uint8_treport[8];report[0]=me->modifier;report[1]=0;/* Reserved. */memcpy(&report[2],&me->data[0],sizeof(me->data));(*me->send)(&report[0],sizeof(report),me->obj);keycode_report_clear(me);}staticvoidconnected_state_on_entry(structecu_hsm*hsm){structkeyboard*me=ECU_HSM_GET_CONTEXT(hsm,structkeyboard,hsm);keycode_report_clear(me);}staticvoidconnected_state_initial(structecu_hsm*hsm){ecu_hsm_change_state(hsm,&DEFAULT_STATE);}staticboolconnected_state_handler(structecu_hsm*hsm,constvoid*event){boolstatus=true;structkeyboard*me=ECU_HSM_GET_CONTEXT(hsm,structkeyboard,hsm);conststructkeyboard_event*e=(conststructkeyboard_event*)event;switch(e->id){caseKEYBOARD_DISCONNECTED_EVENT:{ecu_hsm_change_state(hsm,&DISCONNECTED_STATE);break;}default:{/* Propagate event up hierarchy. */status=false;break;}}returnstatus;}staticbooldefault_state_handler(structecu_hsm*hsm,constvoid*event){boolstatus=true;structkeyboard*me=ECU_HSM_GET_CONTEXT(hsm,structkeyboard,hsm);conststructkeyboard_event*e=(conststructkeyboard_event*)event;switch(e->id){caseKEYBOARD_KEYPRESS_EVENT:{ECU_ASSERT((me->index<sizeof(me->data)));uint8_tkeycode=e->keycode;if(keycode==KEYCODE_CAPS_LOCK){keycode_report_send(me);/* Send report that had keys without caps lock modifier. */me->data[0]=KEYCODE_CAPS_LOCK;/* Set caps lock modifier. */me->index=1;keycode_report_send(me);/* Send report that says caps lock now pressed. */ecu_hsm_change_state(hsm,&CAPS_LOCK_STATE);}elseif(me->index==sizeof(me->data)-1){/* If added keycode causes report to be full, send report. */me->data[me->index]=keycode;keycode_report_send(me);}else{/* Otherwise just store the keycode. */me->data[me->index]=keycode;me->index++;}break;}caseKEYBOARD_SEND_EVENT:{keycode_report_send(me);break;}default:{/* Propagate event up hierarchy. */status=false;break;}}returnstatus;}staticboolcaps_lock_state_handler(structecu_hsm*hsm,constvoid*event){boolstatus=true;structkeyboard*me=ECU_HSM_GET_CONTEXT(hsm,structkeyboard,hsm);conststructkeyboard_event*e=(conststructkeyboard_event*)event;switch(e->id){caseKEYBOARD_KEYPRESS_EVENT:{ECU_ASSERT((me->index<sizeof(me->data)));uint8_tkeycode=e->keycode;if(keycode==KEYCODE_CAPS_LOCK){keycode_report_send(me);/* Send report that had keys with caps lock modifier. */keycode_report_send(me);/* Send report that says caps lock no longer pressed. */ecu_hsm_change_state(hsm,&DEFAULT_STATE);}elseif(me->index==sizeof(me->data)-1){/* If added keycode causes report to be full, send report. Afterwards reinitialize the caps lock modifier. */me->data[me->index]=keycode;keycode_report_send(me);me->data[0]=KEYCODE_CAPS_LOCK;me->index=1;}else{/* Otherwise just store the keycode. */me->data[me->index]=keycode;me->index++;}break;}caseKEYBOARD_SEND_EVENT:{/* Send report then reinitialize caps lock modifier. */keycode_report_send(me);me->data[0]=KEYCODE_CAPS_LOCK;me->index=1;break;}default:{/* Propagate event up hierarchy. */status=false;break;}}returnstatus;}staticbooldisconnected_state_handler(structecu_hsm*hsm,constvoid*event){boolstatus=true;conststructkeyboard_event*e=(conststructkeyboard_event*)event;switch(e->id){caseKEYBOARD_CONNECTED_EVENT:{ecu_hsm_change_state(hsm,&CONNECTED_STATE);break;}default:{/* Propagate event up hierarchy. */status=false;break;}}returnstatus;}voidkeyboard_ctor(structkeyboard*me,void(*send)(constuint8_t*,size_t,void*),void*obj){ecu_hsm_ctor(&me->hsm,&CONNECTED_STATE,2);me->send=send;me->obj=obj;keycode_report_clear(me);}voidkeyboard_start(structkeyboard*me){ecu_hsm_start(&me->hsm);}voidkeyboard_dispatch(structkeyboard*me,constvoid*event){ecu_hsm_dispatch(&me->hsm,event);}
Converts intrusive ecu_hsm 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_hsm*hsm);staticvoidrunning_state_on_exit(structecu_hsm*hsm);staticvoidrunning_state_initial(structecu_hsm*hsm);staticboolrunning_state_handler(structecu_hsm*hsm,constvoid*event);staticconststructecu_hsm_stateRUNNING_STATE=ECU_HSM_STATE_CTOR(&running_state_on_entry,&running_state_on_exit,&running_state_initial,&running_state_handler,&ECU_HSM_TOP_STATE);
Constructor. Initializes the ecu_hsm data structure for use. The state machine’s level starts at 1 since all HSMs are required to use ECU_HSM_TOP_STATE as the default top state. For example:
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_hsmhsm;/* User must allocate memory before constructor. */ecu_hsm_start(&hsm);/* ILLEGAL. Must construct before using. */ecu_hsm_ctor(&hsm,&INIT_STATE,2);ecu_hsm_start(&hsm);/* Ok. */
This function can only be called within a state’s main handler or initial handler. If a transition is signalled in a state’s main handler, it must handle the event by returning true. The HSM must be in a leaf state after all transitions are completed.
Relays event to the HSM where it is processed by the current state’s handler function. The event is propagated up the state hierarchy until it is handled. All state transitions signalled via ecu_hsm_change_state() are also managed in this function. The Theory Section fully explains how the HSM runs.
Warning
This function must run to completion. The HSM must be in a leaf state after this function completes.
Starts the HSM by entering from ECU_HSM_TOP_STATE to the target state supplied to ecu_hsm_ctor(). If the target is a composite state, initial handlers are ran to fully transition down the state hierarchy. The resulting execution order for the following example is: