Skip to content

Advanced 11: Motion Calibration

Prerequisites: Advanced 09 (Encoders & Speed Control) Time: ~120 min


Overview

In Advanced 09, you calibrated a single encoder by manually counting ticks per revolution. That's a good start — but every robot is slightly different. Gear ratios vary, motors have different friction characteristics, wheels wear unevenly, and battery voltage changes over time.

This module builds a complete calibration pipeline that measures your robot's actual behavior using multiple sensors, profiles each motor individually, and validates the results statistically.

You'll learn:

  • How to calibrate encoder-to-distance mapping with multiple methods
  • How to detect motor dead zones and build speed curves
  • How to measure and correct left-right motor imbalance
  • How to validate calibration quality statistically
  • How to save per-robot calibration data persistently

Part 1: Why Calibrate? (~10 min)

Every Robot is Different

Two "identical" robots from the same production batch will behave differently:

Source of Variation Effect
Gear backlash Different distance per tick
Motor winding tolerance Different speed at same PWM
Wheel diameter difference Curved motion instead of straight
Friction and bearing wear Asymmetric drag
Battery voltage Speed changes as battery drains

If you set both motors to PWM 120, they won't actually spin at the same speed. One robot might need set_speed(120, 115) to drive straight while another needs set_speed(120, 125).

What Calibration Gives You

After calibration, you'll have a per-robot profile stored in calibration.json:

{
  "distance": { "mm_per_tick": 0.123, "method": "ultrasonic" },
  "motors": {
    "left":  { "dead_zone": 28, "speed_curve": [[60, 380], [80, 640]] },
    "right": { "dead_zone": 32, "speed_curve": [[60, 370], [80, 635]] },
    "balance_drift_dps": 2.3
  },
  "validation": { "distance_error_pct": 1.8, "grade": "A" }
}

This data enables:

  • Accurate distance control: Drive exactly 500 mm, not "about half a meter"
  • Dead-zone compensation: Skip the PWM range where motors don't move
  • Motor balancing: Automatically trim left-right speed for straight driving
  • Quality assurance: Know how accurate your robot is
Calibration Files

The calibration scripts in this module are located in src/picobot/calibration/. They all use shared helper functions from cal_utils.py and save results to calibration.json on the Pico's filesystem.


Part 2: Basic Distance Calibration (~25 min)

The fundamental calibration value is mm_per_tick — how many millimeters the robot moves per encoder tick. Advanced 09 computed this from PPR and wheel circumference. Now we measure it directly.

Setup

The calibration scripts use the same picobot library. Copy the calibration/ folder to your Pico:

mpremote connect /dev/ttyACM0 cp -r calibration/ :calibration/

Task 1 — Ruler Method

The simplest method: drive, measure, divide.

# Runs automatically when imported:
import calibration.distance_cal

The script shows an interactive menu. Choose Method A (Ruler):

  1. Place the robot at a marked start position
  2. The robot drives forward at PWM 120 for 3 seconds
  3. Measure the distance traveled with a ruler
  4. Enter the measurement when prompted

The script computes:

\[\text{mm\_per\_tick} = \frac{\text{distance (mm)}}{\text{average ticks}}\]

Record your result:

Value Measurement
Encoder ticks (left)
Encoder ticks (right)
Distance measured (mm)
mm_per_tick
Measurement Tips
  • Start and stop marks should be at the wheel contact point, not the front of the robot
  • Use a long straight ruler or tape measure
  • Repeat 2-3 times — if results vary by more than 5%, your measurement technique needs work

Task 2 — Ultrasonic Wall Method

No ruler? Use the robot's own ultrasonic sensor as ground truth.

Choose Method B (Ultrasonic) from the menu:

  1. Face the robot toward a flat wall, about 80 cm away
  2. The script reads baseline distance (median of 5 readings)
  3. The robot drives backward (away from wall) for 3 seconds
  4. The script reads the new distance

The computation:

\[\Delta d = (d_{end} - d_{start}) \times 10 \text{ (cm → mm)}\]
\[\text{mm\_per\_tick} = \frac{\Delta d}{\text{average ticks}}\]

Record your result:

Value Measurement
Start distance (cm)
End distance (cm)
Delta (mm)
Average ticks
mm_per_tick
Why Backward?

We drive the robot away from the wall (backward) to avoid the ultrasonic dead zone (< 2 cm) and to ensure the distance always increases.

Tip

Compare your ruler and ultrasonic results. They should agree within 5%. If they don't, the ultrasonic wall surface may not be ideal — try a large, flat, hard wall (not fabric or rough material).


