Квадрокоптер на ESP32 — как сделать? Подробная статья по созданию дрона на ESP.

Всем привет!
Квадрокоптеры — это интересные устройства, которые можно собрать своими руками при имении определённых навыков и деталей. В этой статье мы рассмотрим создание бюджетного дрона с использованием коллекторных моторов и ESP32. Такой вариант идеален для новичков: коллекторные моторы дешевле и проще в управлении, чем бесколлекторные, а ESP32 обеспечит Wi-Fi управление дроном.
Основные особенности проекта:
- Коллекторные моторы (типа 8520 или 716).
- Управление через MOSFET-транзисторы.
- Простая PID-стабилизация на базе микросхемы гироскопа MPU6050.
Глава 1: Теория и компоненты
1.1. Почему коллекторные моторы?
Коллекторные моторы (brushed DC) — это дешевые и доступные двигатели, которые не требуют сложных регуляторов скорости, как их более крутые и дорогие бесколлекторные братья. Их можно напрямую управлять через MOSFET-транзисторы, что упрощает схему.
Недостатки коллекторных моторов:
- Низкий КПД и срок службы (до 10–20 часов постоянного вращения, у них истираются щётки).
- Ограниченная мощность (подходят для мини-дронов весом до 200 г).
1.2. Установка пропеллеров на мотор/на редуктор
На этом этапе нужно приостановиться и подумать — какого размера и веса будет ваш дрон?
Если маленький (10-70 грамм), то пропеллеры используются маленькие и устанавливаются прямо на вал моторчика.
Если квадрокоптер крупнее (100-300 грамм), то пропеллеры нужны значительно крупнее, и в случае использования коллекторных моторов (а они обычно маленькие) необходим редуктор, чтобы мотор мог раскрутить большой пропеллер.
Если используется редуктор для повышения мощности, то уменьшается скорость пропеллера, а она нужна большая, значит, приходится ускорить моторчик, от этого он быстрее испортится. Рекомендую собирать маленький дрон, если используете коллекторные моторы.
Для больших и мощных дронов используйте бесколлекторные моторы, они мощнее и крупнее. Но об этом в другой статье.
1.3. Основные компоненты
Для сборки квадрокоптера вам понадобятся:
- Рама:
- Готовая карбоновая рама для микро-дронов (100–150 мм), 3D-печатная версия, или самодельная рама из пластика (ПВХ труб, например).
- Моторы:
- 4 коллекторных мотора (например, 716 Coreless с тягой ~50 г каждый. Общий вес дрона до 200 грамм).
- Пропеллеры:
- Пластиковые пропеллеры 65 мм (обязательно соблюдать направление вращения: 2 CW(по часовой стрелке), 2 CCW(против часовой стрелки)).
- MOSFET-транзисторы:
- 4 N-канальных MOSFET (например, IRLZ44N или AO3400) для управления моторами.
- Аккумулятор:
- LiPo 1S (3.7 В, 500–800 мАч) или 2S (7.4 В) в зависимости от моторов. Для коллекторных моторов — 3.7в, для бесколлекторных — 7.4в.
- Примечание: буквой S у аккумуляторов обозначается количество «банок», то есть элементов питания. 1S это одна банка, 3.7 вольт. 2S это уже две банки. 7.4 вольт.
- Датчики:
- MPU6050 (гироскоп + акселерометр).
- Плата управления:
- Плата с Wi-Fi/Bluetooth (например, ESP32 DevKit).
- Дополнительно:
- Резисторы 10 кОм, диоды 1N4007, макетная плата (перфорированная для пайки, или готовая печатная плата под заказ по схеме), провода, паяльный набор.
ВЫБОР ПЛАТЫ ESP32
Заострим внимание на выборе платы ESP32.
Как видите на этих картинках, здесь представлены два варианта платы ESP32. Первая плата — плата для экспериментов на макетной плате. Вторая — плата для готовых проектов. Первая ПЛАТА запитывается от 5 вольт, на ней есть понижайка до 3.3 вольт. Вторая ПЛАТА питается только от 3.3 вольт — на ней нет понижайки.


Глава 2: Схема подключения и сборка
2.1. Управление моторами через MOSFET
Коллекторные моторы подключаются через MOSFET-транзисторы, которые работают как электронные выключатели.

Схема подключения одного мотора:
- Сток (D) MOSFET → Минус мотора.
- Исток (S) MOSFET → Земля (GND).
- Затвор (G) MOSFET → PWM-выход ESP32 через резистор 100–220 Ом.
- Плюс мотора → Напрямую к аккумулятору (через выключатель).
Важно:
- Между плюсом и минусом мотора добавьте диод 1N4007 для защиты от обратной ЭДС.
- ВНИМАНИЕ!! Плату ESP32 необходимо запитывать строго от 3.3В, если нет встроенной «понижайки» с 5-7 вольт до 3.3. Если же «понижайка» есть, тогда лучше запитать плату от 5 вольт на порт VIN.
2.2. Сборка рамы
- Закрепите моторы на лучах рамы винтами.
- Установите ESP32 и MPU6050 в центре рамы, изолируя их от вибраций (например, с помощью поролона).
- Подключите MOSFET-ы на макетной плате рядом с ESP32.

