Skip to content

Lecture 04: Software Architecture

Obuda University -- Embedded Systems

Week 7 | Labs completed: Line Following, IMU Turns


Your Learning Arc (6 Lectures)

Lecture Week Topic
Lecture 1 Week 1 What is Embedded?
Lecture 2 Week 3 Sensors, Signals & Actuators
Lecture 3 Week 5 Peripherals, Buses & Motor Control
Lecture 4 Week 7 Software Architecture ← you are here
Lecture 5 Week 9 Engineering Practice
Lecture 6 Week 11 Course Synthesis & Demo Prep

Your Learning Arc (Detailed)

  • Foundation (Weeks 1–2): What is Embedded?, GPIO, Sensors ✓
  • Sensing & Signals (Weeks 3–4): ADC, I2C, PWM basics, Ultrasonic ✓
  • Peripherals & Control (Weeks 5–6): Bus protocols, motor control, P-control, IMU ✓
  • Software (Weeks 7–8): State Machines, Abstraction ← you are here
  • Engineering (Weeks 9–10): Software Engineering, Integration
  • Synthesis (Weeks 11–12): Course Synthesis, Demo

Narrative

Your code is getting complex. Flags, nested ifs, line-following AND obstacle avoidance AND battery checking -- all tangled together.

Part 1: From spaghetti code to structured design -- organizing behavior with state machines.

Part 2: What happens INSIDE robot.forward(80)? -- understanding the layers between your code and the silicon, and where your robot sits in the embedded architecture landscape.

The story: Your code is getting complex → flags explode → state machines solve this → but what happens INSIDE robot.forward()? → layers of abstraction → Python vs C vs registers → your robot is one point on a spectrum from MicroPython to AUTOSAR.


What You Already Know (From Other Courses)

From... You already know... Today we use it for...
Programming Functions, classes, if/else, loops State machines as code, abstraction layers
Programming Data structures, arrays, dictionaries State tables, transition maps, callback registries
Electronics Digital logic, registers, bus protocols Understanding the layers from Python down to silicon
Control Theory Block diagrams, input → process → output State machine as a controller, feedback loops
Lectures 2--3 P-control, sensors, motor physics, I2C The building blocks your state machine orchestrates

Today's Map

  1. Lab Recap (Labs 5--6)
  2. Architecture Evolution
  3. Why Flags Explode
  4. State Machine Formalism
  5. Design Patterns & Implementation
  6. The Layer Cake: Python to Silicon
  7. Python vs C vs Registers
  8. Embedded Architecture Landscape
  9. Exercises & Quick Checks

Part 1

Lab Recap: Labs 5--6


Lab 5: Line Following

What You Did What You Discovered
Implemented P-control on line sensors Proportional correction = smooth tracking
Tuned Kp with measurement-based starting point Too low = sluggish, too high = oscillation
Used ticks_ms() for non-blocking loop timing Loop rate affects how Kp "feels"
Compared bang-bang vs proportional control Continuous correction eliminates overshoot

You discovered that the formula matters: \(\text{correction} = K_p \times \text{error}\)


Lab 6: IMU Turns & Data Logging

What You Did What You Discovered
Used gyro heading for precise turn angles Replaces unreliable time-based turns
Applied P-control to heading error Same formula, different sensor
Collected data from multiple runs Evidence beats intuition
A/B tested Kp values Data shows trade-offs (speed vs consistency)

Data-driven tuning: you used evidence, not guesswork.


The Growing Problem

But something else happened in those labs...

Your code is getting tangled. Line-following, obstacle detection, battery monitoring -- all mixed together with flags and nested conditions.

How do you handle "follow line" AND "avoid obstacle" AND "check battery" without spaghetti code?

Today we learn a formal technique to organize complex behavior: The State Machine.

And then: what happens inside the library calls your state machine uses.


Architecture Evolution


How Software Architecture Evolves

Every embedded system goes through this progression. A toy with one function sits at stage 1. Your car's engine ECU is at stage 5. The question is not "which is best" -- it's "where does my system's complexity actually sit?" Over-engineering is as bad as under-engineering.

As complexity grows, your code architecture must evolve:

Stage Architecture When It Works When It Breaks
1 Sequential (while True + sleep) One simple task Multiple tasks with different timing
2 Round-Robin (check each task in order) Few independent tasks Tasks with dependencies or priorities
3 Round-Robin + ISR (interrupts for urgent events) Time-critical events + background work Complex state-dependent behavior
4 State Machine (explicit states + transitions) Multiple modes, clear behavior per state Very many states, concurrent subsystems
5 RTOS (real-time OS with threads) True concurrency, complex scheduling Overkill for simple systems

Your robot is at stage 4: multiple behaviors demand explicit state management.


The Architecture Progression

┌───────────────┐      ┌───────────────────┐      ┌─────────────────┐
│  Round-Robin  │ ──→  │ Round-Robin + ISR │ ──→  │  State Machine  │
│  (Super Loop) │      │  (Fg/Bg Model)    │      │  (Event-Driven) │
│               │      │                   │      │                 │
│  Simplest     │      │  More responsive  │      │  Most organized │
│  Polling-based│      │  HW-triggered     │      │  Explicit states│
└───────────────┘      └───────────────────┘      └─────────────────┘
              ────────── complexity grows ──────────→

The simple polling loop from Lab 01 cannot handle the decisions your robot needs to make now.

