Distance Measurement Methods
How to measure distance traveled using different sensor approaches, step by step.
Overview
There are three main approaches to measure distance with our robot:
| Method | Sensor | Accuracy | Drift | Best For |
|---|---|---|---|---|
| Ultrasonic | HC-SR04 | High | None | Absolute distance to wall |
| Bump Counting | Accelerometer | Good | Low | Relative distance traveled |
| Double Integration | Accelerometer | Low | High | Short movements only |
Method 1: Ultrasonic Distance (Simplest)
How It Works
The ultrasonic sensor measures distance to the nearest obstacle:
Formula: $\(distance = \frac{time \times speed\_of\_sound}{2}\)$
Where speed of sound ≈ 343 m/s at 20°C.
Step-by-Step
Step 1: Read distance at start
Step 2: Drive robot forward
Step 3: Read distance at end
Step 4: Calculate travel distance
Advantages
- No calibration needed
- Absolute measurement
- No drift over time
Limitations
- Needs a wall/obstacle in front
- Only measures forward/backward
- Max range ~400 cm
Code Example
def measure_distance_ultrasonic():
"""Measure distance traveled using ultrasonic sensor."""
# Read starting position
start = ultrasonic.measure_cm()
print(f"Start distance: {start:.1f} cm")
# Wait for user to drive robot
input("Drive the robot, then press Enter...")
# Read ending position
end = ultrasonic.measure_cm()
print(f"End distance: {end:.1f} cm")
# Calculate travel
traveled = abs(start - end)
print(f"Distance traveled: {traveled:.1f} cm")
return traveled
Method 2: Bump Counting (Best for Wheels)
How It Works
Wheel imperfections create vibrations detected by the accelerometer:
Wheel cross-section:
___
/ \ ← bump hits ground
| ● |
\___/
As wheel rotates: bump → accelerometer spike → count it
Bumps per rotation: ~10-20 (depends on wheel quality)
Formula: $\(distance = \frac{bump\_count}{bumps\_per\_cm}\)$
Where bumps_per_cm is determined by calibration.
Step-by-Step
Step 1: Record calibration run
Drive robot a known distance (e.g., 50 cm measured with ruler):
# Drive toward wall (using ultrasonic as reference)
start_us = ultrasonic.measure_cm()
motors.set_speed(50, 50)
time.sleep(2)
motors.stop()
end_us = ultrasonic.measure_cm()
known_distance = start_us - end_us
Step 2: Count bumps during run
Apply signal processing:
# 1. Calculate acceleration magnitude
mag = sqrt(ax**2 + ay**2 + az**2) - 1.0
# 2. High-pass filter (remove DC offset)
filtered = alpha * (prev_filtered + mag - prev_mag)
# 3. Count threshold crossings
if filtered > threshold and not was_above:
bump_count += 1
Step 3: Calculate calibration ratio
Step 4: Use calibration for future measurements
Why This Works
- Bumps are consistent (same wheel, same surface)
- High-pass filter isolates bumps from other motion
- Similar to how a car odometer works
Calibration Required
Different surfaces and speeds may need different calibration values.
Signal Processing Pipeline
Raw Accelerometer → Magnitude → High-Pass Filter → Threshold → Count
│ │ │ │ │
ax,ay,az |a|-1.0 isolate bumps detect edge N bumps
(remove g) (remove drift) (rising)
Visualize with:
Complete Calibration Code
class BumpOdometer:
"""Distance estimation from bump counting."""
def __init__(self, bumps_per_cm=5.0):
self.bumps_per_cm = bumps_per_cm
self.bump_count = 0
self.threshold = 0.015
self.hp_alpha = 0.95
# Filter state
self.prev_mag = 0
self.prev_filtered = 0
self.was_above = False
def process_sample(self, ax, ay, az):
"""Process one accelerometer sample, return bump if detected."""
# Step 1: Magnitude minus gravity
mag = math.sqrt(ax**2 + ay**2 + az**2) - 1.0
# Step 2: High-pass filter
filtered = self.hp_alpha * (self.prev_filtered + mag - self.prev_mag)
self.prev_mag = mag
self.prev_filtered = filtered
# Step 3: Threshold crossing (rising edge)
is_above = filtered > self.threshold
bump_detected = is_above and not self.was_above
self.was_above = is_above
if bump_detected:
self.bump_count += 1
return bump_detected
def get_distance(self):
"""Get estimated distance in cm."""
return self.bump_count / self.bumps_per_cm
def calibrate(self, known_distance_cm, bump_count):
"""Set calibration from known distance run."""
self.bumps_per_cm = bump_count / known_distance_cm
print(f"Calibrated: {self.bumps_per_cm:.3f} B/cm")
Method 3: Double Integration (Physics-Based)
How It Works
From physics: - Acceleration → integrate → velocity - Velocity → integrate → position
In discrete time:
Why It's Problematic
Drift Problem
Any small constant error in acceleration accumulates rapidly:
- 0.01g error over 1 second = 0.1 m/s velocity error
- Over 10 seconds = 1 m/s = 100 cm/s drift!
When It Works
Double integration CAN work for: - Very short movements (< 1 second) - High-quality IMU with low noise - Movements that return to zero velocity (can detect and reset)
Implementation (With Caveats)
class DoubleIntegrator:
"""
Distance from acceleration integration.
WARNING: Drifts significantly. Only use for:
- Short movements (< 1s)
- Detecting motion vs stopped
- Rough distance estimates
"""
def __init__(self, sample_rate=500):
self.dt = 1.0 / sample_rate
self.velocity = 0.0
self.position = 0.0
# Bias correction
self.accel_bias = 0.0
self.calibrated = False
def calibrate_bias(self, samples):
"""
Calibrate accelerometer bias when robot is stationary.
Args:
samples: List of (ax, ay, az) when robot is still
"""
total = 0
for ax, ay, az in samples:
# Forward axis acceleration (assuming X is forward)
total += ax
self.accel_bias = total / len(samples)
self.calibrated = True
print(f"Accel bias: {self.accel_bias:.4f} g")
def process_sample(self, ax):
"""
Process one sample, integrate acceleration.
Args:
ax: Forward acceleration in g
"""
# Remove bias
accel_corrected = ax - self.accel_bias
# Convert g to m/s²
accel_ms2 = accel_corrected * 9.81
# Integrate: acceleration → velocity
self.velocity += accel_ms2 * self.dt
# Integrate: velocity → position
self.position += self.velocity * self.dt
def get_distance_cm(self):
"""Get position in cm."""
return self.position * 100 # m to cm
def reset(self):
"""Reset when robot stops."""
self.velocity = 0
self.position = 0
Practical Reality
In practice, double integration gives: - Errors of 10-50% over a few seconds - Completely unusable after 10+ seconds - Useful only for detecting "did it move?" not "how far?"
Method 4: Combined Approach (Best)
Sensor Fusion
Use multiple methods together:
┌──────────────────────────────────────────────────────┐
│ Sensor Fusion │
│ │
│ Ultrasonic ───► Absolute reference (when available) │
│ │ │
│ ▼ │
│ Bump Count ───► Relative distance (always works) │
│ │ │
│ ▼ │
│ Integration ──► Short-term smoothing │
│ │
│ Output: Best estimate of distance traveled │
└──────────────────────────────────────────────────────┘
Implementation
class FusedOdometer:
"""Combines multiple distance estimation methods."""
def __init__(self):
self.bump_odo = BumpOdometer(bumps_per_cm=5.0)
self.last_us = None
self.us_travel = 0
def process_sample(self, ax, ay, az, us_cm):
"""Process sensors, update distance estimate."""
# Method 1: Bump counting (always)
self.bump_odo.process_sample(ax, ay, az)
# Method 2: Ultrasonic (when valid)
if us_cm > 0 and us_cm < 400:
if self.last_us is not None:
delta = self.last_us - us_cm
if abs(delta) < 10: # Sanity check
self.us_travel += delta
self.last_us = us_cm
def get_distance(self):
"""
Get best distance estimate.
Uses ultrasonic when available, falls back to bumps.
"""
bump_dist = self.bump_odo.get_distance()
# If ultrasonic agrees with bumps, use ultrasonic (more accurate)
if abs(self.us_travel - bump_dist) < bump_dist * 0.2:
return self.us_travel
# Otherwise, bumps are more reliable
return bump_dist
def calibrate_from_ultrasonic(self):
"""Use ultrasonic measurement to calibrate bump ratio."""
if self.us_travel > 10: # Need enough distance
self.bump_odo.calibrate(
self.us_travel,
self.bump_odo.bump_count
)
Practical Comparison
Experiment Setup
- Mark a 1-meter track on floor
- Drive robot start to end
- Compare methods
Typical Results
| Method | Measured | Error | Notes |
|---|---|---|---|
| Ruler (truth) | 100.0 cm | - | Ground truth |
| Ultrasonic | 99.2 cm | 0.8% | Needs wall |
| Bump count | 103.5 cm | 3.5% | After calibration |
| Integration | 145 cm | 45% | Drift accumulated |
When to Use Each
Ultrasonic: - Measuring distance to obstacle - Calibrating other methods - Detecting approaching wall
Bump Counting: - Measuring distance traveled - Works without obstacles - Good for speed estimation too
Double Integration: - Detecting motion start/stop - Very short movements - As input to more complex filters (Kalman)
Exercise: Calibrate Your Robot
Step 1: Setup
# Deploy robot code
ampy --port /dev/ttyACM0 put src/robot/pico_collector.py main.py
# Start host collector
cd src/host && python host_collector.py
Step 2: Collect Calibration Data
- Position robot 50+ cm from wall
- Press R to start recording
- Drive forward until ~10 cm from wall
- Press R to stop recording
- Press E to export
Step 3: Analyze
Look at the output:
Ultrasonic Distance:
Start: 48.5 cm
End: 12.3 cm
Travel: 36.2 cm
Bump Count:
Detected: 189 (from accelerometer)
==================================================
BUMPS PER CM = 5.221
==================================================
Step 4: Verify
Do another run and check if the B/cm ratio predicts correctly:
predicted_distance = new_bumps / 5.221
actual_distance = us_start - us_end
error = abs(predicted_distance - actual_distance) / actual_distance * 100
print(f"Prediction error: {error:.1f}%")
Summary
| Scenario | Best Method |
|---|---|
| Distance to wall | Ultrasonic |
| How far did I drive? | Bump counting |
| Am I moving? | Any (including integration) |
| Speed estimation | Bump rate or vibration |
| High precision | Fused + calibration |
Next Steps
- [[Reference/extras/signal-processing/basics|Signal Processing Basics]] - Math behind the algorithms
- [[Reference/extras/signal-processing/robot-data-pipeline|Robot Data Pipeline]] - How data flows through the system
- [[Reference/07-data-analysis/analyzers-reference|Analyzers Reference]] - CLI tools for analysis