Siddhant Kumar
Project 094 · Robotics

Self-Balancing Robot.

A two-wheeled robot that stays upright by constantly correcting its fall — a real inverted pendulum, and the clearest lesson in feedback control there is.

Advanced 14–22 hours 30 min read IMUPIDRobot
Jump to source Bill of materials
Self-Balancing Robot — reference build illustration
Difficulty
Advanced
Build time
14–22 hours
Indicative cost
₹2,200 – ₹4,000
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Robotics
Last updated
28 July 2026
Contents — 26 sections

Project Overview

A two-wheeled robot that stays upright by constantly correcting its fall — a real inverted pendulum, and the clearest lesson in feedback control there is.

A two-wheeled robot standing on its axle is inherently unstable: left alone it falls over, exactly like a broom balanced on your palm. Keeping it upright is not a mechanical trick but a control feat — the robot must sense that it is starting to fall and drive its wheels to catch itself, hundreds of times a second, forever. This is the classic inverted pendulum, one of the foundational problems of control theory, and building a robot that solves it is the single clearest, most visceral lesson in closed-loop feedback that robotics offers: the balance is not a state, it is a continuous, active correction.

The robot senses its tilt with an inertial measurement unit (an accelerometer + gyroscope, e.g. an MPU6050): the accelerometer gives a noisy absolute tilt from gravity, the gyroscope gives a clean but drifting rate of rotation, and a complementary (or Kalman) filter fuses them into a stable, responsive tilt angle. A PID controller then converts the tilt error — how far from upright, how fast it is falling — into a drive command: lean forward and the wheels drive forward to get back under the centre of mass; lean back and they reverse. The robot literally chases its own falling point to stay up.

Getting it to balance is a study in why control is subtle: the loop must run fast (a slow loop cannot catch a fast fall), the tilt estimate must be both stable and responsive (hence sensor fusion), and the PID gains must be tuned or the robot either falls or oscillates itself into instability. It is honest that this is a genuinely demanding tuning problem, that the balance point and gains are sensitive to the robot's mass distribution, and that a real Segway-class machine adds far more robust estimation and safety. But as a working inverted-pendulum balancer built on an IMU, sensor fusion and a tuned PID loop, it teaches the heart of feedback control — sense, filter, correct, fast — better than any simulation, because here the robot really does fall the moment the control gets it wrong.

A small wheeled line-following robot on a track
A self-balancing robot is a real inverted pendulum — upright only because a fast control loop continuously catches its fall. Photograph sourced from Wikimedia Commons — Line following robot.jpg. Reused under the licence stated on that page; please check it before republishing.

What this project does

  • Balances upright on two wheels (inverted pendulum)
  • Senses tilt with an IMU (accelerometer + gyroscope)
  • Fuses sensors into a stable tilt angle (complementary/Kalman)
  • Corrects with a fast PID loop driving the wheels
  • Chases its own falling point to stay up
  • Can be extended to drive while balancing
  • Demonstrates the foundations of feedback control

Real-World Applications