State machines are the natural next step when behavior becomes complex.


Why Flags Explode


The Spaghetti Problem

Look at this code. You've probably written something like this already. It works -- until it doesn't. The moment you add a fifth behavior, you spend an hour tracing through nested conditions to find out why the robot randomly stops. I've reviewed hundreds of student projects. The number one cause of "it works sometimes" is flag spaghetti -- three booleans that accidentally end up in a combination nobody planned for.

As your robot gains features, code like this appears:

# The spaghetti problem
if moving and not obstacle and line_visible:
    if turning:
        if turn_complete:
            moving = True
            turning = False
        else:
            continue_turn()
    else:
        if obstacle_ahead:
            stop()
            obstacle = True
        # ... more nested conditions ...

This code works -- until you try to change it.


Why It Fails

Problem Explanation
Hard to understand What does moving and not obstacle and turning mean?
Hard to debug The robot did something wrong -- but what state was it in?
Hard to extend Adding "search for lost line" requires touching every branch
Hard to test Which combinations of flags have you actually tested?

The Combinatorial Explosion

Each boolean flag doubles the number of possible states:

Flags Example Possible Combinations
1 moving 2
2 + obstacle 4
3 + turning 8
4 + line_lost 16
5 + low_battery 32

Most of these 32 combinations are meaningless or should never occur -- but your code does not prevent them.


The Explosion Visualized

center width:700

5 boolean flags = 32 possible combinations. You intended maybe 6 valid states. The other 26 are bugs waiting to happen.


Impossible Combinations

Any bug can put the robot into a state like:

moving    = True
obstacle  = True
turning   = True
stopped   = True      <-- wait, what?

There is no guard. Nothing prevents this. The code just does... whatever happens.


A Real Robot Scenario

Imagine your robot needs line-following, obstacle avoidance, and search:

Flags:
  is_following    is_avoiding    is_searching
  line_visible    obstacle_near  turning_left
  emergency_stop

7 flags = 2^7 = 128 possible combinations
Meaningful states? Only about 5.
The other 123 combinations? BUGS waiting to happen.

A state machine eliminates 123 impossible combinations by design.


The Root Cause

Boolean flags describe properties of the system. What you actually need is to describe which behavior the system is performing right now.

A single, unambiguous answer to:

"What state is the robot in?"


State Machine Formalism


The Idea

Instead of scattered booleans, explicitly define:

  1. States -- the distinct modes your system can be in
  2. Transitions -- what causes the system to move between states
  3. Actions -- what the system does in each state (and during transitions)

This is called a Finite State Machine (FSM).


State Diagram

w:700

A line-following robot with obstacle detection:

Three states (IDLE, RUNNING, STOPPED). Four transitions (start, stop, obstacle detected, obstacle cleared). No ambiguity.


Reading the Diagram

Element Meaning
Box A state -- one distinct behavior mode
Arrow A transition -- the system changes state
Label on arrow The event that triggers the transition
Starting state Usually marked or obvious (IDLE)

The robot is in exactly one box at any time. No exceptions.


Transition Table

The diagram as a table -- useful for checking completeness:

Current State Event Next State Action
IDLE Start button RUNNING Start motors
RUNNING Stop button IDLE Stop motors
RUNNING Obstacle detected STOPPED Stop motors
STOPPED Obstacle cleared RUNNING Resume motors
STOPPED Stop button IDLE Stay stopped

Have you defined what happens for every event in every state?


Completeness Check

Look at the table on the previous slide:

  • What happens if the start button is pressed while already RUNNING?
  • What about an obstacle event while IDLE?

Events that are not listed are implicitly ignored -- which is fine, as long as you made that choice deliberately.

An unhandled event should be a design decision, not an oversight.


Implementation Pattern

STATE_IDLE = 0
STATE_RUNNING = 1
STATE_STOPPED = 2

state = STATE_IDLE

def update(events):
    global state
    if state == STATE_IDLE:
        if events.start_pressed:
            state = STATE_RUNNING
        return (0, 0)  # Motors off
    elif state == STATE_RUNNING:
        if events.stop_pressed:
            state = STATE_IDLE
            return (0, 0)
        if events.obstacle_detected:
            state = STATE_STOPPED
            return (0, 0)
        return calculate_motor_speeds()

Implementation Pattern (continued)

    elif state == STATE_STOPPED:
        if events.stop_pressed:
            state = STATE_IDLE
        elif events.obstacle_cleared:
            state = STATE_RUNNING
        return (0, 0)

Notice:

  • Current state is one variable, not five booleans
  • Each state is a clearly defined block
  • Transitions are explicit: you can read them directly
  • The robot can only be in one state at a time

What State Machines Buy You

Aspect Without State Machine With State Machine
Behavior Scattered across nested if/else Explicit in diagram and table
Debugging "Why did it do that?" Check current state + last transition
Adding features Modify multiple if/else chains Add a new state + transitions
Testing Hard to cover all flag combos Test each state independently
Documentation "Read the code" Draw the state diagram
Team work Everyone edits the same logic Each person owns a state

The payoff increases with complexity. For two behaviors, either approach works. For five or more, the state machine pays for itself immediately.


State Machines in Engineering

bg right:35%

