freertos-maquinas-estados-rtos

State machines in FreeRTOS

  • 4 min

An RTOS state machine is a way to organize a task’s behavior based on the current system state and the events it receives.

State machines are already useful in sequential programming. With FreeRTOS they are even more so, because they help us avoid tasks full of scattered if statements, global flags, and conditions that no one remembers who put there.

Why use states

Many devices don’t “do one thing”, but rather go through phases:

  • Starting up.
  • Connecting to WiFi.
  • Waiting for configuration.
  • Running.
  • In error.
  • Rebooting.

If we try to represent all that with loose variables, the code quickly becomes confusing. A state machine forces us to answer two very healthy questions:

  • What state am I in?
  • What events can change my state?

This reduces chaos quite a bit. And in concurrent systems, reducing chaos is always a good investment.

FSM within a task

The simplest way is to implement the state machine inside a single task.

enum SystemState {
  STATE_CONNECTING_WIFI,
  STATE_SYNCING_TIME,
  STATE_RUNNING,
  STATE_ERROR
};

void SystemTask(void *pvParameters) {
  SystemState state = STATE_CONNECTING_WIFI;

  for(;;) {
    switch (state) {
      case STATE_CONNECTING_WIFI:
        if (wifiConnected()) {
          state = STATE_SYNCING_TIME;
        }
        break;

      case STATE_SYNCING_TIME:
        if (timeSynced()) {
          state = STATE_RUNNING;
        }
        break;

      case STATE_RUNNING:
        executeMainLogic();
        break;

      case STATE_ERROR:
        handleError();
        break;
    }

    vTaskDelay(pdMS_TO_TICKS(50));
  }
}
Copied!

This approach is easy to understand. One task owns the state and no one else modifies it directly.

Whenever possible, make a single task the owner of the state. Others send it events, but do not modify the state variable directly.

Event-based FSM

A common improvement is for the task not to constantly poll, but to wait for events via a queue.

enum SystemEvent {
  EVENT_WIFI_OK,
  EVENT_WIFI_ERROR,
  EVENT_TIME_OK,
  EVENT_RESET_BUTTON
};

QueueHandle_t eventQueue;

void StateTask(void *pvParameters) {
  SystemState state = STATE_CONNECTING_WIFI;
  SystemEvent event;

  for(;;) {
    if (xQueueReceive(eventQueue, &event, portMAX_DELAY) == pdTRUE) {
      switch (state) {
        case STATE_CONNECTING_WIFI:
          if (event == EVENT_WIFI_OK) {
            state = STATE_SYNCING_TIME;
          }
          else if (event == EVENT_WIFI_ERROR) {
            state = STATE_ERROR;
          }
          break;

        case STATE_SYNCING_TIME:
          if (event == EVENT_TIME_OK) {
            state = STATE_RUNNING;
          }
          break;

        default:
          break;
      }
    }
  }
}
Copied!

Here the task blocks until something happens. It doesn’t waste CPU checking flags and the flow is quite clear.

States distributed across tasks

In larger systems, we can have several tasks with their own state machine:

  • One task for connectivity.
  • One task for sensors.
  • One task for control.
  • One task for user interface.

Each task manages its own little world and communicates with the others via queues, notifications, or Event Groups.

This works well, but we must avoid each task maintaining a different version of “the truth”. If one task thinks the system is connected and another thinks it isn’t, we already have a problem.

A clean strategy is to have:

  • Local states within each task.
  • A minimal global state managed by a coordinator task.
  • Explicit events to announce important changes.

Event Groups for global states

Event Groups fit very well when the state can be represented with flags.

#define BIT_WIFI_OK   (1 << 0)
#define BIT_TIME_OK   (1 << 1)
#define BIT_MQTT_OK   (1 << 2)

EventGroupHandle_t systemEvents;

void MainTask(void *pvParameters) {
  xEventGroupWaitBits(
    systemEvents,
    BIT_WIFI_OK | BIT_TIME_OK | BIT_MQTT_OK,
    pdFALSE,
    pdTRUE,
    portMAX_DELAY
  );

  executeSystemReady();
  vTaskDelete(NULL);
}
Copied!

In this example, the main task waits until WiFi, time, and MQTT are all ready. It is a compact way of expressing “don’t proceed until all of this is true”.

Common mistakes

There are several typical pitfalls when mixing FreeRTOS and state machines:

  • Modifying the state from multiple tasks without protection.
  • Using global flags without a synchronization protocol. volatile by itself does not prevent race conditions or make operations atomic.
  • Putting long delays inside a critical state.
  • Not defining what happens on errors or timeouts.
  • Creating too many tasks when a simple FSM would have sufficed.

A state machine alone does not fix a confusing design. It helps enormously, but states, events, and transitions must be defined with intention.