Community project

ESP32 Quadcopter Flight Controller

ESP32
Photo of ESP32 Quadcopter Flight Controller
Generated with AI

Smruti Sourav Sahoo

Last updated August 11, 2026

This guide builds a quadcopter flight controller around the ESP32 microcontroller, using an MPU-6050 inertial measurement unit for attitude sensing and an SBUS receiver for pilot input. Four brushless ESCs drive the motors in an X-configuration, with power regulation and safety features managed by the flight firmware.

Followers will receive a complete wiring diagram showing connections between the ESP32, MPU-6050, SBUS receiver, and four ESCs, along with a full parts list, the Arduino-based flight control firmware with gyro calibration and PID stabilization, and step-by-step assembly instructions including bench testing procedures before first flight.

Wiring diagram

Wiring diagram for ESP32 Quadcopter Flight Controller

Gather all the parts

QtyComponent
1

DFRobot SEN0142 Fermion MPU-6050 6 DOF Sensor Breakout

MPU-6050

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

1

SBUS RC receiver

SBUS

5 V radio receiver with SBUS serial output. Its SBUS output must be a 3.3 V-compatible non-inverted signal or pass through an external inverter/level shifter before GPIO16.

1

Brushless ESC (front-left)

3S PWM ESC

3S-capable brushless ESC. Connect its three motor leads directly to its matching brushless motor; use only the signal and ground lead to the ESP32. ESC battery leads connect directly to the 3S distribution harness.

1

Brushless ESC (front-right)

3S PWM ESC

3S-capable brushless ESC with a 3.3 V-compatible PWM signal input.

1

Brushless ESC (rear-left)

3S PWM ESC

3S-capable brushless ESC with a 3.3 V-compatible PWM signal input.

1

Brushless ESC (rear-right)

3S PWM ESC

3S-capable brushless ESC with a 3.3 V-compatible PWM signal input.

1

3S LiPo flight battery

3S 11.1 V LiPo

11.1 V nominal (12.6 V fully charged) LiPo flight battery sized for the selected motors and ESCs; connects to a fused power-distribution harness.

1

MP1584 Buck Converter

5 V output

Adjustable buck (step-down) DC-DC converter, 4.5-28 V in -> 0.8-20 V out, ~3 A. Configured to 5 V to step a 9 V / 12 V supply or battery pack down to the board's 5 V rail.

Assemble it in 6 steps

1. Mount the flight electronics

With the battery unplugged, mount imu_1 rigidly near the frame center on thin vibration-damping foam. Align the sensor markings so its +X direction points to the aircraft nose and +Y points to the right. Secure the ESP32 and buck regulator away from propeller arcs and motor wiring.

  • Keep the IMU level and do not let wires tug on it.
  • Use short twisted signal/ground pairs for each ESC signal lead.
  • Never mount or test this controller with propellers installed until motor mapping and rotation direction have been checked.

2. Wire the low-voltage controller and IMU

Connect imu_1 VIN to ESP32 3V3, GND to ESP32 GND, SDA to GPIO21, and SCL to GPIO22. Do not power this MPU6050 board from 5 V because its I2C pull-ups may then expose ESP32 GPIOs to 5 V.

  • Keep the I2C leads short and away from ESC battery wires.
  • ESP32 GPIO pins are 3.3 V only.

3. Prepare the regulated 5 V rail

Before connecting electronics, connect lipo_3s_1 POS to buck_5v_1 VIN and battery negative to buck_5v_1 GND. With a meter, adjust buck_5v_1 VOUT to exactly 5.0 V. Then feed this 5 V rail to the ESP32 5V/VIN pin and sbus_rx_1 VCC; connect their grounds to the common ground rail.

  • Use a regulator rated for at least 1 A continuous current.
  • Install an appropriate fuse between the battery and power-distribution harness.
  • A 3S LiPo is 12.6 V when fully charged: never connect it directly to ESP32 5V/VIN, the receiver VCC, or the MPU6050.
  • Set and verify the buck output before plugging in the ESP32.

