Ball-Balancing Bot
A robot that balances on a single basketball and drives in any direction. Two interacting PID loops, one for balance and one for steering, running on a Raspberry Pi off an IMU at 50 Hz.
1.0 Summary
The Ball-Bot is a dynamically stable robot: it stays upright on a single basketball the way an inverted pendulum does, by constantly correcting. The goal was a real-time feedback loop that held balance, recovered from a shove, and still let a driver steer it around.
The final system runs two PID loops that share one controller, one for balance and one for steering. It reads an IMU for lean angle, computes a corrective torque, and splits that torque across three omni-wheels. With the gains tuned it held balance for a little over seven minutes at a time.
2.0 System & Data Flow
The IMU sits at the center of the system. It streams roll, pitch, and yaw in radians plus angular velocities over I2C at 400 kHz. A Raspberry Pi takes that orientation data and runs the control math to produce a corrective torque.
That torque goes to three Pololu DC omni-wheels arranged symmetrically at 120 degrees, each tilted 45 degrees from the base plate so they grip the ball and transfer torque cleanly. The motor controllers are driven over UART at 115200 baud. The pipeline keeps a strict separation: lean angles and angular velocities flow in from the sensor, normalized torque values from minus one to one stream out to each motor, and the whole loop updates every 20 ms.
3.0 Control Design
Both controllers work in torque. Error is measured as the reference angle, which for balance is zero, minus the angle the IMU reports. The roll and pitch errors become torques Tx and Ty, Tz stays zero because balancing needs no turn, and a kinematic conversion maps those torques onto the three wheels so the robot pushes in the right direction.
3.1 Balance Controller
Tuning was the work. A pure proportional term of P = 6 balanced well but overcorrected, so a derivative term went in to damp it. The torques were tiny near level, so raising the error to the power 1.1 sharpened the response near zero. That needed a small fix, since Python will not raise a negative number to a fractional power, so the sign is stripped, the power applied, and the sign put back.
The final gains were P = 10 and D = 0.1, which held balance for over seven minutes. An integral term was tried and dropped: it helped early but made the balance worse the longer a run went, which points to low static friction in the mechanism rather than a bias an integrator would fix.
3.2 Steering Controller
Steering reuses the balance idea with higher gains, P = 11 and D = 0.2, since moving means deliberately forcing the robot off balance. A PS4 controller's left stick adds to Tx and Ty in the direction the driver wants, scaled by a cap of 0.35 so the motors never lurch, with a 0.3 deadzone so a small or accidental nudge does nothing.
The honest tradeoff: driving is slow. Push the input harder and the balance loop cannot recover in time; raise the gains to compensate and it oscillates and gets unstable. Slow and steady won over fast and chaotic.
4.0 Results
Performance was judged three ways: how tightly it held level, how it answered a commanded step, and how it tracked a driven square. Every run opens with a 20 second startup delay where the motors stay off so the IMU can settle before active balancing begins.
4.1 Holding Balance
Once active, the controller pulled both pitch and roll to within about ±0.02 radians, roughly 1.15 degrees, and held there. The proportional and derivative efforts stayed inside ±0.1 in normal operation, which says the angle errors were small the whole time.
The interesting moment is a deliberate shove around the 430 second mark. The controller efforts spike hard, one proportional term peaking near plus seven, and then settle, so the robot took a real disturbance and recovered. Holding near zero steady-state error with no integral term is the evidence that the mechanism itself is low-friction and close to symmetric.
4.2 Step Response
For a commanded move, the robot is first zeroed on a flat surface so the IMU knows what level is, then handed the negative of its current angle so it drives back toward center and parallel to the ground. The derivative term does most of the visible work here, catching fast swings from one side of zero to the other and damping them before they grow. A visible late spike lined up with the battery dropping from five bars to four, which cost the IMU some precision.
4.3 Driving a Square
The hardest test was driving a two-foot square. Straight lines were fine with small corrections, but 90 degree turns were not: at one corner the robot had to loop to rebalance rather than fall, and it drifted sideways while trying to go straight. The cause is the same tradeoff as steering, balance gains high enough to move are high enough to overshoot.
The fix I would make is to steer by commanding a small target lean instead of adding raw torque: set a desired angle, say 0.02 radians, and let the balance loop chase it, so motion falls out of balancing rather than fighting it.
5.0 Conclusion & Next Steps
The robot balanced and steered on a ball with a PID approach, and the project came down to a few clear lessons. Tuning P and D is what makes or breaks stability. Saturating the motor commands protects the hardware but costs corrective torque. And the system is sensitive at the edges: a slightly off initial placement, a transient zero reading from the IMU, or a falling battery each visibly degraded balance.
With more time the next steps are concrete: a properly gated integral term with anti-windup for long-run drift, a higher-precision IMU such as the BMI088 to cut sensor latency, and steering by target angle rather than raw torque so driving stops fighting the balance loop.