Zephyr: State Machine Framework

BME554L - Fall 2025

Author
Affiliation

Dr. Mark Palmeri, M.D., Ph.D.

Duke University

Published

October 6, 2025

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 */
            device_state = run; // change the state
            break;
        case run:
            /* run device */
            if (condition) {
                device_state = error;
            }
            break;
        case error_entry:
            illuminate_error_led();
            break;
        case error_run:
            if (condition_to_leave_error) {
                device_state = error_exit;
            }
            break
        case error_entry:
            turn_off_error_led();
            device_state = run;
            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

CONFIG_SMF=y

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 */
K_EVENT_DEFINE(button_events);
#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 =
        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 { 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)) {
            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);

    smf_set_state(SMF_CTX(&s_obj), &demo_states[S0]);

}

/* State S0 */
static void s0_entry(void *o)
{
        printk("STATE0\n");
}

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_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)
{
        /* 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_set_state(SMF_CTX(&s_obj), &demo_states[S0]);
        }
}

/* 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 */
        k_event_post(&button_events, FREQ_UP_BTN_PRESS);
}

int main(void)
{
    /* Set initial state */
    smf_set_initial(SMF_CTX(&s_obj), &demo_states[INIT]);

    /* 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 */
        ret = smf_run_state(SMF_CTX(&s_obj));
        if (ret) {
            /* handle return code and terminate state machine */
            smf_set_terminate(SMF_CTX(&s_obj), ret);
            break;
        }
    }
}

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.

Avoiding Global Variables with s_object Struct

As you implement functionality within the SMF, you may wonder how to make a variable available in two different states — which is a good question, because a natural instinct might be to utilize a global variable, which we want to avoid.

Generally, you can leverage the fact that the s_object struct we create for smf_ctx (ie, state machine context) is passed to all states as a pointer and populate it with members for your own purpose. You can declare an s_object struct template in the global scope that contains anything you need to access between states. Something like:

struct s_object {
    struct smf_ctx ctx; // must be first element
    struct struct_type my_struct;
    int16_t my_variable;
    int16_t my_array[ARR_LENGTH];
};

To be clear, this is merely defining a struct type and does not constitute creating a variable. Then, within main(), you can create an instance of this struct (that will be local to main) and populate the members with initial values — here, this example shows how to do this with a helper function:

#define S_OBJ_SUCCESS         0
#define S_OBJ_ERR_NULL_PTR   -1
#define S_OBJ_ERR_BAD_CONFIG -2 // define your own error codes

int8_t init_s_object(struct s_object *s) {
    if (s == NULL) {
        return S_OBJ_ERR_NULL_PTR;  // invalid pointer
    }
    s->my_variable = 0; // initial value
    s->my_struct.member = &s->my_variable; // assign member of my_struct pointer to my_variable 
    s->my_struct.array_size = sizeof(s->my_array[0])*ARR_LENGTH;
    if (s->my_struct.member == NULL) {
        return S_OBJ_ERR_BAD_CONFIG;
    }
    return S_OBJ_SUCCESS;
}

As always, utilize function returns for error handling using exit codes.

int main(void){
    static struct s_object s_obj; // create an instance of s_object called s_obj
    int ret = init_s_object(&s_obj); // initialize s_obj using helper function
    if (ret != 0) {
        LOG_ERR("could not initialize s_object stuct (%d)", ret);
        break;
    }
    smf_set_initial(SMF_CTX(&s_obj), &fsm_states[INIT]);
    for (;;) {
        ret = smf_run_state(SMF_CTX(&s_obj));
        if (ret != 0) {
            LOG_ERR("terminating state machine (%d)", ret);
            break;
        }
    }
    return 0;
}

Then, within a state:

 static void state_run(void *o){
    struct s_object *s = (struct s_object *)o; // cast explicitly for compiler
    (void)my_function(&s->my_variable); // pass pointer to my_variable to my_function
    // remainder of state code
}

Because the default argument for states is a generic pointer (void *o), you will need to cast the input to a pointer for the s_object struct to safely access its members. If you’re unfamiliar with the arrow operator (->), this allows you to access members of a struct via a pointer. This syntax is equivalent to (*s).member , where the . is how we would typically access a member directly (ie, not as a pointer).

Resources