Глава 3: Программирование ESP32
3.1. Настройка PWM для моторов
ESP32 поддерживает 16 каналов PWM. Для управления MOSFET используйте библиотеку ESP32Servo. Так проще управлять ШИМ-сигналом на плате с ESP32.
#include <ESP32Servo.h>
// Пины PWM для моторов
#define MOTOR1_PIN 12
#define MOTOR2_PIN 13
#define MOTOR3_PIN 14
#define MOTOR4_PIN 15
Servo motor1, motor2, motor3, motor4;
void setup() {
motor1.attach(MOTOR1_PIN);
motor2.attach(MOTOR2_PIN);
motor3.attach(MOTOR3_PIN);
motor4.attach(MOTOR4_PIN);
}
void setMotorSpeed(Servo &motor, int speed) {
speed = constrain(speed, 0, 180); // Диапазон 0-180 (аналог 0-100%)
motor.write(speed);
}
3.2. Калибровка MPU6050
Датчик MPU6050 подключается по I2C (SDA – GPIO21, SCL – GPIO22). Используйте библиотеку Adafruit_MPU6050:
#include <Adafruit_MPU6050.h>
Adafruit_MPU6050 mpu;
void setup() {
if (!mpu.begin()) {
Serial.println("MPU6050 не найден!");
while (1);
}
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
}
void loop() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
float pitch = atan2(a.acceleration.y, a.acceleration.z) * 180 / PI;
// Аналогично для roll и yaw
}
3.3. PID-регулятор
Реализуйте простой PID для стабилизации:
float Kp = 1.5, Ki = 0.01, Kd = 0.1;
float error, prevError, integral;
void stabilize() {
float currentAngle = getPitch(); // Получить угол с MPU6050
float targetAngle = 0; // Целевой угол (горизонт)
error = targetAngle - currentAngle;
integral += error;
float derivative = error - prevError;
float output = Kp * error + Ki * integral + Kd * derivative;
adjustMotors(output); // Применить к моторам
prevError = error;
}
Глава 4: Испытания и проблемы
4.1. Первый запуск
- Откалибруйте моторы: подайте минимальный PWM (значение ~30), чтобы они начали вращаться.
- Проверьте направление вращения (два мотора должны крутиться по часовой стрелке, два — против).
Частые ошибки:
- Моторы не запускаются: Проверьте соединение MOSFET, диоды, напряжение на затворе.
- ESP32 перезагружается: Добавьте конденсатор 100 мкФ между питанием и землей.
4.2. Настройка PID
- Начните с коэффициентов Kp=1, Ki=0, Kd=0.
- Постепенно увеличивайте Kp, пока дрон не начнет держать горизонт.
- Добавьте Kd для подавления колебаний.
4.3. Полетные тесты
- Первый полет: Удерживайте дрон в руке и проверьте реакцию на наклоны.
- Взлет: Плавно увеличивайте тягу. Если дрон заваливается в сторону, проверьте балансировку рамы и калибровку датчиков.
Глава 5: Оптимизация и доработки
5.1. Улучшение стабильности
- Добавьте фильтр Калмана для сглаживания данных с MPU6050.
- Используйте прерывания для чтения датчика с частотой 100–200 Гц.
5.2. Wi-Fi управление
ESP32 позволяет управлять дроном через Wi-Fi. Пример кода для сервера:
#include <WiFi.h>
const char* ssid = "DroneNet";
const char* password = "12345678";
WiFiServer server(80);
void setup() {
WiFi.softAP(ssid, password);
server.begin();
}
void loop() {
WiFiClient client = server.available();
if (client) {
String request = client.readStringUntil('\r');
if (request.indexOf("throttle=150") != -1) {
setMotorSpeed(motor1, 150);
}
client.stop();
}
}
Заключение
Собрать квадрокоптер на коллекторных моторах реально даже для начинающих. Основные сложности — точная настройка PID и борьба с вибрациями. Такой дрон станет отличной платформой для экспериментов с автономными полетами и телеметрией через Wi-Fi.
Советы:
- Начинайте с малых высот.
- Используйте защиту пропеллеров.
- Экспериментируйте с датчиками (например, добавьте ультразвуковой сенсор для удержания высоты).
Ниже представлен образец кода для вашего проекта квадрокоптера. Это просто образец кода, вы можете его взять и доработать, если что-то работает не так как хочется.
#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_MPU6050.h>
#include <ESP32Servo.h>
// Настройки Wi-Fi
const char* ssid = "DroneController";
const char* password = "";
// Компоненты
WebServer server(80);
Adafruit_MPU6050 mpu;
Servo motors[4];
// Пины моторов
const int motorPins[4] = {12, 13, 14, 15};
// Управление
volatile int throttle = 0; // 0-100%
volatile int pitch = 0; // -100..+100
volatile int roll = 0; // -100..+100
volatile int yaw = 0; // -100..+100
// PID-коэффициенты (настраиваются экспериментально)
float Kp = 1.2, Ki = 0.01, Kd = 0.15;
float pid_pitch = 0, pid_roll = 0;
// HTML-страница с джойстиками
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Drone Controller</title>
<style>
.joystick {
width: 200px;
height: 200px;
background: #ddd;
border-radius: 50%;
margin: 20px;
touch-action: none;
}
</style>
</head>
<body>
<div style="display: flex;">
<div id="leftJoystick" class="joystick"></div>
<div id="rightJoystick" class="joystick"></div>
</div>
<script>
function sendData(x1, y1, x2, y2) {
fetch('/control?th='+y1+'&px='+y2+'&rl='+x2+'&yw='+x1)
.then(response => console.log('OK'))
}
function setupJoystick(element, callback) {
let dragging = false;
let rect = element.getBoundingClientRect();
let centerX = rect.width/2;
let centerY = rect.height/2;
element.addEventListener('touchstart', (e) => {
e.preventDefault();
dragging = true;
});
element.addEventListener('touchmove', (e) => {
if (!dragging) return;
let touch = e.touches[0];
let x = touch.clientX - rect.left - centerX;
let y = touch.clientY - rect.top - centerY;
callback(x/centerX, -y/centerY);
});
element.addEventListener('touchend', () => {
dragging = false;
callback(0, 0);
});
}
// Инициализация джойстиков
setupJoystick(document.getElementById('leftJoystick'), (x, y) => {
sendData(x*100, (y*50)+50, 0, 0); // Левый: X=Yaw, Y=Throttle
});
setupJoystick(document.getElementById('rightJoystick'), (x, y) => {
sendData(0, 0, x*100, y*100); // Правый: X=Roll, Y=Pitch
});
</script>
</body>
</html>
)rawliteral";
void handleRoot() {
server.send(200, "text/html", htmlPage);
}
void handleControl() {
throttle = server.arg("th").toInt();
yaw = server.arg("yw").toInt();
roll = server.arg("rl").toInt();
pitch = server.arg("px").toInt();
server.send(200, "text/plain", "OK");
}
void setup() {
Serial.begin(115200);
// Инициализация моторов
for(int i=0; i<4; i++) {
motors[i].attach(motorPins[i]);
motors[i].write(0);
}
// Настройка Wi-Fi
WiFi.softAP(ssid, password);
server.on("/", handleRoot);
server.on("/control", handleControl);
server.begin();
// Инициализация MPU6050
if (!mpu.begin()) {
Serial.println("MPU6050 not found!");
while(1);
}
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
}
void calculatePID() {
static float lastTime = 0;
static float pitchError = 0, rollError = 0;
static float pitchIntegral = 0, rollIntegral = 0;
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
// Углы из акселерометра
float dt = (millis() - lastTime)/1000.0;
float accelPitch = atan2(a.acceleration.y, a.acceleration.z) * 180/PI;
float accelRoll = atan2(a.acceleration.x, a.acceleration.z) * 180/PI;
// Комплементарный фильтр
float gyroPitch = g.gyro.y * dt;
float gyroRoll = g.gyro.x * dt;
float currentPitch = 0.98*(pid_pitch + gyroPitch) + 0.02*accelPitch;
float currentRoll = 0.98*(pid_roll + gyroRoll) + 0.02*accelRoll;
// PID-расчет
float targetPitch = map(pitch, -100, 100, -15, 15);
float targetRoll = map(roll, -100, 100, -15, 15);
float pitchError = targetPitch - currentPitch;
float rollError = targetRoll - currentRoll;
pitchIntegral += pitchError * dt;
rollIntegral += rollError * dt;
pid_pitch = Kp * pitchError + Ki * pitchIntegral + Kd * (pitchError - lastPitch)/dt;
pid_roll = Kp * rollError + Ki * rollIntegral + Kd * (rollError - lastRoll)/dt;
lastPitch = pitchError;
lastRoll = rollError;
lastTime = millis();
}
void applyMotorPower() {
int baseThrottle = map(throttle, 0, 100, 1000, 2000);
// Формула миксеров для квадрокоптера
int m1 = baseThrottle + pid_pitch - pid_roll + yaw;
int m2 = baseThrottle + pid_pitch + pid_roll - yaw;
int m3 = baseThrottle - pid_pitch + pid_roll + yaw;
int m4 = baseThrottle - pid_pitch - pid_roll - yaw;
// Ограничение значений
m1 = constrain(m1, 1000, 2000);
m2 = constrain(m2, 1000, 2000);
m3 = constrain(m3, 1000, 2000);
m4 = constrain(m4, 1000, 2000);
motors[0].writeMicroseconds(m1);
motors[1].writeMicroseconds(m2);
motors[2].writeMicroseconds(m3);
motors[3].writeMicroseconds(m4);
}
void loop() {
server.handleClient();
calculatePID();
applyMotorPower();
delay(10);
}
Возможные проблемы: