ESP32 làm mạch điều khiển bay cho flycam mini động cơ có chổi than: lọc bù, PID, bộ trộn, giá thử
Dùng ESP32 làm mạch điều khiển bay cho flycam mini động cơ có chổi than: kênh MOSFET, bộ lọc bù, hai vòng PID, bộ trộn bốn động cơ, điều kiện an toàn và cách chỉnh PID trên giá thử.
Các bài trước trong loạt đã có đủ từng mảnh: đọc góc nghiêng bằng MPU-6050, điều khiển động cơ và tay điều khiển ESP32 gửi lệnh qua ESP-NOW. Bài cuối ghép chúng lại thành một mạch điều khiển bay chạy trên chính ESP32: đọc cảm biến, tính góc, chạy hai vòng PID, trộn lệnh cho bốn động cơ, cùng các điều kiện mở khóa và khóa động cơ để thử an toàn.
Để phần cứng gọn nhất, bài dùng flycam mini với động cơ có chổi than (loại không lõi, cỡ 7–8,5 mm) và pin LiPo 1S: mỗi động cơ chỉ cần một MOSFET, không cần ESC. Đây là dự án để học nguyên lý cân bằng, thử trên giá thử hoặc trong lưới, không phải để thay mạch điều khiển bay thương mại. Ai muốn đi xa hơn có thể tìm hiểu dự án mã nguồn mở ESP-Drone của Espressif.
1. Tổng quan phần cứng

- Nguồn: pin 1S (3,0–4,2 V) cấp thẳng cho động cơ. ESP32 lấy điện qua một LDO 3,3 V loại điện áp rơi thấp, dòng từ 500 mA trở lên, nối vào chân 3V3. Mạch ổn áp có sẵn trên ESP32 DevKit cần điện áp vào cao hơn nhiều nên không dùng chân VIN với pin 1S.
- Đo pin: cầu chia hai điện trở 100 kΩ đưa nửa điện áp pin vào GPIO36 (pin đầy 4,2 V cho 2,1 V, trong dải đo của ADC).
- Cảm biến: MPU-6050 gắn gần tâm khung, mũi tên X về đầu, nối SDA GPIO21, SCL GPIO22 như bài đọc MPU-6050.
- Lệnh điều khiển: tay điều khiển ESP32 ở bài trước, chỉ cần đổi địa chỉ MAC đích sang MAC của mạch này.
2. Linh kiện cần chuẩn bị
- ESP32 DevKit V1 30 chân và module MPU-6050 (GY-521).
- Khung flycam mini cho động cơ có chổi than, 4 động cơ (2 chiều thuận, 2 chiều ngược) và cánh đi kèm.
- 4 MOSFET kênh N mức logic, mở hoàn toàn ở 2,5 V (ví dụ AO3400, SI2302), 4 điện trở 100 Ω, 4 điện trở 10 kΩ, 4 đi-ốt Schottky (ví dụ SS14, 1N5819), tụ 470 µF từ 6,3 V.
- LDO 3,3 V điện áp rơi thấp từ 500 mA, 2 điện trở 100 kΩ, công tắc nguồn.
- Pin LiPo 1S dung lượng 500–1000 mAh, dòng xả cao. Có thể sạc pin bằng jack Type-C tích hợp mạch sạc pin 1S lắp trên một đế sạc riêng.
- Dây cắm test board để thử trên bàn trước khi hàn lên khung.
ESP32 DevKit nặng khoảng 10 g, nên cân cả bộ trước khi lắp: tổng khối lượng không nên vượt quá một nửa tổng lực đẩy của bốn động cơ (tỉ số lực đẩy trên trọng lượng từ 2 trở lên, như bài động cơ và cánh quạt cùng loạt đã phân tích).
3. Kênh MOSFET cho từng động cơ

