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 Goal | Recommended Platform | Verdict |
|---|---|---|
| Learn PID math, raw I2C sensor fusion, and ATmega328P limits | Arduino Nano + MPU6050 | Build this project |
| Fly FPV, use GPS return-to-home, achieve 30-min flights | Matek F405-Wing + Betaflight | Buy a dedicated FC |
| Build an autonomous swarming indoor micro-drone | ESP32-C3 + Crazyflie firmware | Pivot to ESP32/RTOS |
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 Pin | Component | Signal Type | Notes |
|---|---|---|---|
| A4 (SDA) | MPU6050 SDA | I2C Data | Requires 4.7k pull-up to 5V (usually on GY-521) |
| A5 (SCL) | MPU6050 SCL | I2C Clock | Keep wires under 10cm to prevent capacitance issues |
| D3 | ESC 1 (Front-Left) | PWM (50Hz) | CCW Motor |
| D5 | ESC 2 (Front-Right) | PWM (50Hz) | CW Motor |
| D6 | ESC 3 (Back-Right) | PWM (50Hz) | CCW Motor |
| D9 | ESC 4 (Back-Left) | PWM (50Hz) | CW Motor |
| 5V | UBEC 5V Output | Power | Do NOT power via USB while flying |
| GND | UBEC GND / MPU6050 | Ground | Common ground is mandatory for I2C stability |
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.






