Skip to content

Session 2: State Machines in Async Embedded Systems

Synopsis

Shows how explicit state machines improve clarity in event-driven device workflows such as connectivity management, user interaction, and control logic.

Session Content

Session 2: State Machines in Async Embedded Systems

Duration: ~45 minutes
Audience: Python developers with basic programming knowledge learning MicroPython on Raspberry Pi Pico 2 W
Tools: Thonny IDE, Raspberry Pi Pico 2 W, MicroPython firmware


Session Goals

By the end of this session, learners will be able to:

  • Explain why state machines are useful in embedded systems
  • Model embedded behavior using explicit states and transitions
  • Implement a simple non-blocking state machine in MicroPython
  • Combine state machines with uasyncio for responsive asynchronous behavior
  • Build a practical LED/button control system using states

Prerequisites

  • Raspberry Pi Pico 2 W flashed with MicroPython
  • Thonny installed on a computer
  • Basic familiarity with Python syntax
  • Basic wiring skills
  • Components:
  • 1 × Raspberry Pi Pico 2 W
  • 1 × LED
  • 1 × 220Ω resistor
  • 1 × pushbutton
  • 1 × breadboard
  • Jumper wires

Development Environment Setup

1. Install Thonny

  • Download and install Thonny from: https://thonny.org
  • Open Thonny

2. Connect Pico 2 W

  • Hold the BOOTSEL button while connecting the Pico to USB
  • It appears as a USB mass storage device
  • Flash the latest MicroPython firmware for Pico 2 W

3. Configure Thonny

  • Open Tools > Options > Interpreter
  • Select:
  • MicroPython (Raspberry Pi Pico)
  • Choose the correct serial port

4. Verify Connection

Run in the Thonny shell:

import machine
print(machine.freq())

Example output:

150000000

Session Outline

  1. Theory: What is a state machine?
  2. State machines in embedded systems
  3. Transition design patterns
  4. Hands-on exercise 1: Button-controlled LED state machine
  5. Hands-on exercise 2: Async blinking patterns with uasyncio
  6. Wrap-up and review

1) Theory: What Is a State Machine?

A state machine is a design pattern where a system behaves differently depending on its current state.

Core concepts

  • State: the current mode of the system
  • Event: something that causes a change, such as a button press or timer expiry
  • Transition: movement from one state to another
  • Action: what the system does while in a state or during transition

Example in daily life

A traffic light changes between: - Green - Yellow - Red

Each change happens based on timing and follows defined transitions.


2) Why State Machines Matter in Embedded Systems

Embedded devices often need to: - react to button presses - manage LEDs, sensors, and actuators - avoid blocking delays - remain responsive to asynchronous events

Benefits

  • Easier to understand than deeply nested if statements
  • More maintainable
  • Predictable behavior
  • Works well with event-driven and async programming

Common embedded examples

  • Button debounce state machine
  • Device connection state machine
  • Sensor sampling state machine
  • Menu/navigation systems
  • IoT device provisioning workflows

3) State Machines and Async Programming

In async embedded systems, state machines help avoid blocking calls like time.sleep().

Instead of pausing the whole program, the system: - checks inputs periodically - updates state based on conditions - lets other tasks run in parallel

Blocking vs non-blocking

Blocking:

import time
time.sleep(2)

This stops everything else.

Non-blocking with state logic: - record the current time - compare elapsed time - change state when needed - continue other tasks in the meantime


4) Designing a Simple Embedded State Machine

For a button and LED system, define states such as:

  • OFF
  • ON
  • BLINKING

Transition example

  • Button press in OFFON
  • Button press in ONBLINKING
  • Button press in BLINKINGOFF

This creates a predictable cycle of behavior.


Hands-on Exercise 1: Button-Controlled LED State Machine

Objective

Build a simple state machine that cycles through LED modes using a button.

Wiring

LED

  • LED anode (+) → GP15 through 220Ω resistor
  • LED cathode (−) → GND