State machines are not just a coding pattern -- they're a formal engineering tool. Every automotive ECU runs a state machine (AUTOSAR defines the state model). Every communication protocol is defined as a state machine (TCP, USB, CAN). Every safety-critical system uses verifiable state models -- because you can formally prove that no dangerous transition is possible.

UML statecharts are the industry-standard notation. What you're drawing on paper this week, automotive engineers draw in tools like Simulink or Enterprise Architect -- and the tools generate verified C code from the diagram.

  • Traffic lights -- RED, GREEN, YELLOW with timed transitions
  • Network protocols -- TCP, USB, CAN -- all defined as state machines
  • Automotive ECUs -- AUTOSAR state management in every car
  • Your washing machine -- FILL, WASH, RINSE, SPIN, DONE

Design Patterns


Pattern 1: Entry and Exit Actions

Some code should run once when entering or leaving a state, not every loop:

def transition_to(new_state):
    global state
    # Exit action for current state
    if state == STATE_RUNNING:
        stop_motors()

    # Entry action for new state
    if new_state == STATE_RUNNING:
        start_motors()
    elif new_state == STATE_STOPPED:
        obstacle_timer = time.ticks_ms()

    state = new_state

Why a function? It guarantees entry/exit actions always run.


Why Use transition_to()?

If you write state = STATE_RUNNING directly in three different places, you might forget the entry action in one of them.

# BAD -- easy to forget an action
state = STATE_RUNNING          # Did I start motors?
# ... somewhere else ...
state = STATE_RUNNING          # Forgot to start motors!

# GOOD -- always consistent
transition_to(STATE_RUNNING)   # Entry action always runs

One function. One place. Always correct.


Pattern 2: Timed Transitions

"Stay in STOPPED for 1 second, then resume."

The timer starts on entry (inside transition_to):

elif state == STATE_STOPPED:
    if events.stop_pressed:
        transition_to(STATE_IDLE)
    elif time.ticks_diff(time.ticks_ms(), obstacle_timer) > 1000:
        if not events.obstacle_detected:
            transition_to(STATE_RUNNING)
    return (0, 0)

Non-blocking -- the timer is set once on entry, checked each iteration. Same pattern from Theory 05.


Pattern 3: Guard Conditions

Sometimes a transition should only happen if an extra condition is met:

# Only resume if battery is above threshold
elif state == STATE_STOPPED:
    if events.obstacle_cleared and battery_voltage() > 3.3:
        transition_to(STATE_RUNNING)
    elif battery_voltage() <= 3.3:
        transition_to(STATE_IDLE)  # Too low to continue

Guard conditions prevent transitions that would be unsafe or meaningless.


Common Mistakes to Avoid

1. Modifying state from multiple places Always use transition_to(). Never write state = X directly.

2. Forgetting to handle all events in all states Draw the transition table. Check every cell. Unhandled events should be a deliberate choice.

3. Not separating entry actions from per-loop actions Starting motors = entry action (once). Reading sensors = per-loop (every iteration).

4. Too many states If you have 15 states, split into two smaller state machines.


Implementation Patterns


Pattern A: The If-Elif Chain

The simplest approach -- what we have seen so far:

def update(events):
    global state
    if state == STATE_IDLE:
        # handle idle
        ...
    elif state == STATE_RUNNING:
        # handle running
        ...
    elif state == STATE_STOPPED:
        # handle stopped
        ...

Pros: Easy to read, easy to understand. Cons: One giant function that grows with every new state.

Good for: 3-5 states. Beyond that, it gets unwieldy.


Pattern B: Dispatch Table

Use a dictionary to map states to handler functions:

def handle_idle(events):
    if events.start_pressed:
        transition_to(STATE_RUNNING)
    return (0, 0)

def handle_running(events):
    if events.stop_pressed:
        transition_to(STATE_IDLE)
        return (0, 0)
    return calculate_motor_speeds()

handlers = {
    STATE_IDLE:    handle_idle,
    STATE_RUNNING: handle_running,
    STATE_STOPPED: handle_stopped,
}

# Main loop -- one line replaces the if-elif chain
def update(events):
    return handlers[state](events)

Comparing the Two Patterns

Aspect If-Elif Chain Dispatch Table
Readability All logic visible in one place Each state isolated in its own function
Adding states Edit one large function Add a function and one dict entry
Performance Checks states sequentially Direct lookup -- O(1)
Best for Small (3-5 states) Larger (5+ states)

For Lab 07, the if-elif chain is fine. Know that the dispatch table exists for when you need it.


Hierarchical State Machines (Preview)

When behavior gets complex, nest state machines:

TOP LEVEL:      IDLE  -->  RUNNING  -->  E_STOP
                              |
                              v
SUB-MACHINE:     FOLLOW --> AVOID --> SEARCH

The top-level machine handles coarse modes (running, stopped, emergency). The sub-machine handles fine-grained behavior -- but only while the top level is in RUNNING.


Why Hierarchical?

Without hierarchy, adding emergency stop to 6 sub-states means 6 new transitions:

FOLLOW --> E_STOP    AVOID  --> E_STOP    SEARCH --> E_STOP
TURN   --> E_STOP    BACKUP --> E_STOP    SCAN   --> E_STOP

With hierarchy, you add one transition at the top level:

RUNNING --> E_STOP

All sub-states are automatically exited. Much simpler.

We will not implement this in Lab 07, but keep it in mind for your final project.


