Embedded systems run inside washing machines, car braking modules, pacemakers, and factory robots. These devices don't have the luxury of crashing and reloading a page. Every state transition from idle to active, from reading sensors to triggering an actuator must be predictable and well-defined. That's exactly where UML state machine diagrams earn their keep, and why having ready-to-use code snippets saves embedded developers real time and prevents real bugs.

If you work with microcontrollers, RTOS-based firmware, or bare-metal C code, a state machine diagram gives you a visual blueprint of how your system behaves. Translating that blueprint into working code is where most teams either move fast or get stuck. This article shares practical UML state machine diagram code snippets built for embedded contexts, explains the patterns behind them, and shows you how to avoid the traps that waste cycles during integration and testing.

What does a UML state machine diagram actually represent in embedded firmware?

A UML state machine diagram models the finite states a system or component can be in, the events that trigger transitions between those states, and the actions that execute on entry, exit, or during a transition. In embedded systems, this maps directly to how peripherals behave a UART driver sits in an "Idle" state until a byte arrives, transitions to "Receiving," and then moves to "Processing" once a timeout or delimiter is detected.

The diagram uses specific UML notation: rounded rectangles for states, arrows for transitions labeled with event [guard] / action, a filled circle for the initial pseudostate, and a bulls-eye symbol for the final state. Embedded developers care about every piece of this because each element translates to a case in a switch statement, a flag check, or a function call in firmware.

How do I write a basic state machine in C from a UML diagram?

The most common pattern in embedded C is the switch-case state machine. It's simple, readable, and has zero dynamic memory allocation exactly what you want on a resource-constrained target.

Suppose your UML diagram shows three states for a door lock controller: Locked, Unlocking, and Unlocked. The transitions are:

  • Locked + valid_code_entered → Unlocking
  • Unlocking + motor_reached_limit → Unlocked
  • Unlocked + lock_timeout_expired → Locked

Here's the code that maps directly to that diagram:

typedef enum {
 STATE_LOCKED,
 STATE_UNLOCKING,
 STATE_UNLOCKED
} door_state_t;

typedef enum {
 EVENT_VALID_CODE,
 EVENT_MOTOR_LIMIT,
 EVENT_TIMEOUT
} door_event_t;

static door_state_t current_state = STATE_LOCKED;

void door_lock_sm_run(door_event_t event) {
 switch (current_state) {
 case STATE_LOCKED:
 if (event == EVENT_VALID_CODE) {
 motor_forward();
 current_state = STATE_UNLOCKING;
 }
 break;

 case STATE_UNLOCKING:
 if (event == EVENT_MOTOR_LIMIT) {
 motor_stop();
 current_state = STATE_UNLOCKED;
 start_timeout_timer(30000);
 }
 break;

 case STATE_UNLOCKED:
 if (event == EVENT_TIMEOUT) {
 motor_reverse();
 current_state = STATE_LOCKED;
 }
 break;

 default:
 current_state = STATE_LOCKED;
 break;
 }
}

Notice how each case block corresponds to a state in the UML diagram, and each if check mirrors the transition guard condition. This one-to-one mapping is the whole point it makes code review against the diagram straightforward and catches missed transitions early.

What PlantUML syntax should I use to draw the state machine diagram first?

Before writing code, many teams version-control their diagrams using PlantUML. The text-based format diffs cleanly in Git and generates consistent output across machines. Here's the PlantUML for the door lock example above:

@startuml
[] --> Locked

state Locked {
 Locked --> Unlocking : valid_code_entered / motor_forward()
}

state Unlocking {
 Unlocking --> Unlocked : motor_reached_limit / motor_stop(),\n start_timeout_timer(30000)
}

state Unlocked {
 Unlocked --> Locked : lock_timeout_expired / motor_reverse()
}

@enduml

