doc/services/smf/index.rst
.. _smf:
State Machine Framework #######################
.. highlight:: c
The State Machine Framework (SMF) is an application agnostic framework that
provides an easy way for developers to integrate state machines into their
application. The framework can be added to any project by enabling the
:kconfig:option:CONFIG_SMF option.
A state is represented by three functions, where one function implements the
Entry actions, another function implements the Run actions, and the last
function implements the Exit actions. The prototype for the entry and exit
functions are as follows: void funct(void *obj), and the prototype for the
run action is enum smf_state_result funct(void *obj) where the obj
parameter is a user defined structure that has the state machine context,
:c:struct:smf_ctx, as its first member. For example::
struct user_object { struct smf_ctx ctx; /* All User Defined Data Follows */ };
The :c:struct:smf_ctx member must be first because the state machine
framework's functions casts the user defined object to the :c:struct:smf_ctx
type with the :c:macro:SMF_CTX macro.
For example instead of doing this (struct smf_ctx *)&user_obj, you could
use SMF_CTX(&user_obj).
By default, a state can have no ancestor states, resulting in a flat state
machine. But to enable the creation of a hierarchical state machine, the
:kconfig:option:CONFIG_SMF_ANCESTOR_SUPPORT option must be enabled.
The return value of the run action, :c:enum:smf_state_result determines if the
state machine propagates the event to parent run actions
(:c:enum:SMF_EVENT_PROPAGATE) or if the event was handled by the run action
(:c:enum:SMF_EVENT_HANDLED). Flat state machines do not have parent actions,
so the return code is ignored; returning :c:enum:SMF_EVENT_HANDLED is
recommended.
Calling :c:func:smf_set_state prevents calling parent run
actions, even if :c:enum:SMF_EVENT_PROPAGATE is returned.
By default, the hierarchical state machines do not support initial transitions
to child states on entering a superstate. To enable them the
:kconfig:option:CONFIG_SMF_INITIAL_TRANSITION option must be enabled.
The following macro can be used for easy state creation:
SMF_CREATE_STATE Create a stateA state machine is created by defining a table of states that's indexed by an enum. For example, the following creates three flat states::
enum demo_state { S0, S1, S2 };
const struct smf_state demo_states[] = { [S0] = SMF_CREATE_STATE(s0_entry, s0_run, s0_exit, NULL, NULL), [S1] = SMF_CREATE_STATE(s1_entry, s1_run, s1_exit, NULL, NULL), [S2] = SMF_CREATE_STATE(s2_entry, s2_run, s2_exit, NULL, NULL) };
And this example creates three hierarchical states::
enum demo_state { S0, S1, S2 };
const struct smf_state demo_states[] = { [S0] = SMF_CREATE_STATE(s0_entry, s0_run, s0_exit, parent_s0, NULL), [S1] = SMF_CREATE_STATE(s1_entry, s1_run, s1_exit, parent_s12, NULL), [S2] = SMF_CREATE_STATE(s2_entry, s2_run, s2_exit, parent_s12, NULL) };
This example creates three hierarchical states with an initial transition from parent state S0 to child state S2::
enum demo_state { S0, S1, S2 };
/* Forward declaration of state table */ const struct smf_state demo_states[];
const struct smf_state demo_states[] = { [S0] = SMF_CREATE_STATE(s0_entry, s0_run, s0_exit, NULL, demo_states[S2]), [S1] = SMF_CREATE_STATE(s1_entry, s1_run, s1_exit, demo_states[S0], NULL), [S2] = SMF_CREATE_STATE(s2_entry, s2_run, s2_exit, demo_states[S0], NULL) };
To set the initial state, the :c:func:smf_set_initial function should be
called.
To transition from one state to another, the :c:func:smf_set_state
function is used.
.. note:: If :kconfig:option:CONFIG_SMF_INITIAL_TRANSITION is not set,
:c:func:smf_set_initial and :c:func:smf_set_state function should
not be passed a parent state as the parent state does not know which
child state to transition to. Transitioning to a parent state is OK
if an initial transition to a child state is defined. A well-formed
HSM should have initial transitions defined for all parent states.
.. note:: While the state machine is running, :c:func:smf_set_state should
only be called from the Entry or Run function. Calling
:c:func:smf_set_state from Exit functions will generate a warning in the
log and no transition will occur.
To run the state machine, the :c:func:smf_run_state function should be
called in some application dependent way. An application should cease calling
smf_run_state if it returns a non-zero value.
To terminate the state machine, the :c:func:smf_set_terminate function
should be called. It can be called from the entry, run, or exit actions. The
function takes a non-zero user defined value that will be returned by the
:c:func:smf_run_state function.
Leaf State: In the context of a hierarchical state machine, a leaf state is a state that does not contain any child states. It represents the most granular level of state in the hierarchy, where no further decomposition is possible.
Executing State: The executing state refers to the state whose entry, run, or exit action is currently being executed by the state machine. This may be a parent or leaf state, depending on the current operation.
To retrieve the current leaf state, the :c:func:smf_get_current_leaf_state
function should be called.
For example::
const struct smf_state *leaf_state = smf_get_current_leaf_state(SMF_CTX(&s_obj));
.. note:: If :kconfig:option:CONFIG_SMF_INITIAL_TRANSITION is not enabled, or
if the initial state of a parent state is not defined, always set the state
to a leaf state. Otherwise, the state machine may enter a parent state directly,
and :c:func:smf_get_current_leaf_state may return a parent state instead of
a leaf state. Ensure initial transitions are properly configured for all parent
states to avoid malformed hierarchical state machines.
To retrieve the state whose entry, run, or exit action is currently being executed,
use the :c:func:smf_get_current_executing_state function.
SMF follows UML hierarchical state machine rules for transitions i.e., the entry and exit actions of the least common ancestor are not executed on transition, unless said transition is a transition to self.
The UML Specification for StateMachines may be found in chapter 14 of the UML specification available here: https://www.omg.org/spec/UML/
SMF breaks from UML rules in:
smf_set_state in exit actions.SMF also does not provide any pseudostates except the Initial Pseudostate.
Terminate pseudostates can be modelled by calling :c:func:smf_set_terminate
from the entry action of a 'terminate' state. Orthogonal regions are modelled
by calling :c:func:smf_run_state for each region.
Flat State Machine Example
This example turns the following state diagram into code using the SMF, where the initial state is S0.
.. graphviz:: :caption: Flat state machine diagram
digraph smf_flat { node [style=rounded]; init [shape = point]; STATE_S0 [shape = box]; STATE_S1 [shape = box]; STATE_S2 [shape = box];
init -> STATE_S0;
STATE_S0 -> STATE_S1;
STATE_S1 -> STATE_S2;
STATE_S2 -> STATE_S0;
}
Code::
#include <zephyr/smf.h>
/* Forward declaration of state table */
static const struct smf_state demo_states[];
/* List of demo states */
enum demo_state { S0, S1, S2 };
/* User defined object */
struct s_object {
/* This must be first */
struct smf_ctx ctx;
/* Other state specific data add here */
} s_obj;
/* State S0 */
static void s0_entry(void *o)
{
/* Do something */
}
static enum smf_state_result s0_run(void *o)
{
smf_set_state(SMF_CTX(&s_obj), &demo_states[S1]);
return SMF_EVENT_HANDLED;
}
static void s0_exit(void *o)
{
/* Do something */
}
/* State S1 */
static enum smf_state_result s1_run(void *o)
{
smf_set_state(SMF_CTX(&s_obj), &demo_states[S2]);
return SMF_EVENT_HANDLED;
}
static void s1_exit(void *o)
{
/* Do something */
}
/* State S2 */
static void s2_entry(void *o)
{
/* Do something */
}
static enum smf_state_result s2_run(void *o)
{
smf_set_state(SMF_CTX(&s_obj), &demo_states[S0]);
return SMF_EVENT_HANDLED;
}
/* Populate state table */
static const struct smf_state demo_states[] = {
[S0] = SMF_CREATE_STATE(s0_entry, s0_run, s0_exit, NULL, NULL),
/* State S1 does not have an entry action */
[S1] = SMF_CREATE_STATE(NULL, s1_run, s1_exit, NULL, NULL),
/* State S2 does not have an exit action */
[S2] = SMF_CREATE_STATE(s2_entry, s2_run, NULL, NULL, NULL),
};
int main(void)
{
int32_t ret;
/* Set initial state */
smf_set_initial(SMF_CTX(&s_obj), &demo_states[S0]);
/* Run the state machine */
while(1) {
/* State machine terminates if a non-zero value is returned */
ret = smf_run_state(SMF_CTX(&s_obj));
if (ret) {
/* handle return code and terminate state machine */
break;
}
k_msleep(1000);
}
}
Hierarchical State Machine Example
This example turns the following state diagram into code using the SMF, where S0 and S1 share a parent state and S0 is the initial state.
.. graphviz:: :caption: Hierarchical state machine diagram
digraph smf_hierarchical { node [style = rounded]; init [shape = point]; STATE_S0 [shape = box]; STATE_S1 [shape = box]; STATE_S2 [shape = box];
subgraph cluster_0 {
label = "PARENT";
style = rounded;
STATE_S0 -> STATE_S1;
}
init -> STATE_S0;
STATE_S1 -> STATE_S2;
STATE_S2 -> STATE_S0;
}
Code::
#include <zephyr/smf.h>
/* Forward declaration of state table */
static const struct smf_state demo_states[];
/* List of demo states */
enum demo_state { PARENT, S0, S1, S2 };
/* User defined object */
struct s_object {
/* This must be first */
struct smf_ctx ctx;
/* Other state specific data add here */
} s_obj;
/* Parent State */
static void parent_entry(void *o)
{
/* Do something */
}
static void parent_exit(void *o)
{
/* Do something */
}
/* State S0 */
static enum smf_state_result s0_run(void *o)
{
smf_set_state(SMF_CTX(&s_obj), &demo_states[S1]);
return SMF_EVENT_HANDLED;
}
/* State S1 */
static enum smf_state_result s1_run(void *o)
{
smf_set_state(SMF_CTX(&s_obj), &demo_states[S2]);
return SMF_EVENT_HANDLED;
}
/* State S2 */
static enum smf_state_result s2_run(void *o)
{
smf_set_state(SMF_CTX(&s_obj), &demo_states[S0]);
return SMF_EVENT_HANDLED;
}
/* Populate state table */
static const struct smf_state demo_states[] = {
/* Parent state does not have a run action */
[PARENT] = SMF_CREATE_STATE(parent_entry, NULL, parent_exit, NULL, NULL),
/* Child states do not have entry or exit actions */
[S0] = SMF_CREATE_STATE(NULL, s0_run, NULL, &demo_states[PARENT], NULL),
[S1] = SMF_CREATE_STATE(NULL, s1_run, NULL, &demo_states[PARENT], NULL),
/* State S2 do not have entry or exit actions and no parent */
[S2] = SMF_CREATE_STATE(NULL, s2_run, NULL, NULL, NULL),
};
int main(void)
{
int32_t ret;
/* Set initial state */
smf_set_initial(SMF_CTX(&s_obj), &demo_states[S0]);
/* Run the state machine */
while(1) {
/* State machine terminates if a non-zero value is returned */
ret = smf_run_state(SMF_CTX(&s_obj));
if (ret) {
/* handle return code and terminate state machine */
break;
}
k_msleep(1000);
}
}
When designing hierarchical state machines, the following should be considered:
smf_set_state or return :c:enum:SMF_EVENT_HANDLED.CONFIG_SMF_INITIAL_TRANSITION
is not enabled, or when a parent state's initial state is undefined.Event Driven State Machine Example
Events are not explicitly part of the State Machine Framework but an event driven
state machine can be implemented using Zephyr :ref:events.
.. graphviz:: :caption: Event driven state machine diagram
digraph smf_flat { node [style=rounded]; init [shape = point]; STATE_S0 [shape = box]; STATE_S1 [shape = box];
init -> STATE_S0;
STATE_S0 -> STATE_S1 [label = "BTN EVENT"];
STATE_S1 -> STATE_S0 [label = "BTN EVENT"];
}
Code::
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/smf.h>
#define SW0_NODE DT_ALIAS(sw0)
/* List of events */
#define EVENT_BTN_PRESS BIT(0)
static const struct gpio_dt_spec button =
GPIO_DT_SPEC_GET_OR(SW0_NODE, gpios, {0});
static struct gpio_callback button_cb_data;
/* Forward declaration of state table */
static const struct smf_state demo_states[];
/* List of demo states */
enum demo_state { S0, S1 };
/* User defined object */
struct s_object {
/* This must be first */
struct smf_ctx ctx;
/* Events */
struct k_event smf_event;
int32_t events;
/* Other state specific data add here */
} s_obj;
/* State S0 */
static void s0_entry(void *o)
{
printk("STATE0\n");
}
static enum smf_state_result s0_run(void *o)
{
struct s_object *s = (struct s_object *)o;
/* Change states on Button Press Event */
if (s->events & EVENT_BTN_PRESS) {
smf_set_state(SMF_CTX(&s_obj), &demo_states[S1]);
}
return SMF_EVENT_HANDLED;
}
/* State S1 */
static void s1_entry(void *o)
{
printk("STATE1\n");
}
static enum smf_state_result s1_run(void *o)
{
struct s_object *s = (struct s_object *)o;
/* Change states on Button Press Event */
if (s->events & EVENT_BTN_PRESS) {
smf_set_state(SMF_CTX(&s_obj), &demo_states[S0]);
}
return SMF_EVENT_HANDLED;
}
/* Populate state table */
static const struct smf_state demo_states[] = {
[S0] = SMF_CREATE_STATE(s0_entry, s0_run, NULL, NULL, NULL),
[S1] = SMF_CREATE_STATE(s1_entry, s1_run, NULL, NULL, NULL),
};
void button_pressed(const struct device *dev,
struct gpio_callback *cb, uint32_t pins)
{
/* Generate Button Press Event */
k_event_post(&s_obj.smf_event, EVENT_BTN_PRESS);
}
int main(void)
{
int ret;
if (!gpio_is_ready_dt(&button)) {
printk("Error: button device %s is not ready\n",
button.port->name);
return;
}
ret = gpio_pin_configure_dt(&button, GPIO_INPUT);
if (ret != 0) {
printk("Error %d: failed to configure %s pin %d\n",
ret, button.port->name, button.pin);
return;
}
ret = gpio_pin_interrupt_configure_dt(&button,
GPIO_INT_EDGE_TO_ACTIVE);
if (ret != 0) {
printk("Error %d: failed to configure interrupt on %s pin %d\n",
ret, button.port->name, button.pin);
return;
}
gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin));
gpio_add_callback(button.port, &button_cb_data);
/* Initialize the event */
k_event_init(&s_obj.smf_event);
/* Set initial state */
smf_set_initial(SMF_CTX(&s_obj), &demo_states[S0]);
/* Run the state machine */
while(1) {
/* Block until an event is detected */
s_obj.events = k_event_wait(&s_obj.smf_event,
EVENT_BTN_PRESS, true, K_FOREVER);
/* State machine terminates if a non-zero value is returned */
ret = smf_run_state(SMF_CTX(&s_obj));
if (ret) {
/* handle return code and terminate state machine */
break;
}
}
}
State Machine Example With Initial Transitions And Transition To Self
:zephyr_file:tests/lib/smf/src/test_lib_self_transition_smf.c defines a state
machine for testing the initial transitions and transitions to self in a parent
state. The statechart for this test is below.
.. graphviz:: :caption: Test state machine for UML State Transitions
digraph smf_hierarchical_initial { compound=true; node [style = rounded]; "smf_set_initial()" [shape=plaintext fontname=Courier]; ab_init_state [shape = point]; STATE_A [shape = box]; STATE_B [shape = box]; STATE_C [shape = box]; STATE_D [shape = box]; DC[shape=point height=0 width=0 label="" style="invis"]
subgraph cluster_root {
label = "ROOT";
style = rounded;
subgraph cluster_ab {
label = "PARENT_AB";
style = rounded;
ab_init_state -> STATE_A;
STATE_A -> STATE_B;
}
subgraph cluster_c {
label = "PARENT_C";
style = rounded;
STATE_B -> STATE_C [ltail=cluster_ab]
}
STATE_C -> DC [ltail=cluster_c, dir=none];
DC -> STATE_C [lhead=cluster_c];
STATE_C -> STATE_D
}
"smf_set_initial()" -> STATE_A [lhead=cluster_ab]
}
The SMF provides optional instrumentation hooks for observing state machine
behavior during testing. To enable them, set
:kconfig:option:CONFIG_SMF_INSTRUMENTATION.
When enabled, three hook callbacks can be registered on a state machine context
via :c:func:smf_set_hooks:
.. important:: :c:func:smf_set_hooks must be called after
:c:func:smf_set_initial, because :c:func:smf_set_initial resets the
hooks pointer to NULL. As a consequence, entry actions executed during
:c:func:smf_set_initial (i.e. the initial state's entry actions and those
of its ancestors) will not be captured by the hooks.
Example::
#include <zephyr/smf.h>
static void on_action(struct smf_ctx *ctx,
const struct smf_state *state,
smf_action_type action_type)
{
/* Log or record the action */
}
static void on_transition(struct smf_ctx *ctx,
const struct smf_state *source,
const struct smf_state *dest)
{
/* Log or record the transition */
}
static const struct smf_hooks hooks = {
.on_action = on_action,
.on_transition = on_transition,
/* .on_error = NULL — any member may be NULL */
};
void test_example(void)
{
struct s_object s_obj;
/* Set the initial state first */
smf_set_initial(SMF_CTX(&s_obj), &demo_states[S0]);
/* Install hooks after init — initial entry actions are not captured */
smf_set_hooks(SMF_CTX(&s_obj), &hooks);
/* Run the state machine — hooks fire on every action and transition */
while (!smf_run_state(SMF_CTX(&s_obj))) {
/* ... */
}
}
When :kconfig:option:CONFIG_SMF_INSTRUMENTATION is not set, all
instrumentation code is compiled out and there is zero runtime overhead.
.. doxygengroup:: smf