Lecture 04: Interactive Guide
"From Spaghetti to Architecture"
Week 7 | Labs completed: Line Following, IMU Turns Next lab: State Machines
Equipment: - Robot connected to projector - Saleae logic analyzer (optional — for interrupt demo) - REPL terminal open - Students' lap time competition results (if available) - Wokwi tab open (ESP32 state machine example)
Time: ~80 minutes
Block 1: Lab Debrief — What Did You Build? (15 min)
Theme: "You have a line follower and a turn controller. Now try to combine them."
Competition Results (3 min)
Show the leaderboard from the line-following Fastest Lap competition.
Ask the winner: "What was your Kp? Your speed? Show us your error plot."
Compare best plot vs worst plot — the difference between smooth tracking and oscillation.
Live Demo: The Growing Problem (5 min)
Do: Start with the basic line follower running on the oval track:
Works great. Now let's add features live in REPL:
"OK, now I want it to stop at junctions. Let me add that..."
while True:
if robot.sensors.line.at_junction():
robot.stop()
time.sleep(1)
robot.forward(60, 0.3) # Drive through junction
else:
robot.line_follow_step(80, 30)
time.sleep(0.02)
"Now I also want it to avoid obstacles..."
while True:
distance = robot.read_distance() # BLOCKING!
if distance < 15:
robot.stop()
robot.backward(60, 0.5)
robot.turn_right(80, 0.4)
elif robot.sensors.line.at_junction():
robot.stop()
time.sleep(1)
robot.forward(60, 0.3)
else:
robot.line_follow_step(80, 30)
time.sleep(0.02)
Ask: "What's wrong with this code?"
Let students identify problems:
- read_distance() blocks for 25ms every loop — control loop is now 20 Hz instead of 50 Hz
- The if/elif chain is getting long — what about left turns? U-turns? Battery warning?
- What if we need different speeds in different situations?
- What if the obstacle appears DURING a junction crossing?
"This is how every robot project starts. And this is how it becomes spaghetti code."
The Flag Explosion (5 min)
Show: The flag explosion animation (flag-explosion.gif).
Do: Add boolean flags live:
Ask: "How many combinations of 5 booleans? What's the maximum?"
→ 2^5 = 32 combinations. Most are meaningless (you can't be at a junction AND avoiding an obstacle AND turning). But the code has to handle ALL of them.
Show: Code that tries to handle this with nested if — it's immediately unreadable.
"There has to be a better way. And there is — state machines."
Prediction (2 min)
Ask: "What if the robot could only be in ONE mode at a time? FOLLOW, or OBSTACLE, or JUNCTION, or TURNING — never two at once. Would that simplify things?"
→ Yes! That's exactly what a state machine does.
Block 2: State Machines — The Solution (20 min)
Theme: "One state at a time, clear transitions, clean code."
The Concept (3 min)
Draw on board (or show slide):
FOLLOW ──(junction)──→ AT_JUNCTION ──(timer 1s)──→ FOLLOW
| |
(obstacle < 15cm) (obstacle < 15cm)
| |
v v
OBSTACLE ──(distance > 30cm)──→ FOLLOW
"Each bubble is a STATE. Each arrow is a TRANSITION. The robot can only be in ONE state at a time."
Ask: "In the OBSTACLE state, does the robot need to check for junctions?"
→ No! That's the power — each state only handles its own behavior.
Live Demo: State Machine on the Robot (7 min)
Do: Implement the state machine live in REPL:
from picobot import Robot, StateMachine, Timer
robot = Robot()
robot.imu.calibrate()
sm = StateMachine(robot)
sm.add_state("FOLLOW",
action=lambda: robot.line_follow_step(80, 30),
on_enter=lambda: robot.set_leds((0, 255, 0)))
sm.add_state("JUNCTION",
action=robot.stop,
on_enter=lambda: (robot.set_leds((0, 0, 255)),
robot.beep(880, 100)))
sm.add_state("OBSTACLE",
action=lambda: robot.set_motors(-60, -60),
on_enter=lambda: robot.set_leds((255, 0, 0)))
sm.add_transition("FOLLOW", "JUNCTION",
condition=robot.at_junction)
sm.add_transition("JUNCTION", "FOLLOW",
condition=lambda: sm.time_in_state() > 1000)
sm.add_transition("FOLLOW", "OBSTACLE",
condition=lambda: robot.read_distance() < 15)
sm.add_transition("OBSTACLE", "FOLLOW",
condition=lambda: sm.time_in_state() > 500)
sm.run()
Run it. Students see: - Green LEDs = following - Blue + beep = junction detected - Red = backing away from obstacle
Ask: "How many lines of behavior code? How many if statements?"
→ Zero if statements in the behavior code! The state machine handles all the logic.
Debug Challenge: Broken State Machine (5 min)
Show code with a bug:
sm.add_state("FOLLOW",
action=lambda: robot.line_follow_step(80, 30))
sm.add_state("OBSTACLE",
action=lambda: robot.set_motors(-60, -60))
sm.add_transition("FOLLOW", "OBSTACLE",
condition=lambda: robot.read_distance() < 15)
# Bug: no transition back from OBSTACLE!
Ask: "What happens when this runs and the robot sees an obstacle?"
→ It backs up forever. No transition from OBSTACLE back to FOLLOW. The robot never recovers.
"State machines must be COMPLETE — every state needs at least one way out."
Transition Table Exercise (3 min)
Ask students to fill in:
| Current State | Condition | Next State |
|---|---|---|
| FOLLOW | junction detected | ? |
| FOLLOW | obstacle < 15cm | ? |
| JUNCTION | timer > 1s | ? |
| OBSTACLE | timer > 0.5s | ? |
| OBSTACLE | distance > 30cm | ? |
| ? | line lost for > 2s | ? |
Let them think about the last row — "What state should handle 'line lost'? Is it a new state or a transition from FOLLOW?"
State Machines in the Real World (2 min)
Quick examples: - Traffic light: GREEN → YELLOW → RED → GREEN (timed transitions) - Elevator: IDLE → MOVING_UP → DOOR_OPEN → MOVING_DOWN (event transitions) - Vending machine: IDLE → ACCEPTING_COINS → DISPENSING → CHANGE (input transitions) - Your phone: LOCKED → UNLOCKED → APP → LOCKED (gesture/timer transitions) - Automotive ECU: IGNITION → CRANKING → RUNNING → IDLE → SHUTDOWN
"Every embedded system with multiple behaviors uses state machines."
Block 3: Timing, Scheduling & Interrupts (20 min)
Theme: "Your state machine runs in a loop. How fast? What if something is slow?"
Timing Budget (5 min)
Show the timing budget slide (moved from L3):
"Your control loop runs at 50 Hz = 20 ms per iteration. Let's budget:"
| Operation | Time | Notes |
|---|---|---|
| Line sensors (GPIO) | 0.01 ms | 4 pin reads — instant |
| P-control math | 0.01 ms | One multiplication |
| Motor PWM update | 0.05 ms | Register write |
| State machine logic | 0.1 ms | Check transitions |
| Total | 0.17 ms | |
| Budget | 20 ms | |
| Free time | 19.83 ms | 99% idle! |
Ask: "So we have tons of free time. What could go wrong?"
→ Students should remember: read_distance() = 25 ms, oled.show() = 10 ms, print() = 5 ms
"ONE blocking call destroys your entire budget."
Round-Robin Scheduling (5 min)
Show the round-robin pattern:
last_control = time.ticks_ms()
last_distance = time.ticks_ms()
last_display = time.ticks_ms()
while True:
now = time.ticks_ms()
if time.ticks_diff(now, last_control) >= 20: # 50 Hz
last_control = now
state_machine.step() # Line following + state logic
if time.ticks_diff(now, last_distance) >= 200: # 5 Hz
last_distance = now
cached_distance = robot.read_distance() # 25ms but only 5x/sec
if time.ticks_diff(now, last_display) >= 500: # 2 Hz
last_display = now
oled.text(f"State: {sm.state}", 0, 0)
oled.show() # 10ms but only 2x/sec
Ask: "What's the worst-case loop time now?"
→ When ALL three tasks fire simultaneously: 0.17 + 25 + 10 = ~35 ms. But this only happens every 1 second (LCM of 20, 200, 500 ms). Most loops are 0.17 ms.
"This is cooperative scheduling — tasks share the CPU voluntarily. It works until one task hogs the CPU."
Interrupts: When Polling Isn't Enough (5 min)
Live Demo (if Saleae available):
Show the ultrasonic sensor's echo pin on Saleae. Send a trigger pulse, watch the echo come back.
"Your code calls read_distance() and WAITS for this echo. The CPU does NOTHING for 25 ms."
Show the interrupt alternative:
from machine import Pin
import time
echo_start = 0
echo_end = 0
distance = 100
def echo_handler(pin):
global echo_start, echo_end, distance
if pin.value() == 1:
echo_start = time.ticks_us() # Rising edge: echo started
else:
echo_end = time.ticks_us() # Falling edge: echo ended
pulse = time.ticks_diff(echo_end, echo_start)
if 100 < pulse < 25000:
distance = pulse / 58.0 # Convert to cm
echo_pin = Pin(15, Pin.IN)
echo_pin.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=echo_handler)
Ask: "What's the CPU doing while waiting for the echo?"
→ With polling: NOTHING (blocked). With interrupt: RUNNING YOUR CONTROL LOOP. The hardware notifies the CPU when the echo arrives.
"This is what you'll build in the State Machines lab Part 7."
ISR Rules — Why You Can't Do Everything (3 min)
Show this broken ISR:
def bad_isr(pin):
print("Echo received!") # UART is slow — 5ms
distance = pulse / 58.0 # Float math — OK
oled.text(f"D:{distance}", 0, 0) # I2C — BLOCKS!
oled.show() # 10ms of I2C — DISASTER
data.append(distance) # Memory allocation — may trigger GC
Ask: "What's wrong with this ISR?"
Let students identify each problem. Then show the correct pattern:
def good_isr(pin):
global echo_time, echo_flag
echo_time = time.ticks_us() # Just capture timestamp
echo_flag = True # Set flag for main loop
# Main loop handles the heavy work:
if echo_flag:
echo_flag = False
distance = process_echo(echo_time)
print(f"Distance: {distance}") # Safe here — not in ISR
"ISR = capture data and get out. Main loop = process data."
Quick Calculation: Interrupt Latency (2 min)
Ask: "The echo arrives. How long until the ISR runs?"
| Platform | Latency | Why |
|---|---|---|
| Bare metal C | ~100 ns | Direct hardware vector |
| C with RTOS | ~1-10 µs | Context switch overhead |
| MicroPython | ~100-1000 µs | Bytecode interpreter + GC |
"MicroPython's interrupt latency is ~1000× slower than C. For a 25ms echo, that's fine (0.1% error). For a 10 kHz encoder... MicroPython can't keep up."
Block 4: C SDK — What Professionals Use (15 min)
Theme: "MicroPython is great for learning. Here's what the real world looks like."
Same Task, Three Languages (5 min)
Show side by side — "Blink an LED at 1 Hz":
MicroPython:
from machine import Pin
import time
led = Pin(25, Pin.OUT)
while True:
led.toggle()
time.sleep_ms(500)
C SDK:
#include "pico/stdlib.h"
int main() {
gpio_init(25);
gpio_set_dir(25, GPIO_OUT);
while (true) {
gpio_put(25, !gpio_get(25));
sleep_ms(500);
}
}
Register level:
#include <stdint.h>
#define GPIO_OUT_SET (*(volatile uint32_t*)0xd0000014)
#define GPIO_OUT_CLR (*(volatile uint32_t*)0xd0000018)
int main() {
// ... init code ...
while (1) {
GPIO_OUT_SET = (1 << 25); // LED ON
for (volatile int i = 0; i < 750000; i++);
GPIO_OUT_CLR = (1 << 25); // LED OFF
for (volatile int i = 0; i < 750000; i++);
}
}
Ask: "Which would you use for a production medical device? Why?"
→ C or registers. Predictable timing, no garbage collector, certified compilers, formal verification possible.
Timer Interrupt in C (3 min)
Show: A timer interrupt that fires every 1ms — impossible to do reliably in MicroPython:
#include "pico/stdlib.h"
#include "hardware/timer.h"
volatile uint32_t tick_count = 0;
bool timer_callback(repeating_timer_t *t) {
tick_count++; // Increment every 1ms
if (tick_count % 20 == 0) { // Every 20ms
// Read sensors, run P-control — guaranteed 50 Hz
}
return true;
}
int main() {
repeating_timer_t timer;
add_repeating_timer_ms(1, timer_callback, NULL, &timer);
while (true) {
// Main loop: display updates, logging, non-time-critical work
tight_loop_contents();
}
}
"In C, the timer interrupt is GUARANTEED to fire every 1ms. In MicroPython, ticks_ms() polling is 'best effort' — the garbage collector can pause for milliseconds."
RTOS Preview (3 min)
Show: FreeRTOS task structure:
// Task 1: Motor control (highest priority, 1ms period)
void motor_task(void *params) {
while (1) {
read_encoders();
compute_pid();
set_pwm();
vTaskDelay(pdMS_TO_TICKS(1)); // Yield for exactly 1ms
}
}
// Task 2: Sensor fusion (medium priority, 10ms period)
void sensor_task(void *params) {
while (1) {
read_imu();
read_ultrasonic();
update_position();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// Task 3: Display (low priority, 100ms period)
void display_task(void *params) {
while (1) {
update_oled();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
Ask: "What happens if the motor task takes 2ms instead of 1ms?"
→ The RTOS preempts the sensor task to run the motor task. Priorities are enforced by hardware timer interrupts.
"Your MicroPython round-robin is cooperative — tasks must voluntarily yield. An RTOS is preemptive — the scheduler can interrupt any task to run a higher-priority one."
Why Learn Both (2 min)
| MicroPython | C / RTOS | |
|---|---|---|
| Learning speed | Fast — REPL, no compilation | Slow — compile, flash, debug |
| Debugging | print(), REPL |
GDB, logic analyzer, JTAG |
| Performance | 50-100× slower | Native speed |
| Determinism | Poor (GC pauses) | Good (RTOS) / Excellent (bare metal) |
| Used in | Prototyping, education, IoT | Production, automotive, medical, aerospace |
"This course teaches you concepts that transfer to ANY platform. State machines, P-control, timing budgets, interrupt patterns — they're the same in Python, C, Rust, or VHDL."
Wokwi Demo: ESP32 State Machine (2 min)
Show a Wokwi simulation with an ESP32 running a state machine in C: - Button press → change state - LED color changes per state - Timer transitions between states
"Different chip, different language, same pattern. That's the point of learning architecture."
Block 5: What's Next + Q&A (10 min)
This Week's Lab (3 min)
"State Machines lab — you'll build everything we just discussed:
1. Flag-based code → see it fail
2. Replace with StateMachine helper
3. Add obstacle avoidance with states
4. Part 7: Interrupt-driven ultrasonic — no more blocking!
5. Log state transitions and plot them"
Looking Ahead (3 min)
"Remaining labs: HW abstraction, project integration, demo day. The final project combines EVERYTHING: state machines + P-control + sensors + timing + data logging. The skills you're building now are the foundation."
Embedded Career Paths (2 min)
| Path | What you'd build | Languages | Industries |
|---|---|---|---|
| Firmware engineer | Device drivers, RTOS tasks | C, Rust | Automotive, medical, IoT |
| Robotics engineer | Control loops, path planning | C++, Python, ROS | Warehouses, agriculture |
| IoT developer | Cloud connectivity, edge ML | Python, C | Smart home, industrial |
| FPGA engineer | Custom hardware peripherals | VHDL, Verilog | Telecom, defense |
| Test engineer | Hardware validation, automation | Python, C | Any manufacturing |
"All of these use state machines, interrupts, and timing analysis. You're learning the universal embedded toolkit."
Q&A (2 min)
"What was the hardest part of the last two labs? What questions do you have?"
Checklist Before Lecture
- [ ] Robot charged, running line follower code
- [ ] Oval track available for live demo
- [ ] Competition leaderboard ready
- [ ] REPL terminal open for live coding
- [ ] State machine code prepared (but type it live — don't copy-paste)
- [ ] Wokwi ESP32 simulation prepared
- [ ] Saleae connected (optional — for interrupt demo)
- [ ] C SDK examples in text editor ready to show (not compile — just show code)