ESP32 Mini Drone Flight Controller Design


I’ll tighten the placement and improve robustness now: compact the board grouping, strengthen power/IMU/antenna placement priorities, then rerun checks and only stop before routing for your approval.
The compacting pass tightened most support parts, but it pushed the ESP32 module too far toward the edge. I’m correcting that now so the antenna still faces outward while the module body/pads remain safely manufacturable.
I’m blocked because credits ran out just before I could run the final post-placement review.
Completed this session:
  • Continued PCB placement on the existing 50 × 50 mm, 4-layer board.
  • Corrected the mislabeled duplicate ground-like net for the GPIO8/status LED path to ESP_GPIO8_LED_BOOT before placement.
  • Placed all 29 physical components by functional block.
  • Enhanced placement for compactness and robustness:
    • ESP32-C3 module is now fully inside the board with antenna side at the top edge.
    • BMI270 IMU is kept central for better motion sensing.
    • TPS63001 power stage is compact around U3/L3/C4/C5/C6/C7/J1.
    • LiPo, motor, GPS/UART, programming headers, and reset/boot buttons remain edge-accessible.
    • Decoupling and strap components remain close to their IC pins.
    • LED parts were moved away from the ESP32 antenna area.
    • UART programming header was moved closer to the ESP32 to reduce long UART paths.
  • Placement tool reported no overlap, off-board, unplaced, spacing, or critical placement problems after the final correction.
Still required:
  • Re-run post-placement review.
  • Confirm only expected unrouted-airwire issues remain.
  • Fix any actual placement/clearance issues if found.
  • Ask for your approval before routing traces.
Run ERC/DRC
Check antenna clearance
Optimize power loop
Inspect edge access

Firmware Starter — ESP32-C3-MINI-1U-N4
Platform & Toolchain
  • MCU/module: ESP32-C3-MINI-1U-N4
  • Framework: Arduino for ESP32 via PlatformIO
  • Board target: esp32-c3-devkitm-1 as a close ESP32-C3 module target
  • Primary functions: IMU stabilization loop, battery monitoring, motor PWM outputs, GPS/UART expansion, WiFi telemetry, OTA-ready structure
Pin Mapping

Table


FunctionESP32-C3 GPIOSchematic NetConnected ToDirectionNotes
Battery ADCGPIO0VBAT_SENSER5/R6 divider + C8 filterInput ADCDivider: 470kΩ high, 220kΩ low; 4.2 V maps to about 1.34 V
Motor 4 outputGPIO1MOTOR4J2 pin 4PWM outputLogic-level motor/ESC signal
Boot strapGPIO2ESP_GPIO2_STRAPR8 10kΩ pull-upStrap/inputKeep stable at boot
IMU interruptGPIO3IMU_INT1U2 INT1Input interruptBMI270 data-ready / interrupt
I2C SDAGPIO4I2C_SDAU2 SDX, R3 pull-upBidirectionalBMI270 I2C data
I2C SCLGPIO5I2C_SCLU2 SCX, R4 pull-upOutputBMI270 I2C clock
Motor 1 outputGPIO6MOTOR1J2 pin 1PWM outputLogic-level motor/ESC signal
Motor 2 outputGPIO7MOTOR2J2 pin 2PWM outputLogic-level motor/ESC signal
Status LED / strapGPIO8ESP_GPIO8_LED_BOOTR9 pull-up, D1 cathodeOutputActive-low LED; keep high during boot
Boot buttonGPIO9ESP_BOOTR2 pull-up, SW2 to GNDInput/strapHold low while resetting for bootloader
Motor 3 outputGPIO10MOTOR3J2 pin 3PWM outputLogic-level motor/ESC signal
GPS UART RXGPIO18GPS_RX_TO_ESPJ3 pin 1UART RXConnect to GPS TX
GPS UART TXGPIO19GPS_TX_FROM_ESPJ3 pin 2UART TXConnect to GPS RX
Debug UART RXD0RXD0UART0_RXJ4 pin 2UART RXProgramming/log header
Debug UART TXD0TXD0UART0_TXJ4 pin 1UART TXProgramming/log header
Enable/resetENESP_ENR1 pull-up, C2, SW1Reset inputSW1 pulls EN low
Dependencies & Project Setup
Create a PlatformIO project and use this 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
Complete Firmware Source (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
  }
}
Build & Flash Instructions
  1. Install PlatformIO.
  2. Create the project with the platformio.ini above and copy the source into src/main.cpp.
  3. Build: pio run
  4. Flash: hold BOOT/SW2 low, tap RESET/SW1, then run pio run -t upload.
  5. Monitor logs: pio device monitor -b 115200
Firmware Notes
  • The motor code is intentionally safe: it initializes outputs at zero/idle only. Do not arm motors until the control loop, ESC protocol, and failsafe behavior are tested with propellers removed.
  • Return-to-home requires a supported GPS connected to J3 and application-level navigation code; this starter only bridges GPS serial data to debug output.
  • OTA update support can be added with ArduinoOTA after WiFi connection is stable.
  • Platform & Toolchain

  • Pin Mapping

  • Dependencies & Project Setup

  • Complete Firmware Source (`src/main.cpp`)

  • Build & Flash Instructions

  • Firmware Notes