574 lines
16 KiB
ReStructuredText
574 lines
16 KiB
ReStructuredText
.. _smf:
|
|
|
|
State Machine Framework
|
|
#######################
|
|
|
|
.. highlight:: c
|
|
|
|
Overview
|
|
========
|
|
|
|
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.
|
|
|
|
State Creation
|
|
==============
|
|
|
|
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 these functions is as
|
|
follows: ``void 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.
|
|
|
|
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:
|
|
|
|
* :c:macro:`SMF_CREATE_STATE` Create a state
|
|
|
|
State Machine Creation
|
|
======================
|
|
|
|
A 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.
|
|
|
|
State Machine Execution
|
|
=======================
|
|
|
|
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.
|
|
|
|
Preventing Parent Run Actions
|
|
=============================
|
|
|
|
Calling :c:func:`smf_set_handled` prevents calling the run action of parent
|
|
states. It is not required to call :c:func:`smf_set_handled` if the state
|
|
calls :c:func:`smf_set_state`.
|
|
|
|
State Machine Termination
|
|
=========================
|
|
|
|
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.
|
|
|
|
UML State Machines
|
|
==================
|
|
|
|
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:
|
|
|
|
1. Executing the actions associated with the transition within the context
|
|
of the source state, rather than after the exit actions are performed.
|
|
2. Only allowing external transitions to self, not to sub-states. A transition
|
|
from a superstate to a child state is treated as a local transition.
|
|
3. Prohibiting transitions using :c:func:`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.
|
|
|
|
State Machine Examples
|
|
======================
|
|
|
|
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 void s0_run(void *o)
|
|
{
|
|
smf_set_state(SMF_CTX(&s_obj), &demo_states[S1]);
|
|
}
|
|
static void s0_exit(void *o)
|
|
{
|
|
/* Do something */
|
|
}
|
|
|
|
/* State S1 */
|
|
static void s1_run(void *o)
|
|
{
|
|
smf_set_state(SMF_CTX(&s_obj), &demo_states[S2]);
|
|
}
|
|
static void s1_exit(void *o)
|
|
{
|
|
/* Do something */
|
|
}
|
|
|
|
/* State S2 */
|
|
static void s2_entry(void *o)
|
|
{
|
|
/* Do something */
|
|
}
|
|
static void s2_run(void *o)
|
|
{
|
|
smf_set_state(SMF_CTX(&s_obj), &demo_states[S0]);
|
|
}
|
|
|
|
/* 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 void s0_run(void *o)
|
|
{
|
|
smf_set_state(SMF_CTX(&s_obj), &demo_states[S1]);
|
|
}
|
|
|
|
/* State S1 */
|
|
static void s1_run(void *o)
|
|
{
|
|
smf_set_state(SMF_CTX(&s_obj), &demo_states[S2]);
|
|
}
|
|
|
|
/* State S2 */
|
|
static void s2_run(void *o)
|
|
{
|
|
smf_set_state(SMF_CTX(&s_obj), &demo_states[S0]);
|
|
}
|
|
|
|
/* 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:
|
|
- Ancestor entry actions are executed before the sibling entry actions. For
|
|
example, the parent_entry function is called before the s0_entry function.
|
|
- Transitioning from one sibling to another with a shared ancestry does not
|
|
re-execute the ancestor\'s entry action or execute the exit action.
|
|
For example, the parent_entry function is not called when transitioning
|
|
from S0 to S1, nor is the parent_exit function called.
|
|
- Ancestor exit actions are executed after the exit action of the current
|
|
state. For example, the s1_exit function is called before the parent_exit
|
|
function is called.
|
|
- The parent_run function only executes if the child_run function does not
|
|
call either :c:func:`smf_set_state` or :c:func:`smf_set_handled`.
|
|
|
|
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 void 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]);
|
|
}
|
|
}
|
|
|
|
/* State S1 */
|
|
static void s1_entry(void *o)
|
|
{
|
|
printk("STATE1\n");
|
|
}
|
|
|
|
static void 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]);
|
|
}
|
|
}
|
|
|
|
/* 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=<>]
|
|
|
|
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]
|
|
}
|
|
|
|
|
|
API Reference
|
|
=============
|
|
|
|
.. doxygengroup:: smf
|