Button

  • One side of button → GP14
  • Other side of button → GND

We will use the internal pull-up resistor, so the pin reads: - 1 when unpressed - 0 when pressed


Code: main.py

from machine import Pin
import time

# -----------------------------
# Hardware setup
# -----------------------------
led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)

# -----------------------------
# State definitions
# -----------------------------
STATE_OFF = 0
STATE_ON = 1
STATE_BLINKING = 2

state = STATE_OFF

# Timing for blink mode
blink_interval_ms = 500
last_blink_ms = time.ticks_ms()

# Button debounce tracking
last_button_state = 1
last_debounce_ms = time.ticks_ms()
debounce_delay_ms = 50

# -----------------------------
# Helper functions
# -----------------------------
def set_led_for_state(current_state):
    """Set the LED output for the current state."""
    if current_state == STATE_OFF:
        led.off()
    elif current_state == STATE_ON:
        led.on()

def next_state(current_state):
    """Return the next state in the cycle."""
    if current_state == STATE_OFF:
        return STATE_ON
    elif current_state == STATE_ON:
        return STATE_BLINKING
    else:
        return STATE_OFF

# -----------------------------
# Main loop
# -----------------------------
print("Button-controlled state machine started")

while True:
    current_button_state = button.value()
    now = time.ticks_ms()

    # Detect button edge with debounce
    if current_button_state != last_button_state:
        last_debounce_ms = now

    if time.ticks_diff(now, last_debounce_ms) > debounce_delay_ms:
        # Button pressed (active low)
        if last_button_state == 1 and current_button_state == 0:
            state = next_state(state)
            print("State changed to:", state)

            # Apply immediate effect for non-blink states
            if state in (STATE_OFF, STATE_ON):
                set_led_for_state(state)
            else:
                # Reset blink timer when entering blink mode
                last_blink_ms = now

    last_button_state = current_button_state

    # State behavior
    if state == STATE_BLINKING:
        if time.ticks_diff(now, last_blink_ms) >= blink_interval_ms:
            led.value(not led.value())
            last_blink_ms = now

    time.sleep_ms(10)

Expected Behavior

  • First button press: LED turns on
  • Second button press: LED starts blinking
  • Third button press: LED turns off
  • Cycle repeats

Example output in Thonny Shell

Button-controlled state machine started
State changed to: 1
State changed to: 2
State changed to: 0

Exercise Tasks

  1. Change the blink interval to 200 ms and observe the difference
  2. Add a fourth state: fast blinking
  3. Modify the LED behavior so ON becomes a dimming effect using PWM
  4. Add a second LED to show the current state with a color or pattern

5) From Polling to Async State Machines

A state machine can also be controlled by async tasks.

This is useful when: - one task reads a button - another task manages LED patterns - another task handles Wi-Fi or network communication

Each task runs cooperatively using uasyncio.

Why this matters

  • Better responsiveness
  • Cleaner separation of concerns
  • Easier to add networking and sensors later

Hands-on Exercise 2: Async LED State Machine

Objective

Use uasyncio to manage button input and LED state changes without blocking.


Code: main.py

import uasyncio as asyncio
from machine import Pin
import time

# -----------------------------
# Hardware setup
# -----------------------------
led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_UP)

# -----------------------------
# State definitions
# -----------------------------
STATE_OFF = 0
STATE_ON = 1
STATE_BLINKING = 2

state = STATE_OFF

# Timing
blink_interval_ms = 500
debounce_delay_ms = 50

# Button tracking
last_button_value = 1
last_button_change_ms = time.ticks_ms()

# -----------------------------
# State helper
# -----------------------------
def next_state(current_state):
    if current_state == STATE_OFF:
        return STATE_ON
    elif current_state == STATE_ON:
        return STATE_BLINKING
    return STATE_OFF

def apply_state(current_state):
    if current_state == STATE_OFF:
        led.off()
    elif current_state == STATE_ON:
        led.on()

