Commit 531c4575 authored by Glenn Andrews's avatar Glenn Andrews Committed by Anas Nashif
Browse files

Lib: SMF Modify HSM operation for UML-Style transitions



Modify the SMF such that state transitions from parent states choose the
correct Least Common Ancestor based on the transition source rather than
the current state.

SMF set as experimental.

Signed-off-by: default avatarGlenn Andrews <glenn.andrews.42@gmail.com>
parent 49d0dd81
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -493,6 +493,14 @@ State Machine Framework
  now independent of the values of :kconfig:option:`CONFIG_SMF_ANCESTOR_SUPPORT` and
  :kconfig:option:`CONFIG_SMF_INITIAL_TRANSITION`. If the additional arguments are not used, they
  have to be set to ``NULL``. (:github:`71250`)
* SMF now follows a more UML-like transition flow when the transition source is a parent of the
  state called by :c:func:`smf_run_state`. Exit actions up to (but not including) the Least Common
  Ancestor of the transition source and target state will be executed, as will entry actions from
  (but not including) the LCA down to the target state. (:github:`71675`)
* Previously, calling :c:func:`smf_set_state` with a ``new_state`` set to NULL would execute all
  exit actions from the current state to the topmost parent, with the expectation the topmost exit
  action would terminate the state machine. Passing ``NULL`` is now not allowed. Instead create a
  'terminate' state at the top level, and call :c:func:`smf_set_terminate` from its entry action.

ZBus
====
+3 −0
Original line number Diff line number Diff line
@@ -375,6 +375,9 @@ Libraries / Subsystems
* State Machine Framework

  * The :c:macro:`SMF_CREATE_STATE` macro now always takes 5 arguments.
  * Transition sources that are parents of the state that was run now choose the correct Least
    Common Ancestor for executing Exit and Entry Actions.
  * Passing ``NULL`` to :c:func:`smf_set_state` is now not allowed.

* Storage

+73 −45
Original line number Diff line number Diff line
@@ -39,7 +39,7 @@ 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
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.

@@ -87,31 +87,29 @@ from parent state S0 to child state S2::
   };

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)``
called.

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)``
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 will have initial transitions defined for all parent states.
   HSM should 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.
.. 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. The function has the following
prototype: ``int32_t smf_run_state(smf_ctx *ctx)``
smf_run_state if it returns a non-zero value.

Preventing Parent Run Actions
=============================
@@ -124,13 +122,38 @@ 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 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_set_terminate(smf_ctx *ctx, int32_t val)``
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.
@@ -232,7 +255,7 @@ Code::
	}

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.
@@ -342,17 +365,14 @@ When designing hierarchical state machines, the following should be considered:
   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 sibling exit actions. For
   example, the s1_exit function is called before the parent_exit function
   is 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`.
 - Transitions to self in super-states containing sub-states are not supported.
   Transitions to self from the most-nested child state are supported and will
   call the exit and entry function of the child state correctly.

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`.
@@ -499,25 +519,31 @@ Code::
		}
	}

Hierarchical State Machine Example With Initial Transitions
===========================================================
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.

