Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lib: SMF: Add initial transitions to HSMs and add smf_set_handled() #66753

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
116 changes: 96 additions & 20 deletions doc/services/smf/index.rst
Expand Up @@ -20,17 +20,17 @@ 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, ``struct smf_ctx``, as
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 ``struct smf_ctx`` member must be first because the state machine
framework's functions casts the user defined object to the ``struct smf_ctx``
type with the following macro: ``SMF_CTX(o)``
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)``.
Expand All @@ -39,12 +39,19 @@ 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.
keith-zephyr marked this conversation as resolved.
Show resolved Hide resolved

The following macro can be used for easy state creation:

* :c:macro:`SMF_CREATE_STATE` Create a state

**NOTE:** The :c:macro:`SMF_CREATE_STATE` macro takes an additional parameter
when :kconfig:option:`CONFIG_SMF_ANCESTOR_SUPPORT` is enabled.
.. note:: The :c:macro:`SMF_CREATE_STATE` macro takes an additional parameter
for the parent state when :kconfig:option:`CONFIG_SMF_ANCESTOR_SUPPORT` is
enabled . The :c:macro:`SMF_CREATE_STATE` macro takes two additional
parameters for the parent state and initial transition when the
:kconfig:option:`CONFIG_SMF_INITIAL_TRANSITION` option is enabled.

State Machine Creation
======================
Expand All @@ -71,33 +78,61 @@ And this example creates three hierarchical states::
};


To set the initial state, the ``smf_set_initial`` function should be
This example creates three hierarchical states with an initial transition
from parent state S0 to child state S2::
glenn-andrews marked this conversation as resolved.
Show resolved Hide resolved

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. It has the following prototype:
``void smf_set_initial(smf_ctx *ctx, smf_state *state)``

To transition from one state to another, the ``smf_set_state`` function is
used and it has the following prototype:
To transition from one state to another, the :c:func:`smf_set_state`
function is used and it has the following prototype:
``void smf_set_state(smf_ctx *ctx, smf_state *state)``

**NOTE:** While the state machine is running, smf_set_state should only be
called from the Entry and Run functions. Calling smf_set_state from the Exit
functions doesn't make sense and will generate a warning.
.. 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 they will not know which child state
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking suggestion:

Suggested change
not be passed a parent state as they will not know which child state
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 will have
initial transitions defined for all parent states.

.. note:: While the state machine is running, smf_set_state should only be
called from the Entry and Run functions. Calling smf_set_state from the
Exit functions doesn't make sense and will generate a warning.

State Machine Execution
=======================

To run the state machine, the ``smf_run_state`` function should be called in
some application dependent way. An application should cease calling
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. The function has the following
prototype: ``int32_t smf_run_state(smf_ctx *ctx)``

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 ``smf_terminate`` function should be
called. It can be called from the entry, run, or exit action. The function
takes a non-zero user defined value that's returned by the ``smf_run_state``
function. The function has the following prototype:
To terminate the state machine, the :c:func:`smf_terminate` function should
be called. It can be called from the entry, run, or exit action. The
function takes a non-zero user defined value that's returned by the
:c:func:`smf_run_state` function. The function has the following prototype:
``void smf_terminate(smf_ctx *ctx, int32_t val)``

Flat State Machine Example
Expand Down Expand Up @@ -316,8 +351,11 @@ When designing hierarchical state machines, the following should be considered:
- Ancestor exit actions are executed after the sibling exit actions. 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 returns
without transitioning to another state, ie. calling smf_set_state.
- 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`.
- 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this condition is tested. Can you update the test to include this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to be testing to confirm a bug exists? I don't want to see this codified as "the way things work" and add additional effort to fixing it by having to fix the test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation here defines an API contract: A parent state initiating a transition to self does not call the parent_exit. I didn't consider this a bug.
So I was wondering if this condition could explicitly be tested.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's considered a bug here: #66341

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd to document behavior that isn't supported at the moment. Does it make more sense to explicitly state "calling smf_set_state() to transition to self is not supported".


Event Driven State Machine Example
==================================
Expand Down Expand Up @@ -466,3 +504,41 @@ Code::
}
}
}

Hierarchical State Machine Example With Initial Transitions
===========================================================

:zephyr_file:`tests/lib/smf/src/test_lib_initial_transitions_smf.c` defines
a state machine for testing initial transitions and :c:func:`smf_set_handled`.
The statechart for this test is below.

.. graphviz::
:caption: Test state machine for initial trnasitions and ``smf_set_handled``

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo "trnasitions"


digraph smf_hierarchical_initial {
compound=true;
node [style = rounded];
smf_set_initial [shape=plaintext];
ab_init_state [shape = point];
STATE_A [shape = box];
STATE_B [shape = box];
STATE_C [shape = box];
STATE_D [shape = box];

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_C -> STATE_C
}

smf_set_initial -> STATE_A [lhead=cluster_ab]
STATE_B -> STATE_C
STATE_C -> STATE_D
}
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update/create a sample to demonstrate the use of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a rather large sample that implements the PSiCC2 Section 2.3.15 demo app with all levels of transitions. It's about 11K of source, given the large number of states the demo app encompasses.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably too large. I was thinking we had existing samples that use the SMF, but there are not.

However, there are some good unit tests for the SMF here.

Please extend one of the existing tests or create a new one that enables the CONFIG_SMF_INITIAL_TRANSITION option. The ensures that Zephyr's CI will validate that this new feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I've never used the unit tests before so feedback is appreciated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably too large. I was thinking we had existing samples that use the SMF, but there are not.

Would it be worth me submitting my demo app as a separate sample for SMF? I can probably simplify it a bit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably too large. I was thinking we had existing samples that use the SMF, but there are not.

Would it be worth me submitting my demo app as a separate sample for SMF? I can probably simplify it a bit.

Yes, please!


/**
* @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