# -----------------------------
# Async tasks
# -----------------------------
async def button_task():
    global state, last_button_value, last_button_change_ms

    while True:
        now = time.ticks_ms()
        current_value = button.value()

        if current_value != last_button_value:
            last_button_change_ms = now

        if time.ticks_diff(now, last_button_change_ms) > debounce_delay_ms:
            # Detect a valid press event
            if last_button_value == 1 and current_value == 0:
                state = next_state(state)
                print("State changed to:", state)

                if state in (STATE_OFF, STATE_ON):
                    apply_state(state)

        last_button_value = current_value
        await asyncio.sleep_ms(10)

async def blink_task():
    global state

    while True:
        if state == STATE_BLINKING:
            led.toggle()
            await asyncio.sleep_ms(blink_interval_ms)
        else:
            await asyncio.sleep_ms(50)

# -----------------------------
# Main coroutine
# -----------------------------
async def main():
    print("Async state machine started")
    apply_state(state)
    await asyncio.gather(
        button_task(),
        blink_task()
    )

# Run the event loop
asyncio.run(main())

Expected Behavior

  • Button press cycles through states
  • Blink task only acts in blinking state
  • Button remains responsive while blinking

Example output

Async state machine started
State changed to: 1
State changed to: 2
State changed to: 0

6) Key Design Patterns for Embedded State Machines

1. Explicit states

Use named constants instead of magic numbers.

2. Small state handlers

Keep logic in short, clear functions.

3. Non-blocking timing

Use ticks_ms() and ticks_diff() instead of sleep() for timing logic.

4. Debouncing

Always debounce mechanical buttons to avoid false multiple presses.

5. Separation of concerns

Keep input handling, state transitions, and output control separate.


7) Common Mistakes

Mistake: using long blocking delays

This makes the device unresponsive.

Mistake: mixing state change logic with hardware output everywhere

This makes code hard to debug.

Mistake: forgetting debounce

A single button press may register multiple times.

Mistake: using shared mutable variables carelessly

In async code, keep shared state simple and well-defined.


8) Hands-on Challenge: Add a Sensor-Driven State

Objective

Extend the state machine with a sensor input.

Idea

Use a light sensor, temperature sensor, or potentiometer to control one of the states.

Example state flow: - OFF - ON - BLINKING - ALARM

If a threshold is exceeded, switch to ALARM and flash rapidly.

Suggested extension pseudocode

  • Read sensor value every 100 ms
  • If value crosses threshold:
  • change to ALARM
  • In ALARM:
  • blink LED rapidly
  • wait for button press to clear alarm

9) Review Questions

  1. What is the purpose of a state machine?
  2. Why are state machines useful in embedded systems?
  3. How does debouncing help with buttons?
  4. Why is uasyncio useful on the Pico 2 W?
  5. What is the difference between blocking and non-blocking timing?
  6. How would you add a sensor input to the state machine?

10) Summary

In this session, you learned how to:

  • design a state machine for embedded behavior
  • represent system modes as explicit states
  • handle button input with debounce logic
  • implement non-blocking LED control
  • use uasyncio for responsive async state management

11) Further Practice

  • Add a second button to move backward through states
  • Create a menu system with 4–5 states
  • Add Wi-Fi status as a state indicator
  • Combine sensor readings with state transitions
  • Log state transitions to the Thonny console for debugging

12) Suggested File for Thonny

Save the main program as:

main.py

This ensures the Pico runs the program automatically after reset.


13) Mini Project

Project: Smart Device Mode Controller

Build a device with: - button cycling modes - LED indicating the current mode - optional sensor-triggered alert mode - optional Wi-Fi connection state

Example modes

  • Idle
  • Active
  • Alert
  • Network Sync

14) Takeaway Code Pattern

if state == STATE_IDLE:
    do_idle()
elif state == STATE_ACTIVE:
    do_active()
elif state == STATE_ALERT:
    do_alert()

This pattern scales well as your embedded system grows.


Back to Chapter | Back to Master Plan | Previous Session | Next Session