4. Connect the receiver safely

Connect sbus_rx_1 GND to common ground and VCC to the regulated 5 V rail. Connect its SBUS output to GPIO16 only through a suitable inverter/level-shifter interface if the receiver’s SBUS output is inverted and/or above 3.3 V. Configure the transmitter so CH1=roll, CH2=pitch, CH3=throttle, CH4=yaw, and CH5 is a two-position arm switch.

  • Set receiver failsafe with throttle low and arm switch OFF.
  • Confirm the receiver is actually configured to emit SBUS, not PWM or another serial format.
  • Do not connect a 5 V receiver data output directly to GPIO16.

5. Connect ESC signal leads

Connect esc_fl_1 SIG to GPIO25, esc_fr_1 SIG to GPIO26, esc_rl_1 SIG to GPIO27, and esc_rr_1 SIG to GPIO33. Connect every ESC SIG_GND to the ESP32/common ground. Wire each ESC’s battery leads directly to the fused 3S power-distribution harness; connect each ESC’s three motor leads to its matching motor.

  • For a brushless motor, swap any two of its three ESC-to-motor wires to reverse that motor’s rotation.
  • Keep ESC power current out of breadboard traces and thin jumper wires.
  • Do not power the ESP32 from multiple ESC BEC outputs. Use the dedicated buck_5v_1 rail only.
  • Leave all propellers removed for this entire wiring and first-power sequence.

6. Bench-test before fitting propellers

Place the frame securely on a bench with no propellers. Power on with throttle low and CH5 OFF, then use Schematik’s Deploy button. Keep the craft motionless during the 3-second gyro calibration. Confirm receiver-loss or CH5 OFF keeps all motors stopped; then check each motor position and direction at minimal throttle before any outdoor restrained test.

  • If tilt correction is opposite, stop immediately and correct sensor orientation or motor mapping before attempting flight.
  • Tune PID values and validate failsafe behavior in a large clear area, using a low-risk tethered test first.
  • This is an experimental controller, not a certified flight-control system. Do not fly near people, roads, buildings, or restricted airspace.
  • Disconnect the LiPo immediately if a motor starts unexpectedly or wiring becomes hot.

Review all connections

1. Connections between "imu_1" and "ESP32"

Functionimu_1ESP32
powerVIN3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

2. Connections between "sbus_rx_1" and "ESP32"

Functionsbus_rx_1ESP32
powerVCC5V
groundGNDGND
uartSBUSGPIO 16

3. Connections between "esc_fl_1" and "ESP32"

Functionesc_fl_1ESP32
powerBAT+ → Fused 3S power-distribution harness positiveEXT
groundBAT-GND
pwmSIGGPIO 25
groundSIG_GNDGND

4. Connections between "esc_fr_1" and "ESP32"

Functionesc_fr_1ESP32
powerBAT+ → Fused 3S power-distribution harness positiveEXT
groundBAT-GND
pwmSIGGPIO 26
groundSIG_GNDGND

5. Connections between "esc_rl_1" and "ESP32"

Functionesc_rl_1ESP32
powerBAT+ → Fused 3S power-distribution harness positiveEXT
groundBAT-GND
pwmSIGGPIO 27
groundSIG_GNDGND

6. Connections between "esc_rr_1" and "ESP32"

Functionesc_rr_1ESP32
powerBAT+ → Fused 3S power-distribution harness positiveEXT
groundBAT-GND
pwmSIGGPIO 33
groundSIG_GNDGND

7. Connections between "lipo_3s_1" and "ESP32"

Functionlipo_3s_1ESP32
powerPOS → MP1584 Buck Converter VINEXT
groundNEGGND

8. Connections between "buck_5v_1" and "ESP32"

Functionbuck_5v_1ESP32
powerVOUT5V
groundGNDGND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <MPU6050.h>

