ESP32 Mini Drone Flight Controller Design
esp32-c3-devkitm-1 as a close ESP32-C3 module targetTable
| Function | ESP32-C3 GPIO | Schematic Net | Connected To | Direction | Notes |
|---|---|---|---|---|---|
| Battery ADC | GPIO0 | VBAT_SENSE | R5/R6 divider + C8 filter | Input ADC | Divider: 470kΩ high, 220kΩ low; 4.2 V maps to about 1.34 V |
| Motor 4 output | GPIO1 | MOTOR4 | J2 pin 4 | PWM output | Logic-level motor/ESC signal |
| Boot strap | GPIO2 | ESP_GPIO2_STRAP | R8 10kΩ pull-up | Strap/input | Keep stable at boot |
| IMU interrupt | GPIO3 | IMU_INT1 | U2 INT1 | Input interrupt | BMI270 data-ready / interrupt |
| I2C SDA | GPIO4 | I2C_SDA | U2 SDX, R3 pull-up | Bidirectional | BMI270 I2C data |
| I2C SCL | GPIO5 | I2C_SCL | U2 SCX, R4 pull-up | Output | BMI270 I2C clock |
| Motor 1 output | GPIO6 | MOTOR1 | J2 pin 1 | PWM output | Logic-level motor/ESC signal |
| Motor 2 output | GPIO7 | MOTOR2 | J2 pin 2 | PWM output | Logic-level motor/ESC signal |
| Status LED / strap | GPIO8 | ESP_GPIO8_LED_BOOT | R9 pull-up, D1 cathode | Output | Active-low LED; keep high during boot |
| Boot button | GPIO9 | ESP_BOOT | R2 pull-up, SW2 to GND | Input/strap | Hold low while resetting for bootloader |
| Motor 3 output | GPIO10 | MOTOR3 | J2 pin 3 | PWM output | Logic-level motor/ESC signal |
| GPS UART RX | GPIO18 | GPS_RX_TO_ESP | J3 pin 1 | UART RX | Connect to GPS TX |
| GPS UART TX | GPIO19 | GPS_TX_FROM_ESP | J3 pin 2 | UART TX | Connect to GPS RX |
| Debug UART RXD0 | RXD0 | UART0_RX | J4 pin 2 | UART RX | Programming/log header |
| Debug UART TXD0 | TXD0 | UART0_TX | J4 pin 1 | UART TX | Programming/log header |
| Enable/reset | EN | ESP_EN | R1 pull-up, C2, SW1 | Reset input | SW1 pulls EN low |
platformio.ini:Ini
[env:esp32-c3-mini-drone] platform = espressif32 board = esp32-c3-devkitm-1 framework = arduino monitor_speed = 115200 upload_speed = 460800 build_flags = -D ARDUINO_USB_CDC_ON_BOOT=0 lib_deps = adafruit/Adafruit BMI270 Library adafruit/Adafruit Unified Sensor
src/main.cpp)Cpp
#include <Arduino.h> #include <Wire.h> #include <WiFi.h> #include <Adafruit_BMI270.h> #include <Adafruit_Sensor.h> // ── Pin definitions from schematic ────────────────────────────────────────── static constexpr uint8_t PIN_VBAT_ADC = 0; // VBAT_SENSE static constexpr uint8_t PIN_MOTOR4 = 1; // MOTOR4 -> J2 pin 4 static constexpr uint8_t PIN_IMU_INT1 = 3; // IMU_INT1 static constexpr uint8_t PIN_I2C_SDA = 4; // I2C_SDA -> BMI270 SDX static constexpr uint8_t PIN_I2C_SCL = 5; // I2C_SCL -> BMI270 SCX static constexpr uint8_t PIN_MOTOR1 = 6; // MOTOR1 -> J2 pin 1 static constexpr uint8_t PIN_MOTOR2 = 7; // MOTOR2 -> J2 pin 2 static constexpr uint8_t PIN_STATUS_LED = 8; // ESP_GPIO8_LED_BOOT, active-low LED static constexpr uint8_t PIN_BOOT_BUTTON = 9; // ESP_BOOT static constexpr uint8_t PIN_MOTOR3 = 10; // MOTOR3 -> J2 pin 3 static constexpr uint8_t PIN_GPS_RX = 18; // GPS_TX -> ESP RX static constexpr uint8_t PIN_GPS_TX = 19; // ESP TX -> GPS RX // ── Configuration ──────────────────────────────────────────────────────────── static constexpr uint32_t DEBUG_BAUD = 115200; static constexpr uint32_t GPS_BAUD = 9600; static constexpr uint32_t CONTROL_HZ = 250; // lightweight control loop target static constexpr uint32_t TELEMETRY_MS = 500; static constexpr float ADC_FULL_SCALE_V = 3.3f; static constexpr int ADC_MAX_COUNTS = 4095; static constexpr float VBAT_R_HIGH_OHMS = 470000.0f; static constexpr float VBAT_R_LOW_OHMS = 220000.0f; static constexpr float VBAT_DIVIDER_GAIN = (VBAT_R_HIGH_OHMS + VBAT_R_LOW_OHMS) / VBAT_R_LOW_OHMS; static constexpr float VBAT_WARN_V = 3.50f; static constexpr float VBAT_LAND_V = 3.30f; // Motor PWM using ESP32 LEDC peripheral static constexpr uint32_t MOTOR_PWM_HZ = 400; static constexpr uint8_t MOTOR_PWM_BITS = 12; static constexpr uint16_t MOTOR_MIN_DUTY = 0; static constexpr uint16_t MOTOR_IDLE_DUTY = 200; static constexpr uint16_t MOTOR_MAX_DUTY = 4095; const char* WIFI_SSID = "YOUR_SSID"; const char* WIFI_PASSWORD = "YOUR_PASSWORD"; Adafruit_BMI270 bmi270; HardwareSerial GPSSerial(1); uint32_t lastControlUs = 0; uint32_t lastTelemetryMs = 0; bool imuReady = false; void setStatusLed(bool on) { digitalWrite(PIN_STATUS_LED, on ? LOW : HIGH); // active-low LED } float readBatteryVoltage() { uint32_t raw = analogRead(PIN_VBAT_ADC); float adcV = (static_cast<float>(raw) / ADC_MAX_COUNTS) * ADC_FULL_SCALE_V; return adcV * VBAT_DIVIDER_GAIN; } void setupMotorPwm() { ledcSetup(0, MOTOR_PWM_HZ, MOTOR_PWM_BITS); ledcSetup(1, MOTOR_PWM_HZ, MOTOR_PWM_BITS); ledcSetup(2, MOTOR_PWM_HZ, MOTOR_PWM_BITS); ledcSetup(3, MOTOR_PWM_HZ, MOTOR_PWM_BITS); ledcAttachPin(PIN_MOTOR1, 0); ledcAttachPin(PIN_MOTOR2, 1); ledcAttachPin(PIN_MOTOR3, 2); ledcAttachPin(PIN_MOTOR4, 3); ledcWrite(0, MOTOR_MIN_DUTY); ledcWrite(1, MOTOR_MIN_DUTY); ledcWrite(2, MOTOR_MIN_DUTY); ledcWrite(3, MOTOR_MIN_DUTY); } void setMotorDuty(uint16_t m1, uint16_t m2, uint16_t m3, uint16_t m4) { ledcWrite(0, constrain(m1, MOTOR_MIN_DUTY, MOTOR_MAX_DUTY)); ledcWrite(1, constrain(m2, MOTOR_MIN_DUTY, MOTOR_MAX_DUTY)); ledcWrite(2, constrain(m3, MOTOR_MIN_DUTY, MOTOR_MAX_DUTY)); ledcWrite(3, constrain(m4, MOTOR_MIN_DUTY, MOTOR_MAX_DUTY)); } void emergencyLanding() { // Conservative placeholder: reduce to idle. Replace with tested descent logic. setMotorDuty(MOTOR_IDLE_DUTY, MOTOR_IDLE_DUTY, MOTOR_IDLE_DUTY, MOTOR_IDLE_DUTY); setStatusLed(true); } void connectWiFiNonBlockingStart() { WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); } void setupImu() { Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, 400000); imuReady = bmi270.begin_I2C(0x68, &Wire); if (!imuReady) { Serial.println("BMI270 not detected at I2C address 0x68"); return; } bmi270.setAccelerometerRange(BMI2_ACC_RANGE_4G); bmi270.setGyroRange(BMI2_GYR_RANGE_500); bmi270.setAccelerometerRate(BMI2_ACC_ODR_200HZ); bmi270.setGyroRate(BMI2_GYR_ODR_200HZ); Serial.println("BMI270 initialized"); } void runControlLoop() { float vbat = readBatteryVoltage(); if (vbat < VBAT_LAND_V) { emergencyLanding(); return; } sensors_event_t accel, gyro, temp; if (imuReady && bmi270.getEvent(&accel, &gyro, &temp)) { // Lightweight placeholder stabilization: keep motors idle until control gains are tuned. // Use gyro.gyro.x/y/z and accel.acceleration.x/y/z for attitude estimation. setMotorDuty(MOTOR_IDLE_DUTY, MOTOR_IDLE_DUTY, MOTOR_IDLE_DUTY, MOTOR_IDLE_DUTY); } else { // IMU failure is flight-critical. emergencyLanding(); } } void printTelemetry() { Serial.printf("VBAT=%.2f V, WiFi=%s, IMU=%s\n", readBatteryVoltage(), WiFi.status() == WL_CONNECTED ? "connected" : "offline", imuReady ? "ok" : "fail"); while (GPSSerial.available()) { Serial.write(GPSSerial.read()); // transparent GPS/NMEA debug bridge } } void setup() { pinMode(PIN_STATUS_LED, OUTPUT); setStatusLed(false); pinMode(PIN_BOOT_BUTTON, INPUT_PULLUP); pinMode(PIN_IMU_INT1, INPUT); Serial.begin(DEBUG_BAUD); delay(200); Serial.println("ESP32-C3 mini drone flight controller starting..."); analogReadResolution(12); analogSetPinAttenuation(PIN_VBAT_ADC, ADC_11db); GPSSerial.begin(GPS_BAUD, SERIAL_8N1, PIN_GPS_RX, PIN_GPS_TX); setupMotorPwm(); setupImu(); connectWiFiNonBlockingStart(); setStatusLed(true); delay(100); setStatusLed(false); lastControlUs = micros(); } void loop() { const uint32_t nowUs = micros(); const uint32_t periodUs = 1000000UL / CONTROL_HZ; if (nowUs - lastControlUs >= periodUs) { lastControlUs += periodUs; runControlLoop(); } const uint32_t nowMs = millis(); if (nowMs - lastTelemetryMs >= TELEMETRY_MS) { lastTelemetryMs = nowMs; printTelemetry(); } if (readBatteryVoltage() < VBAT_WARN_V) { setStatusLed((nowMs / 250) % 2); // blink low-battery warning } }
platformio.ini above and copy the source into src/main.cpp.pio runpio run -t upload.pio device monitor -b 115200Platform & Toolchain
Pin Mapping
Dependencies & Project Setup
Complete Firmware Source (`src/main.cpp`)
Build & Flash Instructions
Firmware Notes