Skip to content

Inside the Box

Time: 60 min

Background: Abstraction Layers in Embedded Systems

When you call robot.forward(80), several layers execute: your Python code → picobot library → MicroPython C runtime → hardware registers → GPIO pins → H-bridge → motor. Each layer adds convenience but also overhead (~200:1 between Python and bare registers). Understanding these layers lets you debug across the full stack and know when to "drill down" to a lower layer for performance or control.

→ Deep dive: Execution Models

Lab Setup

Connect your Pico via USB, then from the picobot/ directory:

mpremote run clean_pico.py          # optional: wipe old files
mpremote cp -r lib :               # upload libraries

Verify in REPL: from picobot import Robot; Robot() → "Robot ready!" First time? See the full setup guide.


Learning Objectives

By the end of this lab you will be able to:

  • Read and navigate a Python library's source code
  • Identify which abstraction layer to modify for a given requirement
  • Implement closed-loop velocity control (controlling speed, not position)
  • Understand the relationship between motor power, PWM duty cycle, and actual speed
  • Modify or extend library code when high-level APIs are insufficient

You will inspect the picobot library, identify where motor control lives, and extend or bypass it to achieve continuous rotation speed. The goal is to see that abstractions are editable, not magical.


Introduction

High-level library functions (robot.line_follow(), robot.turn_degrees()) abstract away implementation details. This is useful until you need functionality the library doesn't provide.

This lab presents a challenge that cannot be solved using only the picobot high-level API, forcing you to examine how the library works internally and potentially modify or extend it.


Why This Lab Matters

Real engineering rarely matches the API surface you are given. Eventually you must open the box, understand the layers, and decide where to make changes. This lab teaches how abstractions are built, how to navigate code you did not write, and how to safely extend a library without breaking everything else.


The Challenge: Precise Rotation Speed

Make the robot spin continuously at exactly 180°/s (half a rotation per second).

Rules: - The robot must spin in place (not move forward) - The speed must be 180°/s (±10°/s tolerance) - It must spin for at least 5 seconds

This challenge is carefully chosen because it cannot be solved using only the high-level picobot API you've learned so far.

Think about what you know:

Method What It Does
robot.forward(speed=80) Moves forward
robot.turn_degrees(90) Turns exactly 90° and STOPS
robot.set_motors(80, 80) Both motors forward

What you need: spin CONTINUOUSLY at a SPECIFIC angular velocity.

The problem is that turn_degrees() stops after the turn, and set_motors() takes a "power" number, not a speed. What power = 180°/s? How would you even know?

To solve this, you need to understand what's inside picobot and potentially modify or bypass it. This is a common situation in engineering—the tools you have don't quite do what you need.


⚡Hands-on tasks

First Attempt

Try this and see what happens:

from picobot import Robot

robot = Robot()

# Make it spin... but at what speed?
robot.set_motors(-80, 80)  # Left backward, right forward = spin

# How fast is this? No idea!

You'll immediately have questions: 1. What motor power = 180°/s? 2. Does 80 mean 80% speed? 80 RPM? 80 what? 3. How do I measure the actual rotation speed?

✅ Task 1 - Measure Rotation Speed

Measure your actual rotation speed at different power levels:

from picobot import Robot
import time

robot = Robot()
robot.imu.calibrate()

# Try spinning at power 80
robot.set_motors(-80, 80)

start_angle = robot.get_heading()
time.sleep(2.0)
end_angle = robot.get_heading()

robot.stop()

total_rotation = abs(end_angle - start_angle)
rotation_speed = total_rotation / 2.0

print(f"At power 80: {rotation_speed:.1f}°/s")

Expected output (exact value depends on your robot and battery level):

At power 80: 215.3°/s

Record your measurements:

Motor Power Rotation Speed (°/s)
40 ______
60 ______
80 ______
100 ______

📌 Note: To solve this challenge, you need to understand what set_motors(80, 80) actually does to the hardware. You can't solve it by guessing—you need to look inside the box.


Tracing a Function Call

Let's trace what happens when you call robot.forward(80):

Step 1: robot.forward() in robot.py:

def forward(self, speed=80, duration=None):
    self.motors.set_speed(speed, speed)
    if duration:
        time.sleep(duration)
        self.motors.stop()

Step 2: motors.set_speed() in motors.py:

def set_speed(self, left, right):
    self.left.set_speed(left)
    self.right.set_speed(right)

Step 3: Motor.set_speed() in motors.py:

def set_speed(self, speed):
    speed = max(-255, min(255, int(speed)))

    if speed > 0:
        self._fwd.on()
        self._bwd.off()
    elif speed < 0:
        self._fwd.off()
        self._bwd.on()
    else:
        self._fwd.off()
        self._bwd.off()

    duty = int(abs(speed) / 255 * 65535)
    self._pwm.duty_u16(duty)

Step 4: Hardware! - GPIO pins go HIGH or LOW - PWM signal controls motor speed - Motor spins!