: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 transitions and ``smf_set_handled``
   :caption: Test state machine for UML State Transitions

   digraph smf_hierarchical_initial {
      compound=true;
      node [style = rounded];
      smf_set_initial [shape=plaintext];
      "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";
@@ -529,17 +555,19 @@ The statechart for this test is below.
         subgraph cluster_c {
            label = "PARENT_C";
            style = rounded;
	 STATE_C -> STATE_C
            STATE_B -> STATE_C [ltail=cluster_ab]
         }

      smf_set_initial -> STATE_A [lhead=cluster_ab]
      STATE_B -> STATE_C
         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
+24 −16
Original line number Diff line number Diff line
@@ -18,16 +18,17 @@
/**
 * @brief State Machine Framework API
 * @defgroup smf State Machine Framework API
 * @version 0.1.0
 * @ingroup os_services
 * @{
 */

/**
 * @brief Macro to create a hierarchical state.
 * @brief Macro to create a hierarchical state with initial transitions.
 *
 * @param _entry   State entry function
 * @param _run     State run function
 * @param _exit    State exit function
 * @param _entry   State entry function or NULL
 * @param _run     State run function or NULL
 * @param _exit    State exit function or NULL
 * @param _parent  State parent object or NULL
 * @param _initial State initial transition object or NULL
 */
@@ -72,6 +73,7 @@ struct smf_state {
	const state_execution run;
	/** Optional method that will be run when this state exists */
	const state_execution exit;
#ifdef CONFIG_SMF_ANCESTOR_SUPPORT
	/**
	 * Optional parent state that contains common entry/run/exit
	 *	implementation among various child states.
@@ -79,8 +81,8 @@ struct smf_state {
	 *	run:   Parent function executes AFTER child function.
	 *	exit:  Parent function executes AFTER child function.
	 *
	 *	Note: When transitioning between two child states with a shared parent,
	 *	that parent's exit and entry functions do not execute.
	 *	Note: When transitioning between two child states with a shared
	 *      parent,	that parent's exit and entry functions do not execute.
	 */
	const struct smf_state *parent;

@@ -89,7 +91,8 @@ struct smf_state {
	 * Optional initial transition state. NULL for leaf states.
	 */
	const struct smf_state *initial;
#endif
#endif /* CONFIG_SMF_INITIAL_TRANSITION */
#endif /* CONFIG_SMF_ANCESTOR_SUPPORT */
};

/** Defines the current context of the state machine. */
@@ -98,6 +101,11 @@ struct smf_ctx {
	const struct smf_state *current;
	/** Previous state the state machine executed */
	const struct smf_state *previous;

#ifdef CONFIG_SMF_ANCESTOR_SUPPORT
	/** Currently executing state (which may be a parent) */
	const struct smf_state *executing;
#endif /* CONFIG_SMF_ANCESTOR_SUPPORT */
	/**
	 * This value is set by the set_terminate function and
	 * should terminate the state machine when its set to a
@@ -122,8 +130,8 @@ void smf_set_initial(struct smf_ctx *ctx, const struct smf_state *init_state);

/**
 * @brief Changes a state machines state. This handles exiting the previous
 *        state and entering the target state. A common parent state will not
 *        exited nor be re-entered.
 *        state and entering the target state. For HSMs the entry and exit
 *        actions of the Least Common Ancestor will not be run.
 *
 * @param ctx       State machine context
 * @param new_state State to transition to (NULL is valid and exits all states)
+178 −96
Original line number Diff line number Diff line
@@ -9,24 +9,22 @@
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(smf);

/*
 * Private structure (to this file) used to track state machine context.
/**
 * @brief Private structure (to this file) used to track state machine context.
 *        The structure is not used directly, but instead to cast the "internal"
 *        member of the smf_ctx structure.
 */
struct internal_ctx {
	bool new_state: 1;
	bool terminate: 1;
	bool exit      : 1;
	bool is_exit: 1;
	bool handled: 1;
};

static bool share_paren(const struct smf_state *test_state,
			const struct smf_state *target_state)
#ifdef CONFIG_SMF_ANCESTOR_SUPPORT
static bool share_paren(const struct smf_state *test_state, const struct smf_state *target_state)
{
	for (const struct smf_state *state = test_state;
	     state != NULL;
	     state = state->parent) {
	for (const struct smf_state *state = test_state; state != NULL; state = state->parent) {
		if (target_state == state) {
			return true;
		}
@@ -35,17 +33,6 @@ static bool share_paren(const struct smf_state *test_state,
	return false;
}

static bool last_state_share_paren(struct smf_ctx *const ctx,
				   const struct smf_state *state)
{
	/* Get parent state of previous state */
	if (!ctx->previous) {
		return false;
	}

	return share_paren(ctx->previous->parent, state);
}

static const struct smf_state *get_child_of(const struct smf_state *states,
					    const struct smf_state *parent)
{
@@ -68,22 +55,55 @@ static const struct smf_state *get_last_of(const struct smf_state *states)
}

/**
 * @brief Execute all ancestor entry actions
 * @brief Find the Least Common Ancestor (LCA) of two states
 *
 * @param source transition source
 * @param dest transition destination
 * @return LCA state, or NULL if states have no LCA.
 */
static const struct smf_state *get_lca_of(const struct smf_state *source,
					  const struct smf_state *dest)
{
	for (const struct smf_state *ancestor = source->parent; ancestor != NULL;
	     ancestor = ancestor->parent) {
		if (ancestor == dest) {
			return ancestor->parent;
		} else if (share_paren(dest, ancestor)) {
			return ancestor;
		}
	}

	return NULL;
}

/**
 * @brief Executes all entry actions from the direct child of topmost to the new state
 *
 * @param ctx State machine context
 * @param target The entry actions of this target's ancestors are executed
 * @param new_state State we are transitioning to
 * @param topmost State we are entering from. Its entry action is not executed
 * @return true if the state machine should terminate, else false
 */
__unused static bool smf_execute_ancestor_entry_actions(
		struct smf_ctx *const ctx, const struct smf_state *target)
static bool smf_execute_all_entry_actions(struct smf_ctx *const ctx,
					  const struct smf_state *new_state,
					  const struct smf_state *topmost)
{
	struct internal_ctx *const internal = (void *)&ctx->internal;

	for (const struct smf_state *to_execute = get_last_of(target);
	     to_execute != NULL && to_execute != target;
	     to_execute = get_child_of(target, to_execute)) {
		/* Execute parent state's entry */
		if (!last_state_share_paren(ctx, to_execute) && to_execute->entry) {
	if (new_state == topmost) {
		/* There are no child states, so do nothing */
		return false;
	}

	for (const struct smf_state *to_execute = get_child_of(new_state, topmost);
	     to_execute != NULL && to_execute != new_state;
	     to_execute = get_child_of(new_state, to_execute)) {
		/* Execute every entry action EXCEPT that of the topmost state */
		if (to_execute->entry) {
			/* Keep track of the executing entry action in case it calls
			 * smf_set_State()
			 */
			ctx->executing = to_execute;
			to_execute->entry(ctx);

			/* No need to continue if terminate was set */
@@ -93,6 +113,16 @@ __unused static bool smf_execute_ancestor_entry_actions(
		}
	}

	/* and execute the new state entry action */
	if (new_state->entry) {
		new_state->entry(ctx);

		/* No need to continue if terminate was set */
		if (internal->terminate) {
			return true;
		}
	}

	return false;
}

@@ -103,7 +133,7 @@ __unused static bool smf_execute_ancestor_entry_actions(
 * @param target The run actions of this target's ancestors are executed
 * @return true if the state machine should terminate, else false
 */
__unused static bool smf_execute_ancestor_run_actions(struct smf_ctx *ctx)
static bool smf_execute_ancestor_run_actions(struct smf_ctx *ctx)
{
	struct internal_ctx *const internal = (void *)&ctx->internal;
	/* Execute all run actions in reverse order */
@@ -126,9 +156,10 @@ __unused static bool smf_execute_ancestor_run_actions(struct smf_ctx *ctx)
	}

	/* Try to run parent run actions */
	for (const struct smf_state *tmp_state = ctx->current->parent;
	     tmp_state != NULL;
	for (const struct smf_state *tmp_state = ctx->current->parent; tmp_state != NULL;
	     tmp_state = tmp_state->parent) {
		/* Keep track of where we are in case an ancestor calls smf_set_state()  */
		ctx->executing = tmp_state;
		/* Execute parent run action */
		if (tmp_state->run) {
			tmp_state->run(ctx);
@@ -156,40 +187,36 @@ __unused static bool smf_execute_ancestor_run_actions(struct smf_ctx *ctx)
}

/**
 * @brief Execute all ancestor exit actions
 * @brief Executes all exit actions from ctx->current to the direct child of topmost
 *
 * @param ctx State machine context
 * @param target The exit actions of this target's ancestors are executed
 * @param topmost State we are exiting to. Its exit action is not executed
 * @return true if the state machine should terminate, else false
 */
__unused static bool smf_execute_ancestor_exit_actions(
		struct smf_ctx *const ctx, const struct smf_state *target)
static bool smf_execute_all_exit_actions(struct smf_ctx *const ctx, const struct smf_state *topmost)
{
	struct internal_ctx *const internal = (void *)&ctx->internal;

	/* Execute all parent exit actions in reverse order */

	for (const struct smf_state *tmp_state = ctx->current->parent;
	     tmp_state != NULL;
	     tmp_state = tmp_state->parent) {
		if ((target == NULL || !share_paren(target->parent, tmp_state)) &&
		    tmp_state->exit) {
			tmp_state->exit(ctx);
	for (const struct smf_state *to_execute = ctx->current; to_execute != topmost;
	     to_execute = to_execute->parent) {
		if (to_execute->exit) {
			to_execute->exit(ctx);

			/* No need to continue if terminate was set */
			/* No need to continue if terminate was set in the exit action */
			if (internal->terminate) {
				return true;
			}
		}
	}

	return false;
}
#endif /* CONFIG_SMF_ANCESTOR_SUPPORT */

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
@@ -199,94 +226,146 @@ void smf_set_initial(struct smf_ctx *ctx, const struct smf_state *init_state)
		init_state = init_state->initial;
	}
#endif
	internal->exit = false;

	internal->is_exit = false;
	internal->terminate = false;
	ctx->current = init_state;
	ctx->previous = NULL;
	ctx->terminate_val = 0;

	if (IS_ENABLED(CONFIG_SMF_ANCESTOR_SUPPORT)) {
		internal->new_state = false;
#ifdef CONFIG_SMF_ANCESTOR_SUPPORT
	ctx->executing = init_state;
	const struct smf_state *topmost = get_last_of(init_state);

		if (smf_execute_ancestor_entry_actions(ctx, init_state)) {
	/* Execute topmost state entry action, since smf_execute_all_entry_actions()
	 * doesn't
	 */
	if (topmost->entry) {
		topmost->entry(ctx);
		if (internal->terminate) {
			/* No need to continue if terminate was set */
			return;
		}
	}

	/* Now execute the initial state's entry action */
	if (smf_execute_all_entry_actions(ctx, init_state, topmost)) {
		/* No need to continue if terminate was set */
		return;
	}
#else
	/* execute entry action if it exists */
	if (init_state->entry) {
		init_state->entry(ctx);
	}
#endif
}

void smf_set_state(struct smf_ctx *const ctx, const struct smf_state *target)
void smf_set_state(struct smf_ctx *const ctx, const struct smf_state *new_state)
{
	struct internal_ctx *const internal = (void *)&ctx->internal;

	if (new_state == NULL) {
		LOG_ERR("new_state cannot be NULL");
		return;
	}

	/*
	 * It does not make sense to call set_state in an exit phase of a state
	 * It does not make sense to call smf_set_state in an exit phase of a state
	 * since we are already in a transition; we would always ignore the
	 * intended state to transition into.
	 */
	if (internal->exit) {
		LOG_WRN("Calling %s from exit action", __func__);
	if (internal->is_exit) {
		LOG_ERR("Calling %s from exit action", __func__);
		return;
	}

	internal->exit = true;
#ifdef CONFIG_SMF_ANCESTOR_SUPPORT
	const struct smf_state *topmost;

	/* Execute the current states exit action */
	if (ctx->current->exit) {
		ctx->current->exit(ctx);
	if (share_paren(ctx->executing, new_state)) {
		/* new state is a parent of where we are now*/
		topmost = new_state;
	} else if (share_paren(new_state, ctx->executing)) {
		/* we are a parent of the new state */
		topmost = ctx->executing;
	} else {
		/* not directly related, find LCA */
		topmost = get_lca_of(ctx->executing, new_state);
	}

		/*
		 * No need to continue if terminate was set in the
		 * exit action
		 */
		if (internal->terminate) {
	internal->is_exit = true;
	internal->new_state = true;

	/* call all exit actions up to (but not including) the topmost */
	if (smf_execute_all_exit_actions(ctx, topmost)) {
		/* No need to continue if terminate was set in the exit action */
		return;
	}
	}

	if (IS_ENABLED(CONFIG_SMF_ANCESTOR_SUPPORT)) {
		internal->new_state = true;
	/* if self-transition, call the exit action */
	if ((ctx->executing == new_state) && (new_state->exit)) {
		new_state->exit(ctx);

		if (smf_execute_ancestor_exit_actions(ctx, target)) {
		/* No need to continue if terminate was set in the exit action */
		if (internal->terminate) {
			return;
		}
	}

	internal->exit = false;
	internal->is_exit = false;

	/* if self transition, call the entry action */
	if ((ctx->executing == new_state) && (new_state->entry)) {
		new_state->entry(ctx);

		/* No need to continue if terminate was set in the entry action */
		if (internal->terminate) {
			return;
		}
	}
#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;
	while (new_state->initial) {
		new_state = new_state->initial;
	}
#endif

	/* update the state variables */
	ctx->previous = ctx->current;
	ctx->current = target;
	ctx->current = new_state;

	if (IS_ENABLED(CONFIG_SMF_ANCESTOR_SUPPORT)) {
		if (smf_execute_ancestor_entry_actions(ctx, target)) {
	/* call all entry actions (except those of topmost) */
	if (smf_execute_all_entry_actions(ctx, new_state, topmost)) {
		/* No need to continue if terminate was set in the entry action */
		return;
	}
#else
	/* Flat state machines have a very simple transition: */
	if (ctx->current->exit) {
		internal->is_exit = true;
		ctx->current->exit(ctx);
		/* No need to continue if terminate was set in the exit action */
		if (internal->terminate) {
			return;
		}
		internal->is_exit = false;
	}
	/* update the state variables */
	ctx->previous = ctx->current;
	ctx->current = new_state;

	/* Now execute the target entry action */
	if (ctx->current->entry) {
		ctx->current->entry(ctx);
		/*
		 * If terminate was set, it will be handled in the
		 * smf_run_state function
		 */
		/* No need to continue if terminate was set in the entry action */
		if (internal->terminate) {
			return;
		}
	}
#endif
}

void smf_set_terminate(struct smf_ctx *ctx, int32_t val)
{
@@ -312,15 +391,18 @@ int32_t smf_run_state(struct smf_ctx *const ctx)
		return ctx->terminate_val;
	}

#ifdef CONFIG_SMF_ANCESTOR_SUPPORT
	ctx->executing = ctx->current;
#endif

	if (ctx->current->run) {
		ctx->current->run(ctx);
	}

	if (IS_ENABLED(CONFIG_SMF_ANCESTOR_SUPPORT)) {
#ifdef CONFIG_SMF_ANCESTOR_SUPPORT
	if (smf_execute_ancestor_run_actions(ctx)) {
		return ctx->terminate_val;
	}
	}

#endif
	return 0;
}
Loading