A two-wheeled self-balancing robot built around an ESP32, an MPU-9250 IMU and a TB6612FNG dual motor driver. The robot keeps itself upright using a cascaded PID controller and can be tuned, calibrated and driven in real time from a built-in web interface over WiFi.
Inverted-pendulum control running on FreeRTOS, with a responsive web dashboard for live telemetry, PID tuning, sensor calibration and remote control.
- Features
- Hardware
- Wiring / Pin Map
- Software Architecture
- Getting Started
- Configuration
- Web Interface & API
- Control Theory
- Project Layout
- Troubleshooting
- Contributing
- License
- Self-balancing control using a cascaded PID loop (angle → angular-rate) for pitch, plus a rate loop for yaw steering.
- Cooperative scheduler running the control loop at a fixed rate alongside the async web server, with a spinlock guarding shared state between the control task and the network task.
- Built-in web dashboard served from SPIFFS for live telemetry, PID tuning, IMU calibration and joystick-style remote control.
- REST API + WebSocket interface (HTTP basic-auth protected) for both human and programmatic access.
- Persistent configuration stored in NVS (ESP32
Preferences) so PID gains and calibration survive reboots. - Dual WiFi mode: connects to your home network (STA) and simultaneously runs its own access point (AP) so the robot stays controllable even when the home network is unavailable.
- Failsafe: motors are cut if remote-control commands stop arriving.
- Quadrature motor encoders for wheel-position feedback.
| Component | Description |
|---|---|
| Microcontroller | ESP32 development board |
| IMU | MPU-9250 (9-DoF accelerometer / gyro / magnetometer), optional MS5611 barometer |
| Motor driver | TB6612FNG dual H-bridge |
| Motors | 2 × JGB37-520 geared DC motors with quadrature encoders |
| Power | LiPo battery + step-down regulator |
| Chassis | Aluminum-alloy two-wheel self-balancing frame |
Datasheets and reference photos are available in the Docs/ folder,
and a 3D-printable PCB bracket is provided in 3D/.
Pin assignments are defined in the firmware source. Default values:
| Signal | ESP32 GPIO | Source |
|---|---|---|
| Motor 1 AIN1 | 14 | Stabilizer.cpp |
| Motor 1 AIN2 | 12 | Stabilizer.cpp |
| Motor 1 PWM | 32 | Stabilizer.cpp |
| Motor 2 BIN1 | 26 | Stabilizer.cpp |
| Motor 2 BIN2 | 25 | Stabilizer.cpp |
| Motor 2 PWM | 33 | Stabilizer.cpp |
| STBY (standby) | 27 | Stabilizer.cpp |
| Signal | ESP32 GPIO | Source |
|---|---|---|
| Motor 1 encoder A (yellow) | 13 | MotorEncoder.cpp |
| Motor 1 encoder B (green) | 15 | MotorEncoder.cpp |
| Motor 2 encoder A (yellow) | 4 | MotorEncoder.cpp |
| Motor 2 encoder B (green) | 5 | MotorEncoder.cpp |
| Signal | ESP32 GPIO |
|---|---|
| SDA | 21 (default Wire) |
| SCL | 22 (default Wire) |
A breadboard wiring reference is included at
Docs/Photo/self balancing_bb.png.
The firmware is organized into focused modules:
| Module | Responsibility |
|---|---|
main.cpp |
Setup and registration of scheduled control loops |
Task.cpp/.h |
Lightweight cooperative scheduler (circular task queue) |
Sensor.cpp/.h |
IMU read, calibration application and orientation update |
SimpleMPU9250 |
Minimal MPU-9250 driver |
DCM |
Direction-Cosine-Matrix orientation filter (yaw/pitch/roll) |
Stabilizer.cpp/.h |
Cascaded PID control loop and motor mixing |
ESP32_TB6612 |
Motor-driver abstraction |
MotorEncoder.cpp/.h |
Quadrature encoder counting via interrupts |
Server.cpp/.h |
Async web server, REST API and WebSocket endpoint |
Communication.cpp/.h |
Binary WebSocket message protocol |
Configuration.cpp/.h |
Persisting/loading config from NVS |
Secrets.h |
WiFi and HTTP credentials (git-ignored) |
The control loop and the AsyncTCP/web-server task run concurrently. Shared
state (PID gains, RC command, enable flag, failsafe counter) is guarded by a
portMUX_TYPE spinlock (dataMux) so the control loop never reads a
half-updated value while the web task is writing.
- Arduino IDE (or PlatformIO) with the ESP32 board support package.
- The following Arduino libraries:
- A SPIFFS upload tool to flash the
data/web assets (e.g. the Arduino ESP32 filesystem uploader).
-
Clone the repository
git clone https://github.com/anwarminarso/ESP32-SelfBalancing.git cd ESP32-SelfBalancing -
Create your credentials file
Copy the template and fill in your own values:
cp SelfBalancing/Secrets.example.h SelfBalancing/Secrets.h
Secrets.his git-ignored, so your credentials stay local. -
Open the project (
SelfBalancing/SelfBalancing.ino) in the Arduino IDE and select your ESP32 board and port. -
Upload the firmware with the standard upload button.
-
Upload the web assets in
SelfBalancing/data/to SPIFFS using the filesystem uploader. -
Open the Serial Monitor at
115200baud to see the assigned IP address.
WiFi and authentication credentials are defined in SelfBalancing/Secrets.h
(copied from Secrets.example.h):
// WiFi station (your existing network)
#define WIFI_STA_SSID "YOUR_WIFI_SSID"
#define WIFI_STA_PASSWORD "YOUR_WIFI_PASSWORD"
// Soft-AP (the robot's own hotspot)
#define WIFI_AP_SSID "Self Balancing Robot"
#define WIFI_AP_PASSWORD "CHANGE_ME_AP_PASSWORD"
// HTTP / WebSocket basic-auth credentials
#define HTTP_AUTH_USER "admin"
#define HTTP_AUTH_PASS "CHANGE_ME_HTTP_PASSWORD"Security note: Always change the default AP and HTTP passwords before deploying. The web UI, REST API and control WebSocket are all protected by HTTP basic auth.
PID gains and IMU calibration values are persisted to NVS and can be edited live
from the web UI. Default values are defined in Configuration.cpp
(resetConfig).
Once connected, browse to the robot's IP address (STA) or to its access point.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/info |
Chip / firmware / heap info |
| GET | /api/ypr |
Yaw, pitch, roll |
| GET | /api/gyro |
Gyroscope readings |
| GET | /api/acc |
Accelerometer readings |
| GET | /api/motor |
Encoder counters |
| GET | /api/accMinMax |
Accelerometer calibration range |
| GET | /api/cal |
Full calibration + PID snapshot |
| POST | /api/savePID |
Save PID gains (param pid, JSON) |
| POST | /api/saveGyroOffset |
Save gyro offset (param gyroOffset, JSON) |
| POST | /api/saveAccMinMax |
Save accel min/max (param accMinMax, JSON) |
| POST | /api/toggleStabilizer |
Enable/disable balancing |
| POST | /api/reloadCfg |
Reload config from NVS |
| POST | /api/resetCfg |
Reset config to defaults and restart |
| POST | /api/restart |
Restart the ESP32 |
All POST endpoints require HTTP basic auth.
A compact binary protocol is used for high-rate telemetry and control. Each
message starts with a one-byte message code (see Communication.h). Highlights:
| Code | Name | Direction | Purpose |
|---|---|---|---|
| 11 | MSG_IMU_DATA_ALL |
get | Full IMU + motor snapshot |
| 14 | MSG_IMU_DATA_YPR |
get | Yaw/pitch/roll |
| 34 | MSG_CAL_PID |
get | Current PID gains |
| 44 | MSG_SET_CAL_PID |
set | Update PID gains |
| 52 | MSG_SET_STABILZE_STATE |
set | Enable/disable balancing |
| 61–64 | MSG_ACC_CAL_* |
set/get | Accelerometer calibration capture |
| 101 | MSG_SET_RC |
set | Remote-control command (forward/back, left/right) |
| 255 | MSG_RESET |
set | Restart the ESP32 |
The robot is an inverted pendulum. The balancing controller is cascaded:
- Angle (outer) loop — compares the desired tilt (from the RC forward/back command) against the measured pitch from the DCM filter and produces a target angular rate.
- Rate (inner) loop — compares that target rate against the measured gyro rate and produces the motor command.
A separate yaw rate loop handles left/right steering. The two axis outputs are
combined in a mixer (PIDMIX) and written to the two motors, clamped to the
PWM range. Derivative terms are smoothed with a 3-sample moving average and
integral terms are clamped to prevent wind-up.
ESP32-SelfBalancing/
├── 3D/ # 3D-printable PCB bracket (STL, gcode, Inventor)
├── Docs/ # Datasheets, photos and component specs
├── SelfBalancing/ # Firmware (Arduino/ESP32)
│ ├── data/ # Web UI assets (uploaded to SPIFFS)
│ ├── Secrets.example.h # Credentials template
│ └── *.cpp / *.h # Firmware modules
├── LICENSE
└── README.md
- Robot won't balance / oscillates — re-tune PID gains from the web UI,
starting from the defaults in
Configuration.cpp. - Drifting orientation — re-run the gyro and accelerometer calibration from the web UI.
- Can't reach the web UI — check the Serial Monitor for the assigned IP. If
the home network is unavailable, connect to the robot's own access point
(
WIFI_AP_SSID). - Motors don't respond — confirm the stabilizer is enabled and verify the TB6612FNG wiring and STBY pin.
- Build errors about missing headers — make sure ESPAsyncWebServer, AsyncTCP and ArduinoJson are installed and the ESP32 board package is selected.
Contributions are welcome. Please read CONTRIBUTING.md for guidelines on reporting issues and submitting pull requests.
This project is licensed under the MIT License.
© 2019 Anwar Minarso