Nested conditional logic main loops are hard to read and maintain.
State machines are a common way to implement complex logic.
States have transitions that are triggered by events or conditions.
States can have entry / exit routines that are executed when the state is entered / exited.
The “run” status of a state is commonly referred to as the “state machine tick” and recurrently loops.
State diagrams are used to visualize state machines.
State structures are used to capture variables associated with describing the state.
Implementation
Switch-Case
The simplest implementation of a state machine is a switch-case statement.
The switch statement is used to select the current state.
The case statements are used to implement the logic for each state.
Cases can be nested to implement sub-states.
Enumerations can be used to give states verbose names instead of numbers.
The break statement is used to exit the switch statement.
The default statement is used to handle unexpected states.
Use the State to Dictate Function
Avoid testing for a condition in a state to figure out what to do; let being in the state dictate the function (i.e., you shouldn’t have to test for the state you are in to know what to do).
Use entry and exit routines to handle state transitions; do not test for first or last iteration of that state.
“Run” states can iterate in a loop until a condition is met to change the state, or can wait for an event.
If you find you need to timeout an event wait to allow other stuff to happen, consider using k_event_test().
Pseudo-Code
enum device_states { init, run, error_entry, error_run, error_exit };int device_state = init;// initialize state/* structure to bookkeep state variables */struct device_state_vars {int var1;int var2;};while(1){switch(device_state){case init:/* do stuff to initialize device */ device_state = run;// change the statebreak;case run:/* run device */if(condition){ device_state = error;}break;case error_entry: illuminate_error_led();break;case error_run:if(condition_to_leave_error){ device_state = error_exit;}breakcase error_entry: turn_off_error_led(); device_state = run;break;default:/* handle unexpected state */break;}}
The switch-case approach loses some of its elegance when there are many states and many transitions and states have entry / exit routines.
The state machine is the foundation of describing the behavior of a system.
Threads as a tool to help implement a function of the state machine that is not readily captured by an exclusive state.
Threads add overhead and complexity to the system, making debugging more difficult.
Threads are not readily captured in a state diagram, but are more commonly described using a sequence diagram.
Avoiding Global Variables with s_object Struct
As you implement functionality within the SMF, you may wonder how to make a variable available in two different states — which is a good question, because a natural instinct might be to utilize a global variable, which we want to avoid.
Generally, you can leverage the fact that the s_object struct we create for smf_ctx (ie, state machine context) is passed to all states as a pointer and populate it with members for your own purpose. You can declare an s_object struct template in the global scope that contains anything you need to access between states. Something like:
struct s_object {struct smf_ctx ctx;// must be first elementstruct struct_type my_struct;int16_t my_variable;int16_t my_array[ARR_LENGTH];};
To be clear, this is merely defining a struct type and does not constitute creating a variable. Then, within main(), you can create an instance of this struct (that will be local to main) and populate the members with initial values — here, this example shows how to do this with a helper function:
#define S_OBJ_SUCCESS 0#define S_OBJ_ERR_NULL_PTR -1#define S_OBJ_ERR_BAD_CONFIG -2// define your own error codesint8_t init_s_object(struct s_object *s){if(s == NULL){return S_OBJ_ERR_NULL_PTR;// invalid pointer} s->my_variable =0;// initial value s->my_struct.member =&s->my_variable;// assign member of my_struct pointer to my_variable s->my_struct.array_size =sizeof(s->my_array[0])*ARR_LENGTH;if(s->my_struct.member == NULL){return S_OBJ_ERR_BAD_CONFIG;}return S_OBJ_SUCCESS;}
As always, utilize function returns for error handling using exit codes.
int main(void){staticstruct s_object s_obj;// create an instance of s_object called s_objint ret = init_s_object(&s_obj);// initialize s_obj using helper functionif(ret !=0){ LOG_ERR("could not initialize s_object stuct (%d)", ret);break;} smf_set_initial(SMF_CTX(&s_obj),&fsm_states[INIT]);for(;;){ ret = smf_run_state(SMF_CTX(&s_obj));if(ret !=0){ LOG_ERR("terminating state machine (%d)", ret);break;}}return0;}
Then, within a state:
staticvoid state_run(void*o){struct s_object *s =(struct s_object *)o;// cast explicitly for compiler(void)my_function(&s->my_variable);// pass pointer to my_variable to my_function// remainder of state code}
Because the default argument for states is a generic pointer (void *o), you will need to cast the input to a pointer for the s_object struct to safely access its members. If you’re unfamiliar with the arrow operator (->), this allows you to access members of a struct via a pointer. This syntax is equivalent to (*s).member , where the . is how we would typically access a member directly (ie, not as a pointer).
Passing State Object Data to Timer Handlers
In main():
k_timer_user_data_set(&my_timer,&s_obj);// make *s_obj available to my_timer
void timer_handler(struct k_timer *my_timer){struct s_object *s = k_timer_user_data_get(my_timer);// use s->member as needed}
So, we can make *s_obj available inside a timer handler.
Passing State Object Data to Work Queue Handlers
Now, what if we want to pass the same pointer to a work queue thread and what if we want to do so from inside the above timer handler? (in the context where the timer submits work to be done).
One way to do this is to create a struct wrapped around struct k_work work and declare a placeholder pointer as a secondary member:
Note here that this just a template for a struct of this type (my_work) and does not constitute creating an instance of such a struct. We can create an instance of this wrapper struct from inside timer_handler() (or elsewhere, as needed):
void timer_handler(struct k_timer *my_timer){struct s_object *s = k_timer_user_data_get(my_timer);// from beforestaticstruct my_work mw;// create a static instance of wrapper struct mw.s_obj_ptr = s;// assign desired pointer to placeholder k_work_init(&mw.work, my_work_handler);// use wrapper to init work k_work_submit(&mw.work);// use wrapper to submit work}
Now, within my_work_handler():
void my_work_handler(struct k_work *work){struct my_work *mw = CONTAINER_OF(work,struct my_work, work);struct s_object *s = mw->s_obj_ptr;// use s-> member as needed}
Here, we make use of the CONTAINER_OF macro (documentation)to obtain a pointer to the structure containing the element work, in this case my_work, the wrapper struct. From this, we can then recover a pointer to our s_object instance, s_obj, and use it as needed thereafter.
More generically, how can we pass a pointer to a user created thread?
int main(void){staticstruct s_object s_obj;// create an instance of s_object called s_objint ret = init_s_object(&s_obj);// initialize s_obj using helper functionif(ret !=0){ LOG_ERR("could not initialize s_object stuct (%d)", ret);} k_thread_create(&thread_id, thread_stack,1024, my_thread,&s_obj, NULL, NULL,// p1, p2, p35,0, K_NO_WAIT); k_timer_user_data_set(&my_timer,&s_obj);// make *s_obj available to my_timer smf_set_initial(SMF_CTX(&s_obj),&fsm_states[INIT]);for(;;){ ret = smf_run_state(SMF_CTX(&s_obj));if(ret !=0){ LOG_ERR("terminating state machine (%d)", ret);}}return0;}
In the call to k_thread_create() the 5th through 7th arguments are the pointers *p1, *p2, *p3 that appear as arguments to the thread itself:
void my_thread(void*p1,void*p2,void*p3){struct s_object *s =(struct s_object *)p1;// type cast generic pointerwhile(1){// access s->members// do things in thread}}