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

Sơ đồ khối: pin LiPo 1S qua công tắc cấp cho bốn động cơ có chổi than nối với bốn MOSFET kênh N, cấp cho LDO 3,3 V nuôi ESP32 và cầu chia 100k/100k vào GPIO36; ESP32 xuất bốn kênh PWM cho MOSFET, đọc MPU-6050 qua I2C và nhận gói 8 kênh ESP-NOW từ tay điều khiển
Hình minh họa: các khối của flycam mini dùng ESP32.
  • 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ơ

Sơ đồ một kênh: động cơ nối giữa cực dương pin và cực máng MOSFET kênh N, đi-ốt Schottky song song với động cơ catốt về phía cực dương, GPIO27 qua điện trở 100 ohm vào cổng, điện trở 10 kilo ohm kéo cổng xuống GND, tụ 470 µF giữa nguồn và GND
Hình minh họa: một trong bốn kênh MOSFET giống nhau.
  • 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

Quadcopter khung X nhìn từ trên: M4 trước trái quay thuận GPIO25, M2 trước phải quay ngược GPIO26, M1 sau phải quay thuận GPIO27, M3 sau trái quay ngược GPIO33; bảng bộ trộn của bốn động cơ theo ga, roll, pitch, yaw
Hình minh họa: vị trí, chiều quay, chân điều khiển và công thức 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

Chuỗi sáu bước chạy mỗi 4 ms: đọc MPU-6050, lọc bù, vòng góc, vòng tốc độ PID, bộ trộn, xuất LEDC 20 kHz; khung điều kiện mở khóa và khung điều kiện khóa động cơ
Hình minh họa: các bước trong 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:

  1. 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ới kpGoc = 4 nghĩa là cần quay về với tốc độ 20 °/s.
  2. 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 Serial
LệnhTác dụng
mBậ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, lHiệu chuẩn con quay; lấy góc hiện tại làm góc cân bằng
t1…t4Cho độ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ằng gỗ: thanh gỗ dài đặt trên một trục ngang, động cơ gắn ở đầu thanh, mạch điện tử gắn trên thanh
Giá thử kiểu bập bênh: thanh gỗ chỉ quay được quanh một trục, động cơ gắn ở hai đầu thanh. Ảnh: Shatton8111, CC BY-SA 3.0, Wikimedia Commons, đã cắt, thu nhỏ và làm mờ dụng cụ đo ở nền.

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ăm bước chỉnh PID: thử từng động cơ bằng t1 đến t4, kiểm tra dấu bằng cách nghiêng tay, trên giá bập bênh tăng p tới khi rung rồi giảm khoảng 30 phần trăm, thêm d và i, làm lại với trục còn lại và chỉnh y cho yaw
Hình minh họa: thứ tự gợi ý khi chỉnh PID.
  1. Nạp và ghép đôi: nạp chương trình, chép dòng MAC drone vào MAC_BO_THU của tay điều khiển, nạp lại tay điều khiển.
  2. 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.
  3. Cân bằng: đặt flycam trên mặt phẳng ngang, gõ c rồi l.
  4. 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.
  5. Trên giá bập bênh: lắp cánh, đặt i0 và d0, tăng dần p tới khi thanh bắt đầu rung qua lại, rồi giảm khoảng 30 %. Thêm d để giảm dao động, thêm i để thanh giữ ngang được lâu khi một bên nặng hơn.
  6. 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 y cho 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

Hình minh họa Serial Monitor: hiệu chuẩn con quay, MAC drone, dòng hệ số PID, dòng sẵn sàng, các dòng theo dõi góc roll pitch, ga, bốn mức động cơ, điện áp pin và trạng thái, dòng mở khóa và khóa động cơ do công tắc AUX1
Hình minh họa: một lượt thử trên giá, từ khởi động tới khóa động cơ.

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

