Community project
ESP32 Stabilized Mini Drone
This guide builds a four-rotor stabilized mini drone around an ESP32 microcontroller, using an MPU-6050 inertial measurement unit to sense pitch and roll, and a DRV8833 motor driver to control four coreless brushed motors. The drone maintains level flight through real-time gyro feedback and motor mixing, with command input via a web interface.
You'll receive a complete wiring diagram showing motor and sensor connections, a full parts list with recommended suppliers, Arduino firmware with IMU calibration and stabilization loops, and step-by-step assembly instructions that emphasize safe testing—including motor verification before propeller installation and a restrained hover test to confirm stable flight.
Wiring diagram

Gather all the parts
Assemble it in 7 steps
1. Keep the propellers off for the first test
Build the frame, place the ESP32 in the middle, and fasten the MPU6050 flat with its printed top facing upward. Leave every propeller off until the motors have been checked one at a time; loose props can cut skin or pull wiring loose.
- Mark the frame front with tape before mounting the MPU6050 so the on-screen directions match the real drone.
- Do not attach propellers during wiring or the first power test — an unexpected motor start can cause injury.
2. Connect the motion sensor
Wire MPU6050 VIN → ESP32 3V3 (power), GND → ESP32 GND (ground), SDA → GPIO21 (data), and SCL → GPIO22 (data). Keep these four wires short and away from motor leads so electrical noise does not make the drone think it is tilting.
- Make sure VIN and GND are not swapped — swapped power can damage the sensor.
- Do not connect the MPU6050 VIN wire to 5 V; this design uses the ESP32's 3.3 V output.
3. Wire the front pair of motors
On the front DRV8833 board, wire AIN1 → GPIO4 (motor-control signal), AIN2 → GPIO13 (motor-control signal), BIN1 → GPIO14 (motor-control signal), and BIN2 → GPIO16 (motor-control signal). Wire AOUT1 and AOUT2 to the two leads of the front-left motor, and BOUT1 and BOUT2 to the two leads of the front-right motor. Wire the driver GND → ESP32 GND (ground).
- If a motor spins the wrong direction during the propeller-free test, swap only that motor's two wires at AOUT or BOUT.
- Do not connect a motor directly to an ESP32 pin — the motor current can damage the board.
4. Wire the rear pair of motors
On the rear DRV8833 board, wire AIN1 → GPIO17 (motor-control signal), AIN2 → GPIO18 (motor-control signal), BIN1 → GPIO19 (motor-control signal), and BIN2 → GPIO23 (motor-control signal). Wire AOUT1 and AOUT2 to the rear-left motor, and BOUT1 and BOUT2 to the rear-right motor. Wire the driver GND → ESP32 GND (ground).
- Use thicker, short wires for motor power and twist each motor's two wires together when possible; this reduces interference.
- All grounds must join together — without the shared ground, the ESP32 motor-control signals cannot be read reliably.
5. Build the protected battery chain
Connect LiPo +V → TP4056 B+ (battery power) and LiPo GND → TP4056 B− (battery ground). Connect TP4056 OUT+ → arm switch COM (switched battery power), and TP4056 OUT− → the shared GND line (ground). Connect the switch A terminal to both DRV8833 VM pins and the boost converter VIN+ (switched power). Connect boost VIN− → GND, boost VOUT+ → ESP32 VIN or 5V (board power), and boost VOUT− → ESP32 GND (ground).
- Set the arm switch to OFF before connecting the LiPo. Charge only through TP4056 IN+ and IN− using a normal 5 V USB charging lead.
- Never connect a LiPo directly to ESP32 3V3, and never charge it through the drone's switched motor wiring — either mistake can overheat or damage parts.
6. Check motor order before fitting propellers
With the arm switch OFF, connect the ESP32 by USB and press Deploy. Then turn the arm switch ON, join your phone to Wi‑Fi named ESP32-Drone using password flysafe123, open 192.168.4.1, and press ARM. Briefly press UP once: all motors should turn slowly. Test each direction while holding the frame firmly, then press EMERGENCY STOP and turn the arm switch OFF.
- The control page stops the motors if it loses commands for about half a second. Lay the drone still and level while it first powers up so it can learn level.
- If any motor starts before you press ARM, immediately turn the arm switch OFF and remove the battery before checking wiring.
7. Fit matched propellers and make a restrained first hover
Only after every motor direction is correct, fit one clockwise and one counter-clockwise propeller pair in the correct positions for your frame. Test outdoors or inside a clear propeller guard, with people and loose objects well away. Start with a low, restrained hover and be ready to use EMERGENCY STOP and the arm switch.
- If it flips or pulls hard to one side, stop immediately and correct motor or propeller direction before trying again.
- This is an experimental brushed-motor controller, not a safety-certified flight controller. Never fly near people, animals, roads, or property.
Review all connections
1. Connections between "battery" and "ESP32"
2. Connections between "charger" and "ESP32"
3. Connections between "arm_switch" and "ESP32"
4. Connections between "boost" and "ESP32"
5. Connections between "imu" and "ESP32"
6. Connections between "driver_front" and "ESP32"
7. Connections between "driver_rear" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <MPU6050.h>
// Forward declarations
void motorSetup(int pin1, int pin2, uint8_t channel);
void setMotor(uint8_t channel, int value);
void stopMotors();
void calibrateLevel();
void updateImu();
void applyMix();
void neutral();
void handleCommand();
void handleStatus();
constexpr int FL_IN1 = 4;
constexpr int FL_IN2 = 13;
constexpr int FR_IN1 = 14;
constexpr int FR_IN2 = 16;
constexpr int RL_IN1 = 17;
constexpr int RL_IN2 = 18;
constexpr int RR_IN1 = 19;
constexpr int RR_IN2 = 23;
constexpr int IMU_SDA = 21;
constexpr int IMU_SCL = 22;
constexpr uint32_t PWM_FREQ = 20000;
constexpr uint8_t PWM_BITS = 8;
constexpr uint8_t FL_CH = 0;
constexpr uint8_t FR_CH = 1;
constexpr uint8_t RL_CH = 2;
constexpr uint8_t RR_CH = 3;
constexpr uint32_t COMMAND_TIMEOUT_MS = 500;
constexpr int IDLE_THROTTLE = 0;
WebServer server(80);
MPU6050 mpu;
bool imuReady = false;
bool armed = false;
int throttleCommand = 0;
int rollCommand = 0;
int pitchCommand = 0;
int yawCommand = 0;
float rollOffset = 0.0f;
float pitchOffset = 0.0f;
float filteredRoll = 0.0f;
float filteredPitch = 0.0f;
unsigned long lastImuMs = 0;
unsigned long lastCommandMs = 0;
const char PAGE[] PROGMEM = R"HTML(
<!doctype html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'>
<style>body{font-family:Arial;text-align:center;background:#17212b;color:#eef;margin:0;padding:18px}button{font-size:20px;padding:16px;margin:7px;min-width:110px;border-radius:10px;border:0}#arm{background:#18a558;color:white}#stop{background:#e63946;color:white}#pad{display:grid;grid-template-columns:repeat(3,1fr);max-width:380px;margin:auto}#status{font-size:18px}</style></head><body>
<h2>ESP32 Drone Control</h2><p id='status'>Connecting…</p><button id='arm' onclick='arm()'>ARM</button><button id='stop' onclick='cmd("stop")'>EMERGENCY STOP</button>
<p>Hold a button only while you want that movement. Releasing it stops that command.</p><div id='pad'>
<button></button><button onpointerdown='hold("forward")' onpointerup='release()' onpointerleave='release()'>FORWARD</button><button></button>
<button onpointerdown='hold("left")' onpointerup='release()' onpointerleave='release()'>LEFT</button><button onpointerdown='hold("up")' onpointerup='release()' onpointerleave='release()'>UP</button><button onpointerdown='hold("right")' onpointerup='release()' onpointerleave='release()'>RIGHT</button>
<button></button><button onpointerdown='hold("down")' onpointerup='release()' onpointerleave='release()'>DOWN</button><button></button></div>
<p><button onpointerdown='hold("yawleft")' onpointerup='release()' onpointerleave='release()'>TURN LEFT</button><button onpointerdown='hold("yawright")' onpointerup='release()' onpointerleave='release()'>TURN RIGHT</button></p>
<script>let timer;function cmd(x){fetch('/cmd?x='+x).then(()=>status())}function arm(){cmd('arm')}function hold(x){cmd(x);clearInterval(timer);timer=setInterval(()=>cmd(x),180)}function release(){clearInterval(timer);cmd('neutral')}function status(){fetch('/status').then(r=>r.text()).then(t=>document.getElementById('status').textContent=t)}setInterval(status,700);status();</script></body></html>
)HTML";
void motorSetup(int pin1, int pin2, uint8_t channel) {
pinMode(pin2, OUTPUT);
digitalWrite(pin2, LOW);
ledcSetup(channel, PWM_FREQ, PWM_BITS);
ledcAttachPin(pin1, channel);
ledcWrite(channel, 0);
}
void setMotor(uint8_t channel, int value) {
ledcWrite(channel, constrain(value, 0, 255));
}
void stopMotors() {
setMotor(FL_CH, 0); setMotor(FR_CH, 0); setMotor(RL_CH, 0); setMotor(RR_CH, 0);
}
void calibrateLevel() {
if (!imuReady) return;
long ax, ay, az, gx, gy, gz;
float rollSum = 0, pitchSum = 0;
for (int i = 0; i < 100; ++i) {
mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
rollSum += atan2f((float)ay, (float)az) * 57.29578f;
pitchSum += atan2f(-(float)ax, sqrtf((float)ay * ay + (float)az * az)) * 57.29578f;
delay(5);
}
rollOffset = rollSum / 100.0f;
pitchOffset = pitchSum / 100.0f;
}
void updateImu() {
if (!imuReady) return;
unsigned long now = millis();
if (now - lastImuMs < 10) return;
lastImuMs = now;
int16_t ax, ay, az, gx, gy, gz;
mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
float roll = atan2f((float)ay, (float)az) * 57.29578f - rollOffset;
float pitch = atan2f(-(float)ax, sqrtf((float)ay * ay + (float)az * az)) * 57.29578f - pitchOffset;
filteredRoll = filteredRoll * 0.90f + roll * 0.10f;
filteredPitch = filteredPitch * 0.90f + pitch * 0.10f;
}
void applyMix() {
if (!armed || millis() - lastCommandMs > COMMAND_TIMEOUT_MS) { stopMotors(); return; }
int stabilizeRoll = constrain((int)(filteredRoll * 2.0f), -25, 25);
int stabilizePitch = constrain((int)(filteredPitch * 2.0f), -25, 25);
int base = constrain(throttleCommand, 0, 180);
int fl = base - pitchCommand - rollCommand - stabilizePitch - stabilizeRoll - yawCommand;
int fr = base - pitchCommand + rollCommand - stabilizePitch + stabilizeRoll + yawCommand;
int rl = base + pitchCommand - rollCommand + stabilizePitch - stabilizeRoll + yawCommand;
int rr = base + pitchCommand + rollCommand + stabilizePitch + stabilizeRoll - yawCommand;
setMotor(FL_CH, fl); setMotor(FR_CH, fr); setMotor(RL_CH, rl); setMotor(RR_CH, rr);
}
void neutral() { rollCommand = 0; pitchCommand = 0; yawCommand = 0; }
void handleCommand() {
String x = server.arg("x");
lastCommandMs = millis();
if (x == "arm") { armed = !armed; throttleCommand = IDLE_THROTTLE; neutral(); if (!armed) stopMotors(); }
else if (x == "stop") { armed = false; throttleCommand = 0; neutral(); stopMotors(); }
else if (x == "neutral") neutral();
else if (armed) {
neutral();
if (x == "up") throttleCommand = min(180, throttleCommand + 12);
else if (x == "down") throttleCommand = max(0, throttleCommand - 12);
else if (x == "forward") pitchCommand = 28;
else if (x == "back") pitchCommand = -28;
else if (x == "left") rollCommand = -28;
else if (x == "right") rollCommand = 28;
else if (x == "yawleft") yawCommand = -18;
else if (x == "yawright") yawCommand = 18;
}
server.send(200, "text/plain", "OK");
}
void handleStatus() {
String s = armed ? "ARMED — throttle " : "SAFE — motors disabled";
if (armed) s += String(throttleCommand) + "/180";
if (!imuReady) s += " — MPU6050 not found";
server.send(200, "text/plain", s);
}
void setup() {
Serial.begin(115200);
motorSetup(FL_IN1, FL_IN2, FL_CH); motorSetup(FR_IN1, FR_IN2, FR_CH);
motorSetup(RL_IN1, RL_IN2, RL_CH); motorSetup(RR_IN1, RR_IN2, RR_CH);
Wire.begin(IMU_SDA, IMU_SCL);
mpu.initialize();
imuReady = mpu.testConnection();
if (imuReady) calibrateLevel();
WiFi.mode(WIFI_AP);
WiFi.softAP("ESP32-Drone", "flysafe123");
server.on("/", [](){ server.send_P(200, "text/html", PAGE); });
server.on("/cmd", handleCommand);
server.on("/status", handleStatus);
server.begin();
lastCommandMs = millis();
}
void loop() {
server.handleClient();
updateImu();
applyMix();
}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




