Skip to content

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:

robot.line_follow(speed=80, kp=30)

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:

at_junction = False
avoiding_obstacle = False
turning = False
lost_line = False
battery_low = False

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)