Building a quadcopter from scratch using a raw microcontroller is one of the most demanding embedded systems projects you can tackle. Unlike plug-and-play flight controllers, an Arduino drone forces you to handle I2C sensor polling, PWM signal generation, and PID control loop math at the register and code level. This guide provides the exact hardware, pin mappings, and compilable C++ code to get an ATmega328P-based quadcopter off the bench and into the air.

The 'Arduino Drone' Decision Matrix

Before cutting wires, determine if a raw microcontroller build aligns with your actual goals. Dedicated flight controllers (FCs) have largely replaced raw Arduinos for practical flying, but the Arduino remains the superior tool for control theory education.

Primary GoalRecommended PlatformVerdict
Learn PID math, raw I2C sensor fusion, and ATmega328P limitsArduino Nano + MPU6050Build this project
Fly FPV, use GPS return-to-home, achieve 30-min flightsMatek F405-Wing + BetaflightBuy a dedicated FC
Build an autonomous swarming indoor micro-droneESP32-C3 + Crazyflie firmwarePivot to ESP32/RTOS
Default Recommendation: If your goal is to understand how a flight controller actually works under the hood, terminate your decision here and build the Arduino Nano + MPU6050 platform detailed below. If you just want to fly a camera drone this weekend, buy a pre-built DJI or a Matek F405 kit.

Parts List & Spec Sheet

This build targets a 450-class quadcopter. This size provides enough physical mass to dampen high-frequency motor vibrations, which is critical when using a basic IMU without advanced hardware low-pass filtering.

  • Flight Brain: Arduino Nano (ATmega328P, 16MHz, 5V logic) — ~$6. Do not use the Nano 33 IoT or BLE for this specific codebase; the 5V logic of the classic Nano interfaces directly with standard RC receivers and older ESC opto-isolators without logic level shifters.
  • IMU Sensor: GY-521 Breakout board (MPU-6050 6-axis) — ~$4. Ensure it has the onboard 3.3V LDO voltage regulator and 4.7kΩ I2C pull-up resistors.
  • Motors: Emax RS2205 2300KV Brushless (x4) — ~$18 each. 2300KV is optimal for 3S LiPo power and 5-inch propellers.
  • ESCs: Hobbywing Skywalker 30A Opto (x4) — ~$15 each. 'Opto' means they lack a built-in Battery Eliminator Circuit (BEC), requiring a separate 5V power supply for the Arduino.
  • Frame: F450 V2 Quadcopter Frame (450mm diagonal) — ~$25.
  • Battery: Zippy Compact 3S 2200mAh 25C LiPo — ~$22.
  • Power: Standalone 5V 3A UBEC (to power the Nano and receiver from the 3S LiPo).

Pin Mapping & Power Distribution

The ATmega328P has limited hardware PWM pins. We must map the four ESCs to pins that support the Servo library's hardware timer interrupts to ensure jitter-free 50Hz PWM signals.

Arduino Nano PinComponentSignal TypeNotes
A4 (SDA)MPU6050 SDAI2C DataRequires 4.7k pull-up to 5V (usually on GY-521)
A5 (SCL)MPU6050 SCLI2C ClockKeep wires under 10cm to prevent capacitance issues
D3ESC 1 (Front-Left)PWM (50Hz)CCW Motor
D5ESC 2 (Front-Right)PWM (50Hz)CW Motor
D6ESC 3 (Back-Right)PWM (50Hz)CCW Motor
D9ESC 4 (Back-Left)PWM (50Hz)CW Motor
5VUBEC 5V OutputPowerDo NOT power via USB while flying
GNDUBEC GND / MPU6050GroundCommon ground is mandatory for I2C stability
Vibration Dampening: Do not hard-bolt the MPU6050 to the carbon fiber frame. High-frequency motor noise will alias into the gyroscope readings and destroy your PID derivative (D) term. Mount the IMU on a piece of 3mm Sorbothane or thick double-sided foam tape.

Compilable Flight Stabilization Code

The following code targets the Arduino Nano (ATmega328P). It initializes the MPU6050, configures the digital low-pass filter (DLPF), and runs a basic proportional (P) stabilization loop. Full PID integration requires tuning specific to your frame's moment of inertia, but this base code provides the exact I2C read structure and motor mixing required to achieve a stable hover.

#include <Wire.h>
#include <Servo.h>

// --- PIN DEFINITIONS ---
#define ESC_FL_PIN 3  // Front Left (CCW)
#define ESC_FR_PIN 5  // Front Right (CW)
#define ESC_BR_PIN 6  // Back Right (CCW)
#define ESC_BL_PIN 9  // Back Left (CW)

// --- IMU REGISTERS ---
#define MPU6050_ADDR 0x68
#define PWR_MGMT_1   0x6B
#define CONFIG       0x1A
#define GYRO_CONFIG  0x1B
#define ACCEL_XOUT_H 0x3B

Servo escFL, escFR, escBR, escBL;

float gyroX, gyroY, gyroZ;
float pitch, roll;
float pitchError, rollError;
float Kp_pitch = 1.2; // Proportional gain (Requires physical tuning)
float Kp_roll = 1.2;
int throttleBase = 1150; // Base PWM for hover (approx 50% throttle)