- MOSFET kênh N mức logic đặt ở phía GND của động cơ. Mức cao 3,3 V của ESP32 đủ mở hoàn toàn loại MOSFET này; loại thường cần 10 V ở cổng sẽ nóng và yếu.
- Điện trở 100 Ω hạn chế dòng nạp tụ cổng mỗi lần đóng cắt. Điện trở 10 kΩ giữ MOSFET tắt khi ESP32 đang khởi động hoặc nạp chương trình (lúc đó chân GPIO chưa được cấu hình), để động cơ không tự quay.
- Đi-ốt Schottky dập điện áp cảm ứng của cuộn dây động cơ mỗi khi MOSFET tắt, bảo vệ MOSFET.
- Tụ 470 µF đặt sát chỗ cấp điện cho động cơ, giữ điện áp không sụt khi tăng ga đột ngột. Thiếu tụ, ESP32 dễ khởi động lại giữa chừng.
- Chương trình phát PWM 20 kHz, cao hơn ngưỡng nghe của tai người nên động cơ không kêu rít; độ phân giải 10 bit cho 1024 mức tốc độ.
4. Vị trí động cơ và bộ trộn

Thứ tự và chiều quay theo quy ước mặc định của Betaflight (cánh quay vào trong). Bộ trộn chia lệnh của ba trục cho bốn động cơ: muốn nghiêng phải thì tăng hai động cơ bên trái, giảm hai động cơ bên phải; muốn ngóc đầu thì tăng phía trước; muốn xoay phải thì tăng hai động cơ quay ngược chiều kim đồng hồ.
- Động cơ có chổi than đổi chiều theo cực tính: loại quay thuận và quay ngược thường có màu dây khác nhau. Lắp xong, kiểm tra bằng lệnh
t1…t4(mục 7), sai thì đảo hai dây của động cơ đó. - Bốn chân 25, 26, 27, 33 không phải chân cấu hình lúc khởi động của ESP32, nên không làm mạch khởi động sai khi có MOSFET nối vào.
- Kênh ga nhân 0,8 để luôn chừa 20 % duty cho điều khiển: kể cả lúc ga cao, động cơ phía cần tăng vẫn còn chỗ để tăng.
5. Vòng điều khiển