PlantUML state diagrams support nested states, parallel regions, and entry/exit actions all of which you'll need for more complex embedded subsystems like communication protocol handlers. If you're building diagrams for other system layers, our PlantUML sequence diagram examples for REST API workflows cover how to model request-response interactions that might sit above your embedded firmware in a larger IoT stack.

How do I handle hierarchical (nested) states in embedded code?

Real embedded systems rarely stay flat. A radio module might have a top-level state machine for "Powered Off," "Initializing," and "Active," but inside "Active" it runs a sub-state machine for "Idle," "Transmitting," and "Receiving." UML supports this with composite states (also called nested states or substates).

In code, you manage this with two variables one for the parent state and one for the sub-state:

typedef enum {
 TOP_OFF,
 TOP_INIT,
 TOP_ACTIVE
} radio_top_state_t;

typedef enum {
 SUB_IDLE,
 SUB_TX,
 SUB_RX
} radio_sub_state_t;

static radio_top_state_t top_state = TOP_OFF;
static radio_sub_state_t sub_state = SUB_IDLE;

void radio_sm_run(radio_event_t event) {
 switch (top_state) {
 case TOP_OFF:
 if (event == EVENT_POWER_ON) {
 hw_init_radio();
 top_state = TOP_INIT;
 }
 break;

 case TOP_INIT:
 if (event == EVENT_CALIBRATION_DONE) {
 top_state = TOP_ACTIVE;
 sub_state = SUB_IDLE;
 }
 break;

 case TOP_ACTIVE:
 switch (sub_state) {
 case SUB_IDLE:
 if (event == EVENT_TX_REQUEST) {
 load_tx_buffer();
 start_transmission();
 sub_state = SUB_TX;
 }
 if (event == EVENT_RX_FRAME) {
 sub_state = SUB_RX;
 }
 break;

 case SUB_TX:
 if (event == EVENT_TX_COMPLETE) {
 sub_state = SUB_IDLE;
 }
 break;

 case SUB_RX:
 if (event == EVENT_RX_DONE) {
 process_rx_frame();
 sub_state = SUB_IDLE;
 }
 break;
 }

 / Parent state handles power-down from any sub-state /
 if (event == EVENT_POWER_OFF) {
 shutdown_radio();
 top_state = TOP_OFF;
 }
 break;
 }
}

The key detail here: the EVENT_POWER_OFF check sits at the parent level, so it's handled regardless of which sub-state is active. That matches the UML semantics where an event on a composite state can be taken from any of its children. Miss this pattern and you'll spend hours debugging why your device won't shut down cleanly during certain operating modes.

Why should I use an event queue instead of calling the state machine directly?

The examples above call the state machine function synchronously fine for simple systems or cooperative schedulers. But in many embedded designs, events arrive from interrupts (a UART byte received, a timer fired, a GPIO edge detected). You can't run complex state machine logic inside an ISR.

The solution is a producer-consumer event queue. ISRs push events into a lock-free ring buffer, and the main loop (or an RTOS task) pulls events out and feeds them to the state machine:

#define EVENT_QUEUE_SIZE 32

static volatile door_event_t event_queue[EVENT_QUEUE_SIZE];
static volatile uint8_t eq_head = 0;
static volatile uint8_t eq_tail = 0;

/ Called from ISR context keep it short /
void door_event_post(door_event_t evt) {
 uint8_t next = (eq_head + 1) % EVENT_QUEUE_SIZE;
 if (next != eq_tail) {
 event_queue[eq_head] = evt;
 eq_head = next;
 }
 / else: queue full, drop event or set overflow flag /
}

/ Called from main loop or RTOS task /
void door_event_dispatch(void) {
 while (eq_tail != eq_head) {
 door_event_t evt = event_queue[eq_tail];
 eq_tail = (eq_tail + 1) % EVENT_QUEUE_SIZE;
 door_lock_sm_run(evt);
 }
}

This pattern decouples interrupt timing from state machine logic, which makes the code testable on a host machine (just push events and check state transitions) and keeps your ISR latency low.

What about guard conditions and actions that take real time?

