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.
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
| Setting | How it is used |
|---|---|
| Control-theory education | The inverted pendulum, PID and sensor fusion made physical. |
| Personal-transporter principle | The core of Segway-class self-balancing machines. |
| Dynamics/robotics research learning | Estimation, fast control loops and stability. |
| Balancing platforms | A 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
| Attribute | Value |
|---|---|
| Difficulty level | Advanced |
| Estimated completion time | 14–22 hours |
| Indicative build cost | ₹2,200 – ₹4,000 |
| Primary discipline | Robotics |
| Reference platform | ESP32 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.
| Component | Key specification | Qty | Approx. 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 DAC | 1 | ₹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 DMP | 1 | ₹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 encoder | 1 | ₹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 PWM | 1 | ₹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 discharge | 1 | ₹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 max | 1 | ₹950 |
| IMU (MPU6050) The balance sensor | 6-axis accelerometer + gyroscope for tilt | 1 | ₹150 |
| Geared motors + encoders | Two motors (encoders help position/velocity) | 2 | ₹1,400 |
| Motor driver | TB6612/L298N H-bridge, PWM | 1 | ₹200 |
| Tall chassis + battery | Two-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
| Part | Specification | Supply | Interface | Reference |
|---|---|---|---|---|
| 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 DAC | 3.3 V logic / 5 V USB | UART, SPI, I²C, I²S, CAN, PWM | Datasheet |
| MPU-6050 6-axis IMU | 3-axis gyro ±250–2000 °/s, 3-axis accel ±2–16 g, 16-bit ADC, on-chip DMP | 2.375–3.46 V (module 5 V tolerant) | I²C (0x68/0x69) | Datasheet |
| N20 micro gear motor (6 V, 200 rpm) with encoder | 6 V, 200 rpm, 0.4 kg·cm, 12 mm × 10 mm gearbox, magnetic encoder | 3–9 V | PWM + H-bridge | Datasheet |
| TB6612FNG dual MOSFET motor driver | 2 × 1.2 A continuous (3.2 A peak), 2.5–13.5 V motors, 100 kHz PWM | 2.7–5.5 V logic | AIN/BIN + PWM + STBY | Datasheet |
| 18650 Li-ion cell 3400 mAh + holder | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 3.0–4.2 V | Holder / spot-welded tabs | Datasheet |
| Quadrature rotary encoder (600 PPR optical) | 600 pulses/rev, A/B/Z channels, 5–24 V open collector, 5000 rpm max | 5–24 V | Quadrature pulses | Datasheet |
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.
| Load | Supply rail | Typical current (mA) | Notes |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | 3.3 V logic / 5 V USB | 160 | Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly. |
| MPU-6050 6-axis IMU | 2.375–3.46 V (module 5 V tolerant) | 3.9 | Gyro bias drifts with temperature — re-zero at boot while the device is still. |
| N20 micro gear motor (6 V, 200 rpm) with encoder | 3–9 V | 120 | The encoder makes closed-loop speed control trivial — worth the extra cost. |
| TB6612FNG dual MOSFET motor driver | 2.7–5.5 V logic | 1.5 | MOSFET output means ~0.5 V drop — noticeably more runtime than an L298N. |
| Quadrature rotary encoder (600 PPR optical) | 5–24 V | 40 | 4× 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.jsonunder 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
dialoutgroup:sudo usermod -aG dialout $USERand 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
| Library | Why it is needed | Install |
|---|---|---|
| MPU6050_light / Adafruit MPU6050 1.3.x | IMU register access, calibration and complementary-filter angles. | Library Manager → "MPU6050_light" by rfetick |
| PID_v1 1.2.1 | Proportional-integral-derivative controller with anti-windup. | Library Manager → "PID" by Brett Beauregard |
| Preferences (NVS) bundled | Wear-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.
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.
| Peripheral | Peripheral pin | Controller pin | Signal |
|---|---|---|---|
| IMU (MPU6050) | SDA/SCL | GPIO 21/22 | Tilt (accel+gyro) |
| Motor encoders | A/B | GPIO 34/35 | Wheel velocity |
| Motor driver L | PWM/IN | GPIO 25/26 | Left wheel |
| Motor driver R | PWM/IN | GPIO 27/14 | Right wheel |
| Battery | + | VIN | Power |
| Fall cut-off | STBY | GPIO 33 | Disable 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.
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.
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)
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
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
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.
Assembly Instructions
Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.
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.
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.
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.
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.inofloat 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.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.
/* ═══════════════════════════════════════════════════════════════
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
}
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.
Tilt estimate
Verify the fused tilt is stable at rest (no drift) and responsive when tipped; align the IMU to the true tilt axis.
Set-point trim
Trim the upright set-point so the robot balances without slowly driving off — compensating the real centre of mass.
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.
| State | Left motor | Right motor | Result |
|---|---|---|---|
| Upright (balanced) | Hold/trim | Hold/trim | Stays up |
| Tilting forward | Drive forward | Drive forward | Wheels catch the fall |
| Tilting back | Reverse | Reverse | Wheels catch the fall |
| Drive forward (intended) | Forward | Forward | Lean set-point forward |
| Steer | Faster | Slower | Turn while balancing |
| Fallen (large tilt) | Cut | Cut | Motors 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.
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.
| Test | What you should see |
|---|---|
| Hold upright and release | Balances hands-off |
| Nudge it | Corrects and re-settles (no growing oscillation) |
| Check at rest | Tilt estimate steady, no drift |
| Tip past the fall angle | Motors cut (safety) |
| Command forward | Leans and drives forward while balancing |
| Change battery/mass | May 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.
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.
Troubleshooting: Common Errors & Fixes
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 amillis()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_twhere 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
References & Learning Resources
These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.
- Inverted pendulumReference
- Self-balancing robot / personal transporterReference
- PID controllerReference
- Complementary / Kalman filterReference
- MPU6050 IMUTDK InvenSense