Skip to content

Commit

Permalink
Lib: SMF: Add initial conditions and smf_set_handled()
Browse files Browse the repository at this point in the history
Brings SMF framework closer into alignment with accepted Hierarchical State
Machine operation by:
1. Allowing 'programming by difference' by having some child states handle
   events and prevent propagation up to the parent run actions while others
   propagate events up to a common handler in a parent state.
2. Optionally allow initial transitions within a parent state to determine
   the most nested child state to transition to.

There was discussion in #55344
about not making the initial transition a Kconfig option, but I'm not sure
of any way else of doing it without permanently adding a pointer to each
`smf_state` entry, which is a problem for RAM-constrained devices.

This does not fix #66341
but documentation has been updated to warn users of the issue.

Signed-off-by: Glenn Andrews <glenn.andrews.42@gmail.com>
  • Loading branch information
glenn-andrews committed Jan 22, 2024
1 parent 60a9f33 commit a7aa0db
Show file tree
Hide file tree
Showing 8 changed files with 622 additions and 2 deletions.
32 changes: 31 additions & 1 deletion doc/services/smf/index.rst
Expand Up @@ -39,6 +39,10 @@ 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 machine does 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
Expand Down Expand Up @@ -71,6 +75,21 @@ And this example creates three hierarchical states::
};


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 ``smf_set_initial`` function should be
called. It has the following prototype:
``void smf_set_initial(smf_ctx *ctx, smf_state *state)``
Expand All @@ -91,6 +110,13 @@ some application dependent way. An application should cease calling
smf_run_state if it returns a non-zero value. The function has the following
prototype: ``int32_t smf_run_state(smf_ctx *ctx)``

Preventing Parent Run Actions
=============================

Calling ``smf_set_handled`` prevents calling the run action of parent states.
It is not required to call ``smf_set_handled`` if the state calls
``smf_set_state``.

State Machine Termination
=========================

Expand Down Expand Up @@ -317,7 +343,11 @@ When designing hierarchical state machines, the following should be considered:
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 returns
without transitioning to another state, ie. calling smf_set_state.
without transitioning to another state, ie. calling smf_set_state, or if
``smf_set_handled`` is called within the child_run function.
- When a parent state intitiates a transition to self, the parents's exit
action is not called, e.g. instead of child_exit, parent_exit, parent_entry
it performs child_exit, parent_entry

Event Driven State Machine Example
==================================
Expand Down
36 changes: 36 additions & 0 deletions include/zephyr/smf.h
Expand Up @@ -18,13 +18,33 @@
* @param _exit State exit function
* @param _parent State parent object or NULL
*/
#ifndef CONFIG_SMF_INITIAL_TRANSITION
#define SMF_CREATE_STATE(_entry, _run, _exit, _parent) \
{ \
.entry = _entry, \
.run = _run, \
.exit = _exit, \
.parent = _parent \
}
#else
/**
* @brief Macro to create a hierarchical state.
*
* @param _entry State entry function
* @param _run State run function
* @param _exit State exit function
* @param _parent State parent object or NULL
* @param _initial State initial transition object or NULL
*/
#define SMF_CREATE_STATE(_entry, _run, _exit, _parent, _initial) \
{ \
.entry = _entry, \
.run = _run, \
.exit = _exit, \
.parent = _parent, \
.initial = _initial \
}
#endif /* CONFIG_SMF_INITIAL_TRANSITION */

#else

Expand Down Expand Up @@ -87,6 +107,13 @@ struct smf_state {
* that parent's exit and entry functions do not execute.
*/
const struct smf_state *parent;

#ifdef CONFIG_SMF_INITIAL_TRANSITION
/**
* Optional initial transition state. NULL for leaf states.
*/
const struct smf_state *initial;
#endif
};

/** Defines the current context of the state machine. */
Expand Down Expand Up @@ -136,6 +163,15 @@ void smf_set_state(struct smf_ctx *ctx, const struct smf_state *new_state);
*/
void smf_set_terminate(struct smf_ctx *ctx, int32_t val);

