Skip to content

Advanced 14: Drive Straight

Prerequisites: Advanced 13 (Encoder Fundamentals) Time: ~60 min


Overview

In the Motor Control lab, your robot couldn't drive a straight line — two motors at "the same" PWM always drift apart. Now that you have calibrated encoders, you can measure the drift and correct it in real time.

This module builds a differential speed controller: independent P-controllers for left and right wheels that keep their tick rates matched.

You'll learn:

  • How to quantify straight-line drift with encoder data
  • Why matched PWM ≠ matched speed (motor manufacturing tolerance, friction, battery sag)
  • How to build independent left/right speed controllers
  • How to verify improvement with a repeatable measurement

Part 1: Quantify the Problem (~15 min)

Task 1 — Measure Open-Loop Drift

Drive the robot at equal PWM for both wheels, measure how many ticks each wheel produces:

from picobot import Robot
import time

robot = Robot(encoders=True)
robot.encoders.reset()

PWM = 80
DURATION = 3  # seconds

print(f"Open-loop test: both motors at PWM {PWM}")
input("Place robot on a flat surface. Press Enter...")

robot.set_motors(PWM, PWM)
time.sleep(DURATION)
robot.stop()

left_ticks = robot.encoders.left.ticks
right_ticks = robot.encoders.right.ticks
diff = left_ticks - right_ticks
diff_pct = (diff / max(abs(left_ticks), abs(right_ticks), 1)) * 100

print(f"Left:  {left_ticks} ticks")
print(f"Right: {right_ticks} ticks")
print(f"Difference: {diff} ticks ({diff_pct:+.1f}%)")
print()
if abs(diff_pct) > 5:
    print("Significant drift — closed-loop control will help!")
else:
    print("Your motors are unusually well-matched. Lucky!")

Run 3 times and record:

Run Left ticks Right ticks Difference Drift direction
1 left / right
2
3
Note

Even 5% difference between wheels causes visible drift over 1 meter. This isn't a defective robot — it's normal. Motor manufacturing tolerance, bearing friction, and even floor surface cause asymmetry.

Task 2 — Measure Physical Drift

Mark a starting line on the floor. Drive the robot at PWM 80 for 3 seconds and measure:

  • Distance traveled: ______ cm
  • Lateral drift from straight line: ______ cm

This is your open-loop baseline. You'll compare against it at the end.


Part 2: Close the Loop (~20 min)

The Idea

Instead of commanding equal PWM (and hoping), command a target speed and let each wheel's controller adjust its own PWM to match:

Target speed ──►(+)──► P-Controller LEFT  ──► PWM_L ──► Motor L ──┐
                │-│                                                │
                ▲                                                  │
                └──────────────── Encoder L ◄──────────────────────┘

Target speed ──►(+)──► P-Controller RIGHT ──► PWM_R ──► Motor R ──┐
                │-│                                                │
                ▲                                                  │
                └──────────────── Encoder R ◄──────────────────────┘

Two independent controllers, one per wheel. Each corrects its own motor.

Task 3 — Dual-Wheel Speed Controller

from picobot import Robot
import time

robot = Robot(encoders=True)

TARGET_SPEED = 40   # ticks/sec — adjust for your robot
KP = 5             # Start small
pwm_left = 0.0
pwm_right = 0.0

print(f"Closed-loop: target {TARGET_SPEED} tps, Kp = {KP}")
input("Place robot on starting line. Press Enter...")

robot.encoders.reset()

try:
    for i in range(20):  # ~5 seconds at 50 ms intervals
        robot.encoders.update()

        # Left controller
        error_l = TARGET_SPEED - robot.encoders.left.speed_tps
        pwm_left = max(0, min(255, pwm_left + KP * error_l))

        # Right controller
        error_r = TARGET_SPEED - robot.encoders.right.speed_tps
        pwm_right = max(0, min(255, pwm_right + KP * error_r))

        robot.set_motors(int(pwm_left), int(pwm_right))

        if i % 20 == 0:
            print(f"L: {robot.encoders.left.speed_tps:6.0f} tps "
                  f"(PWM {pwm_left:5.1f})  "
                  f"R: {robot.encoders.right.speed_tps:6.0f} tps "
                  f"(PWM {pwm_right:5.1f})")

        time.sleep(0.05)