// X-quad viewed from above with nose forward. MPU6050 +X faces forward, +Y right.

// Forward declarations
float clampFloat(float v, float low, float high);
float mapChannel(uint16_t v, float low, float high);
void writeEsc(uint8_t channel, uint16_t us);
void stopMotors();
void readSbus();
void calibrateGyro();

constexpr int IMU_SDA = 21;
constexpr int IMU_SCL = 22;
constexpr int SBUS_RX = 16;
constexpr int ESC_FL = 25;
constexpr int ESC_FR = 26;
constexpr int ESC_RL = 27;
constexpr int ESC_RR = 33;
constexpr uint16_t ESC_MIN_US = 1000;
constexpr uint16_t ESC_MAX_US = 2000;
constexpr uint16_t ESC_IDLE_US = 1070;
constexpr uint32_t ESC_PWM_HZ = 400;
constexpr uint32_t CONTROL_PERIOD_US = 4000;
constexpr uint32_t RX_TIMEOUT_MS = 120;
constexpr int SBUS_CENTER = 992;

MPU6050 imu;
HardwareSerial SbusSerial(2);
uint16_t rc[16] = {SBUS_CENTER};
bool sbusFailsafe = true;
bool sbusFrameLost = true;
uint32_t lastRxMs = 0;
float gyroXBias = 0.0f, gyroYBias = 0.0f;
float rollDeg = 0.0f, pitchDeg = 0.0f, rollI = 0.0f, pitchI = 0.0f;
float prevRollError = 0.0f, prevPitchError = 0.0f;
uint32_t lastControlUs = 0;
bool armed = false;

float clampFloat(float v, float low, float high) { return v < low ? low : (v > high ? high : v); }
float mapChannel(uint16_t v, float low, float high) {
  float n = clampFloat((static_cast<float>(v) - 172.0f) / 1639.0f, 0.0f, 1.0f);
  return low + n * (high - low);
}
void writeEsc(uint8_t channel, uint16_t us) {
  us = constrain(us, ESC_MIN_US, ESC_MAX_US);
  uint32_t duty = (static_cast<uint32_t>(us) * ESC_PWM_HZ * 65535UL) / 1000000UL;
  ledcWrite(channel, duty);
}
void stopMotors() { for (uint8_t i = 0; i < 4; ++i) writeEsc(i, ESC_MIN_US); }

void readSbus() {
  static uint8_t frame[25];
  static uint8_t index = 0;
  while (SbusSerial.available()) {
    uint8_t b = static_cast<uint8_t>(SbusSerial.read());
    if (index == 0 && b != 0x0F) continue;
    frame[index++] = b;
    if (index != 25) continue;
    index = 0;
    rc[0] = (frame[1] | frame[2] << 8) & 0x07FF;
    rc[1] = (frame[2] >> 3 | frame[3] << 5) & 0x07FF;
    rc[2] = (frame[3] >> 6 | frame[4] << 2 | frame[5] << 10) & 0x07FF;
    rc[3] = (frame[5] >> 1 | frame[6] << 7) & 0x07FF;
    rc[4] = (frame[6] >> 4 | frame[7] << 4) & 0x07FF;
    sbusFrameLost = frame[23] & 0x04;
    sbusFailsafe = frame[23] & 0x08;
    lastRxMs = millis();
  }
}

void calibrateGyro() {
  int32_t sx = 0, sy = 0;
  for (int i = 0; i < 600; ++i) {
    int16_t ax, ay, az, gx, gy, gz;
    imu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
    sx += gx; sy += gy;
    delay(5);
  }
  gyroXBias = sx / 600.0f; gyroYBias = sy / 600.0f;
}

