Skip to content

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.py saved 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
# 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:

drive_forward.py
# 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

  1. With the USB cable plugged in, save your program to the board as main.py.
  2. Unplug the USB cable and plug in the power bank. The ESP32 restarts and runs main.py.
  3. 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_square.py
# 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_90 is 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
dance.py
# 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.