Checkpoint

You can trace a call from robot.forward(80) through motors.set_speed() down to the PWM duty cycle calculation. You understand that speed=80 maps to duty = int(80 / 255 * 65535) which equals roughly 20560.

✅ Task 2 - Explore the Source Code

Look at the actual picobot source code on your robot:

from picobot import motors
print(motors.__file__)  # See where it lives

Expected output (path may vary):

/lib/picobot/motors.py
Checkpoint

You can locate the picobot library files on the Pico's filesystem, open motors.py, and identify the Motor class and its set_speed method. You understand that the library is ordinary Python code you can read.

Stuck?

If print(motors.__file__) raises an AttributeError, the module may be a built-in or frozen module. Try import picobot; help(picobot) to list submodules. You can also use Thonny's file browser (left panel) to navigate to /lib/picobot/ directly on the Pico.


Direct Hardware Access

Now let's bypass picobot and talk to the hardware directly. This is what the library does internally—we're just removing the abstraction layer.

Each motor needs 3 wires: - Forward direction pin (GPIO) - Backward direction pin (GPIO) - Speed control pin (PWM)

The H-bridge motor driver uses these signals to control which way current flows through the motor.

from machine import Pin, PWM
import time

# Left motor pins (same as picobot uses internally)
left_fwd = Pin(13, Pin.OUT)    # HIGH = forward path enabled
left_bwd = Pin(12, Pin.OUT)    # HIGH = backward path enabled
left_pwm = PWM(Pin(11))        # PWM controls power
left_pwm.freq(1000)            # 1000 Hz = smooth operation

# How to control:
#   Forward:  FWD=HIGH, BWD=LOW,  PWM=speed
#   Backward: FWD=LOW,  BWD=HIGH, PWM=speed
#   Stop:     FWD=LOW,  BWD=LOW,  PWM=0
#   Brake:    FWD=HIGH, BWD=HIGH (motor shorts!)

# Make the left motor spin forward at 50%
left_fwd.on()
left_bwd.off()
left_pwm.duty_u16(32768)       # 50% duty = 32768/65535

