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
uasynciofor 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
- Theory: What is a state machine?
- State machines in embedded systems
- Transition design patterns
- Hands-on exercise 1: Button-controlled LED state machine
- Hands-on exercise 2: Async blinking patterns with
uasyncio - 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
ifstatements - 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:
OFFONBLINKING
Transition example
- Button press in
OFF→ON - Button press in
ON→BLINKING - Button press in
BLINKING→OFF
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
- Change the blink interval to 200 ms and observe the difference
- Add a fourth state: fast blinking
- Modify the LED behavior so
ONbecomes a dimming effect using PWM - 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
- What is the purpose of a state machine?
- Why are state machines useful in embedded systems?
- How does debouncing help with buttons?
- Why is
uasynciouseful on the Pico 2 W? - What is the difference between blocking and non-blocking timing?
- 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
uasynciofor 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