Preview: When the Main Loop Isn't Enough -- RTOS

A state machine handles one thing at a time within the main loop. But what about truly concurrent tasks?

Problem Why Single-Loop Fails
Multiple deadlines (motor 1kHz + display 10Hz + logging 1Hz) Hard to guarantee all rates when tasks have variable execution time
Priority inversion (low-priority task blocks high-priority) No preemption in single loop -- long task blocks everything
Team development (multiple engineers adding features) Single loop = everyone modifies same code, conflicts

A Real-Time Operating System (RTOS) gives you multiple threads with OS-managed scheduling and priorities.

For this course, state machines are sufficient. But know that RTOS exists as the next step when your system outgrows a single main loop.


Part 1 Summary


Five Things to Remember (State Machines)

  1. Flags multiply -- 5 booleans = 32 possible combinations, most of them bugs

  2. State machines make behavior explicit -- one variable, clear transitions, no ambiguity

  3. Draw first, code second -- the state diagram IS the design

  4. Use a transition function -- entry/exit actions run exactly once, every time

  5. Check completeness -- every state must handle every possible event (even if the answer is "ignore it")


Part 2

The Layer Cake and Abstraction Levels

From robot.forward(80) to Silicon


The Open Question

Your state machine calls robot.forward(80) and the wheels turn. One line of Python, instant result.

What actually happens when you call robot.forward(80)?

You write one line of Python. But between that line and the wheels turning, there are six layers of abstraction at work.

Last week you learned HOW devices communicate (I2C, SPI, UART). Today we look at the software layers that make it all work — and where your robot sits in the broader embedded world.


When Abstraction Breaks

Abstraction works beautifully -- until it does not.

  • OLED froze -- is the problem in your code, the display library, the I2C bus, or the wiring?
  • Motor speed is off -- is it your control logic, the picobot library, the PWM configuration, or the H-bridge?
  • Sensor stopped responding -- loose wire, wrong I2C address, or bus contention?

When you only know the top layer, every failure is a mystery. When you understand ALL the layers, failures become diagnosable.

Understanding layers does not make you a better coder. It makes you a better debugger.


The Layer Cake


What Happens When You Call robot.forward(80)

┌─────────────────────────────────────────────┐  ← You write this
│  Your Code:  robot.forward(80)              │
├─────────────────────────────────────────────┤
│  picobot Library:  motors.set_speeds(80,80) │
├─────────────────────────────────────────────┤
│  Driver Layer:  PWM(Pin(10)).duty_u16(…)    │
├─────────────────────────────────────────────┤
│  MicroPython:  C function mp_hal_pin_write()│
├─────────────────────────────────────────────┤
│  Hardware Regs:  Write to PWM_CH5_CC reg    │
├─────────────────────────────────────────────┤  ← Silicon does this
│  Silicon:  PWM counter compares, toggles pin│
└─────────────────────────────────────────────┘
Layer stack: Your Code → picobot Library → Driver → MicroPython → Hardware Registers → Silicon. One line of code, six layers execute.

You write one line. Six layers execute.


Why Layers?

Each layer in the stack has a single job:

  • Your Code -- express intent: "go forward at 80%"
  • picobot Library -- translate intent into motor commands
  • Driver Layer -- configure MicroPython hardware objects (PWM, Pin)
  • MicroPython Runtime -- call C functions that manipulate registers
  • Hardware Registers -- memory-mapped addresses controlling peripherals
  • Silicon -- transistors toggling pins at the requested frequency

Each layer only knows about the layer directly below it.


The Car Analogy

Think of driving a car:

┌──────────────────────┬────────────────┐
│ You: "Turn left"     │  Intent        │
├──────────────────────┼────────────────┤
│ Steering wheel       │  Interface     │
├──────────────────────┼────────────────┤
│ Power steering pump  │  Amplification │
├──────────────────────┼────────────────┤
│ Rack and pinion      │  Mechanism     │
├──────────────────────┼────────────────┤
│ Wheels turn          │  Physics       │
└──────────────────────┴────────────────┘
Layer stack: "Turn left" (intent) → Steering wheel (interface) → Power steering (amplification) → Rack and pinion (mechanism) → Wheels turn (result).

You do not think about hydraulic fluid pressure when making a left turn.

But if the power steering fails, you need to understand the layers below.


Key Principle

Abstraction lets you USE a system without understanding every layer. But to DEBUG or DESIGN a system, you must see through the layers.

This is why we are looking under the hood today.


Benefits of Layered Design

Problem Without Layers With Layers
Change motor pins Edit 20 files that reference pin numbers Edit 1 driver file
Add a different motor type Rewrite application logic Write a new driver, same API
Test line-following Need a physical robot every time Mock the driver, test on PC
Debug a motor issue "Where is PWM configured?" -- search everywhere Look in the motor driver module

The Full Layer Stack

The complete abstraction stack for your robot, from intent down to silicon:

┌─────────────────────┬───────────────────────────────┐
│ Application Logic   │  Follow the line, avoid walls │
├─────────────────────┼───────────────────────────────┤
│ Robot API           │  robot.forward(80)            │
├─────────────────────┼───────────────────────────────┤
│ Driver Layer        │  Motor driver, sensor driver  │
├─────────────────────┼───────────────────────────────┤
│ Hardware Abstraction│  Pin, PWM, ADC, I2C classes   │
├─────────────────────┼───────────────────────────────┤
│ Hardware (MCU)      │  Physical registers, silicon  │
└─────────────────────┴───────────────────────────────┘
Each layer only talks to its direct neighbor ↕
Layer stack: Application Logic → Robot API → Driver Layer → Hardware Abstraction → Hardware (MCU). Each layer only talks to its neighbor.

