Zephyr: State Machine Framework
BME554L -Fall 2025 - Palmeri
State Machine Framework
- Nested conditional logic main loops are hard to read and maintain.
- State machines are a common way to implement complex logic.
- States have transitions that are triggered by events or conditions.
- States can have entry / exit routines that are executed when the state is entered / exited.
- The “run” status of a state is commonly referred to as the “state machine tick” and recurrently loops.
- State diagrams are used to visualize state machines.
- State structures are used to capture variables associated with describing the state.
Implementation
Switch-Case
- The simplest implementation of a state machine is a switch-case statement.
- The switch statement is used to select the current state.
- The case statements are used to implement the logic for each state.
- Cases can be nested to implement sub-states.
- Enumerations can be used to give states verbose names instead of numbers.
- The break statement is used to exit the switch statement.
- The default statement is used to handle unexpected states.
Use the State to Dictate Function
- Avoid testing for a condition in a state to figure out what to do; let being in the state dictate the function (i.e., you shouldn’t have to test for the state you are in to know what to do).
- Use entry and exit routines to handle state transitions; do not test for first or last iteration of that state.
- “Run” states can iterate in a loop until a condition is met to change the state, or can wait for an event.
- If you find you need to timeout an event wait to allow other stuff to happen, consider using
k_event_test()
.
Pseudo-Code
enum device_states { init, run, error_entry, error_run, error_exit };
int device_state = init; // initialize state
/* structure to bookkeep state variables */
struct device_state_vars {
int var1;
int var2;
};
while (1) {
switch (device_state) {
case init:
/* do stuff to initialize device */
= run; // change the state
device_state break;
case run:
/* run device */
if (condition) {
= error;
device_state }
break;
case error_entry:
();
illuminate_error_ledbreak;
case error_run:
if (condition_to_leave_error) {
= error_exit;
device_state }
break
case error_entry:
();
turn_off_error_led= run;
device_state break;
default:
/* handle unexpected state */
break;
}
}
The switch-case approach loses some of its elegance when there are many states and many transitions and states have entry / exit routines.
State Machine Framework
State machine implementations are so common that Zephyr provides a state machine framework.
Pseudo-Code
prj.conf
=y CONFIG_SMF
Flat State Machine Example
main.c
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/smf.h>
#define SW0_NODE DT_ALIAS(sw0)
/* List of events */
(button_events);
K_EVENT_DEFINE#define FREQ_UP_BTN_PRESS BIT(0)
#define FREQ_DOWN_BTN_PRESS BIT(1)
#define SLEEP_BTN_PRESS BIT(2)
#define RESET_BTN_PRESS BIT(3)
static const struct gpio_dt_spec button =
(SW0_NODE, gpios, {0});
GPIO_DT_SPEC_GET_OR
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 { INIT, S0, S1 };
SMF Context Struct
/* User defined object */
struct s_object {
/* This must be first */
struct smf_ctx ctx;
/* Other state specific data add here */
} s_obj;
- Struct that stores information about the previous and current state.
- Stores state termination value.
Zephyr Docs: SMF Context Struct
static void init_run(void *o)
{
int ret;
if (!gpio_is_ready_dt(&button)) {
("Error: button device %s is not ready\n",
printk.port->name);
buttonreturn;
}
= gpio_pin_configure_dt(&button, GPIO_INPUT);
ret if (ret != 0) {
("Error %d: failed to configure %s pin %d\n",
printk, button.port->name, button.pin);
retreturn;
}
= gpio_pin_interrupt_configure_dt(&button,
ret );
GPIO_INT_EDGE_TO_ACTIVEif (ret != 0) {
("Error %d: failed to configure interrupt on %s pin %d\n",
printk, button.port->name, button.pin);
retreturn;
}
(&button_cb_data, button_pressed, BIT(button.pin));
gpio_init_callback(button.port, &button_cb_data);
gpio_add_callback
(SMF_CTX(&s_obj), &demo_states[S0]);
smf_set_state
}
/* State S0 */
static void s0_entry(void *o)
{
("STATE0\n");
printk}
static void s0_run(void *o)
{
/* Change states on Button Press Event */
int32_t events = k_event_wait(&button_events, FREQ_UP_BTN_PRESS, true, K_FOREVER);
if (events & FREQ_UP_BTN_PRESS) {
(SMF_CTX(&s_obj), &demo_states[S1]);
smf_set_state}
}
/* State S1 */
static void s1_entry(void *o)
{
("STATE1\n");
printk}
static void s1_run(void *o)
{
/* Change states on Button Press Event */
int32_t events = k_event_wait(&button_events, FREQ_DOWN_BTN_PRESS, true, K_FOREVER);
if (events & FREQ_DOWN_BTN_PRESS) {
(SMF_CTX(&s_obj), &demo_states[S0]);
smf_set_state}
}
/* Populate state table */
static const struct smf_state demo_states[] = {
[INIT] = SMF_CREATE_STATE(NULL, init_run, NULL, NULL, NULL),
[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 */
(&button_events, FREQ_UP_BTN_PRESS);
k_event_post}
int main(void)
{
/* Set initial state */
(SMF_CTX(&s_obj), &demo_states[INIT]);
smf_set_initial
/* Run the state machine */
while(1) {
/* Block until an event is detected */
int events = k_event_wait(button_events, EVENT_BTN_PRESS, true, K_FOREVER);
/* State machine terminates if a non-zero value is returned */
= smf_run_state(SMF_CTX(&s_obj));
ret if (ret) {
/* handle return code and terminate state machine */
(SMF_CTX(&s_obj), ret);
smf_set_terminatebreak;
}
}
}
Heirarchical State Machine Example
If multiple states share the same entry / exit routines, they can be grouped into a “superstate”.
https://docs.zephyrproject.org/latest/services/smf/index.html#hierarchical-state-machine-example
When to Use Threads vs. States
- The state machine is the foundation of describing the behavior of a system.
- Threads as a tool to help implement a function of the state machine that is not readily captured by an exclusive state.
- Threads add overhead and complexity to the system, making debugging more difficult.
- Threads are not readily captured in a state diagram, but are more commonly described using a sequence diagram.