SettingHow it is used
Control-theory educationThe inverted pendulum, PID and sensor fusion made physical.
Personal-transporter principleThe core of Segway-class self-balancing machines.
Dynamics/robotics research learningEstimation, fast control loops and stability.
Balancing platformsA base for two-wheel balancing robots and toys.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • IMU tilt sensing with sensor fusion
  • Fast PID balance loop
  • Motor drive coupled to tilt correction
  • Tunable gains (and honest tuning workflow)
  • Fall detection / safe cut-off
  • Extensible to steering/driving while balanced
  • Honest about tuning sensitivity and estimation limits

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelAdvanced
Estimated completion time14–22 hours
Indicative build cost₹2,200 – ₹4,000
Primary disciplineRobotics
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • IMU reading and sensor fusion (complementary/Kalman filter)
  • PID control tuning for an unstable plant
  • Fast control-loop timing
  • Motor drive coupled to a control law
  • Stability reasoning and fall/safety handling

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
MPU-6050 6-axis IMU
Gyro bias drifts with temperature — re-zero at boot while the device is still.
3-axis gyro ±250–2000 °/s, 3-axis accel ±2–16 g, 16-bit ADC, on-chip DMP1₹190
N20 micro gear motor (6 V, 200 rpm) with encoder
The encoder makes closed-loop speed control trivial — worth the extra cost.
6 V, 200 rpm, 0.4 kg·cm, 12 mm × 10 mm gearbox, magnetic encoder1₹420
TB6612FNG dual MOSFET motor driver
MOSFET output means ~0.5 V drop — noticeably more runtime than an L298N.
2 × 1.2 A continuous (3.2 A peak), 2.5–13.5 V motors, 100 kHz PWM1₹260
18650 Li-ion cell 3400 mAh + holder
Never charge below 0 °C; always use a protected cell or a BMS.
3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge1₹450
Quadrature rotary encoder (600 PPR optical)
4× decoding gives 2400 counts/rev — use a hardware timer in encoder mode.
600 pulses/rev, A/B/Z channels, 5–24 V open collector, 5000 rpm max1₹950
IMU (MPU6050)
The balance sensor
6-axis accelerometer + gyroscope for tilt1₹150
Geared motors + encodersTwo motors (encoders help position/velocity)2₹1,400
Motor driverTB6612/L298N H-bridge, PWM1₹200
Tall chassis + batteryTwo-wheel chassis with mass up high (easier to balance)1₹600

Estimated total: ₹5,070, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
MPU-6050 6-axis IMU3-axis gyro ±250–2000 °/s, 3-axis accel ±2–16 g, 16-bit ADC, on-chip DMP2.375–3.46 V (module 5 V tolerant)I²C (0x68/0x69)Datasheet
N20 micro gear motor (6 V, 200 rpm) with encoder6 V, 200 rpm, 0.4 kg·cm, 12 mm × 10 mm gearbox, magnetic encoder3–9 VPWM + H-bridgeDatasheet
TB6612FNG dual MOSFET motor driver2 × 1.2 A continuous (3.2 A peak), 2.5–13.5 V motors, 100 kHz PWM2.7–5.5 V logicAIN/BIN + PWM + STBYDatasheet
18650 Li-ion cell 3400 mAh + holder3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge3.0–4.2 VHolder / spot-welded tabsDatasheet
Quadrature rotary encoder (600 PPR optical)600 pulses/rev, A/B/Z channels, 5–24 V open collector, 5000 rpm max5–24 VQuadrature pulsesDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
MPU-6050 6-axis IMU2.375–3.46 V (module 5 V tolerant)3.9Gyro bias drifts with temperature — re-zero at boot while the device is still.
N20 micro gear motor (6 V, 200 rpm) with encoder3–9 V120The encoder makes closed-loop speed control trivial — worth the extra cost.
TB6612FNG dual MOSFET motor driver2.7–5.5 V logic1.5MOSFET output means ~0.5 V drop — noticeably more runtime than an L298N.
Quadrature rotary encoder (600 PPR optical)5–24 V404× decoding gives 2400 counts/rev — use a hardware timer in encoder mode.

Summed typical draw is 325.4 mA. With a 1.5× design margin the supply should deliver at least 500 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
MPU6050_light / Adafruit MPU6050 1.3.xIMU register access, calibration and complementary-filter angles.Library Manager → "MPU6050_light" by rfetick
PID_v1 1.2.1Proportional-integral-derivative controller with anti-windup.Library Manager → "PID" by Brett Beauregard
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

Self-Balancing Robot — system block diagramFunctional block diagram of the Self-Balancing Robot system. SenseIMUaccel+gyroFusetilt angleControlPIDtilt errorDrive cmdcatch fallActLeft motorPWMRight motorPWMLoop fastRepeat100s/secFall cut-offsafetyrightrightnone
Self-Balancing Robot — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