Part 3: Advanced Calibration Methods (~20 min)

Task 3 — Opto Track Calibration

The most accurate method uses a striped calibration track:

Track specification: - Alternating black and white stripes - Each stripe is 5 mm wide (10 mm per cycle) - At least 30 cm total length - Print on paper or use electrical tape on white cardboard

The robot drives over the track slowly while the first line sensor (GP2) counts 0→1 and 1→0 transitions (edges). Each edge corresponds to exactly 5 mm.

Choose Method C (Opto) from the menu.

How it works:

Track:  ██████░░░░░░██████░░░░░░██████░░░░░░
Sensor:  1  0  1  0  1  0  1  0  1  0
Edges:    ↑    ↑    ↑    ↑    ↑    ↑
Count:    1    2    3    4    5    6  → 6 edges × 5mm = 30mm
\[\text{mm\_per\_tick} = \frac{\text{edges} \times 5}{\text{average ticks}}\]
Note

The opto method uses a slower PWM (80) for better edge accuracy. At least 4 edges must be detected for a valid measurement.

Record your result:

Value Measurement
Edges detected
Track distance (mm)
Average ticks
mm_per_tick

Task 4 — Cross-Validate All Methods

Run all available methods and choose Q to see the comparison table:

========================================
  Method Comparison
========================================

  Method       mm/tick
  ------------ ----------
  ruler          0.1234
  ultrasonic     0.1228
  opto           0.1231

  Mean: 0.1231  Std: 0.0003  Range: 0.0006
Checkpoint — Methods Agree

If all methods agree within 2-3%, your calibration is solid. The script automatically saves the best result (preferring opto > ToF > ultrasonic > ruler).

Prediction exercise: Before running, predict which method you think will be most accurate and why. Record your prediction, then compare with results.

Method Predicted Accuracy Actual mm/tick Notes
Ruler
Ultrasonic
Opto

Part 4: Motor Characterization (~25 min)

Distance calibration tells you how far each tick goes. Motor characterization tells you how each motor behaves at different power levels.

Task 5 — Dead Zone Detection

Every motor has a "dead zone" — a range of PWM values too low to overcome static friction. Sending PWM 10 to a motor wastes power without producing motion.

import calibration.motor_profile

The script ramps each motor's PWM from 0 to 80 in steps of 2, waiting 200 ms per step. The first PWM where the encoder detects motion (> 5 ticks/sec) is the dead zone boundary.

Record your measurements:

Motor Dead Zone PWM Notes
Left
Right
Tip

Dead zones are typically 20-40 PWM. If a motor's dead zone is above 50, check for mechanical binding or low battery.

Warning

Dead zones change with battery level! A freshly charged battery has lower dead zones. Consider re-running this test at different battery levels.

Task 6 — Speed Curve

The script tests each motor at multiple PWM levels and records the average speed in ticks per second:

========================================
  Speed Curve: left motor
========================================
    PWM      tps
  -----  --------
     38     120.5
     60     380.2
     80     640.8
    100     850.3
    120    1020.5
    160    1380.1
    200    1640.3
    255    1850.6

Record the speed curve for your motors:

PWM Left tps Right tps
dead_zone + 10
60
80
100
120
160
200
255
Analyzing the Curve

The speed curve reveals:

  • Linearity: Is speed proportional to PWM? (Roughly yes in the middle range, but saturates at the top)
  • Matching: Do left and right give similar speeds at the same PWM?
  • Maximum speed: The flat region at high PWM is where the motor is saturated

If you've studied Advanced 07 — Data-Driven Methods, you can fit a polynomial to this data!

Task 7 — Balance Test

The script calibrates the IMU, drives both motors at PWM 120 for 3 seconds, and measures heading drift:

  Total heading drift: 4.2 deg
  Drift rate: 1.4 deg/s
  Acceptable — small trim may help.

Record your result:

Measurement Value
Heading drift (deg)
Drift rate (deg/s)
Suggested correction
Note
  • < 1 deg/s: Excellent balance — no correction needed
  • 1-3 deg/s: Acceptable, but a small PWM trim improves straight-line driving
  • > 3 deg/s: Significant imbalance — reduce the faster motor's PWM by drift_rate × 2
Using Balance Data

If your right motor is faster (robot drifts left, positive heading), reduce right PWM:

# Instead of:
robot.set_motors(120, 120)
# Use:
trim = 3  # From calibration data
robot.set_motors(120, 120 - trim)


Part 5: Validation (~20 min)

Calibration without validation is just guessing with extra steps. This part measures how accurate your calibrated robot actually is.

Task 8 — Distance Accuracy Test