loop() và điều kiện an toàn.Chương trình dùng hai vòng nối tiếp, giống chế độ tự cân bằng của mạch bay thương mại:
- Vòng góc (ngoài): so góc đặt từ cần điều khiển (tối đa 20°) với góc đo được sau bộ lọc bù, nhân với
kpGocđể ra tốc độ góc cần có. Lệch 5° vớikpGoc = 4nghĩa là cần quay về với tốc độ 20 °/s. - Vòng tốc độ góc (trong): PID so tốc độ cần có với tốc độ con quay đo được, cho ra lệnh roll, pitch. Trục yaw chỉ dùng vòng này: cần yaw đặt thẳng tốc độ xoay.
- Thành phần D tính theo số đo con quay chứ không theo sai số, nên lệnh thay đổi đột ngột không gây giật.
- Thành phần I bị giới hạn và xóa về 0 mỗi khi động cơ tắt, tránh tích lũy lúc flycam còn nằm trên mặt đất.
- Mỗi đầu ra PID bị giới hạn ±0,30 duty để một trục không chiếm hết công suất.
Mạch chỉ mở khóa khi đủ tất cả điều kiện trong khung xanh, và tắt động cơ ngay khi gặp bất kỳ điều kiện nào trong khung đỏ. Điều kiện "đã thấy AUX1 ở vị trí khóa trong lúc có sóng" ngăn trường hợp tay điều khiển bật lên với công tắc đang ở vị trí mở khóa.
6. Chương trình
Lệnh trên Serial Monitor (115200 baud, "New Line"). Các lệnh đổi thông số chỉ có tác dụng khi động cơ đang khóa:
| Lệnh | Tác dụng |
|---|---|
m | Bật/tắt dòng theo dõi 10 lần/giây |
p0.002, i0.004, d0.00005 | Đặt Kp, Ki, Kd vòng tốc độ cho cả roll và pitch |
y0.004, g4 | Đặt Kp vòng tốc độ của yaw; Kp vòng góc |
c, l | Hiệu chuẩn con quay; lấy góc hiện tại làm góc cân bằng |
t1…t4 | Cho động cơ 1–4 quay 25 % trong 1 giây (đã tháo cánh) |
? | In các hệ số hiện tại |
/*
PYWORLD.VN - ESP32 làm mạch điều khiển bay drone mini động cơ có chổi than: lọc bù, PID góc và tốc độ góc, bộ trộn
MPU-6050: SDA GPIO21, SCL GPIO22; MOSFET: M1 sau phải GPIO27, M2 trước phải GPIO26, M3 sau trái GPIO33, M4 trước trái GPIO25
Nhận gói 8 kênh từ tay điều khiển ESP-NOW; đo pin 1S qua cầu chia 100k/100k vào GPIO36; chỉ thử trên giá thử hoặc trong lưới
*/
#include <WiFi.h>
#include <esp_now.h>
#include <esp_mac.h>
#include <Wire.h>
#include <math.h>
#define CHAN_SDA 21
#define CHAN_SCL 22
#define CHAN_DO_PIN 36
#define TAN_SO_PWM 20000 // Hz, trên ngưỡng nghe
#define DO_PHAN_GIAI 10 // duty 0-1023
#define CHU_KY_US 4000 // vòng điều khiển 250 lần/giây
#define MAT_SONG_MS 300
#define GOC_TOI_DA 20.0 // độ khi đẩy hết cần roll, pitch
#define YAW_TOI_DA 120.0 // độ/giây khi đẩy hết cần yaw
#define GA_TOI_DA 0.80 // phần duty dành cho ga, phần còn lại cho điều khiển
#define GA_BAT_DAU 0.05 // dưới mức này động cơ đứng yên
#define GOC_CAT_DONG_CO 60.0
#define PIN_YEU_V 3.30
#define HE_SO_LOC 0.98
const uint8_t CHAN_DONG_CO[4] = {27, 26, 33, 25}; // M1, M2, M3, M4
const char *TEN_DONG_CO[4] = {"1 sau phai (CW)", "2 truoc phai (CCW)", "3 sau trai (CCW)", "4 truoc trai (CW)"};
const uint8_t DIA_CHI_MPU = 0x68;
const float RAD_SANG_DO = 57.29578;
typedef struct __attribute__((packed)) {
uint8_t dau;
uint8_t soThuTu;
uint16_t kenh[8]; // roll, pitch, ga, yaw, AUX1 (mở khóa), AUX2..AUX4
uint8_t kiemTra;
} GoiLenh;
// Thông số điều khiển: giá trị khởi đầu, phải chỉnh lại trên giá thử
float kpGoc = 4.0; // độ/giây cho mỗi độ sai lệch góc
float kpToc[3] = {0.0020, 0.0020, 0.0040}; // roll, pitch, yaw: phần duty cho mỗi độ/giây
float kiToc[3] = {0.0040, 0.0040, 0.0020};
float kdToc[3] = {0.00005, 0.00005, 0.0};
const float GIOI_HAN_I = 0.15, GIOI_HAN_RA = 0.30;
portMUX_TYPE khoaDuLieu = portMUX_INITIALIZER_UNLOCKED;
volatile uint16_t kenhNhan[8] = {1500, 1500, 1000, 1500, 1000, 1000, 1000, 1000};
volatile unsigned long lanNhanCuoi = 0;
float lechGx = 0, lechGy = 0, lechGz = 0;
float canRoll = 0, canPitch = 0; // góc coi là cân bằng (lệnh l)
float gocRoll = 0, gocPitch = 0;
float tocDo[3] = {0, 0, 0}; // roll, pitch, yaw đo được (độ/giây)
float tich[3] = {0, 0, 0}, doTruoc[3] = {0, 0, 0};
float ra[3] = {0, 0, 0};
float muc[4] = {0, 0, 0, 0};
float dienApPin = 4.2;
bool moKhoa = false, daThayKhoa = false, inTheoDoi = false;
unsigned long lanTinhTruoc = 0, lanInTruoc = 0, lanPinTruoc = 0, pinYeuTu = 0;
const char *lyDoKhoa = "khoi dong";
uint8_t tinhKiemTra(const GoiLenh &g) {
const uint8_t *p = (const uint8_t *)&g;
uint8_t x = 0;
for (size_t i = 0; i < sizeof(GoiLenh) - 1; i++) {
x ^= p[i];
}
return x;
}
void khiNhan(const esp_now_recv_info_t *thongTin, const uint8_t *duLieu, int doDai) {
if (doDai != sizeof(GoiLenh)) {
return;
}
GoiLenh g;
memcpy(&g, duLieu, sizeof(g));
if (g.dau != 0xA5 || g.kiemTra != tinhKiemTra(g)) {
return;
}
portENTER_CRITICAL(&khoaDuLieu);
for (int i = 0; i < 8; i++) {
kenhNhan[i] = constrain(g.kenh[i], 1000, 2000);
}
lanNhanCuoi = millis();
portEXIT_CRITICAL(&khoaDuLieu);
}
bool ghiThanhGhi(uint8_t thanhGhi, uint8_t giaTri) {
Wire.beginTransmission(DIA_CHI_MPU);
Wire.write(thanhGhi);
Wire.write(giaTri);
return Wire.endTransmission() == 0;
}
bool docCamBien(float &ax, float &ay, float &az, float &gx, float &gy, float &gz) {
Wire.beginTransmission(DIA_CHI_MPU);
Wire.write(0x3B);
if (Wire.endTransmission(false) != 0 || Wire.requestFrom(DIA_CHI_MPU, (uint8_t)14) != 14) {
return false;
}
int16_t v[7];
for (int i = 0; i < 7; i++) {
uint8_t cao = Wire.read();
uint8_t thap = Wire.read();
v[i] = (int16_t)((cao << 8) | thap);
}
ax = v[0] / 16384.0;
ay = v[1] / 16384.0;
az = v[2] / 16384.0;
gx = v[4] / 65.5;
gy = v[5] / 65.5;
gz = v[6] / 65.5;
return true;
}
void datDongCo(int i, float tiLe) {
ledcWrite(CHAN_DONG_CO[i], (uint32_t)(constrain(tiLe, 0.0f, 1.0f) * 1023));
}
void tatDongCo() {
for (int i = 0; i < 4; i++) {
muc[i] = 0;
datDongCo(i, 0);
}
}
void khoa(const char *lyDo) {
if (moKhoa) {
Serial.printf("KHOA dong co: %s\n", lyDo);
}
moKhoa = false;
lyDoKhoa = lyDo;
tatDongCo();
}
void hieuChuanConQuay() {
Serial.println("Hieu chuan con quay: de drone nam yen tren mat phang...");
double tx = 0, ty = 0, tz = 0;
int n = 0;
while (n < 500) {
float ax, ay, az, gx, gy, gz;
if (docCamBien(ax, ay, az, gx, gy, gz)) {
tx += gx;
ty += gy;
tz += gz;
n++;
}
delay(4);
}
lechGx = tx / n;
lechGy = ty / n;
lechGz = tz / n;
Serial.printf("Lech con quay (do/s): X %.2f Y %.2f Z %.2f\n", lechGx, lechGy, lechGz);
}
void inThongSo() {
Serial.printf("kpGoc %.2f | roll/pitch: kp %.5f ki %.5f kd %.6f | yaw: kp %.5f ki %.5f\n", kpGoc, kpToc[0], kiToc[0],
kdToc[0], kpToc[2], kiToc[2]);
}
void thuDongCo(int i) {
Serial.printf("Thu dong co %s trong 1 giay (THAO CANH)\n", TEN_DONG_CO[i]);
datDongCo(i, 0.25);
delay(1000);
datDongCo(i, 0);
}
void xuLyLenh() {
if (!Serial.available()) {
return;
}
String lenh = Serial.readStringUntil('\n');
lenh.trim();
if (lenh.length() == 0) {
return;
}
char ma = lenh.charAt(0);
float so = lenh.substring(1).toFloat();
if (ma == 'm') {
inTheoDoi = !inTheoDoi;
return;
}
if (moKhoa) {
Serial.println("Dang mo khoa: tat cong tac AUX1 truoc khi doi thong so");
return;
}
if (ma == 'p' && so > 0) {
kpToc[0] = kpToc[1] = so;
} else if (ma == 'i' && so >= 0) {
kiToc[0] = kiToc[1] = so;
} else if (ma == 'd' && so >= 0) {
kdToc[0] = kdToc[1] = so;
} else if (ma == 'y' && so > 0) {
kpToc[2] = so;
} else if (ma == 'g' && so > 0) {
kpGoc = so;
} else if (ma == 'c') {
hieuChuanConQuay();
} else if (ma == 'l') {
canRoll += gocRoll;
canPitch += gocPitch;
Serial.printf("Lay goc hien tai lam can bang: roll %.1f, pitch %.1f\n", canRoll, canPitch);
} else if (ma == 't' && so >= 1 && so <= 4) {
thuDongCo((int)so - 1);
} else if (ma != '?') {
Serial.println("Lenh: m theo doi | p/i/d<so> PID roll-pitch | y<so> kp yaw | g<so> kp goc | c hieu chuan | l lay can bang | t1..t4 thu dong co | ? thong so");
}
inThongSo();
}
float pidTocDo(int truc, float datVao, float doDuoc, float dt) {
float sai = datVao - doDuoc;
tich[truc] = constrain(tich[truc] + sai * dt, -GIOI_HAN_I / max(kiToc[truc], 1e-6f), GIOI_HAN_I / max(kiToc[truc], 1e-6f));
float dao = -(doDuoc - doTruoc[truc]) / dt; // đạo hàm theo số đo, không giật khi đổi lệnh
doTruoc[truc] = doDuoc;
return constrain(kpToc[truc] * sai + kiToc[truc] * tich[truc] + kdToc[truc] * dao, -GIOI_HAN_RA, GIOI_HAN_RA);
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < 4; i++) {
ledcAttach(CHAN_DONG_CO[i], TAN_SO_PWM, DO_PHAN_GIAI);
}
tatDongCo();
analogSetPinAttenuation(CHAN_DO_PIN, ADC_11db);
Wire.begin(CHAN_SDA, CHAN_SCL, 400000);
delay(200);
Serial.println();
if (!ghiThanhGhi(0x6B, 0x00) || !ghiThanhGhi(0x1A, 0x03) || !ghiThanhGhi(0x1B, 0x08) || !ghiThanhGhi(0x1C, 0x00)) {
Serial.println("Khong thay MPU-6050: kiem tra day, drone se khong mo khoa duoc");
}
delay(100);
hieuChuanConQuay();
WiFi.mode(WIFI_STA);
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
Serial.printf("MAC drone: {0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X, 0x%02X}\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
if (esp_now_init() == ESP_OK) {
esp_now_register_recv_cb(khiNhan);
} else {
Serial.println("Khoi dong ESP-NOW that bai");
}
inThongSo();
Serial.println("San sang. Mo khoa: AUX1 bat, ga thap nhat, drone nam ngang");
lanTinhTruoc = micros();
}
void loop() {
xuLyLenh();
unsigned long bayGio = micros();
if (bayGio - lanTinhTruoc < CHU_KY_US) {
return;
}
float dt = (bayGio - lanTinhTruoc) / 1000000.0;
lanTinhTruoc = bayGio;
// 1. Đọc cảm biến, lọc bù (cùng quy ước với bài đọc MPU-6050)
float ax, ay, az, gx, gy, gz;
if (!docCamBien(ax, ay, az, gx, gy, gz)) {
khoa("loi doc cam bien");
return;
}
tocDo[0] = gx - lechGx;
tocDo[1] = -(gy - lechGy);
tocDo[2] = -(gz - lechGz);
float rollGt = atan2(ay, az) * RAD_SANG_DO;
float pitchGt = atan2(ax, sqrt(ay * ay + az * az)) * RAD_SANG_DO;
gocRoll = HE_SO_LOC * (gocRoll + tocDo[0] * dt) + (1 - HE_SO_LOC) * (rollGt - canRoll);
gocPitch = HE_SO_LOC * (gocPitch + tocDo[1] * dt) + (1 - HE_SO_LOC) * (pitchGt - canPitch);
// 2. Lệnh từ tay điều khiển
uint16_t k[8];
portENTER_CRITICAL(&khoaDuLieu);
for (int i = 0; i < 8; i++) {
k[i] = kenhNhan[i];
}
unsigned long cuoi = lanNhanCuoi;
portEXIT_CRITICAL(&khoaDuLieu);
bool coSong = cuoi != 0 && millis() - cuoi <= MAT_SONG_MS;
float ga = constrain((k[2] - 1000) / 1000.0, 0.0, 1.0);
// 3. Đo pin
if (millis() - lanPinTruoc >= 20) {
lanPinTruoc = millis();
dienApPin = 0.95 * dienApPin + 0.05 * (analogReadMilliVolts(CHAN_DO_PIN) * 2 / 1000.0);
pinYeuTu = (dienApPin < PIN_YEU_V) ? (pinYeuTu ? pinYeuTu : millis()) : 0;
}
// 4. Mở khóa, khóa và các điều kiện an toàn
// Chỉ cho mở khóa sau khi đã thấy công tắc AUX1 ở vị trí khóa trong lúc có sóng
if (!coSong) {
daThayKhoa = false;
} else if (k[4] < 1300) {
daThayKhoa = true;
}
if (moKhoa) {
if (!coSong) {
khoa("mat song");
} else if (k[4] < 1300) {
khoa("cong tac AUX1");
} else if (fabs(gocRoll) > GOC_CAT_DONG_CO || fabs(gocPitch) > GOC_CAT_DONG_CO) {
khoa("nghieng qua 60 do");
} else if (pinYeuTu && millis() - pinYeuTu > 2000) {
khoa("pin yeu");
}
} else if (coSong && daThayKhoa && k[4] > 1700 && k[2] < 1050 && fabs(gocRoll) < 10 && fabs(gocPitch) < 10 &&
dienApPin > PIN_YEU_V) {
moKhoa = true;
for (int i = 0; i < 3; i++) {
tich[i] = 0;
}
Serial.println("MO KHOA dong co");
}
// 5. Vòng góc, vòng tốc độ góc, bộ trộn
if (moKhoa && ga > GA_BAT_DAU) {
float datRoll = (k[0] - 1500) / 500.0 * GOC_TOI_DA; // cần phải sang phải: nghiêng phải
float datPitch = -(k[1] - 1500) / 500.0 * GOC_TOI_DA; // đẩy cần lên: chúc đầu, bay tới
float datYaw = (k[3] - 1500) / 500.0 * YAW_TOI_DA;
ra[0] = pidTocDo(0, kpGoc * (datRoll - gocRoll), tocDo[0], dt);
ra[1] = pidTocDo(1, kpGoc * (datPitch - gocPitch), tocDo[1], dt);
ra[2] = pidTocDo(2, datYaw, tocDo[2], dt);
float nen = ga * GA_TOI_DA;
muc[0] = nen - ra[0] - ra[1] - ra[2]; // M1 sau phải
muc[1] = nen - ra[0] + ra[1] + ra[2]; // M2 trước phải
muc[2] = nen + ra[0] - ra[1] + ra[2]; // M3 sau trái
muc[3] = nen + ra[0] + ra[1] - ra[2]; // M4 trước trái
for (int i = 0; i < 4; i++) {
datDongCo(i, muc[i]);
}
} else {
for (int i = 0; i < 3; i++) {
tich[i] = 0;
ra[i] = 0;
}
tatDongCo();
}
// 6. Theo dõi 10 lần/giây
if (inTheoDoi && millis() - lanInTruoc >= 100) {
lanInTruoc = millis();
Serial.printf("R %5.1f P %5.1f | ga %.2f | M %3d %3d %3d %3d | pin %.2f V | %s\n", gocRoll, gocPitch, ga,
(int)(constrain(muc[0], 0.0f, 1.0f) * 1023), (int)(constrain(muc[1], 0.0f, 1.0f) * 1023),
(int)(constrain(muc[2], 0.0f, 1.0f) * 1023), (int)(constrain(muc[3], 0.0f, 1.0f) * 1023), dienApPin,
moKhoa ? "MO KHOA" : lyDoKhoa);
}
}
Các hệ số PID trong chương trình chỉ là giá trị khởi đầu để bắt đầu chỉnh. Giá trị phù hợp phụ thuộc khung, động cơ, cánh, pin, và phải tìm lại trên giá thử theo mục 7.
7. Thử trên giá và chỉnh PID