In UML notation, a transition looks like event [guard] / action. The guard is a boolean condition that must be true for the transition to fire. In embedded code, guards show up as additional if checks:

case STATE_IDLE:
 if (event == EVENT_BUTTON_PRESS) {
 if (battery_voltage_ok()) { / guard condition /
 start_measurement(); / action /
 current_state = STATE_MEASURING;
 } else {
 set_error_led(); / else-action when guard fails /
 }
 }
 break;

For actions that take real time like an ADC conversion, an SPI flash erase, or a motor ramp don't block inside the state machine. Instead, start the operation and let the completion event drive the next transition. This is the non-blocking state machine pattern, and it's essential for any system that needs to remain responsive while one subsystem is busy.

For example, instead of:

/ BAD: blocks the entire system /
case STATE_ERASING:
 spi_flash_erase_sector(addr); / takes 50-400ms /
 current_state = STATE_ERASED;
 break;

Do this:

/ GOOD: non-blocking /
case STATE_ERASE_START:
 spi_flash_erase_begin(addr);
 current_state = STATE_ERASING;
 break;

case STATE_ERASING:
 if (event == EVENT_ERASE_COMPLETE) {
 current_state = STATE_ERASED;
 }
 if (event == EVENT_ERASE_ERROR) {
 current_state = STATE_ERROR;
 }
 break;

What are the most common mistakes embedded developers make with state machines?

After working with dozens of embedded firmware teams, these errors come up repeatedly:

  • Unhandled events in a state. If your UML diagram doesn't show a transition for an event in a given state, the code still needs to decide: ignore it silently, log it, or go to an error state. Silent ignoring makes debugging painful.
  • Using raw integers instead of enums. if (state == 3) is a bug magnet. Named enums let the compiler catch type mismatches and make the code self-documenting.
  • Missing default cases. Always include a default in your switch to catch corrupted state values, especially on safety-critical systems where a single-bit flip is a real scenario.
  • Blocking actions inside transitions. Already covered above, but worth repeating it's the #1 cause of timing failures in cooperative scheduling designs.
  • No diagram-code synchronization. If the UML diagram lives in a separate Word doc that nobody updates, it becomes fiction. Use text-based diagram tools like PlantUML that live in the same repository as your code.
  • Forgetting entry and exit actions. UML supports entry/ and exit/ actions on states. In code, these are function calls right after the state variable changes and right before it changes. They're perfect for enabling/disabling peripherals, toggling LEDs, or resetting watchdog timers.

How do entry and exit actions look in code?

UML state machine diagrams support entry actions (run when entering a state), exit actions (run when leaving a state), and do activities (ongoing behavior while in a state). In embedded code, entry and exit actions map cleanly to function calls placed precisely at state transitions:

/ Helper functions for entry/exit actions /
static void enter_measuring(void) {
 adc_enable();
 start_sample_timer(100); / 100ms interval /
 set_status_led(LED_BLUE);
}

static void exit_measuring(void) {
 stop_sample_timer();
 adc_disable();
}

void sensor_sm_run(sensor_event_t event) {
 switch (current_state) {
 case STATE_IDLE:
 if (event == EVENT_START_CMD) {
 exit_idle(); / exit action for IDLE /
 enter_measuring(); / entry action for MEASURING /
 current_state = STATE_MEASURING;
 }
 break;

 case STATE_MEASURING:
 if (event == EVENT_SAMPLES_DONE) {
 exit_measuring(); / exit action for MEASURING /
 compute_result();
 enter_reporting(); / entry action for REPORTING /
 current_state = STATE_REPORTING;
 }
 break;
 }
}

This pattern keeps your state transition logic clean and your peripheral management in named functions. When a teammate asks "what happens when we leave the measuring state?" the exit_measuring() function answers that directly.

Can I use this pattern in C++ or with an RTOS task?

Yes, and the patterns scale well. In C++, you can model each state as a class using the State pattern from the Gang of Four book, where each state object handles events and returns the next state. This works well when states have complex internal logic or when you want to leverage virtual dispatch.