Self-Balancing Robot — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBIMU (MPU6050)GPIO 21/22Tilt (accel+gyro)Motor encodersGPIO 34/35Wheel velocityMotor driver LGPIO 25/26Left wheelMotor driver RGPIO 27/14Right wheelBatteryVINPowerFall cut-offGPIO 33Disable motors
Self-Balancing Robot — wiring schematic
PeripheralPeripheral pinController pinSignal
IMU (MPU6050)SDA/SCLGPIO 21/22Tilt (accel+gyro)
Motor encodersA/BGPIO 34/35Wheel velocity
Motor driver LPWM/INGPIO 25/26Left wheel
Motor driver RPWM/INGPIO 27/14Right wheel
Battery+VINPower
Fall cut-offSTBYGPIO 33Disable motors

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • Mount the IMU rigidly on the chassis centreline, aligned to the tilt axis — a loose or misaligned IMU wrecks the tilt estimate.
  • Wire the two motors through an H-bridge with PWM; both must respond quickly.
  • Keep the loop hardware fast — I2C for the IMU, direct PWM to the driver.
  • Put battery mass up high to raise the centre of mass (a taller pendulum is slower to fall and easier to balance).
  • Provide a way to cut motors on a fall (large tilt) for safety.
A brushed DC gear motor
An IMU senses tilt and sensor fusion turns noisy accelerometer and drifting gyro data into a stable balance angle. Photograph sourced from Wikimedia Commons — DC motor.jpg. Reused under the licence stated on that page; please check it before republishing.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

Self-Balancing Robot — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · MPU-6050 6-axis IMUDriver layermpu · pid · preferencesApplication logicsampling loop · filtering · thresholds · state machinePresentation layerlocal display · serial console · logged output
Self-Balancing Robot — architecture stack

Working Principle

A self-balancing robot is an inverted pendulum — a mass whose natural, stable state is lying down, held upright only by active effort. Balancing it is not a matter of building it well; a perfectly-built one still falls, because the upright position is an unstable equilibrium, like a pencil on its point. The only thing that keeps it up is a controller that continuously notices the incipient fall and moves the base to get back underneath the centre of mass — the same thing you do unconsciously balancing a broom on your hand. This makes the robot the purest physical demonstration of closed-loop feedback control: remove the loop for even a fraction of a second and it topples.

The first hard part is knowing the tilt, and it is harder than it looks because no single sensor gives a good tilt angle. An accelerometer senses gravity's direction and so gives an absolute tilt — but it also senses the robot's own accelerations, making it noisy and jittery. A gyroscope senses the rate of rotation very cleanly — but integrating rate to get angle accumulates drift, so its angle slowly wanders away from truth. The classic solution is sensor fusion: a complementary filter trusts the gyro over short timescales (smooth, responsive) and the accelerometer over long timescales (drift-free), blending them into a tilt estimate that is both stable and fast. (A Kalman filter does this optimally.) Good balance is impossible without a good tilt estimate, which is why fusion is central, not optional.

The second hard part is correcting, and this is a PID controller. The error is the tilt away from the upright set-point. The proportional term drives the wheels harder the further the robot has leaned — the basic "catch the fall" response. The derivative term reacts to how fast it is falling, damping the motion and preventing overshoot (without it, the robot corrects, overshoots, and oscillates). The integral term trims small steady biases (an off-centre mass, motor imbalance) that would otherwise leave it slowly drifting. The controller's output drives both wheels forward or back so the base chases the falling point. Crucially the whole loop — read, fuse, PID, drive — must run fast (typically hundreds of times per second), because a fall accelerates, and a loop too slow to catch it early loses the race. Slow loop, poor fusion, or mistuned gains and the robot either falls limply or shakes itself apart.