finally:
    robot.stop()

# Final tick comparison
left_ticks = robot.encoders.left.ticks
right_ticks = robot.encoders.right.ticks
diff = left_ticks - right_ticks
print(f"\nLeft: {left_ticks}  Right: {right_ticks}  Diff: {diff}")

Task 4 — Tune Kp

Run the controller with different Kp values and observe:

Kp Left-right tick difference Behavior
0.01
0.05
0.1
0.3
0.5
What to Look For
  • Too low Kp: Speed converges slowly, wheels may still drift noticeably
  • Good Kp: Both wheels reach target quickly, small tick difference, smooth motion
  • Too high Kp: Jerky motion, PWM values oscillate, robot may vibrate

Part 3: Verify the Improvement (~15 min)

Task 5 — Side-by-Side Comparison

Repeat the physical measurement from Task 2, now with closed-loop control:

Test Distance (cm) Lateral drift (cm) Left-right tick diff
Open-loop (PWM 80, 80)
Closed-loop (encoder feedback)

Task 6 — The 1-Meter Challenge

Mark a 1-meter track on the floor. Drive the robot along it and measure lateral deviation at the end:

from picobot import Robot
import time

robot = Robot(encoders=True)

# === YOUR CALIBRATION ===
MM_PER_TICK = ___  # From Module 13
TARGET_DISTANCE_MM = 1000
# ========================

TARGET_SPEED = 400
KP = ___  # Your tuned value from Task 4
pwm_left = 0.0
pwm_right = 0.0

print("1-meter straight line challenge")
input("Align robot at start. Press Enter...")

robot.encoders.reset()

try:
    while True:
        robot.encoders.update()

        # Check if we've traveled far enough (average of both wheels)
        avg_ticks = (robot.encoders.left.ticks + robot.encoders.right.ticks) / 2
        distance_mm = avg_ticks * MM_PER_TICK

        if distance_mm >= TARGET_DISTANCE_MM:
            break

        # Speed controllers
        error_l = TARGET_SPEED - robot.encoders.left.speed_tps
        pwm_left = max(0, min(255, pwm_left + KP * error_l))

        error_r = TARGET_SPEED - robot.encoders.right.speed_tps
        pwm_right = max(0, min(255, pwm_right + KP * error_r))

        robot.set_motors(int(pwm_left), int(pwm_right))
        time.sleep(0.05)

finally:
    robot.stop()

print(f"Encoder distance: {distance_mm:.0f} mm")
print(f"Left ticks: {robot.encoders.left.ticks}")
print(f"Right ticks: {robot.encoders.right.ticks}")
print(f"Measure lateral drift with a ruler: ______ cm")

Goal: less than 2 cm lateral drift over 1 meter.

Checkpoint — Straight Line Achieved

Compare your open-loop drift from Task 2 to your closed-loop result. The improvement should be dramatic. The robot still won't be perfect — there's no heading feedback yet (that comes in Module 17 with encoder-based turns). But matched wheel speeds eliminate the largest source of drift.


What You Discovered

Concept What You Learned
Motor asymmetry Equal PWM ≠ equal speed — this is normal, not a defect
Independent controllers One P-controller per wheel, each correcting its own motor
Measurable improvement Quantify before/after with tick differences and ruler measurements
Distance-based stopping Use encoder odometry to stop at a target distance, not a time

What's Next?

You've driven a straight line with matched speeds. Next: Drive to Distance — use encoder odometry to command exact distances with smooth velocity profiles.


← Back to Advanced Topics