Zephyr: State Machine Framework

BME554L - Spring 2026

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

Passing State Object Data to Timer Handlers

In main():

k_timer_user_data_set(&my_timer, &s_obj); // make *s_obj available to my_timer

Docs for k_timer_user_data_set()

Within the timer handler:

void timer_handler(struct k_timer *my_timer){
    struct s_object *s = k_timer_user_data_get(my_timer);
    // use s->member as needed
}

So, we can make *s_obj available inside a timer handler.

Passing State Object Data to Work Queue Handlers

Now, what if we want to pass the same pointer to a work queue thread and what if we want to do so from inside the above timer handler? (in the context where the timer submits work to be done).

One way to do this is to create a struct wrapped around struct k_work work and declare a placeholder pointer as a secondary member:

struct my_work {
    struct k_work work;
    void *s_obj_ptr;
}; 

Note here that this just a template for a struct of this type (my_work) and does not constitute creating an instance of such a struct. We can create an instance of this wrapper struct from inside timer_handler() (or elsewhere, as needed):

void timer_handler(struct k_timer *my_timer){
    struct s_object *s = k_timer_user_data_get(my_timer); // from before
    static struct my_work mw; // create a static instance of wrapper struct
    mw.s_obj_ptr = s; // assign desired pointer to placeholder
    k_work_init(&mw.work, my_work_handler); // use wrapper to init work
    k_work_submit(&mw.work); // use wrapper to submit work
}

Now, within my_work_handler():

void my_work_handler(struct k_work *work){
    struct my_work *mw = CONTAINER_OF(work, struct my_work, work);
    struct s_object *s = mw->s_obj_ptr;
    // use s-> member as needed
}

Here, we make use of the CONTAINER_OF macro (documentation)to obtain a pointer to the structure containing the element work, in this case my_work, the wrapper struct. From this, we can then recover a pointer to our s_object instance, s_obj, and use it as needed thereafter.

More generically, how can we pass a pointer to a user created thread?

void my_thread(void *p1, void *p2, void *p3); // forward declaration

K_THREAD_STACK_DEFINE(thread_stack, 1024);
struct k_thread thread_id;

Then from main():

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);
    }
    k_thread_create(&thread_id, thread_stack, 1024, my_thread, 
                    &s_obj, NULL, NULL, // p1, p2, p3
                    5, 0, K_NO_WAIT);
    k_timer_user_data_set(&my_timer, &s_obj); // make *s_obj available to my_timer
    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);
        }
    }
    return 0;
}

In the call to k_thread_create() the 5th through 7th arguments are the pointers *p1, *p2, *p3 that appear as arguments to the thread itself:

void my_thread(void *p1, void *p2, void *p3) {
    struct s_object *s = (struct s_object *)p1; // type cast generic pointer
    while (1) {
        // access s->members
        // do things in thread
    }
}

Resources