What makes this project honest — and such a good teacher — is that the difficulty is real and unavoidable. The PID gains must be tuned to the specific robot, because they depend on its mass, height, wheel size and motor response; there is a genuine tuning workflow (raise P until it holds but oscillates, add D to damp, add a little I to remove drift), and the robot gives instant, unambiguous feedback — it stays up or it falls. The balance is sensitive to the centre of mass (mass up high, making a slower pendulum, is markedly easier), and safety matters: a large tilt means it has lost balance, and the motors should cut so it does not drive itself off a table. It is candid that production self-balancing machines add far more robust estimation, redundancy and safety. But precisely because it really falls when the control is wrong, it delivers the fundamentals of feedback — sensing, fusion, and a fast tuned control loop stabilising an unstable system — with a clarity no stable robot or simulation can match.

The maths behind it

Complementary filter (tilt fusion)

plainComplementary filter (tilt fusion)
angle = α·(angle + gyro_rate·dt) + (1−α)·accel_angle

  gyro term  → smooth, responsive (short-term)
  accel term → drift-free absolute (long-term)
  α ≈ 0.98    (trust gyro over dt, accel over time)

Stable + fast tilt — neither sensor alone is enough.

PID balance law

plainPID balance law
error = tilt − setpoint      (setpoint ≈ upright)

  drive = Kp·error + Ki·∫error dt + Kd·d(error)/dt

  Kp: catch the fall   Kd: damp / anticipate
  Ki: trim steady bias

Output drives BOTH wheels to get under the CoM.

Why speed matters

plainWhy speed matters
A fall accelerates: θ̈ ∝ sin θ (grows as it tips).
The loop must sample fast enough to correct while θ is
still small:

  f_loop  ≫  fall dynamics   (100s of Hz typical)

Too slow → it tips past recovery before the next correction.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Self-Balancing Robot — firmware flowchartControl flow through the main program loop. Read IMU (accel + gyro)Fuse → stable tilt angleFallen past safe angle?Cut motors (safety)PID on tilt errorCut motors (safety)PID on tilt errorDrive wheels to catch the fallLoop fast (repeat)
Self-Balancing Robot — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

  1. Build a slightly top-heavy chassis with a rigid IMU

    Assemble the two-wheel chassis with mass up high, mount the IMU rigidly on the centreline aligned to the tilt axis, and wire the motors through the H-bridge with fast PWM.

  2. Fuse the IMU into a stable tilt

    Read the accelerometer and gyroscope and fuse them (complementary/Kalman) into a tilt angle that is both drift-free and responsive.

  3. Tune the PID balance loop

    Run the loop fast; raise Kp until it holds but oscillates, add Kd to damp, add a little Ki to remove drift; then add steering/driving.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

  1. Fuse tilt, then stabilise with PID

    Fuse accelerometer and gyroscope into a stable tilt angle, and drive both wheels from a PID on the tilt error — fast.

    cppbalance.ino
    float angle = 0;                    // fused tilt (deg)
    float Kp=22, Ki=0.4, Kd=1.1, integ=0, prevErr=0;
    const float SETPOINT = 0.0;         // upright (trim for CoM)
    const float FALL = 40.0;            // deg: lost balance
    
    // Complementary filter: gyro (smooth) + accel (drift-free).
    float fuseTilt(float accelAngle, float gyroRate, float dt){
      angle = 0.98f*(angle + gyroRate*dt) + 0.02f*accelAngle;
      return angle;
    }
    
    int balance(float tilt, float dt){
      if (fabs(tilt) > FALL) return 0;          // fallen -> cut motors
      float err = tilt - SETPOINT;
      integ += err*dt;
      float d = (err - prevErr)/dt; prevErr = err;
      float u = Kp*err + Ki*integ + Kd*d;       // PID -> drive command
      return constrain((int)u, -255, 255);
    }
    
    void loop(){
      float dt = tick();                         // small, fast
      float tilt = fuseTilt(accelAngle(), gyroRate(), dt);
      int u = balance(tilt, dt);
      driveBoth(u);                              // both wheels chase the fall
    }
    angle = 0.98f*(angle + gyroRate*dt) + 0.02f*accelAngle;The complementary filter trusts the gyro over the short interval and the accelerometer over the long run, giving a tilt that is both responsive and drift-free.
    if (fabs(tilt) > FALL) return 0; // fallen -> cut motorsPast a large tilt the robot has lost balance, so the motors cut — a safety stop that also prevents it driving off an edge.
    float u = Kp*err + Ki*integ + Kd*d; // PID -> drive commandThe PID converts tilt error into a wheel drive: P catches the fall, D damps overshoot, I trims steady bias.
    driveBoth(u); // both wheels chase the fallBoth wheels are driven together to move the base under the centre of mass — the robot literally chases its falling point.
  2. Add driving and steering on top of balance

    Once it balances solidly, drive by biasing the tilt set-point forward/back and steer by adding a left/right wheel difference — motion layered on stable balance.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cppself_balancing.ino
