Community project

ESP32 Stabilized Mini Drone

ESP32
Photo of ESP32 Stabilized Mini Drone
Generated with AI

Yathartha Chakrawal

Published September 16, 2026

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

Wiring diagram for ESP32 Stabilized Mini Drone

Gather all the parts

QtyComponent
1

DFRobot SEN0142 Fermion MPU-6050 6 DOF Sensor Breakout

MPU6050

DFRobot SEN0142 MPU-6050 breakout with 3-5 V board input, I2C interface, onboard I2C pull-ups, and i2cdevlib Arduino example coverage.

1

DRV8833 Dual DC Motor Driver

DRV8833 dual motor driver

Dual H-bridge DC/stepper motor driver IC or carrier module. VM motor supply range is 2.7V-10.8V; logic inputs support 1.8V, 3.3V, and 5V controllers. TI rates the PWP/RTY package options at 1.5A RMS and 2A peak per H-bridge at VM=5V and 25C; carrier-board current depends on board layout and cooling. Controlled with AIN1/AIN2 and BIN1/BIN2 inputs, which may be driven as digital or PWM signals. nSLEEP must be high for the bare IC, while some carrier boards pull it high by default. nFAULT is an open-drain fault output.

1

DRV8833 Dual DC Motor Driver

DRV8833 dual motor driver

Dual H-bridge DC/stepper motor driver IC or carrier module. VM motor supply range is 2.7V-10.8V; logic inputs support 1.8V, 3.3V, and 5V controllers. TI rates the PWP/RTY package options at 1.5A RMS and 2A peak per H-bridge at VM=5V and 25C; carrier-board current depends on board layout and cooling. Controlled with AIN1/AIN2 and BIN1/BIN2 inputs, which may be driven as digital or PWM signals. nSLEEP must be high for the bare IC, while some carrier boards pull it high by default. nFAULT is an open-drain fault output.

1

LiPo 3.7V 1000mAh Battery

1S 3.7 V LiPo, 1000 mAh or higher, high-discharge

Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

1

TP4056 Li-Ion/LiPo charger module with protection

TP4056 protected 6-pad charger

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

Boost Converter

5 V boost converter, at least 1 A

Small adjustable MT3608-style DC-DC boost converter module for stepping a lower DC input up to a higher rail such as 5V, 9V, or 12V. It is a power-path module with VIN/VOUT terminals, not a GPIO peripheral.

1

Breadboard-friendly SPDT Slide Switch

SPDT slide switch

Compact through-hole SPDT slide switch with three pins for direct breadboard insertion.

1

Coreless brushed DC motor (front-left)

Your coreless motor

One of the small spinning motors that provides lift at the front-left corner.

1

Coreless brushed DC motor (front-right)

Your coreless motor

One of the small spinning motors that provides lift at the front-right corner.

1

Coreless brushed DC motor (rear-left)

Your coreless motor

One of the small spinning motors that provides lift at the rear-left corner.

1

Coreless brushed DC motor (rear-right)

Your coreless motor

One of the small spinning motors that provides lift at the rear-right corner.

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"

FunctionbatteryESP32
power+VTP4056 Li-Ion/LiPo charger module with protection B+EXT
groundGNDTP4056 Li-Ion/LiPo charger module with protection B-EXT

2. Connections between "charger" and "ESP32"

FunctionchargerESP32
powerIN+5 V USB charging lead onlyEXT
groundIN-USB charging lead ground onlyEXT
powerOUT+Breadboard-friendly SPDT Slide Switch COMEXT
groundOUT-GND

3. Connections between "arm_switch" and "ESP32"

Functionarm_switchESP32
digitalADRV8833 Dual DC Motor Driver VMEXT
digitalADRV8833 Dual DC Motor Driver VMEXT
digitalABoost Converter VIN+EXT

4. Connections between "boost" and "ESP32"

FunctionboostESP32
groundVIN-GND
powerVOUT+VIN
groundVOUT-GND

5. Connections between "imu" and "ESP32"

FunctionimuESP32
powerVIN3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

6. Connections between "driver_front" and "ESP32"

Functiondriver_frontESP32
groundGNDGND
pwmAIN1GPIO 4
pwmAIN2GPIO 13
pwmBIN1GPIO 14
pwmBIN2GPIO 16
dataAOUT1Coreless brushed DC motor (front-left) M+EXT
dataAOUT2Coreless brushed DC motor (front-left) M-EXT
dataBOUT1Coreless brushed DC motor (front-right) M+EXT
dataBOUT2Coreless brushed DC motor (front-right) M-EXT

7. Connections between "driver_rear" and "ESP32"

Functiondriver_rearESP32
groundGNDGND
pwmAIN1GPIO 17
pwmAIN2GPIO 18
pwmBIN1GPIO 19
pwmBIN2GPIO 23
dataAOUT1Coreless brushed DC motor (rear-left) M+EXT
dataAOUT2Coreless brushed DC motor (rear-left) M-EXT
dataBOUT1Coreless brushed DC motor (rear-right) M+EXT
dataBOUT2Coreless brushed DC motor (rear-right) M-EXT

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.

Open in Schematik