???+ abstract "Math Connection: Linear Mapping Between Ranges"
     The formula $duty = \frac{speed}{255} \times 65535$ is a *linear mapping*converting a value from one range [0, 255] to another [0, 65535]. This is the same math used to normalize data in machine learning (scale to [0, 1]) or convert between coordinate systems in graphics.

     The general formula is: $y = \frac{x - x_{min}}{x_{max} - x_{min}} \times (y_{max} - y_{min}) + y_{min}$. It preserves proportions50% in the input range maps to 50% in the output range.

     📚 [Linear Interpolation (Wikipedia)](https://en.wikipedia.org/wiki/Linear_interpolation) · [Feature Scaling](https://en.wikipedia.org/wiki/Feature_scaling)

time.sleep(2)

# Stop
left_fwd.off()
left_pwm.duty_u16(0)

✅ Task 3 - Direct Motor Control

Run this code. You just controlled a motor without picobot! This is exactly what robot.set_motors() does internally—the library just wraps these low-level calls in a nicer interface.

GPIO and PWM

GPIO = General Purpose Input/Output (digital pins that can be HIGH or LOW) PWM = Pulse Width Modulation (rapidly switching on/off to control average power) → GPIO Basics | → PWM Explained


Understanding the Layers

When you call robot.forward(80), you don't need to know that underneath there are GPIO pins switching on and off thousands of times per second. This is abstraction—hiding complexity behind a simple interface.

Layer Code Thinks In
Your Code robot.forward(80) Behaviors
picobot Library motors.set_speed(80, 80) Motor commands
MicroPython Pin(13).on(); PWM(11).duty_u16(20642) Hardware objects
RP2350 Hardware GPIO_OUT |= (1 << 13) Bits and addresses
Electricity 3.3V on pin 13, PWM signal Voltages
Physics Current → magnetic field → force Wheels spin!

Why layers matter: - Normally: You work at the top layer, ignoring everything below - Today: You learned to "drill down" to lower layers when needed - The skill: Knowing WHICH layer to work at for a given problem

💡 Best Practice: Most of the time, work at the highest layer possible (simplest code). But when you hit a limitation, knowing the layers lets you drop down one level and solve problems the high-level API doesn't support.


Solving the Challenge

Now that you understand the layers, you can solve the 180°/s challenge by combining what you know:

from picobot import Robot
import time

robot = Robot()
robot.imu.calibrate()

# From your measurements, find what power gives ~180°/s
# Use closed-loop control to hit exactly 180°/s!

TARGET_SPEED = 180  # degrees per second
kp = 0.5

motor_power = 60  # Starting guess

while True:
    # Measure actual speed over 0.1 seconds
    start_angle = robot.get_heading()
    robot.set_motors(-motor_power, motor_power)
    time.sleep(0.1)
    end_angle = robot.get_heading()

    actual_speed = abs(end_angle - start_angle) * 10  # °/s

    # Adjust power based on error
    error = TARGET_SPEED - actual_speed
    motor_power = motor_power + kp * error
    motor_power = max(20, min(100, motor_power))  # Clamp

    print(f"Target: {TARGET_SPEED}°/s, Actual: {actual_speed:.1f}°/s, Power: {motor_power:.0f}")
Math Connection: Controlling Velocity, Not Just Position

This is velocity control—regulating the rate of change (\(\omega = \frac{\Delta\theta}{\Delta t}\)) rather than position itself. Your car's cruise control does exactly this: it adjusts throttle to maintain speed, not to reach a specific location.

Advanced systems use cascaded control: an outer loop controls position, which commands a target velocity to an inner loop. Drone flight controllers use three nested loops: position → velocity → attitude → motor commands. Each loop runs faster than the one above it.

📚 Velocity Control · Cruise Control

✅ Task 4 - Closed-Loop Speed Control

Run this and watch it converge on 180°/s. You combined: 1. Understanding how set_motors() works 2. Direct IMU access for measurement 3. Closed-loop control from Seeing the Line!

Expected output (values will vary per robot):

Target: 180°/s, Actual: 245.3°/s, Power: 60
Target: 180°/s, Actual: 210.1°/s, Power: 45
Target: 180°/s, Actual: 172.8°/s, Power: 49
Target: 180°/s, Actual: 181.2°/s, Power: 48
Target: 180°/s, Actual: 179.5°/s, Power: 48

The actual speed should converge toward 180 within a few iterations.

Checkpoint

Your closed-loop controller converges on approximately 180 deg/s (within the +/-10 deg/s tolerance). You can explain why open-loop control (fixed power) is insufficient and how the proportional correction adjusts motor power each iteration.

Stuck?

If modifying a library file causes an ImportError or unexpected behavior, make sure you saved the file to the Pico (not just locally). In Thonny, use "File > Save as" and select "Raspberry Pi Pico" as the destination. If the robot behaves erratically after editing library code, re-upload the original picobot folder from the course materials to restore a known-good state.


Write Your Own Motor Class

Now that you understand how it works, try building it yourself:

from machine import Pin, PWM

class MyMotor:
    """Single motor controller - YOUR VERSION."""

    def __init__(self, fwd_pin, bwd_pin, pwm_pin):
        # TODO: Create Pin objects for fwd and bwd
        # TODO: Create PWM object for speed control
        # TODO: Set PWM frequency to 1000 Hz
        pass

    def set_speed(self, speed):
        # TODO: Clamp speed to valid range (-255 to 255)
        # TODO: Set direction pins based on sign
        # TODO: Set PWM duty based on magnitude
        pass

    def stop(self):
        # TODO: Stop the motor
        pass

# Test it!
if __name__ == "__main__":
    import time
    motor = MyMotor(13, 12, 11)

    print("Forward...")
    motor.set_speed(100)
    time.sleep(1)

    print("Backward...")
    motor.set_speed(-100)
    time.sleep(1)

    print("Stop")
    motor.stop()
Solution
from machine import Pin, PWM

class MyMotor:
    def __init__(self, fwd_pin, bwd_pin, pwm_pin):
        self._fwd = Pin(fwd_pin, Pin.OUT)
        self._bwd = Pin(bwd_pin, Pin.OUT)
        self._pwm = PWM(Pin(pwm_pin))
        self._pwm.freq(1000)
        self.stop()

    def set_speed(self, speed):
        speed = max(-255, min(255, int(speed)))

        if speed > 0:
            self._fwd.on()
            self._bwd.off()
        elif speed < 0:
            self._fwd.off()
            self._bwd.on()
        else:
            self._fwd.off()
            self._bwd.off()

        duty = int(abs(speed) / 255 * 65535)
        self._pwm.duty_u16(duty)

    def stop(self):
        self._fwd.off()
        self._bwd.off()
        self._pwm.duty_u16(0)

What You Learned

  1. picobot is just Python code—you can read it, understand it, modify it
  2. Abstractions hide complexity—but you can peek underneath when needed
  3. Hardware is simple—GPIO pins, PWM signals, that's it
  4. You can rebuild it—understanding lets you customize

"Any sufficiently advanced abstraction is indistinguishable from magic... until you read the source code."

Now picobot isn't magic anymore. It's just well-organized Python.


Recap

Abstractions hide details but can be inspected. You can move between high-level API and hardware control, and reading library code is a core embedded skill.


Challenges

Challenge: Rebuild LineSensors

Write your own line sensor class that calculates error from raw GPIO reads.

Challenge: Add a Feature

Add a new method to picobot (e.g., robot.spin(speed) that spins continuously).

Challenge: Debug Mode

Add a debug flag to Motor that prints every speed change.


➡ Next Steps


← State Machines | Labs Overview | Next: Architecture →