void setup() {
  Serial.begin(115200);
  Wire.begin();
  Wire.setClock(400000); // 400kHz Fast Mode

  // Initialize MPU6050 with Error Handling
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(PWR_MGMT_1);
  Wire.write(0); // Wake up
  uint8_t status = Wire.endTransmission();
  
  if (status != 0) {
    Serial.print("MPU6050 I2C read failed: status ");
    Serial.println(status);
    while(1); // Halt execution if IMU is missing
  }

  // Set DLPF to 44Hz (crucial for filtering motor noise)
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(CONFIG);
  Wire.write(0x03); 
  Wire.endTransmission();

  // Set Gyro to +/- 500 deg/s
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(GYRO_CONFIG);
  Wire.write(0x08); 
  Wire.endTransmission();

  // Attach ESCs
  escFL.attach(ESC_FL_PIN, 1000, 2000);
  escFR.attach(ESC_FR_PIN, 1000, 2000);
  escBR.attach(ESC_BR_PIN, 1000, 2000);
  escBL.attach(ESC_BL_PIN, 1000, 2000);

  // ESC Arming Sequence (Throttle must be zero)
  escFL.writeMicroseconds(1000);
  escFR.writeMicroseconds(1000);
  escBR.writeMicroseconds(1000);
  escBL.writeMicroseconds(1000);
  delay(3000); // Wait for ESC arming beeps
}

void loop() {
  // 1. Read Raw Gyro Data
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x43); // Gyro X high byte register
  Wire.endTransmission(false);
  Wire.requestFrom(MPU6050_ADDR, 6, true);
  
  int16_t rawGX = Wire.read() << 8 | Wire.read();
  int16_t rawGY = Wire.read() << 8 | Wire.read();
  int16_t rawGZ = Wire.read() << 8 | Wire.read();

  // Convert to deg/s (500 deg/s scale = 65.5 LSB/deg/s)
  gyroX = rawGX / 65.5;
  gyroY = rawGY / 65.5;

  // 2. Calculate Error (Assuming 0 deg is level setpoint)
  // Note: In a full implementation, integrate gyro over dt to get angle.
  // Here we use rate-mode stabilization for immediate response.
  pitchError = 0 - gyroX; 
  rollError = 0 - gyroY;

  // 3. PID Output (P-term only for base stability)
  float pitchCorrection = pitchError * Kp_pitch;
  float rollCorrection = rollError * Kp_roll;

  // 4. Motor Mixing (Quad X configuration)
  int pwmFL = throttleBase + pitchCorrection + rollCorrection;
  int pwmFR = throttleBase + pitchCorrection - rollCorrection;
  int pwmBR = throttleBase - pitchCorrection - rollCorrection;
  int pwmBL = throttleBase - pitchCorrection + rollCorrection;

  // 5. Constrain and Write PWM
  escFL.writeMicroseconds(constrain(pwmFL, 1000, 2000));
  escFR.writeMicroseconds(constrain(pwmFR, 1000, 2000));
  escBR.writeMicroseconds(constrain(pwmBR, 1000, 2000));
  escBL.writeMicroseconds(constrain(pwmBL, 1000, 2000));

  delay(4); // Maintain ~250Hz control loop
}

Debugging: First Three Things to Check

When your drone fails to arm or flips on takeoff, do not blindly change PID gains. Follow this ranked troubleshooting path.

1. Serial Monitor outputs: MPU6050 I2C read failed: status 2

Cause: The Wire.endTransmission() function returns a status of 2 when the sensor NACKs the address. This means the Arduino cannot see the IMU on the I2C bus.

  • Fix A: Verify SDA is on A4 and SCL is on A5. Swapping these is the most common bench mistake.
  • Fix B: Check the GY-521 3.3V LDO. If you are feeding the IMU 5V but the onboard regulator is blown, the MPU6050 core will brownout and drop off the bus. Measure the VCC pin on the breakout with a multimeter; it must read >4.5V.
  • Fix C: Add external 4.7kΩ pull-up resistors to SDA and SCL if your specific GY-521 clone omitted them to save $0.02 in manufacturing.

2. ESCs beep continuously and refuse to arm

Cause: Hobbywing and standard SimonK ESCs require a 1000µs (0% throttle) pulse for 2-3 seconds upon power-up to calibrate their lower bound. If the Arduino boots and sends 1150µs immediately, the ESC enters programming mode or safety-lock.

  • Fix: Ensure the physical RC transmitter throttle stick is physically at the bottom before you plug in the LiPo battery. If using the test code above, ensure no external RC receiver interrupts pin D3 during the 3-second delay(3000) arming sequence.

3. Drone violently flips the moment it leaves the ground

Cause: This is rarely a PID tuning issue; it is almost always a motor mixing or propeller orientation error.

  • Fix: Pick the drone up (props removed) and tilt it forward. The rear motors must spin faster. Tilt it left; the right motors must spin faster. If the wrong motors spool up, swap the PWM pins in the code or swap the motor wires on the ESC. Verify CW and CCW propellers are on the correct diagonals.

Extending or Simplifying the Build

Once you have achieved a stable hover using the P-controller rate-mode code above, you will hit the physical limits of the ATmega328P's processing speed and memory when attempting to add GPS or advanced filtering.

To Simplify: If the raw I2C polling and math become overwhelming, flash the Arduino Nano with the legacy MultiWii firmware. It handles the sensor fusion and PID loops in the background, allowing you to configure the drone via a GUI. Alternatively, abandon the Nano and purchase a Matek F405 flight controller, which runs Betaflight and handles all stabilization natively.

To Extend: The most logical next step for this specific hardware stack is adding Z-axis altitude hold. Wire an MS5611 barometric pressure sensor to the secondary I2C bus (using a software I2C library on pins D2/D4, as the hardware I2C bus is saturated by the MPU6050 at 400kHz). Feed the barometer's altitude delta into a secondary PID loop to modulate the throttleBase variable dynamically. For deeper sensor fusion theory, consult the official TDK InvenSense MPU-6050 datasheet to configure the onboard Digital Motion Processor (DMP), which offloads the quaternion math from the Arduino's CPU.