/* ═══════════════════════════════════════════════════════════════
   Self-Balancing Robot — inverted pendulum

   Fuses an IMU into a stable tilt (complementary filter), stabilises
   the unstable upright with a fast tuned PID driving both wheels, and
   cuts motors on a fall. Drive/steer layered on top of balance.
   The clearest physical lesson in closed-loop feedback control.
   ══════════════════════════════════════════════════════════════════ */

#include <Wire.h>
#include <MPU6050.h>
MPU6050 imu;

float angle=0, integ=0, prevErr=0;
float Kp=22, Ki=0.4, Kd=1.1;
const float SETPOINT_TRIM = 0.5;      // CoM trim (deg)
const float FALL = 40.0;
float driveBias=0, steer=0;           // set by remote/driving

uint32_t last=0;

float readTilt(float dt){
  int16_t ax,ay,az,gx,gy,gz; imu.getMotion6(&ax,&ay,&az,&gx,&gy,&gz);
  float accelAngle = atan2f(ay, az) * 57.2958f;      // deg from gravity
  float gyroRate   = gx / 131.0f;                    // deg/s
  angle = 0.98f*(angle + gyroRate*dt) + 0.02f*accelAngle;   // fuse
  return angle;
}

void driveWheels(int u){
  setMotor(LEFT,  constrain(u + steer, -255, 255));
  setMotor(RIGHT, constrain(u - steer, -255, 255));
}

void setup(){
  Wire.begin(21,22); imu.initialize();
  motorInit(); last = micros();
}

void loop(){
  uint32_t now = micros();
  float dt = (now - last) * 1e-6f; last = now;       // fast loop dt
  if (dt <= 0 || dt > 0.05f) return;

  float tilt = readTilt(dt);

  if (fabs(tilt) > FALL){ driveWheels(0); integ=0; return; }  // fallen: cut

  float err = (tilt - SETPOINT_TRIM) - driveBias;    // drive by biasing setpoint
  integ = constrain(integ + err*dt, -50, 50);        // anti-windup
  float d = (err - prevErr)/dt; prevErr = err;
  int u = constrain((int)(Kp*err + Ki*integ + Kd*d), -255, 255);

  driveWheels(u);                                    // both wheels chase the fall
}
angle = 0.98f*(angle + gyroRate*dt) + 0.02f*accelAngle; // fuseSensor fusion produces the stable, responsive tilt that balance depends on — the gyro for smoothness, the accelerometer to stop drift.
float dt = (now - last) * 1e-6f; last = now; // fast loop dtThe loop measures its own timestep and runs fast, because a slow loop cannot catch an accelerating fall.
if (fabs(tilt) > FALL){ driveWheels(0); integ=0; return; } // fallen: cutOn a fall the motors cut and the integrator resets, a safety stop and a clean restart when picked back up.
float err = (tilt - SETPOINT_TRIM) - driveBias; // drive by biasing setpointDeliberate driving comes from biasing the balance set-point, so the robot leans and drives to chase it — you steer a balancer through its balance point.
integ = constrain(integ + err*dt, -50, 50); // anti-windupThe integral is clamped to prevent wind-up, a practical necessity in a real PID loop.

Configuration & Calibration