Giá thử bập bênh giữ flycam chỉ nghiêng được theo một trục, không thể lật hay bay đi, nên chỉnh PID an toàn hơn nhiều so với thả bay. Giá trong ảnh dùng động cơ không chổi than cỡ lớn; với flycam mini, làm giá nhỏ hơn theo cùng nguyên lý: kẹp thân flycam vào một thanh tròn xuyên ngang tâm, gác hai đầu thanh lên hai giá đỡ.

- Nạp và ghép đôi: nạp chương trình, chép dòng
MAC dronevàoMAC_BO_THUcủa tay điều khiển, nạp lại tay điều khiển. - Thử động cơ: tháo cánh, gõ
t1…t4, đối chiếu vị trí và chiều quay với hình ở mục 4. - Cân bằng: đặt flycam trên mặt phẳng ngang, gõ
crồil. - Kiểm tra dấu (vẫn tháo cánh): gõ
m, mở khóa, đẩy ga lên một chút, nghiêng flycam bằng tay. Động cơ ở phía bị hạ thấp phải quay nhanh lên. Nếu ngược lại, dấu cảm biến hoặc vị trí động cơ đang sai: dừng lại sửa trước khi lắp cánh. - Trên giá bập bênh: lắp cánh, đặt
i0vàd0, tăng dầnptới khi thanh bắt đầu rung qua lại, rồi giảm khoảng 30 %. Thêmdđể giảm dao động, thêmiđể thanh giữ ngang được lâu khi một bên nặng hơn. - Xoay giá để chỉnh trục còn lại (hai trục dùng chung hệ số, chỉ cần kiểm tra lại). Cuối cùng thả bay thấp trong lưới để chỉnh
ycho trục yaw.
Hệ số chỉnh bằng lệnh chỉ nằm trong RAM, mất khi khởi động lại. Chỉnh xong, sửa các giá trị khởi đầu trong chương trình rồi nạp lại.
8. Kết quả trên Serial Monitor

