Making the ProtoBot Self-Balance Forever

2s → 60shands-free balance
1.2 KBonboard brain
5multiplications in final formula
52firmware versions in 5 days
The ProtoBot balancing on a desk in front of its live dashboard

1:07 · watch on YouTube

I made the wonderful ProtoBot self-balance as a learning experience to prepare work on a quadruped robot for my niece.

Out of the box, it balanced for just a few seconds on the stock PD controller.

One thing that made it hard was that the motors used (N20s) have no encoders, so the robot can't measure its wheel speed and overshoots corrections.

Over 2-3 days with AI, I built a dashboard with telemetry over bluetooth, gave the robot a model of its own motors and wheels based on observations, and trained a tiny neural network to self-balance forever.

From the 1.2kB neural network, I distilled a small linear model from the traces, which can also maintain balance for 60 seconds.

A couple days later, self-balance no longer worked because the battery level had changed.

I added interpolation on the motor and wheel models to account for battery voltage and the robot is now pretty stable regardless of battery charge.

The final linear controller

One linear formula, run 100 times per second. It's a linear regression of what the trained network does near upright, and it matches 99% of the network's behavior.

push = 5.36×lean − 0.45×tip_rate + 1.38×wheel_speed + 0.23×position + 0.35×last_push + 0.5
TermWeightWhat it reads, and why it's there
lean5.36 duty/°degrees from the true balance point (0.55° off vertical on my unit) - the main push
tip_rate−0.45the gyro, in °/s - damping that arrives before the lean grows
wheel_speed+1.38the imaginary wheel (below) - pushes with the motion to get back under the fall; my hand tuning had the sign backwards
position+0.23drifted distance - pulls the robot back home
last_push+0.35its own previous output - memory that rides out the 20-25 ms sensor-to-motor delay
constant+0.5a small standing bias the regression found

The 1.2 KB network that taught it (7 inputs, two layers of 32, 8-bit weights, 2.7 ms per decision) also works onboard as a switchable mode.

Seeing the robot

You can't debug a robot you can't see, so telemetry came before any control work. The firmware streams 50 measurements per second over Bluetooth Low Energy into a browser dashboard: live charts, a pose view, and 22 settings adjustable while the robot runs. Every run archives itself with its exact settings, about 300 in total.

Balance dashboard: robot pose views, live charts, tune panel
dutyThe motor command, 0 to 100: the fraction of full power sent to the wheels. Most numbers in this report are duty units.
IMUInertial measurement unit, the robot's only motion sense. It answers two questions: which way is down, and how fast am I tipping.

What the robot measured about itself

Every constant we guessed was wrong, so the firmware grew a 25-second self-test: duty staircases, step trials, battery sag, and an open-loop fall.

QuantityValueNote
Sensor-to-motor delay20-25 msthe wall every gain runs into; past ~8 duty/° the loop rings at 5 Hz
Friction thresholdduty 48 ± 4 flat & coldwanders 36-56 with heat and battery
Warm motors vs cold−20-25 % torqueconfirmed two independent ways
Balance point≈ −0.55°this unit's crookedness; every build has its own
Top speed0.26-0.28 m/sa 2° standing error falls in ~0.75 s
Robot weight94 glighter than a deck of cards
Brain size & speed1.2 KB · 2.7 ms100 decisions/s on a chip with no floating-point unit
Longest run65.5 sstopped by hand; runs now end at 60 s with a rainbow
Straightest run1.47° error rmsby the five-multiply formula
Battery life170 mAh → 15-20 minof continuous balancing

The techniques, in the order we needed them

Tuned PD control

The stock controller is a PD controller, and tuning it well is the honest first step. It reached 15 to 18 second sessions and stopped there: past a gain of about 8 duty per degree, the 20-25 ms delay turns every correction into a new disturbance and the robot rocks at 5 Hz. That wall is physical. Everything after this section exists to go around it.

PD controllerPush in proportion to how far you lean (P), minus a bit for how fast the lean changes (D). Two numbers, surprisingly capable, and the standard first answer to balancing.
The delay wallThe time between sensing a lean and the wheels reacting. Raise the gains past what the delay allows and corrections arrive late enough to fuel the wobble instead of fixing it.

