First Drive¶
Your robot is built and both wheels spin the right way. Now let's drive! In
this lesson you'll meet the robot.py library, which turns "set four PWM
pins" into simple commands like robot.forward(). Then you'll drive in a
square and program a dance.
You'll need:
- Your assembled robot (see Build the Robot)
-
robot.pysaved on the ESP32 - Some floor space, away from stairs!
The robot library¶
robot.py
contains two classes. A class is a blueprint for an object that bundles
up some data with the functions that work on it. You've already used
classes like Pin and Button.
robot.py
# robot.py - drive a two-wheeled robot with a DRV8833 motor driver.
#
# Wiring (see the Robot > Build page):
# Left motor: DRV8833 AIN1 -> GPIO 16, AIN2 -> GPIO 17
# Right motor: DRV8833 BIN1 -> GPIO 18, BIN2 -> GPIO 19
#
# Usage:
# from robot import Robot
# robot = Robot()
# robot.forward(60) # speeds are percentages, 0 to 100
# robot.spin_left(50)
# robot.stop()
from machine import Pin, PWM
LEFT_PINS = (16, 17)
RIGHT_PINS = (18, 19)
# If a wheel spins backwards when it should go forwards, set its INVERT to
# True (or swap the motor's two wires on the DRV8833).
LEFT_INVERT = False
RIGHT_INVERT = False
# No two motors are exactly alike. If your robot curves to the right when it
# should go straight, the left motor is faster: set TRIM to a small positive
# number (like 5) to slow it down. Curving left? Use a negative number.
TRIM = 0
# Below about a third of full power, TT motors hum but don't turn. Speed 1
# is mapped to MIN_POWER, so every speed from 1 to 100 actually moves.
MIN_POWER = 35
# 20,000 pulses a second is above human hearing, so the motors don't whine.
PWM_FREQ = 20_000
class Motor:
def __init__(self, pin_a, pin_b, invert=False, min_power=MIN_POWER):
self.pwm_a = PWM(Pin(pin_a), freq=PWM_FREQ, duty_u16=0)
self.pwm_b = PWM(Pin(pin_b), freq=PWM_FREQ, duty_u16=0)
self.invert = invert
self.min_power = min_power
self.speed = 0
def set_speed(self, speed):
# speed: -100 (full reverse) to 100 (full forward), 0 = stop
speed = int(max(-100, min(100, speed)))
self.speed = speed
if self.invert:
speed = -speed
if speed == 0:
self.pwm_a.duty_u16(0) # both inputs low: the motor coasts
self.pwm_b.duty_u16(0)
return
power = self.min_power + (100 - self.min_power) * abs(speed) / 100
duty = int(power * 65535 / 100)
if speed > 0:
self.pwm_a.duty_u16(duty) # pulse one input...
self.pwm_b.duty_u16(0) # ...and hold the other low
else:
self.pwm_a.duty_u16(0)
self.pwm_b.duty_u16(duty)
def stop(self):
self.set_speed(0)
class Robot:
def __init__(self, trim=TRIM):
self.left = Motor(LEFT_PINS[0], LEFT_PINS[1], LEFT_INVERT)
self.right = Motor(RIGHT_PINS[0], RIGHT_PINS[1], RIGHT_INVERT)
self.trim = trim
def drive(self, left_speed, right_speed):
# Set each wheel separately: the basis of every other move
if self.trim > 0:
left_speed = left_speed * (100 - self.trim) / 100
elif self.trim < 0:
right_speed = right_speed * (100 + self.trim) / 100
self.left.set_speed(left_speed)
self.right.set_speed(right_speed)
def forward(self, speed=60):
self.drive(speed, speed)
def backward(self, speed=60):
self.drive(-speed, -speed)
def spin_left(self, speed=50):
# wheels turn opposite ways: the robot turns on the spot
self.drive(-speed, speed)
def spin_right(self, speed=50):
self.drive(speed, -speed)
def stop(self):
self.drive(0, 0)
Motor controls one motor through two driver pins, using the same idea
as set_motor() in the DC Motors lesson, with one
improvement: it maps speeds 1–100 onto 35–100 % power. Remember how the
motor only hummed below about a third of full power? With this mapping,
set_speed(1) means "as slow as possible while still moving", so every
speed you ask for actually does something.
Robot creates two Motor objects, one per wheel, and gives you these
commands. All speeds are percentages from 0 to 100:
| Command | What the wheels do | What the robot does |
|---|---|---|
robot.forward(speed) |
Both forwards | Drives forwards |
robot.backward(speed) |
Both backwards | Reverses |
robot.spin_left(speed) |
Left backwards, right forwards | Turns left on the spot |
robot.spin_right(speed) |
Left forwards, right backwards | Turns right on the spot |
robot.drive(left, right) |
Each wheel set separately, -100 to 100 | Anything! |
robot.stop() |
Both off | Stops |
drive() is the one that does all the work. Every other command is just a
shortcut for it. For example, drive(60, 30) makes the left wheel faster
than the right, so the robot curves to the right.
Your first drive¶
Put the robot on the floor, switch the battery pack on, and run:
# The robot's first drive: forwards, stop, backwards
import time
from robot import Robot
robot = Robot()
time.sleep(3) # time to put the robot on the floor!
robot.forward(60)
time.sleep(1.5)
robot.stop()
time.sleep(0.5)
robot.backward(60)
time.sleep(1.5)
robot.stop()
The three-second pause at the start gives you time to unplug the USB cable
and put the robot down. Or swap to the power bank, save the program as
main.py, and press the ESP32's EN button to start it.
The untethered workflow
- With the USB cable plugged in, save your program to the board as
main.py. - Unplug the USB cable and plug in the power bank.
The ESP32 restarts and runs
main.py. - To try again, press the ESP32's EN (reset) button.
Drive in a square¶
Driving in a square is four times "go straight, then turn 90°". There's no compass on the robot, so it doesn't know how far it has turned. It only knows how long it has been turning. That's called time-based driving.
# Drive in a square: forwards, turn 90 degrees, four times
import time
from robot import Robot
SPEED = 60
SIDE_MS = 1200 # how long to drive along each side
TURN_MS_90 = 450 # how long a 90-degree spin takes: tune this!
robot = Robot()
def drive_for(left, right, ms):
robot.drive(left, right)
time.sleep_ms(ms)
robot.stop()
time.sleep_ms(200) # a short pause makes each move more accurate
time.sleep(3) # put the robot down
for side in range(4):
drive_for(SPEED, SPEED, SIDE_MS) # straight
drive_for(SPEED, -SPEED, TURN_MS_90) # spin right
print("Square complete!")
drive_for()sets the wheels, waits, then stops and pauses. Stopping between moves makes each turn more consistent, because the robot starts every move from standstill.TURN_MS_90is how long a 90° spin takes. It depends on your motors, your batteries and even the floor, so you have to tune it: if the robot turns too far, make it smaller; not far enough, make it bigger.
Why it doesn't come back to exactly where it started¶
However carefully you tune it, your square won't be perfect. That's normal, and it's one of the big challenges in robotics:
- No two motors are identical. One is always a little faster, so the robot drifts to one side when "driving straight".
- Batteries run down, so everything gets slower over time, and your carefully tuned turns come up short.
- Wheels slip, especially on smooth floors or carpet.
You can correct the drift with the TRIM setting at the top of
robot.py. If the robot curves to the right, the left motor is faster, so
set TRIM = 5 (or so) to slow it down. Curving left? Try TRIM = -5.
Adjust until it drives straight for a metre or two.
Real robots solve these problems with sensors: wheel encoders that count how far each wheel has turned, compasses and gyroscopes that measure the heading. For our robot, the sensors in the next few lessons let it react to the world instead of driving blind.
A wheel turns the wrong way?
If you swapped motors or wires since the wheel test, you can fix the
direction in code instead of rewiring: set LEFT_INVERT or
RIGHT_INVERT to True at the top of robot.py.
Challenge: make it dance¶
A dance is just a list of moves played back in order. Each move could be a
(left speed, right speed, milliseconds) tuple. Write a program that plays
through a list like this, and choreograph your own routine!
Show a solution
# A robot dance routine: a list of moves played back in order
import time
from robot import Robot
robot = Robot()
# Each move is (left speed, right speed, milliseconds)
DANCE = [
(60, 60, 500), # forwards
(-60, -60, 500), # backwards
(70, -70, 300), # quick spin right
(-70, 70, 300), # quick spin left
(0, 80, 800), # swing around the left wheel
(80, 0, 800), # swing around the right wheel
(100, -100, 1200), # big finish: full spin!
]
time.sleep(3)
for left, right, ms in DANCE:
robot.drive(left, right)
time.sleep_ms(ms)
robot.stop()
Storing the moves as data in a list, separate from the code that plays them, makes it easy to write new routines without touching the loop. Try adding the buzzer from the Buzzers lesson so it plays a note with each move.
Next up: give your robot eyes. Obstacle Avoider.