Key principle: Each layer only talks to the layer directly below it.

Your application code should never touch GPIO pins directly -- it talks to the Robot API, which talks to drivers, which talk to hardware abstraction.


Good vs Bad Abstraction

BAD -- Application code touches hardware directly:

# Scattered throughout main.py
pwm_left = PWM(Pin(10))
pwm_right = PWM(Pin(11))
dir_left = Pin(12, Pin.OUT)
dir_right = Pin(13, Pin.OUT)

dir_left.value(1)
pwm_left.duty_u16(52428)  # what does 52428 mean?!

GOOD -- Application code uses a driver:

class MotorDriver:
    def __init__(self, pwm_pin, dir_pin):
        self.pwm = PWM(Pin(pwm_pin))
        self.dir = Pin(dir_pin, Pin.OUT)
    def set_speed(self, percent):
        self.dir.value(0 if percent >= 0 else 1)
        self.pwm.duty_u16(int(abs(percent) / 100 * 65535))

left_motor = MotorDriver(10, 12)
left_motor.set_speed(80)   # 80% forward -- clear!

NeoPixel Case Study: Three Levels of Understanding

Level 1 -- Library user:

robot.set_leds((255, 0, 0))  # red -- just works

Level 2 -- Understanding the protocol:

WS2812 requires precise timing: each bit is a HIGH pulse (short = 0, long = 1). 24 bits per LED at 800 kHz -- each bit takes 1.25 us.

  Bit "1":  ┌──────┐           Bit "0":  ┌───┐
            │ 700ns│              │350ns│
  ──────────┘      └──────       ─────────┘   └─────────
                    600ns                      800ns
            total: 1.3 µs                total: 1.15 µs

The LED distinguishes 1 from 0 by the width of the HIGH pulse — wider = 1, narrower = 0. Digital waveforms: "1" bit = 700 ns HIGH + 600 ns LOW (long pulse). "0" bit = 350 ns HIGH + 800 ns LOW (short pulse). Total ~1.25 µs per bit at 800 kHz.


NeoPixel Case Study: The Abstraction Trade-off

How can we generate these precise signals?

Approach Result Why
Bit-bang in Python? Fails Each GPIO toggle takes ~5 us, need ~0.4 us
Bit-bang in C? Works but wastes CPU Consumes 100% CPU during transmission
Use PIO hardware? Perfect Exact timing, zero CPU load

The driver author made this decision FOR you. That is the power of abstraction -- you call set_leds() without caring how the bits are sent.

But when something goes wrong (wrong colors, flickering, timing glitches), understanding the layers below helps you diagnose the problem.


The Cost of Abstraction

Abstraction is not free. Each layer adds:

  • Execution time -- function calls, type checks, memory allocation
  • Memory usage -- each layer has its own data structures
  • Complexity -- more code to maintain and understand

Abstraction hides what happens, not how long it takes. Calling robot.forward(80) feels instant, but dozens of function calls and register writes happen underneath.


Python vs C vs Registers

The Same Operation at Three Levels


GPIO -- Turn On an LED: Python

Highest abstraction:

from machine import Pin

led = Pin(13, Pin.OUT)
led.on()  # One line!

Three concepts compressed into two lines: pin selection, direction, and output value.


GPIO -- Turn On an LED: C SDK

Middle abstraction:

#include "hardware/gpio.h"

gpio_init(13);                // Configure pin
gpio_set_dir(13, GPIO_OUT);   // Set direction
gpio_put(13, 1);              // Set HIGH

Three explicit steps. Nothing is hidden.


GPIO -- Turn On an LED: Registers

No abstraction -- this is what both Python and C eventually do:

// 1. Select GPIO function (SIO) for pin 13
*(volatile uint32_t *)(IO_BANK0_BASE + 0x06C) = 5;

// 2. Set pin 13 as output
*(volatile uint32_t *)(SIO_BASE + 0x024) = (1 << 13);

// 3. Set pin 13 HIGH
*(volatile uint32_t *)(SIO_BASE + 0x014) = (1 << 13);

These "magic numbers" are memory-mapped register addresses from the RP2350 datasheet. The datasheet is the ground truth. Everything else is convenience.


PWM -- Motor at 50%: Python

from machine import Pin, PWM

motor = PWM(Pin(10))
motor.freq(1000)
motor.duty_u16(32768)  # 50% duty cycle

Three lines: create PWM, set frequency, set duty cycle. Done.


PWM -- Motor at 50%: C SDK

#include "hardware/pwm.h"

uint slice = pwm_gpio_to_slice_num(10);       // Which PWM slice?
gpio_set_function(10, GPIO_FUNC_PWM);         // Route pin to PWM
pwm_set_wrap(slice, 65535);                   // Set period (top)
pwm_set_chan_level(slice, PWM_CHAN_A, 32768);  // 50% compare
pwm_set_enabled(slice, true);                 // Start PWM

Five explicit steps. You see the slice, channel, wrap value, and explicit enable -- all hidden in Python.


What the PWM Hardware Does