Lỗi thường gặp
Hiện tượngNguyên nhân thường gặpCách xử lý
Bật AUX1 mà không mở khóaChưa thấy AUX1 ở vị trí khóa sau khi có sóng; ga chưa thấp; đang nghiêng; pin yếuGạ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ậtSai vị trí hoặc chiều quay động cơ, cánh lắp ngược, dấu cảm biến saiLàm lại bước thử động cơ và kiểm tra dấu ở mục 7
Rung, dao động nhanhp hoặc d quá lớn; cảm biến bị rungGiảm p, d; thêm đệm chống rung cho MPU-6050
Luôn trôi về một phíaChưa lấy góc cân bằng; trọng tâm lệch; động cơ không đềuGõ l khi nằm ngang; dời pin để cân trọng tâm; tăng i
Tự xoay trònHai độ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 gaSụt áp do pin yếu, thiếu tụ, LDO không đủ dòngThêm tụ sát động cơ; dùng LDO dòng lớn hơn; pin xả cao

11. Xem thêm

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.


Tin tức liên quan

Tưới cây tự động với ESP32: cảm biến độ ẩm đất, relay và bơm mini, ngưỡng có trễ
Tưới cây tự động với ESP32: cảm biến độ ẩm đất, relay và bơm mini, ngưỡng có trễ

Làm hệ thống tưới cây tự động với ESP32: cảm biến độ ẩm đất điện dung, hiệu chỉnh khô ướt, relay điều khiển bơm mini, ngưỡng có trễ, giới hạn thời gian bơm và thí nghiệm STEM.

Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên
Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên

Hướng dẫn nối module LoRa SX1278 Ra-02 433 MHz với ESP32 qua SPI, cài thư viện LoRa trên Arduino IDE, viết chương trình gửi và nhận gói tin, đọc RSSI, SNR và xử lý lỗi thường gặp.

Phased array siêu âm 40 kHz với ESP32 (phần 1): nguyên lý lái búp sóng và tạo 8 kênh lệch pha
Phased array siêu âm 40 kHz với ESP32 (phần 1): nguyên lý lái búp sóng và tạo 8 kênh lệch pha

Nguyên lý phased array và cách dùng ESP32 tạo 8 kênh xung 40 kHz lệch pha để lái búp sóng siêu âm: công thức độ trễ, giới hạn do khoảng cách đầu phát, cấu hình LEDC với hpoint, sơ đồ nối và code.

Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno
Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno

Hướng dẫn nối màn hình LCD 1602 I2C với Arduino Uno, cài thư viện LiquidCrystal_I2C, chạy code hiển thị và quét địa chỉ I2C 0x27/0x3F.

Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn
Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn

Hướng dẫn đưa ESP32 vào Deep Sleep trên Arduino IDE để tiết kiệm pin: các chế độ nguồn, đánh thức bằng timer và nút nhấn, giữ biến trong bộ nhớ RTC và ước tính thời lượng pin.

ESP32 điều khiển động cơ bước NEMA 17 với A4988: vi bước, chỉnh dòng VREF, tăng tốc mượt
ESP32 điều khiển động cơ bước NEMA 17 với A4988: vi bước, chỉnh dòng VREF, tăng tốc mượt

Hướng dẫn điều khiển động cơ bước NEMA 17 bằng ESP32 qua driver A4988: nối dây, chỉnh dòng VREF, chọn vi bước 1/16, tăng tốc giảm tốc hình thang và thí nghiệm STEM.

ESP32 điều khiển servo SG90 bằng PWM, không cần thư viện: quét góc, biến trở, hiệu chỉnh
ESP32 điều khiển servo SG90 bằng PWM, không cần thư viện: quét góc, biến trở, hiệu chỉnh

Hướng dẫn điều khiển servo SG90 bằng ESP32 với bộ PWM LEDC 50 Hz có sẵn: xung 0,5–2,4 ms, sơ đồ nối, quét góc, vặn biến trở, hiệu chỉnh hai đầu hành trình và thí nghiệm STEM.

Phát hiện chuyển động bằng sóng Wi-Fi (CSI) trên ESP32: không cần cảm biến
Phát hiện chuyển động bằng sóng Wi-Fi (CSI) trên ESP32: không cần cảm biến

Giải thích Wi-Fi CSI và hướng dẫn dùng ESP32 phát hiện người di chuyển trong phòng chỉ bằng sóng Wi-Fi từ router: lấy dữ liệu CSI, tính điểm chuyển động, đặt ngưỡng và xem trên Serial Plotter.


Bình luận
Đã thêm vào giỏ hàng