In an RTOS context, wrap the event queue in a FreeRTOS xQueue and dedicate a task to running the state machine. The task blocks on the queue, wakes when an event arrives, and processes it naturally non-blocking and easy to assign a priority:

QueueHandle_t door_event_queue;

void door_sm_task(void pvParameters) {
 door_event_t event;
 for (;;) {
 if (xQueueReceive(door_event_queue, &event, portMAX_DELAY) == pdTRUE) {
 door_lock_sm_run(event);
 }
 }
}

For teams building service-oriented architectures on top of firmware, our article on UML activity diagram markup for microservices architecture covers how to model higher-level workflows that might trigger state transitions in your embedded layer.

How do I test a state machine against its UML diagram?

The biggest testing win is table-driven state machines. Instead of a switch-case, you define a transition table as a static array and use a generic engine to process events:

typedef struct {
 state_t current_state;
 event_t event;
 state_t next_state;
 action_fn action;
} transition_t;

static const transition_t transitions[] = {
 { STATE_LOCKED, EVENT_VALID_CODE, STATE_UNLOCKING, action_start_motor },
 { STATE_UNLOCKING, EVENT_MOTOR_LIMIT, STATE_UNLOCKED, action_stop_motor },
 { STATE_UNLOCKED, EVENT_TIMEOUT, STATE_LOCKED, action_reverse_motor },
};

#define NUM_TRANSITIONS (sizeof(transitions) / sizeof(transitions[0]))

void sm_dispatch(state_t state, event_t event) {
 for (size_t i = 0; i < NUM_TRANSITIONS; i++) {
 if (transitions[i].current_state == state &&
 transitions[i].event == event) {
 if (transitions[i].action != NULL) {
 transitions[i].action();
 }
 state = transitions[i].next_state;
 return;
 }
 }
 / No matching transition log or handle error /
}

With this structure, you can auto-generate the transition table from your PlantUML source, or write a test harness that iterates every row in the table and verifies the expected state and action. That closes the loop between your UML diagram and your implementation no more manual cross-checking.

What tools help generate code from UML state machine diagrams?

Several tools bridge the gap between diagram and firmware:

  • PlantUML text-based diagramming that lives in version control. Doesn't generate C code, but gives you an authoritative visual spec to code against.
  • Yakindu Statechart Tools (now itemis CREATE) an Eclipse-based tool that generates C, C++, or Java code from statecharts, including support for hierarchy, concurrency, and timed events.
  • Quantum Platform (QP) a framework specifically for event-driven state machines in embedded C/C++. Provides a UML-based modeling tool and a run-to-completion kernel.
  • SMC (State Machine Compiler) an open-source tool that takes a state machine definition file and generates code in many languages, including C.

If you also work on the protocol or API layer above your firmware, check out the PlantUML sequence diagram code examples for REST API workflows they show how to document the communication between your embedded device and the cloud services it talks to.

Quick checklist: before you commit your state machine code

  1. Every state in your UML diagram has a matching case in your switch (or a row in your transition table).
  2. Every transition in the diagram has an event check and, where specified, a guard condition.
  3. Unhandled events in each state are explicitly dealt with ignore with a comment, log, or go to error state.
  4. Entry and exit actions are placed at the right side of state variable assignment.
  5. No blocking operations inside state transition logic.
  6. Enums are used for states and events, never raw integers.
  7. A default case catches unexpected state values.
  8. Your PlantUML diagram file lives in the same repo as the source code, and they're reviewed together in pull requests.
  9. Unit tests cover every row in the transition table, including guard failures.
  10. Interrupt-driven events go through a queue, not directly into the state machine function.

Start by writing the PlantUML diagram for your next module's state behavior. Keep it in your repo. Then translate it to code using the patterns above. That single habit diagram-first, code-second, both versioned together will prevent more embedded bugs than any testing framework can catch after the fact.