PWM counter:  0 -> 1 -> 2 -> ... -> 32767 -> 32768 -> ... -> 65535 -> 0
                                       |
                                 Compare match!
                                 Pin goes LOW

Output pin:   ████████████████████░░░░░░░░░░░░░░░░░░░░
              |<--- ON (50%) --->|<--- OFF (50%) --->|

The PWM hardware is a counter that runs continuously. Below the compare value = HIGH. Above = LOW. This square wave drives your motor.


Interpreter vs Compiler

Where does MicroPython fit?

Compiled (C):
  source.c  -->  [Compiler]  -->  machine code  -->  runs on CPU

Interpreted (MicroPython):
  main.py   -->  [Interpreter reads line-by-line at runtime]  -->  CPU
  • Compiled (C): source code is translated to machine code before execution. Fast, efficient, catches errors at compile time.
  • Interpreted (MicroPython): the interpreter reads your Python at runtime, line by line. Slower, more memory, but much faster development.

Key insight: The MicroPython interpreter IS a compiled C program running on the MCU. It reads your .py files and executes them.


Timer Interrupts: Python vs C

The difference matters most for deterministic timing:

Python -- polling with ticks_diff():

while True:
    now = time.ticks_ms()
    if time.ticks_diff(now, last_time) >= 10:
        last_time = now
        control_loop()  # Timing depends on loop speed

C -- hardware timer interrupt:

volatile bool timer_fired = false;
bool timer_callback(repeating_timer_t *rt) {
    timer_fired = true;  // Hardware fires every 10 ms -- guaranteed
    return true;
}
Aspect Python (ticks_diff) C (hardware timer)
Jitter Depends on loop time Microseconds
Determinism Best-effort Guaranteed
Use case Prototyping, learning Production, safety-critical

volatile tells the C compiler: "this variable changes from an interrupt -- do not optimize away reads."


Layers Are Real: Timing Comparison

Level What You Write Approx. Time
Python Pin(13).on() 1--2 us
C SDK gpio_put(13, 1) ~30 ns
Register Direct memory write ~7 ns

Each layer adds convenience but costs time.

The ratio is roughly 200:1 between Python and C for simple hardware operations.


Choose the Right Level

  • Python -- fast to write, easy to debug, good enough for most tasks in this course
  • C SDK -- when you need speed or deterministic timing (production, safety-critical)
  • Registers -- rarely written by hand; understanding them helps you read datasheets and debug hardware

You do not need to write register-level code. But knowing that every Pin(13).on() eventually becomes a register write makes you a better embedded engineer.


Embedded Architecture Landscape

Beyond MicroPython — How the Industry Builds Systems


The RP2350 Under the Hood

w:750

Your machine.PWM and machine.I2C calls configure these peripherals through the bus. Once configured, the hardware runs autonomously — the CPU is free to do other work.


Your Robot's Software Stack

w:550

Every line you write passes through these layers. When the OLED freezes or a motor behaves wrong, the bug lives in one of these layers — knowing them tells you where to look.


What If One Loop Isn't Enough? — RTOS

Your robot runs a single while True loop. What happens when you need:

  • Motor control at 1 kHz (1 ms)
  • Sensor reading at 100 Hz (10 ms)
  • Display update at 10 Hz (100 ms)

A Real-Time Operating System gives each task its own thread with guaranteed timing:

// FreeRTOS: three tasks, three priorities
void motor_task(void *p)   { while(1) { pid_step();    vTaskDelay(1);   } }
void sensor_task(void *p)  { while(1) { read_imu();    vTaskDelay(10);  } }
void display_task(void *p) { while(1) { oled_update(); vTaskDelay(100); } }

RTOS Task Scheduling

w:750

The scheduler preempts lower-priority tasks when a higher-priority task is ready. Your cooperative ticks_ms loop can't do this — if one task takes too long, everything waits.


When Do You Need an RTOS?

Your Robot (Cooperative) RTOS (Preemptive)
If motor task takes too long → everything waits Scheduler interrupts it
Timing guarantee → best effort Timing → provable worst-case
Task isolation → shared globals Tasks → separate stacks, queues
Works for → learning, prototyping Works for → drones, robots, industrial

For this course, your single-loop state machine is sufficient. But when a system outgrows one loop, RTOS is the next step — not "rewrite everything," just add a scheduler.


Five Architectures, One Pattern

w:850


The Same Pattern at Every Level

Despite massive differences in complexity, ALL embedded architectures share this:

Application Logic     ← "What the system does"
        |
   Middleware / OS    ← "How tasks are scheduled"
        |
   HAL / Drivers      ← "How to talk to hardware"
        |
   Hardware Regs      ← "What the silicon provides"

Your P-control formula, your state machine, your I2C protocol — all the same across architectures. Only the API changes.


Same GPIO, Five Levels

Level Code
MicroPython Pin(25, Pin.OUT).value(1)
C SDK gpio_put(25, 1)
Register *(uint32_t*)(0xd0000014) = (1<<25)
Linux sysfs echo 1 > /sys/class/gpio/gpio25/value
AUTOSAR Dio_WriteChannel(LED, STD_HIGH)

Same pin goes HIGH. Same transistor switches. Five different ways to express it.


Where Does Each Architecture Live?