Configuration steps

  • Configure the IMU, tilt-axis alignment and the complementary-filter coefficient.
  • Configure PID gains (Kp/Ki/Kd) and the upright set-point trim.
  • Configure the fall angle and motor cut-off.
  • Configure loop rate and (optionally) drive/steer inputs.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Tilt estimate

    Verify the fused tilt is stable at rest (no drift) and responsive when tipped; align the IMU to the true tilt axis.

  2. Set-point trim

    Trim the upright set-point so the robot balances without slowly driving off — compensating the real centre of mass.

  3. PID gains

    Raise Kp to hold (accepting oscillation), add Kd to damp, add a little Ki for drift; re-tune if mass/height changes.

Mechanical Assembly & Motion

Mechanical assembly

  • Two-wheel chassis with the wheel axle low and the mass (battery, boards) carried up high to raise the centre of mass.
  • The IMU mounted rigidly on the centreline, aligned to the tilt axis — any looseness or misalignment corrupts the tilt estimate.
  • Matched motors and wheels so equal commands give equal drive; imbalance shows up as steady drift the integral term must fight.
  • A tall, slightly top-heavy build is deliberately easier: a longer pendulum falls more slowly, giving the loop more time to react.

Motion logic

Motion is balance-first: both wheels are driven together by the PID output to keep the robot upright, and steering (a left/right difference) is layered on top only once balance is solid.

To move forward deliberately, the controller nudges the tilt set-point slightly forward so the robot "falls" forward and drives to chase it — you drive a balancer by biasing its balance point, not by simply spinning the wheels.

StateLeft motorRight motorResult
Upright (balanced)Hold/trimHold/trimStays up
Tilting forwardDrive forwardDrive forwardWheels catch the fall
Tilting backReverseReverseWheels catch the fall
Drive forward (intended)ForwardForwardLean set-point forward
SteerFasterSlowerTurn while balancing
Fallen (large tilt)CutCutMotors off (safety)

Sensor integration

  • IMU (accelerometer + gyroscope): the tilt sensor, fused into a stable angle.
  • Wheel encoders (optional but valuable): wheel velocity/position for drift control and driving.
  • Battery voltage: motor authority drops as voltage sags, affecting balance.

Actuator explanation

Two geared DC motors via an H-bridge, PWM-driven, must respond quickly and symmetrically to the control output.

The PID output is applied to both wheels for balance; a steering term is added/subtracted for turning once balance is reliable.

Kinematics

The relevant dynamics are the inverted pendulum: the tilt accelerates away from upright, and wheel motion under the base is the control input that arrests it.

plainkinematics
Inverted-pendulum intuition:
  θ̈ ≈ (g/L)·sinθ − (control via wheel acceleration)
  small θ:  θ̈ ≈ (g/L)·θ − k·u

Balance: choose u (wheel drive) so θ → 0
  u = Kp·θ + Kd·θ̇ + Ki·∫θ      # PID stabilises the unstable θ

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Hold upright and releaseBalances hands-off
Nudge itCorrects and re-settles (no growing oscillation)
Check at restTilt estimate steady, no drift
Tip past the fall angleMotors cut (safety)
Command forwardLeans and drives forward while balancing
Change battery/massMay need re-trim/re-tune (sensitivity)

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

A robot that stands and stays upright on two wheels, recovers from nudges, and can drive while balancing.

plainbalance-loop.txt
tilt: +1.8 deg (leaning forward)
fused: stable, dt = 3.1 ms (322 Hz)
PID -> drive: +58 (both wheels forward)
result: caught the fall -> tilt returning to 0
state: BALANCING

Leaning 1.8° forward, the fast loop drove both wheels forward and caught the fall — the tilt is already returning to upright. The whole robot is this correction, repeated hundreds of times a second.

An 18650 lithium-ion cell
A tuned PID loop drives both wheels to chase the falling point — the clearest physical lesson in feedback control. Photograph sourced from Wikimedia Commons — 18650 Li-ion battery.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

Falls immediately

Likely cause. Bad tilt estimate / wrong sign / gains too low

Fix. Fix IMU alignment and fusion; check drive sign; raise Kp