void setup() {
  Serial.begin(115200);
  Wire.begin(IMU_SDA, IMU_SCL);
  Wire.setClock(400000);
  imu.initialize();
  ledcSetup(0, ESC_PWM_HZ, 16); ledcAttachPin(ESC_FL, 0);
  ledcSetup(1, ESC_PWM_HZ, 16); ledcAttachPin(ESC_FR, 1);
  ledcSetup(2, ESC_PWM_HZ, 16); ledcAttachPin(ESC_RL, 2);
  ledcSetup(3, ESC_PWM_HZ, 16); ledcAttachPin(ESC_RR, 3);
  stopMotors();
  // SBUS: 100000 baud, 8E2, inverted. Input must be no more than 3.3 V.
  SbusSerial.begin(100000, SERIAL_8E2, SBUS_RX, -1, true);
  if (!imu.testConnection()) {
    Serial.println("MPU6050 missing: motors locked off.");
    while (true) { stopMotors(); delay(100); }
  }
  Serial.println("Hold aircraft motionless for gyro calibration.");
  calibrateGyro();
  lastControlUs = micros();
}

void loop() {
  readSbus();
  uint32_t now = micros();
  if (now - lastControlUs < CONTROL_PERIOD_US) return;
  float dt = (now - lastControlUs) / 1000000.0f;
  lastControlUs = now;
  bool receiverHealthy = (millis() - lastRxMs < RX_TIMEOUT_MS) && !sbusFailsafe && !sbusFrameLost;
  float throttle = mapChannel(rc[2], 0.0f, 1.0f);
  bool armSwitch = rc[4] > 1200;
  if (!receiverHealthy || !armSwitch) armed = false;
  else if (!armed && throttle < 0.05f) { armed = true; rollI = 0; pitchI = 0; }
  if (!armed) { stopMotors(); return; }

  int16_t ax, ay, az, gxRaw, gyRaw, gzRaw;
  imu.getMotion6(&ax, &ay, &az, &gxRaw, &gyRaw, &gzRaw);
  float gx = (gxRaw - gyroXBias) / 131.0f;
  float gy = (gyRaw - gyroYBias) / 131.0f;
  float accelRoll = atan2f(static_cast<float>(ay), static_cast<float>(az)) * 180.0f / PI;
  float accelPitch = atan2f(-static_cast<float>(ax), sqrtf(static_cast<float>(ay) * ay + static_cast<float>(az) * az)) * 180.0f / PI;
  rollDeg = 0.98f * (rollDeg + gx * dt) + 0.02f * accelRoll;
  pitchDeg = 0.98f * (pitchDeg + gy * dt) + 0.02f * accelPitch;

  float rollError = mapChannel(rc[0], -20.0f, 20.0f) - rollDeg;
  float pitchError = mapChannel(rc[1], -20.0f, 20.0f) - pitchDeg;
  rollI = clampFloat(rollI + rollError * dt, -25, 25);
  pitchI = clampFloat(pitchI + pitchError * dt, -25, 25);
  float rollOut = 5.0f * rollError + 1.2f * rollI + 0.08f * (rollError - prevRollError) / dt;
  float pitchOut = 5.0f * pitchError + 1.2f * pitchI + 0.08f * (pitchError - prevPitchError) / dt;
  prevRollError = rollError; prevPitchError = pitchError;
  float yawOut = mapChannel(rc[3], -80.0f, 80.0f);
  float base = ESC_IDLE_US + throttle * (ESC_MAX_US - ESC_IDLE_US);
  writeEsc(0, static_cast<uint16_t>(clampFloat(base + pitchOut - rollOut + yawOut, ESC_IDLE_US, ESC_MAX_US)));
  writeEsc(1, static_cast<uint16_t>(clampFloat(base + pitchOut + rollOut - yawOut, ESC_IDLE_US, ESC_MAX_US)));
  writeEsc(2, static_cast<uint16_t>(clampFloat(base - pitchOut - rollOut - yawOut, ESC_IDLE_US, ESC_MAX_US)));
  writeEsc(3, static_cast<uint16_t>(clampFloat(base - pitchOut + rollOut + yawOut, ESC_IDLE_US, ESC_MAX_US)));
}

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