Architecture Typical Use Timing Example
MicroPython Education, IoT prototypes Best effort Your PicoBot
Bare Metal C Simple products, sensors Deterministic Thermostat, remote
RTOS Drones, robots, industrial Hard real-time Flight controller
Linux Cameras, UI, networking Soft real-time Raspberry Pi
AUTOSAR Automotive, medical, aero Certified Engine ECU, ABS

Increasing complexity → more layers → stricter timing guarantees.

You started at MicroPython. The Linux in Embedded Systems course takes you to the Linux column. The concepts you learned here — state machines, layers, bus protocols — apply at every level.


Exercises


Exercise 1: Design a State Machine (4 Behaviors)

Your robot must handle four distinct behaviors:

State Behavior
IDLE Motors off, waiting for start button press
LINE_FOLLOW Following the line using proportional control
OBSTACLE_AVOID Obstacle detected -- stop, turn, resume
SEARCH Line lost -- execute a search pattern to find it

On paper, draw:

  1. 4 boxes for the states
  2. Arrows between them for every valid transition
  3. Labels on each arrow: what event triggers that transition?
  4. Entry actions noted inside or beside each box

Take 3 minutes. Draw it now.


Exercise 1: Transition Table

Complete every cell. Empty cells are where bugs hide.

Current State Event Next State Action
IDLE Start button _____ _____
LINE_FOLLOW Obstacle detected _____ _____
LINE_FOLLOW Line lost _____ _____
LINE_FOLLOW Stop button _____ _____
OBSTACLE_AVOID Obstacle cleared _____ _____
OBSTACLE_AVOID Stop button _____ _____
SEARCH Line found _____ _____
SEARCH Timeout (5s) _____ _____
any state Emergency stop _____ _____

Exercise 1: Check Your Table

Questions to verify your design:

  • Can the robot get stuck in any state with no way out?
  • Is there a path from every state back to IDLE?
  • Does emergency stop work from every state?
  • What happens if an obstacle is detected during SEARCH?
  • What happens if the line is lost during OBSTACLE_AVOID?

If you find a gap, add a transition.


Exercise 1: Sample Solution

w:800

Four states: IDLE → LINE_FOLLOW (on start), LINE_FOLLOW → AVOID (obstacle detected), LINE_FOLLOW → SEARCH (line lost), SEARCH → IDLE (timeout), AVOID → LINE_FOLLOW (cleared). Emergency stop returns to IDLE from any state.


Exercise 1: Sample Transition Table

Current State Event Next State Action
IDLE Start button LINE_FOLLOW Start motors, LED on
LINE_FOLLOW Obstacle detected OBSTACLE_AVOID Stop, begin turn
LINE_FOLLOW Line lost SEARCH Start search pattern
LINE_FOLLOW Stop button IDLE Stop motors
OBSTACLE_AVOID Obstacle cleared LINE_FOLLOW Resume forward
OBSTACLE_AVOID Stop button IDLE Stop motors
SEARCH Line found LINE_FOLLOW Resume tracking
SEARCH Timeout (5s) IDLE Stop motors, LED blink
any state Emergency stop IDLE Kill motors immediately

Exercise 2: Trace robot.forward(80) Through All Layers

Fill in what happens at each layer:

Layer 1 - Your Code:      robot.forward(80)
                          |
Layer 2 - picobot Library: ???
                          |
Layer 3 - Driver Layer:    ???
                          |
Layer 4 - MicroPython:     ???
                          |
Layer 5 - Hardware Regs:   ???
                          |
Layer 6 - Silicon:         ???

Work in pairs. You have 3 minutes.


Exercise 2: Solution

Layer 1 - Your Code:      robot.forward(80)
                          |
Layer 2 - picobot Library: motors.set_speeds(80, 80)
                          |
Layer 3 - Driver Layer:    PWM(Pin(10)).duty_u16(52428)
                          |   Pin(12).value(1)  # direction
                          |
Layer 4 - MicroPython:     C function mp_hal_pin_write()
                          |
Layer 5 - Hardware Regs:   Write 52428 to PWM_CH5_CC register
                          |
Layer 6 - Silicon:         PWM counter toggles pin at duty cycle

80% of 65535 = 52428. The library converts your percentage to a 16-bit duty value.


Exercise 3: Choose the Architecture

For each product, decide which architecture level is appropriate and why:

Product Architecture? Why?
Smart thermostat (reads temp, controls relay) _ _
Quadcopter drone (4 motors, IMU, GPS, camera) _ _
Digital alarm clock _ _
Car anti-lock braking system (ABS) _ _
Home security camera with face detection _ _

Work in pairs. 2 minutes.


Exercise 3: Solution

Product Architecture Why
Smart thermostat Bare Metal C Simple, deterministic, one task
Quadcopter drone RTOS Multiple deadlines (motor 1kHz, IMU 100Hz, GPS 1Hz)
Digital alarm clock Bare Metal C Simple, low power, no concurrency needed
Car ABS AUTOSAR Safety-certified, ISO 26262, provable timing
Security camera + face detection Linux Needs camera drivers, ML framework, networking

The question is never "which is best" — it's "what does my system's complexity require?"


Exercise 4: Cooperative vs Preemptive

Your robot runs motor control (needs 1 ms), sensor reading (needs 3 ms), and OLED update (needs 15 ms) in a single loop.