Oscillates and shakes apart

Likely cause. Kp too high / no Kd / slow loop

Fix. Add Kd; lower Kp; speed up the loop

Slowly drives away

Likely cause. Set-point/CoM off; no I

Fix. Trim set-point; add small Ki; use encoders

Drifts at rest (tilt)

Likely cause. Gyro drift / poor fusion

Fix. Tune complementary α; calibrate gyro bias

Erratic when battery low

Likely cause. Motor authority drops

Fix. Monitor voltage; keep charged; scale output

Drives off the table

Likely cause. No fall cut-off

Fix. Cut motors past the fall angle

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

An I²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an I²C scanner sketch first — it should print the device address. Most breakout boards include 4.7 kΩ pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

Performance Optimisation

  • Run the loop fast — hundreds of Hz — to catch fast falls.
  • Fuse the IMU for a tilt that is both stable and responsive.
  • Tune PID in order: P to hold, D to damp, I for drift.
  • Cut motors on a fall for safety and a clean restart.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
  • Profile before optimising — print micros() deltas around each stage and fix the slowest one first.

Safety Precautions

  • Provide a motor cut-off on a large tilt so a fallen robot cannot drive off a table or into people.
  • Keep fingers clear of the wheels; a balancing robot moves suddenly when correcting.
  • Secure the battery and boards up high firmly — a shifting mass changes the balance point mid-run.
  • Expect falls during tuning; test over a soft surface or with a tether.
  • Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 °C, and never leave a charging pack unattended on a wooden desk.
  • Moving parts pinch. Keep fingers, cables and hair out of gear trains and wheels, and always test motion with the drivetrain unloaded and the robot on blocks first.
  • Motors are inductive — always fit a flyback diode across a DC coil, or use a driver that already has one, or the back-EMF spike will destroy your GPIO.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Re-trim/re-tune after any change to mass, height or wheels.
  • Calibrate the gyro bias periodically; keep the IMU mount tight.
  • Check motors/encoders for wear that introduces asymmetry.
  • Keep the battery charged — sagging voltage degrades balance.
  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life — replace it.
  • Listen for gear chatter. A servo that buzzes at rest is either stripped or fighting a mechanical bind — fix it before it burns out.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Upgrade to a Kalman filter for optimal tilt estimation.
  • Add encoder-based velocity/position control for steady driving.
  • Add remote control or autonomous driving while balancing.
  • Add stand-up-from-fallen and disturbance-rejection tricks.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add connectivity — an ESP32 and an MQTT publish turn a local gadget into something you can graph, alert on and analyse over months.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

Why does it fall without control?

Because upright is an unstable equilibrium — like a pencil on its point. Nothing mechanical holds it; only a controller that continuously senses the fall and drives the wheels to get back under the centre of mass keeps it up.

Why fuse two sensors for tilt?

The accelerometer gives an absolute tilt but is noisy; the gyroscope is smooth but drifts. A complementary (or Kalman) filter blends the gyro's short-term smoothness with the accelerometer's long-term stability into a tilt that is both responsive and drift-free — which balance requires.

What does each PID term do?

P drives harder the more it has leaned (catch the fall); D reacts to how fast it is falling (damps overshoot, stops oscillation); I trims small steady biases (off-centre mass) that would otherwise make it drift. All three, tuned, are needed.

Why must the loop be fast?

A fall accelerates. If the loop is too slow, the robot tips past the point of recovery before the next correction. Balancing needs the correction to happen while the tilt is still small — typically hundreds of times a second.

How do you make it drive somewhere?

You bias the balance set-point: lean it slightly forward and it "falls" forward and drives to chase the point, staying balanced the whole time. Steering adds a left/right wheel difference. You drive a balancer through its balance point, not by simply spinning the wheels.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. Inverted pendulumReference
  2. Self-balancing robot / personal transporterReference
  3. PID controllerReference
  4. Complementary / Kalman filterReference
  5. MPU6050 IMUTDK InvenSense