The validation script uses your mm_per_tick from Part 2 to command exact distances:

import calibration.validate

The test procedure (repeated 5 times):

  1. Face a flat wall at ~60 cm
  2. Read baseline distance (ultrasonic or ToF)
  3. Drive forward exactly 200 mm by encoder count
  4. Read end distance
  5. Compare: actual distance moved vs commanded 200 mm
========================================
  Distance Accuracy (5 trials)
========================================
  Commanded: 200 mm per trial

  Trial    Start      End   Actual    Error
  -----  -------  -------  -------  -------
      1      602      405     197   -1.5%
      2      598      401     197   -1.5%
      3      605      402     203   +1.5%
      4      601      399     202   +1.0%
      5      603      401     202   +1.0%

Record your data:

Trial Start (mm) End (mm) Actual (mm) Error (%)
1
2
3
4
5

Task 9 — Turn Accuracy Test

The validation script also tests turn accuracy using IMU feedback:

  1. Calibrate IMU (keep robot still!)
  2. Command 90° turn using IMU heading
  3. Record final heading vs 90° target
  4. Repeat 5 times, alternating with return turns

Record your data:

Trial Actual (deg) Error (deg)
1
2
3
4
5

Grading

The script assigns a letter grade based on error statistics:

Grade Distance Error Turn Error Meaning
A < 2% < 2° Excellent — competition-ready
B < 5% < 5° Good — reliable for most tasks
C < 10% < 10° Acceptable — needs improvement
F > 10% > 10° Re-calibrate!

Your grade: ______

Not Getting Grade A?

Common causes of poor accuracy:

  • Wheel slippage on smooth surfaces — try rougher surface or add rubber bands to wheels
  • Stale calibration — battery level changed since calibration
  • Ultrasonic noise — use ToF sensor for better validation or use a harder wall surface
  • IMU drift — make sure calibration runs on a completely still robot

Part 6: Per-Robot Calibration File (~10 min)

Task 10 — Save, Load, and Use

After running all three scripts (distance_cal, motor_profile, validate), your calibration.json contains a complete robot profile. Verify it:

import json

with open("calibration.json") as f:
    cal = json.load(f)

print(json.dumps(cal, indent=2))

Use the calibration in your own scripts:

import json

# Load calibration
with open("calibration.json") as f:
    cal = json.load(f)

mm_per_tick = cal["distance"]["mm_per_tick"]
left_dz = cal["motors"]["left"]["dead_zone"]
right_dz = cal["motors"]["right"]["dead_zone"]

# Drive exactly 300mm
from picobot import Robot
robot = Robot(encoders=True)

target_ticks = 300 / mm_per_tick
robot.encoders.reset()

# Use dead zone info: start above dead zone
robot.motors.set_speed(max(80, left_dz + 20), max(80, right_dz + 20))

import time
while True:
    left = abs(robot.encoders.left.ticks)
    right = abs(robot.encoders.right.ticks) if robot.encoders.right else left
    if (left + right) / 2 >= target_ticks:
        break
    time.sleep(0.01)

robot.motors.stop()
print(f"Driven 300 mm ({target_ticks:.0f} ticks)")
Calibration Workflow

Establish this routine:

  1. Beginning of each lab session: Run distance_cal.py (Method B — fast, no ruler)
  2. After swapping motors or wheels: Run full motor_profile.py
  3. Before competitions or demos: Run validate.py to confirm accuracy
  4. Keep your calibration.json — it's your robot's "passport"

What You Discovered

Concept What You Learned
Manufacturing variation No two robots are identical
Multi-sensor calibration Different sensors give different accuracy
Dead zones Motors need minimum PWM to overcome friction
Speed curves Motor response is non-linear
Balance testing Equal PWM ≠ equal speed
Statistical validation Calibration quality needs measurement
Persistent configuration Per-robot data enables portability

Connections to Other Modules

Mini-Project

Calibrate your robot to drive within 2% distance accuracy (Grade A).

Requirements:

  1. Run at least 2 distance calibration methods and cross-validate
  2. Complete motor profiling for both motors
  3. Achieve Grade A or B on the validation test
  4. Demonstrate driving a precise distance (e.g., exactly 500 mm to a wall)
  5. Show your calibration.json and explain each value

Further Reading

  • The Handbook of Small Electric Motors — Chapter on DC motor characterization
  • Probabilistic Robotics (Thrun et al.) — Chapter 5: Odometry calibration
  • Embedded Systems: Introduction to Arm Cortex-M Microcontrollers (Valvano) — Motor control and calibration

← Back to Advanced Topics