Questions:

  1. What is the total loop time? Can you guarantee 1 ms motor updates?
  2. If the OLED update takes 25 ms instead of 15, what happens to motor control?
  3. How would an RTOS solve this? What would the task priorities be?

Work in pairs. 3 minutes.


Exercise 4: Solution

  1. Total loop = 1 + 3 + 15 = 19 ms. Motor runs at ~52 Hz, not 1000 Hz. Cannot guarantee 1 ms.
  2. Loop stretches to 29 ms. Motor control is starved — wheels jerk or drift.
  3. RTOS: Motor = HIGH priority (1 ms), Sensor = MEDIUM (10 ms), Display = LOW (100 ms). The scheduler preempts display to run motor — motor always meets its deadline.

Summary & Quick Checks


Ten Things to Remember

State Machines (Part 1):

  1. Flags multiply -- 5 booleans = 32 possible combinations, most of them bugs
  2. State machines make behavior explicit -- one variable, clear transitions, no ambiguity
  3. Draw first, code second -- the state diagram IS the design
  4. Use a transition function -- entry/exit actions run exactly once, every time
  5. Check completeness -- every state must handle every possible event

Architecture & Abstraction (Part 2):

  1. Abstraction layers hide complexity but add overhead -- each layer does one job
  2. Python → C → registers: same operation, different levels -- trading convenience for speed at ~200:1
  3. Understanding layers makes you a better debugger -- when something breaks, you know where to look
  4. Five architecture levels -- MicroPython → Bare Metal C → RTOS → Linux → AUTOSAR, same pattern at every level
  5. Choose architecture by complexity -- over-engineering is as bad as under-engineering

Quick Check 1

Why is a single state variable better than multiple boolean flags?

A single variable can only hold one value at a time, so the system is always in exactly one state. Boolean flags can be combined into impossible or meaningless configurations. 5 booleans = 32 combinations, but only ~5 are meaningful.


Quick Check 2

What is the purpose of entry and exit actions in a state machine?

Entry actions run once when entering a state (e.g., start motors, reset timer). Exit actions run once when leaving (e.g., stop motors). They guarantee setup and cleanup happen exactly once, not every loop iteration.


Quick Check 3

How many possible combinations do 6 boolean flags create?

2^6 = 64 combinations. Most of them are meaningless or buggy. A state machine with 6 named states has exactly 6 possible states -- no impossible combinations.


Quick Check 4

What is the approximate speed difference between Python and C for a GPIO toggle?

Level Time Ratio
Python (Pin(13).on()) ~1-2 us 1x
C SDK (gpio_put(13, 1)) ~30 ns ~50x faster
Register (direct write) ~7 ns ~200x faster

The MicroPython interpreter adds overhead for type checking, object lookup, and bytecode execution before reaching the same register write that C does directly.


Quick Check 5

Your robot needs motor control at 1 kHz and display at 10 Hz. Why can't a single while True loop guarantee both?

If the display update takes 15 ms, the motor misses 15 deadlines. A cooperative loop runs tasks sequentially — one slow task starves everything. An RTOS solves this with preemptive scheduling: the motor task interrupts the display task every 1 ms.


Quick Check 6

What architecture would you choose for a drone flight controller, and why?

RTOS (e.g., FreeRTOS, Zephyr). Drones need hard real-time guarantees: motor control at 1 kHz, IMU at 500 Hz, GPS at 1 Hz — all with provable worst-case timing. MicroPython is too slow, bare metal can't manage multiple deadlines, Linux has no hard RT guarantees, AUTOSAR is overkill.


Bridge to Labs & Next Lecture


This Week: Lab 7 -- State Machines

Implement a state machine for your robot:

IDLE  -->  FOLLOWING  -->  AVOIDING  -->  SEARCHING

Your workflow:

  1. Draw the state diagram first -- on paper, before any code
  2. Fill in the transition table -- every state, every event, every action
  3. Implement using the transition_to() pattern
  4. Test each transition -- trigger every event in every state

10 minutes of design saves 30 minutes of debugging.

Tutorial: State Machines


Next Week: Lab 8 -- HW Abstraction + Architecture

Step What You Do What You Learn
1 Read the picobot source code How the library is layered
2 Control an LED using Pin directly The driver layer, without the library
3 Control a motor using PWM directly PWM configuration that picobot hides
4 Scan the I2C bus and read raw IMU data Communication protocols in practice
5 Compare direct code with picobot calls The value (and cost) of abstraction

Understanding what the library does makes you a better user of it.

Tutorial: Hardware Abstraction


Bridge to Lecture 5 (Week 9)

Your state machine organizes behavior. Your abstraction layers organize code. Each piece works in isolation.

But your main.py has been growing for 8 weeks -- motor control, sensor reading, control logic, display updates, configuration values, all in one file.

What if changing one thing breaks three others?

Next lecture: Software engineering principles that make code survive change.

Separation of concerns, modules, clean interfaces. The same ideas that make the picobot library well-organized -- applied to YOUR code.

"Each piece works alone. But put them together..."


End

Lab 7 (this week): State Machines -- design and implement a 4-state robot behavior. Lab 8 (next week): HW Abstraction + Architecture -- peel apart the layers.

Next lecture is in Week 9: Engineering Practice.

"You're designing a state machine for 4 behaviors. The same formalism governs every traffic light and every nuclear plant."

"You're reading an IMU over I2C. The same protocol connects thousands of sensors in a modern factory."