Community project

ESP32 Quadcopter Flight Controller

Smruti Sourav Sahoo

Published July 29, 2026

ESP328 components6 assembly steps
Remix this project
Photo of ESP32 Quadcopter Flight Controller

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

Interactive · read-only
Wiring diagram for ESP32 Quadcopter Flight Controller

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
DFRobot SEN0142 Fermion MPU-6050 6 DOF Sensor BreakoutMPU-60501DFRobot SEN0142 MPU-6050 breakout with 3-5 V board input, I2C interface, onboard I2C pull-ups, and i2cdevlib Arduino example coverage.
SBUS RC receiverSBUS15 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.
Brushless ESC (front-left)3S PWM ESC13S-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.
Brushless ESC (front-right)3S PWM ESC13S-capable brushless ESC with a 3.3 V-compatible PWM signal input.
Brushless ESC (rear-left)3S PWM ESC13S-capable brushless ESC with a 3.3 V-compatible PWM signal input.
Brushless ESC (rear-right)3S PWM ESC13S-capable brushless ESC with a 3.3 V-compatible PWM signal input.
3S LiPo flight battery3S 11.1 V LiPo111.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.
MP1584 Buck Converter5 V output1Adjustable 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.

Assembly

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.

    • Tip: Keep the IMU level and do not let wires tug on it.
    • Tip: 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.

    • Tip: 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.

    • Tip: Use a regulator rated for at least 1 A continuous current.
    • Tip: 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.

    • Tip: Set receiver failsafe with throttle low and arm switch OFF.
    • Tip: 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.

    • Tip: For a brushless motor, swap any two of its three ESC-to-motor wires to reverse that motor’s rotation.
    • Tip: 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.

    • Tip: If tilt correction is opposite, stop immediately and correct sensor orientation or motor mapping before attempting flight.
    • Tip: 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.

Pin assignments

Board wiring reference
PinConnectionType
3V3imu_1 VINpower
GNDimu_1 GNDground
GPIO 21imu_1 SDAi2c
GPIO 22imu_1 SCLi2c
5Vsbus_rx_1 VCCpower
GNDsbus_rx_1 GNDground
GPIO 16sbus_rx_1 SBUSuart
EXTesc_fl_1 BAT+Fused 3S power-distribution harness positivepower
GNDesc_fl_1 BAT-ground
GPIO 25esc_fl_1 SIGpwm
GNDesc_fl_1 SIG_GNDground
EXTesc_fr_1 BAT+Fused 3S power-distribution harness positivepower
GNDesc_fr_1 BAT-ground
GPIO 26esc_fr_1 SIGpwm
GNDesc_fr_1 SIG_GNDground
EXTesc_rl_1 BAT+Fused 3S power-distribution harness positivepower
GNDesc_rl_1 BAT-ground
GPIO 27esc_rl_1 SIGpwm
GNDesc_rl_1 SIG_GNDground
EXTesc_rr_1 BAT+Fused 3S power-distribution harness positivepower
GNDesc_rr_1 BAT-ground
GPIO 33esc_rr_1 SIGpwm
GNDesc_rr_1 SIG_GNDground
EXTlipo_3s_1 POSMP1584 Buck Converter VINpower
GNDlipo_3s_1 NEGground
5Vbuck_5v_1 VOUTpower
GNDbuck_5v_1 GNDground

Firmware

ESP32
main.cppDeploy to device
#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)));
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

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