State Machines
Why Do We Need This?
Try it in Lab 07
Build a navigation state machine that combines line following, junction detection, and obstacle avoidance. β State Machines tutorial
Your line-following robot needs to: - Follow the line normally - Stop when it sees an obstacle - Back up and go around - Resume line following
You start coding with if-else chains:
if obstacle and not backing_up and not turning:
stop()
backing_up = True
elif backing_up and timer > 500:
backing_up = False
turning = True
...
Soon you have 5 boolean flags, nested conditions 4 levels deep, and bugs where the robot gets "stuck" in impossible states. Every change breaks something else.
The problem: Your robot has modes (following, avoiding, stopped), but your code doesn't represent them explicitly. State machines fix this by making modes first-class citizens in your design.
Organizing Logic with Finite State Machines (FSMs)
As embedded systems become more complexβespecially those with multiple operational modes, user interaction, or real-time controlβorganizing control logic in a clear, scalable way becomes essential. One of the most effective techniques for managing this complexity is using a Finite State Machine (FSM).
A state machine models how a system behaves over time in response to external inputs. It acts like a behavioral blueprint for your system, much like a flowchart, where:
- States represent the current mode or condition of the system.
- Events are external triggers (e.g., button presses, sensor readings, timer expirations).
- Transitions define how the system moves from one state to another based on events.
- Actions are operations performed when entering, exiting, or during a state.
π What Is an Event?
In a general sense, an event is a discrete occurrence that signals the system to respond or change its behavior.
Formally, an event is an external or internal trigger that, when received by a system in a particular state, may cause it to transition to another state and possibly perform actions.
For example, in an embedded system:
- A button press generates an
ev_ButtonPressedevent. - A sensor threshold crossed might generate
ev_TemperatureHigh. - A timeout might trigger
ev_TimerExpired.
Using named events (often defined using Enums in Python) makes your state machine robust, readable, and safe from invalid input.
β Why Use FSMs in Embedded Systems?
FSMs provide a structured way to describe how the system should behave in response to external events over time. This is especially useful for:
- User interface flow (menus, input modes)
- Device operation modes (e.g., OFF, RUNNING, ERROR)
- Communication protocols (e.g., waiting for request, sending data)
FSMs promote:
- Modularity β states are clearly separated
- Clarity β transitions are explicit and predictable
- Maintainability β itβs easy to add or modify behavior
Example: LED Control with IR remote
Letβs revisit a classic beginner scenario: controlling an LED using a button. Rather than using flags or nested conditionals, we can model the behavior as a state machine.
We might define:
Each arrow represents a transition, triggered by a key press event. Each mode of the LED (OFF, ON) is a clearly defined state.
In the Idle state, turn on the red LED. In the On state, turn on the green LED and turn off the red one. So, only one LED is on at a time as show bellow.
UML State Machines for Embedded Design
To formalize and visualize FSMs in a standardized way, we use UML (Unified Modeling Language) State Machines. Unlike purely theoretical models like Mealy and Moore machinesβwhich are abstract and hardware-centricβUML state machines are tailored for software design.
UML diagrams make it easier to design, understand, and communicate system behaviorβespecially as complexity grows. A UML State Machine Diagram is a way to visualize the behavior of a system that:
- Has different modes of operation (states),
- Changes mode based on events (like button presses),
- Performs actions during or between states.
The example above in UML State Machine diagram:
States
A state represents a mode of operation the system can be in. Each state can have associated actions:
entry / actionβ runs when the state is entereddo / actionβ runs while the system remains in the stateexit / actionβ runs just before leaving the stat
Transitions
A transition is a link between two states, triggered by an event.
Format:
- Event β the trigger (e.g., a button press, a timeout, or a sensor input)
- Guard (optional) β a condition (
[guard])that must be true for the transition to occur - Action (optional) β code to run during the transition (not in either state)
State Machine Describe Behavior β Not Structure or Interaction
Itβs important to understand what FSMs are not:
- FSMs do not describe the systemβs structure (i.e., hardware components, modules, or classes)
- FSMs do not describe interaction diagrams (e.g., message sequences or signal flow)
Instead, FSMs are focused solely on system behaviorβthey answer:
- What is the system doing now?
- What event should cause it to do something else?
- What happens during that transition?
This makes FSMs a great fit for handling mode control, event-driven logic, and system states over time.
A simple State Machine Implementation in MicroPython
The state machine diagram describe only the behavior of the system. The structural design not part of it. The best practice is to split in tow half: - event generation - event handling
This architecture simplify the system. In one part we should card about how the events generated and the other one how the events handled and the state of the state machines working.
State Machine Implementation in MicroPython
Let's implement it! But how? So, we know what behavior we want from our system but we need a structure for it. As we implement it in MicroPython, to make it simple, we'll implement it in modules applying Python classes.
main.pycontains thesm.pycontains the structure of the state machineevent.pycontains the handling and generating the eventsled.pycontains the LED
How it will interact with each other? Letβs break it down using the diagram below:
Why This Structure?
Weβve built the system this way for several important reasons:
- Separation of concerns β each part does one job well.
- Modularity β you can easily swap or extend parts (like using buttons or Bluetooth instead of IR).
- Readability β the main loop is simple and easy to understand.
- Reusability β components like the LED controller or state machine can be reused in other projects.
Donβt worry if it doesnβt all click right away β thatβs normal! As you explore more embedded Python projects and look at different code bases, these design choices will start to make more sense.
π Thereβs never just one βcorrectβ structure β this is just one clean and practical example. With experience, youβll find what works best for different problems.
Let's see the contents.
main.py
The main.py script runs the core control loop of the system.
from sm import StateMachine
from event import EventGenerator
# Initialize state machine and event system
sm = StateMachine()
event_gen = EventGenerator()
while True:
# Generate events
event = event_gen.get_event()
# Process the events
sm.process(event)
Inside the Infinite Loop
-
It polls for a new event (e.g., button press, remote signal).
- This abstracts input handling β the main loop doesn't need to know how the event was generated, only that it happened.
-
It passes the event to the state machine, which decides how the system should respond (e.g., turn on an LED, start blinking, etc.).
This clean separation ensures that the core behavior logic (the state machine) stays decoupled from the hardware-specific input mechanisms. As a result, the system becomes more modular and easier to maintain or extend.
Caution
Avoid Blocking Calls in States
The state machine is processed within the main infinite loop.
This means every state handler must be non-blocking β if a state includes delays (e.g., sleep()) or long computations, it will prevent the system from polling new events in time.
Instead, use non-blocking techniques, such as:
-
Tracking elapsed time using timestamps (e.g.,
time.ticks_ms()) -
Updating outputs only when necessary
-
Breaking long tasks into smaller steps over multiple cycles
This approach ensures responsive, real-time behavior and keeps the system reactive to new inputs.
sm.py
The sm.py module defines the behavior of the system using a state machine. This is where the system decides how to respond to different events, like button presses, and what actions to perform (like turning LEDs on or off).
from led import LED
# State constants
class State:
INIT = 0
IDLE = 1
ON = 2
# add more states here
# Event constants
class Event:
RIGHT = "ev_RightButtonPressed"
LEFT = "ev_LeftButtonPressed"
# add more events here
class StateMachine:
"""
This state machine handles transitions and LED control.
It owns the LED controller as a part of its internal state.
"""
def __init__(self):
self.state = State.INIT
self.led = LED()
def process(self, event):
"""
Handle state transitions and actions.
The event can come from IR, sensor, etc.
"""
if self.state == State.INIT:
self.state = State.IDLE
print(">>> INIT β IDLE")
# init state entry
self.led.on('red')
elif self.state == State.IDLE:
if event == Event.RIGHT:
self.state = State.ON
print(">>> IDLE β ON")
# actual state exit
self.led.off()
# next state entry
self.led.on('green')
elif self.state == State.ON:
if event == Event.LEFT:
self.state = State.IDLE
print(">>> ON β IDLE")
# actual state exit
self.led.off()
# next state entry
self.led.on('red')
# add further state here
# This branch should normally never be reached.
# It serves as a fallback for error handling in case
# an invalid or unexpected state occurs.
else:
self.state = State.IDLE
Let's break down a bit.
Constants
State Constants
-
Represents all possible modes (states) the system can be in.
Event Constants -
Represents external events that trigger transitions.
- These come from user inputs (e.g., IR remote, buttons).
Why do we assign strings or numbers?
The actual value (
0,"ev_...", etc.) doesnβt matter much. The key is using well-named constants to improve code clarity and avoid bugs from mistyped strings or reused numbers.
process(event) β Core Behavior Logic
The process() method is the heart of the state machine. It checks the current state, looks at the incoming event, and performs:
- Transitions to the next state
- Exit actions for the current state
- Entry actions for the next state
Letβs walk through it:
-
On the very first call, the machine is in
INIT. It transitions toIDLEand performs the entry action of that state (red LED on, green off). -
Once in
IDLE, if it receivesEvent.RIGHT, it transitions toON:- Exits the
IDLEstate (turns red LED off) - Enters the
ONstate (turns green LED on)
- Exits the
-
In
ON, if it receivesEvent.LEFT, it goes back toIDLE, reversing the LED colors.
Each transition typically includes:
- Exit actions from the current state
- State change
- Entry actions for the new state
This separation helps structure complex logic into manageable pieces.
Transitions happen at the same time as entering and exiting states β this is where your behavior should be defined.
How to Use Classes (in this context)
This StateMachine is a class, which means:
- It stores state (
self.state) and data (self.led) - It encapsulates behavior in methods like
process() - Itβs reusable and extendable β you can use it in different projects or tests
event.py
The event module is responsible for generating and handling system events. In this initial stage, our focus is solely on integrating the IR remote module.
Since the IR remote operates through polling, we periodically check its state to detect key press events. When a key press is detected, the module returns the corresponding event as defined in the sm (state machine) module.
For easier debugging and testing, each detected event is printed to the debug console.
from pico_car import ir
from sm import Event
class EventGenerator:
"""
Handles all event generation from input devices (currently IR remote).
More sources can be added later (GPIO, Bluetooth, etc.)
"""
def __init__(self):
self.ir = ir() # IR receiver object
self.last_ir_code = None
# Mapping IR codes to events
self.ir_event_map = {
6: Event.RIGHT,
4: Event.LEFT
}
def get_event(self):
# First check IR
event = self._check_ir()
if event:
print(f"Event: {event}")
return event
# Then check other sources for example:
# event = self._check_gpio()
# if event:
# return event
return None
def _check_ir(self):
ir_code = self.ir.Getir()
if ir_code is None:
self.last_ir_code = None
return None
if ir_code == self.last_ir_code:
return None # Button held down β skip
self.last_ir_code = ir_code
return self.ir_event_map.get(ir_code)
led.py
The LED module provides a class for controlling addressable LED strip. It encapsulates all functionality needed to manage and update the LED states. Please feel free to modify it.
from machine import Pin
import neopixel
import time
# Define basic color names
NAMED_COLORS = {
'red': (255, 0, 0),
'green': (0, 255, 0),
'blue': (0, 0, 255),
'yellow': (255, 255, 0),
'white': (255, 255, 255),
'off': (0, 0, 0)
}
class LED:
"""
NeoPixel LED controller with support for:
- setting color only (color())
- turning on with last or new color (on())
- blinking current color (blink())
"""
def __init__(self, pin_num=6, count=8):
self.strip = neopixel.NeoPixel(Pin(pin_num), count)
self.count = count
self._color = NAMED_COLORS['red']
self._state = False
self._last_time = time.ticks_ms()
def color(self, name):
"""Set the internal color, does not light up the LED yet"""
if name in NAMED_COLORS:
self._color = NAMED_COLORS[name]
else:
raise ValueError(f"Unknown color name: {name}")
def on(self, name=None):
"""
Turn on the LED.
If a color name is given, set and use it.
Otherwise, use the previously stored color.
"""
if name:
self.color(name)
for i in range(self.count):
self.strip[i] = self._color
self.strip.write()
def off(self):
"""Turn all LEDs off"""
for i in range(self.count):
self.strip[i] = (0, 0, 0)
self.strip.write()
def blink(self, interval_ms):
"""Non-blocking blink of the current color"""
now = time.ticks_ms()
if time.ticks_diff(now, self._last_time) > interval_ms:
self._last_time = now
self._state = not self._state
if self._state:
self.on()
else:
self.off()
It also implements a non-blocking blink method. This is useful because it allows the method to be called at any timeβthe LED state will only change when the specified interval has passed. This approach supports non-blocking function calls, enabling smoother multitasking within the application.
Extending the Example: Add a Blinking State
Letβs extend our LED example by adding a blinking state. This introduces a new behavior: the LED should toggle on and off at a timed interval while in the LED_BLINK state.
Weβll now walk through how to design and implement this extended FSM step by step, starting with:
- Updating the state diagram
- Adding a timer or using
utime.ticks_ms()to handle blinking
Professional Context: Industrial & Automotive State Machines
Your simple if-else state machine works for LED control. Professional systems use sophisticated state machine architectures for complex behavior, safety certification, and formal verification. Here's how they compare:
State Machine Comparison
| Feature | Simple FSM (yours) | UML Statecharts | AUTOSAR BSW | Safety-Critical |
|---|---|---|---|---|
| States | Flat list | Hierarchical (nested) | Mode management | Formally specified |
| Transitions | Simple if-else | Guards, actions, events | Standardized API | Verified exhaustive |
| History | None | Deep/shallow history | State persistence | Deterministic recovery |
| Concurrency | None | Orthogonal regions | Task-based | Proven non-interference |
| Verification | Manual testing | Model simulation | Configuration tools | Model checking (SPIN, UPPAAL) |
| Code generation | Manual | UML tools | AUTOSAR generators | Certified code gen |
| Documentation | Comments | UML diagrams | ARXML | Formal specs + traceability |
| Certification | None | Optional | ISO 26262 | DO-178C, IEC 61508 |
Hierarchical State Machines (Statecharts)
Your flat FSM doesn't scale. With 10 states and 5 events, you might have 50 transitions to manage. Statecharts solve this:
Your FSM (flat):
IDLE βββΊ RUNNING βββΊ ERROR βββΊ IDLE
IDLE βββΊ PAUSED βββΊ RUNNING
RUNNING βββΊ PAUSED
... many more transitions
UML Statecharts (hierarchical):
ββββββββββββββββββββ OPERATIONAL ββββββββββββββββββββ
β βββββββββββ βββββββββββ βββββββββββ β
β β IDLE βββββββΊβ RUNNING βββββββΊβ PAUSED β β
β βββββββββββ βββββββββββ βββββββββββ β
β β² β β
β βββββββββββββββββββββββββββ β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β error event
βΌ
βββββββββββββββ
β ERROR β
βββββββββββββββ
Key features:
- OPERATIONAL is a superstate containing IDLE, RUNNING, PAUSED
- Error event from ANY substate β ERROR (one transition, not three)
- History: return to RUNNING or PAUSED after error recovery
- Orthogonal regions: motor control || sensor reading (concurrent)
AUTOSAR State Management
Automotive software uses standardized state management:
Your robot:
state = IDLE
if event == START:
state = RUNNING
AUTOSAR Basic Software (BSW):
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ECU State Manager β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β BswM (Basic Software Mode Manager) β
β βββ Controls mode transitions across all modules β
β β
β ComM (Communication Manager) β
β βββ COMM_NO_COM β COMM_SILENT β COMM_FULL_COM β
β β
β EcuM (ECU Manager) β
β βββ STARTUP β RUN β SLEEP β SHUTDOWN β
β β
β All transitions: β
β - Defined in ARXML configuration β
β - Generated code (no manual implementation) β
β - Standardized across all automotive suppliers β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Formal Verification
For safety-critical systems, "it seems to work" isn't enough:
Your robot:
Test cases: "Press button, LED turns on" β
Coverage: Maybe 60-80% of code paths
Proof: None
Formally verified FSM:
Model checker input:
States: {IDLE, ARMED, FIRING}
Events: {arm, fire, safe}
Properties to verify:
- No FIRING without ARMED first (safety)
- Always possible to reach IDLE (liveness)
- No deadlock (progress)
SPIN/UPPAAL output:
β Property 1: VERIFIED (all 847 states checked)
β Property 2: VERIFIED
β Property 3: VERIFIED
β PROVEN CORRECT for all possible executions
Result: Mathematical proof, not just testing
Industrial PLC: Grafcet/SFC
Factory automation uses IEC 61131-3 Sequential Function Chart:
Your Python FSM:
if state == FILL:
if level > 80:
state = HEAT
IEC 61131-3 SFC (Grafcet):
βββββββ
β 1 β INIT
ββββ¬βββ
β start_button
ββββΌβββ
β 2 β FILL βββ action: open_valve
ββββ¬βββ
β level > 80%
ββββΌβββ
β 3 β HEAT βββ action: heater_on
ββββ¬βββ
β temp > 70Β°C
ββββΌβββ
β 4 β MIX ββββ action: mixer_on, timer(60s)
ββββ¬βββ
β timer_done
ββββΌβββ
β 5 β DRAIN ββ action: drain_valve
ββββ¬βββ
β level < 5%
βββββΊ back to step 1
Features:
- Graphical programming (visual, not code)
- Parallel branches (simultaneous operations)
- Standardized across all PLC vendors
- Directly maps to ladder logic / function blocks
State Machine Code Generation
Professional tools generate code from models:
Manual coding (your approach):
1. Draw state diagram
2. Write Python code
3. Test manually
4. Hope diagram matches code
Model-based development:
1. Draw state diagram in tool (Enterprise Architect, Rhapsody, etc.)
2. Tool generates code automatically
3. Code is GUARANTEED to match model
4. Changes to model β regenerate code
Example tools:
- IBM Rhapsody β C/C++ code
- Mathworks Stateflow β C code
- Yakindu β C, C++, Java
- QM (Quantum Modeling) β C/C++ for embedded
Safety-Critical State Machines
For systems where bugs = injuries:
Your robot:
Bug in state machine β robot behaves weird
Consequence: Annoyance
Aircraft flight control:
Bug in state machine β control surface wrong position
Consequence: Crash, fatalities
Requirements (DO-178C Level A):
- Every state transition documented
- Every transition tested
- 100% code coverage (MC/DC)
- Independent verification
- Formal methods often required
- Traceability: requirement β design β code β test
Automotive (ISO 26262 ASIL-D):
- Similar rigor for steering, braking
- WCET analysis for state handlers
- Redundant state machines cross-checking
- Safe state defined and reachable from any state
What the Industry Uses
| Manufacturer | Product | Application |
|---|---|---|
| IBM | Rhapsody | UML statecharts, code generation |
| MathWorks | Stateflow | Simulink-integrated state machines |
| Quantum Leaps | QP Framework | Lightweight embedded state machines |
| Vector | DaVinci | AUTOSAR state machine configuration |
| ANSYS | SCADE | Safety-critical certified code gen |
| Siemens | TIA Portal | PLC SFC/Grafcet programming |
| itemis | YAKINDU | Open-source statechart tools |
Hardware Limits Principle
What Software Can and Cannot Fix
Software CAN improve: - Code organization β state machine pattern vs if-else spaghetti - Readability β named states, events, clear transitions - Maintainability β modular design, separation of concerns - Testing β well-defined states easier to test - Debugging β state logging, transition tracing
Software CANNOT fix: - No formal verification β model checkers need mathematical models - No certification evidence β safety standards need tool qualification - Real-time guarantees β your Python GC can pause state processing - Concurrent state correctness β need proper synchronization primitives - Exhaustive testing β only formal methods prove all paths - Hardware failures β state machine can't recover from dead MCU
The lesson: A well-structured state machine makes your code maintainable and debuggable. But for safety-critical systems requiring formal proof of correctness, you need specialized tools (model checkers), certified code generators, and often different languages (SPARK Ada, formal C subsets). The architecture is similarβstates, events, transitionsβbut the tooling and rigor are worlds apart.
Real Example: Complexity Growth
| States | Events | Your FSM | Statecharts | Certified |
|---|---|---|---|---|
| 3 | 2 | Easy | Overkill | Overkill |
| 10 | 5 | Manageable | Recommended | Formal model |
| 50 | 20 | Unmaintainable | Required | Model checking |
| 200+ | 50+ | Impossible | Essential | Full formal verification |
Your 3-state LED controller is fine with if-else. A car's drive mode selector with 50+ states (Park, Reverse, Neutral, Drive, Sport, Eco, Snow, Tow, Launch, Limp, Error variants...) needs proper statechart tools. A flight control system needs mathematically proven state machines.
Further Reading
- Interrupts - Event-driven programming basics
- Architectures - System design patterns
- UML State Machines - Official UML specification
- Quantum Leaps QP - Embedded state machine framework
- Stateflow Documentation - Industry-standard modeling