Pulse-density modulation (PDM)

Below the friction threshold the wheels don't turn at all, so nearly half the command range did nothing. PDM makes the dead zone invisible: small commands become full-strength 10 ms pulses, spaced so their average equals what was asked, like pushing a swing less often instead of more softly. The standing duty floor is 33-35, well below the flat threshold of 48. The pulses do the breaking-away.

ASKEDSENT10 ms eachsame average

A small ask (top) becomes spaced full-strength pulses (bottom). Each pulse clears the friction threshold; the dashed line is the average the robot actually feels.

BreakawayThe duty below which static friction in the gearbox wins and the wheels stay still. It's a fixed amount of torque, which becomes a moving duty threshold (more on that below).

The imaginary wheel

The motors have no encoders, so the robot can't feel its own wheels. It keeps an estimate instead: speed += (command − speed) · dt / lag, with a 0.05 s lag when coasting because friction stops wheels faster than motors spin them up. Speed integrates into a position estimate with a 10-second leak. Without this, any small bias carries the robot to its 26 cm/s top speed, where nothing is left for balancing.

EncoderA sensor that counts wheel rotation. These motors don't have one, so wheel speed must be guessed from what was commanded.
First-order lagThe simplest model of "it takes a moment": the estimate moves toward the target a fixed fraction per tick. One parameter, the lag time, fitted from data.

The balance point

No build is perfectly symmetric, so "straight up" is not where the robot balances. We ran two candidate setpoints, watched which way it drifted under each, and interpolated to the angle with zero drift: 0.55° off vertical on my unit. Guessing this costs you a slow walk across the desk.

SetpointThe angle the controller treats as "upright" and steers toward. Measure it, don't assume it.

Training in simulation

To get past the classical ceiling we rebuilt the robot inside a physics simulator and let it fall ten thousand times a minute until a habit of balancing emerged. About 30 minutes per brain on a desktop CPU.

measure the robotmirror it in MuJoCotrain with PPOcalibration gateflash to the robotdistill to a formula
  • The calibration gate refuses to train unless the sim reproduces the measured torque curve and falling speed. Our first two attempts skipped this and learned violence from dishonest worlds.
  • A sim is only trusted once the policy that already works on hardware behaves the same inside it. Judge the world by whether reality's champion survives it.
  • The network that passed everything balances 60+ seconds, recovers from driving over a cable, and works upside-down, a pose it saw in only 20% of training.
PPOProximal Policy Optimization, a standard reinforcement-learning method: try things, make the choices that kept you upright a little more likely, repeat a few million times.
Reverse validationTesting the simulator against the robot instead of the robot against the simulator. If the proven policy acts differently in sim than on the desk, fix the sim.

Distillation

Near its balance point, any good controller behaves almost linearly. So we fit a plain regression to the network's own decisions across its good runs, and got the five-multiply formula at the top of this page. It matches 99% of the network's behavior, and dropping the accelerometer input costs almost nothing (0.989 to 0.987), which is why the formula needs only parts every balancer already has.

DistillationCompressing a trained network into a simpler model by fitting the simple model to the network's outputs. The formula is the network's habits, written down.

Battery and temperature tracking

The last technique, and the one the first four days didn't know they needed. Gearbox friction is a fixed torque, but duty buys torque in proportion to battery volts, so the dead-zone threshold moves opposite to the charge, and warm grease lets go earlier. End to end, a full battery makes the drivetrain 1.5 to 2 times stronger for the same command.

SymptomCauseFix in firmware
Overreacts on a full chargemore torque per duty unitscale all duty by 3820 mV / V, clamped 0.85-1.1
Drifts off at full charge, looking calmreal wheels outrun the imaginary onewheel lag slides 0.30→0.20 s across 3.75-4.05 V
First cold runs starve and fall earlycold grease raises the thresholdfloor slides 35→33 by voltage, and an adaptive trim (±8) compares delivered acceleration to commanded effort

Florent, with Claude as AI pair for code and analysis
ProtoBot firmware fork v9.x on CodeCell C6 Drive · August 2026

← back to the shelf