/**
* @brief Tell the SMF to stop propagating the event to ancestors. This allows
* HSMs to implement 'programming by difference' where substates can
* handle events on their own or propagate up to a common handler.
*
* @param ctx State machine context
*/
void smf_set_handled(struct smf_ctx *ctx);

/**
* @brief Runs one iteration of a state machine (including any parent states)
*
Expand Down
6 changes: 6 additions & 0 deletions lib/smf/Kconfig
Expand Up @@ -13,4 +13,10 @@ config SMF_ANCESTOR_SUPPORT
help
If y, then the state machine framework includes ancestor state support

config SMF_INITIAL_TRANSITION
depends on SMF_ANCESTOR_SUPPORT
bool "Support initial transitions for ancestor states"
help
If y, then each state can have an initial transition to a sub-state

endif # SMF
40 changes: 40 additions & 0 deletions lib/smf/smf.c
Expand Up @@ -18,6 +18,7 @@ struct internal_ctx {
bool new_state : 1;
bool terminate : 1;
bool exit : 1;
bool handled : 1;
};

static bool share_paren(const struct smf_state *test_state,
Expand Down Expand Up @@ -118,6 +119,12 @@ __unused static bool smf_execute_ancestor_run_actions(struct smf_ctx *ctx)
return true;
}

if (internal->handled) {
/* Event was handled by this state. Stop propagating */
internal->handled = false;
return false;
}

/* Try to run parent run actions */
for (const struct smf_state *tmp_state = ctx->current->parent;
tmp_state != NULL;
Expand All @@ -133,6 +140,12 @@ __unused static bool smf_execute_ancestor_run_actions(struct smf_ctx *ctx)
if (internal->new_state) {
break;
}

if (internal->handled) {
/* Event was handled by this state. Stop propagating */
internal->handled = false;
break;
}
}
}

Expand Down Expand Up @@ -175,6 +188,16 @@ void smf_set_initial(struct smf_ctx *ctx, const struct smf_state *init_state)
{
struct internal_ctx * const internal = (void *) &ctx->internal;


#ifdef CONFIG_SMF_INITIAL_TRANSITION
/*
* The final target will be the deepest leaf state that
* the target contains. Set that as the real target.
*/
while (init_state->initial) {
init_state = init_state->initial;
}
#endif
internal->exit = false;
internal->terminate = false;
ctx->current = init_state;
Expand Down Expand Up @@ -234,6 +257,16 @@ void smf_set_state(struct smf_ctx *const ctx, const struct smf_state *target)

internal->exit = false;

#ifdef CONFIG_SMF_INITIAL_TRANSITION
/*
* The final target will be the deepest leaf state that
* the target contains. Set that as the real target.
*/
while (target->initial) {
target = target->initial;
}
#endif

/* update the state variables */
ctx->previous = ctx->current;
ctx->current = target;
Expand Down Expand Up @@ -262,6 +295,13 @@ void smf_set_terminate(struct smf_ctx *ctx, int32_t val)
ctx->terminate_val = val;
}

void smf_set_handled(struct smf_ctx *ctx)
{
struct internal_ctx *const internal = (void *)&ctx->internal;

internal->handled = true;
}

int32_t smf_run_state(struct smf_ctx *const ctx)
{
struct internal_ctx * const internal = (void *) &ctx->internal;
Expand Down
4 changes: 3 additions & 1 deletion tests/lib/smf/CMakeLists.txt
Expand Up @@ -6,7 +6,9 @@ project(smf)

target_sources(app PRIVATE src/main.c)

if(CONFIG_SMF_ANCESTOR_SUPPORT)
if(CONFIG_SMF_INITIAL_TRANSITION)
target_sources(app PRIVATE src/test_lib_initial_transitions_smf.c)
elseif(CONFIG_SMF_ANCESTOR_SUPPORT)
target_sources(app PRIVATE src/test_lib_hierarchical_smf.c
src/test_lib_hierarchical_5_ancestor_smf.c)
else()
Expand Down

0 comments on commit a7aa0db

Please sign in to comment.