← All work
Project 004 · 2026

Deadreckoner

An autonomous ground rover that holds a heading without GPS, using a fused IMU estimate and a closed-loop PID controller.

RoboticsESP32Sensor FusionPID
ControllerESP32-S3
IMUMPU6050
Drive2× N20 / 150 RPM
Loop rate200 Hz
Heading err±1.8°
Runtime48 min
HEADING θ · GREEN = TOLERANCE BAND

The idea: A rover that knows where it is pointing

Indoor robots lose GPS the moment they cross a doorway. Most hobby platforms paper over this by driving in short bursts and hoping the wheels behave. They don’t — motor variance, tire compression and floor texture all push the platform off course within a couple of metres.

Deadreckoner was a bet that a $6 IMU and an honest control loop could hold a straight line across a 20 m corridor better than a much more expensive encoder-only platform.

The problem: Two sensors, both wrong

The MPU6050 gives you a gyroscope and an accelerometer. The gyro is precise over short windows but drifts — integrate its rate over a minute and your heading is meaningless. The accelerometer doesn’t drift, but it reads every bump in the floor as a tilt.

Neither one alone produces a usable heading. The whole project rests on combining them so each covers the other’s failure.

Constraint

No magnetometer. The drive motors sit 40 mm from the sensor board and swamp any magnetic heading reference with their own field.

The approach: Complementary filter, then close the loop

We fuse the two sources with a complementary filter — a high-pass on the gyro integration, a low-pass on the accelerometer-derived angle. It is not a Kalman filter and does not pretend to be; at 200 Hz with this noise profile it lands within a degree of one and costs a fraction of the compute.

That fused heading becomes the process variable for a PID controller. The setpoint is the heading we want. The output is a differential term added to one motor and subtracted from the other.

// heading fusion — alpha weights the gyro
float alpha = 0.98f;
angle = alpha * (angle + gyroZ * dt) + (1.0f - alpha) * accAngle;

// PID on heading error, wrapped to ±180°
float err = wrap180(setpoint - angle);
integral += err * dt;
float out = Kp*err + Ki*integral + Kd*((err - prevErr)/dt);

The build: Three boards, one chassis

The stack is deliberately boring: an ESP32-S3 carrier, a TB6612FNG motor driver, and the IMU on a short ribbon away from the motors. Power is a 2S pack through a buck converter, with the logic rail decoupled from the motor rail so stall current doesn’t brown out the controller mid-correction.

Everything is logged over UART at 50 Hz to a laptop. Without that log the rest of this project would not have happened.

ERR +12°0 m → 20 m
Fig. 2 — Heading error across a 20 m corridor run. Grey: open loop. Black: closed loop. Green band: ±1.8° tolerance.

What broke: It kept turning right

The first twelve runs all veered right. Consistently, repeatably, about 4° per metre. We replaced the motors. We swapped the driver. We re-balanced the chassis. It kept turning right.

The fault was in the integral term. The gyro had a small constant bias — about 0.9 °/s — that the complementary filter’s accelerometer correction could not fully absorb while the rover was moving. The integrator faithfully accumulated that bias into a permanent steering offset.

Root cause

Uncalibrated gyro bias, integrated. The fix was 200 stationary samples at boot, averaged and subtracted from every subsequent read — eight lines of code after four evenings of suspecting the hardware.

The result: ±1.8° over twenty metres

After bias calibration and a retune, heading error settled within ±1.8° across a 20 m run, with no cumulative veer. The rover now drives corridor legs reliably enough to be a platform for the next problem rather than the problem itself.

Run condition Mean err Peak err Veer / 20 m
Open loop — — 78 cm
PID, uncalibrated 4.6° 11.2° 63 cm
PID, bias corrected 0.7° 1.8° 4 cm

What we learned: Suspect the software you trust

Every instinct on this build pointed at hardware, because the symptom was mechanical and repeatable. A consistent physical bias feels like a physical cause. It wasn’t.

The lasting change is procedural: every sensor on every subsequent Blue Bird build now gets a boot-time calibration routine and a logged bias figure, whether or not we think it needs one.

Carried forward

Boot calibration is now a standing item on the studio’s bring-up checklist. Three later projects caught bias faults before first drive because of it.