Build guide: Self-balancing robot kit (ESP32, steppers)
Code for this guide
A two-wheeled robot that stays upright by itself. An IMU measures how far it leans, and the ESP32 drives two stepper motors 500 times a second to keep the wheels under it. It is its own Wi-Fi hotspot: drive it and tune it from your phone. Build time: 2–3 hours. Level: beginner.
1. What's in the box
| Part | Qty | Used for |
|---|---|---|
| ESP32-C3 SuperMini | 1 | Controller with Wi-Fi |
| MPU-6050 IMU (GY-521 board) | 1 | Measures the tilt |
| NEMA 17 stepper motors, 40 mm | 2 | Drive the wheels |
| TMC2209 stepper drivers | 2 | Drive the motors quietly and smoothly |
| 3S LiPo battery, 2200 mAh, with XT60 connector | 1 | Power |
| XT60 connector pair | 1 | Battery lead |
| XY3606 buck converter (set to 5 V) | 1 | 5 V for the electronics |
| M3 screw and nut kit | 1 | Assembly |
| Printed parts: three decks, two motor brackets, two wheels, battery holder with bumpers | 8 | Structure |
| Pre-wired driver and IMU harness, power switch and battery lead with voltage divider | 1 set | Wiring |
| M3 × 40 standoffs (8), rubber tyre bands (4), hook-and-loop battery strap | 1 set | Frame, tyres, battery |
You also need: a computer, a USB-C cable, the Arduino IDE (free), a 2.5 mm hex key, a small screwdriver, and a 3S LiPo balance charger (such as the IMAX B6AC, sold separately). A phone or laptop with Wi-Fi drives the robot. Python 3 with numpy is optional (for designing your own controller).
2. Assembly
- Motors into the brackets. Fix each motor to a bracket's face plate with four M3 × 8 screws, shaft through the plate. The two brackets are mirror images: the shafts point outwards.
- Brackets onto the bottom deck. Screw each bracket's flange to the underside of the bottom deck with two M3 × 10 screws and nuts. The motors sit back to back under the deck.
- Wheels. Stretch two rubber bands into the grooves of each wheel. Push each wheel onto its shaft, flat of the shaft towards the grub screw, leaving 1–2 mm between the wheel and the bracket. Tighten the M3 grub screw onto the flat.
- Decks. Screw four standoffs onto the bottom deck, put the middle deck on them, four more standoffs, then the top deck.
- IMU. Screw the GY-521 board to the centre of the middle deck with two M3 screws and nuts, its X arrow pointing the same way as the arrow on the deck (that is the robot's front). Use a nut under the board as a spacer.
- Electronics. Zip-tie the driver board and the ESP32 to the middle deck, one each side of the IMU, and the buck converter to the bottom deck. Route the motor cables up through the slots.
- Battery holder. Screw the holder to the top deck with four M3 screws, bumpers to the front and back. Thread the strap through its slots. Don't fit the battery yet.
3. Wiring
The harness is pre-wired. Connect it as follows:
| ESP32-C3 | Connects to |
|---|---|
| GPIO0 | Left TMC2209 STEP |
| GPIO1 | Left TMC2209 DIR |
| GPIO3 | Right TMC2209 STEP |
| GPIO10 | Right TMC2209 DIR |
| GPIO21 | Both TMC2209 EN |
| GPIO6 | MPU-6050 SDA |
| GPIO7 | MPU-6050 SCL |
| GPIO4 | Battery divider (100 kΩ to battery +, 22 kΩ to GND) |
| 3V3 | MPU-6050 VCC; both TMC2209 VIO |
| 5V, GND | Buck converter 5 V output and GND |
- Both TMC2209s: MS1 and MS2 → GND (1/8 microstepping). VM → battery + through the power switch, GND → battery − (the 100 µF capacitor across VM/GND is on the harness: never run a driver without it).
- The buck converter's input goes to the battery through the power switch too. Check its output is 5.0 V with a multimeter before connecting the ESP32.
- Left is the robot's left when you look at it from behind (the arrow on the middle deck points away from you).
- Never unplug a motor while the power is on: it can destroy its driver.
4. Firmware
- Install the Arduino IDE. In Settings → Additional boards manager URLs add
https://espressif.github.io/arduino-esp32/package_esp32_index.json, then install esp32 by Espressif (version 3.x) from the Boards Manager. - Download the firmware (
balancer.inoandpage.h, linked at the top of this guide) into one folder namedbalancerand openbalancer.ino. - Tools → Board: ESP32C3 Dev Module; USB CDC On Boot: Enabled. Connect the ESP32 by USB-C (battery not needed) and upload. If upload doesn't start, hold the BOOT button while plugging in.
- Open the Serial Monitor at 115200 baud: it prints the robot's Wi-Fi name.
5. First run
- Charge the battery with a balance charger (3S, 2.2 A). Fit it in the holder with the strap, and connect it to the XT60 lead.
- Join the robot's Wi-Fi from your phone: network
Voltomni-Balancer-XXXX, passwordvoltomni. Open http://192.168.4.1. - Motor test. Lay the robot on its back, wheels in the air, and press Motor test. Both wheels should turn for two seconds as if the robot were driving forwards (towards the deck arrow). If a wheel turns backwards, switch off and swap the two wires of one coil of that motor.
- Calibrate. Hold the robot upright, at the angle where it balances by itself (it feels weightless in your fingers), keep it still and press Calibrate. Do this once; it's saved.
- Balance. Hold it upright and still: after half a second it starts balancing. Let go gently.
- Drive. Drag the orange circle: up drives forward, sideways turns. Let go and it stops.
If it lies down (falls past about 35°) it stops its motors; stand it up again to restart.
6. Tuning
The page shows the four controller gains (the robot's commanded wheel acceleration is
a = −(k1·x + k2·tilt + k3·speed + k4·tilt rate)), plus the trim. Change them and press Save:
they're kept when it's switched off.
- Oscillates quickly while balancing: reduce
k4(towards zero) ork2. - Falls slowly in one direction: calibrate again, or adjust
trimby 0.01 at a time. - Drifts away and comes back: make
k1andk3more negative.
Design your own controller. balancer.py holds the model of the robot. Weigh your robot's parts
and put them in Robot.parts, then run:
python3 balancer.py
It prints LQR gains for your robot and simulates recovering from a push and driving.
7. Experiments
- Complementary filter: change the 0.98 in
control()to 0.9 and then 0.995, and compare how noisy and how steady the tilt on the page is while you hold the robot still and then tilt it. - PID vs LQR: replace the LQR line in
control()with a PID on the tilt, and compare how far the robot rolls when you push it. - Centre of mass: move the battery to the middle deck and see how the gains and the "feel" change (the model predicts it).
- Line following or obstacle avoidance: add a sensor to the free GPIO5 and drive
vReffrom it.
8. Troubleshooting
| Symptom | Fix |
|---|---|
| "MPU-6050 not found" | Check SDA on GPIO6, SCL on GPIO7 and 3.3 V |
| Wheels don't turn in Motor test | Battery connected and switched on? Check EN on GPIO21 |
| Wheels turn but don't balance | Motor test: both wheels must turn forwards. Then calibrate again |
| Runs away in the direction it leans | The IMU's X arrow points backwards: turn the board round |
| Falls immediately | Recalibrate while holding it at its balance point |
| Wi-Fi network doesn't appear | Check the Serial Monitor; power-cycle the robot |
| Status shows "Battery low" | Charge the battery (it stops below 10.5 V to protect it) |
9. Battery safety
- Charge LiPo batteries only with a balance charger, on a non-flammable surface, and never leave them charging unattended.
- Don't use a battery that is swollen, punctured or hot. Don't discharge it below 3.3 V per cell: the robot stops at 10.5 V; charge it then.
- Disconnect the battery when you're done, and store it at storage voltage (about 3.8 V per cell).
- The wheels are strong enough to pinch: keep fingers clear while it drives.
© 2026 Voltomni. The firmware and Python model are free to use, change and share for learning and teaching. Questions: [email protected].