Cột cuối của dòng theo dõi cho biết trạng thái: MO KHOA khi động cơ đang được điều khiển, hoặc lý do khóa gần nhất (mat song, cong tac AUX1, nghieng qua 60 do, pin yeu…). Bốn số sau chữ M là mức duty (0–1023) của động cơ 1 đến 4.
9. An toàn
- Mọi bước kiểm tra dấu và thử động cơ đều làm khi đã tháo cánh.
- Thử bay đầu tiên trong lưới bảo vệ hoặc phòng trống, không có người đứng gần, ga thấp, tay luôn đặt sẵn ở công tắc AUX1 để khóa động cơ.
- Không bay ngoài trời, gần người, gần đường dây điện. Khi bay ngoài trời phải tuân thủ quy định về tàu bay không người lái (bài quy định bay cùng loạt).
- Rút pin ngay sau khi thử; pin LiPo không để cạn dưới 3,0 V, không dùng pin phồng.
10. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Bật AUX1 mà không mở khóa | Chưa thấy AUX1 ở vị trí khóa sau khi có sóng; ga chưa thấp; đang nghiêng; pin yếu | Gạt AUX1 về khóa rồi bật lại; hạ ga; đặt nằm ngang; xem cột trạng thái bằng lệnh m |
| Vừa tăng ga đã lật | Sai vị trí hoặc chiều quay động cơ, cánh lắp ngược, dấu cảm biến sai | Làm lại bước thử động cơ và kiểm tra dấu ở mục 7 |
| Rung, dao động nhanh | p hoặc d quá lớn; cảm biến bị rung | Giảm p, d; thêm đệm chống rung cho MPU-6050 |
| Luôn trôi về một phía | Chưa lấy góc cân bằng; trọng tâm lệch; động cơ không đều | Gõ l khi nằm ngang; dời pin để cân trọng tâm; tăng i |
| Tự xoay tròn | Hai động cơ cùng chiều đặt sai vị trí; cánh sai chiều | Đối chiếu lại chiều quay; tăng y |
| ESP32 khởi động lại khi tăng ga | Sụt áp do pin yếu, thiếu tụ, LDO không đủ dòng | Thêm tụ sát động cơ; dùng LDO dòng lớn hơn; pin xả cao |
11. Xem thêm
- Giữ tốc độ động cơ DC ổn định bằng PID trên ESP32: chỉnh Kp, Ki, Kd
- PWM trên ESP32: analogWrite và LEDC
- Đọc ADC trên ESP32 và đo điện áp pin bằng cầu phân áp
- ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch
- Cách đấu jack Type-C tích hợp mạch sạc với pin Li-po
Cùng loạt: cấu tạo flycam; mạch điều khiển bay (IMU, PID, Betaflight); ESP32 đọc MPU-6050 bằng bộ lọc bù; tự làm tay điều khiển flycam bằng ESP32; quy định bay flycam tại Việt Nam.
Xem thêm