Tự làm tay điều khiển flycam bằng ESP32 và ESP-NOW, bộ thu xuất PPM, SBUS cho mạch điều khiển bay

Tự làm tay điều khiển flycam bằng ESP32: hai joystick, hai công tắc, gửi 8 kênh qua ESP-NOW; bộ thu ESP32 xuất PPM bằng RMT và SBUS bằng UART, có failsafe, dùng với Betaflight.

Tay điều khiển flycam thật ra chỉ làm một việc: đọc vị trí hai cần và các công tắc, đổi thành các "kênh" rồi gửi không dây xuống flycam. Với hai mạch ESP32, hai joystick và giao thức ESP-NOW, có thể tự làm một bộ như vậy để hiểu rõ từng khâu: kênh, gói lệnh, giao thức bộ thu và failsafe.

Bài viết gồm hai chương trình. Tay điều khiển gửi 8 kênh 50 lần mỗi giây. Bộ thu đặt trên flycam, nhận gói lệnh rồi xuất ra PPM hoặc SBUS, nên dùng được với mạch điều khiển bay chạy Betaflight, INAV như một bộ thu thông thường. Bài sau trong loạt dùng lại đúng gói lệnh này cho ESP32 tự làm mạch điều khiển bay.

1. Hệ thống gồm những gì

Sơ đồ hệ thống: tay điều khiển ESP32 gửi 50 gói mỗi giây qua ESP-NOW tới bộ thu ESP32, bộ thu xuất SBUS hoặc PPM cho mạch điều khiển bay; cùng gói lệnh có thể gửi tới ESP32 trên flycam mini tự làm mạch bay
Hình minh họa: hai hướng dùng tay điều khiển ESP32.
  • Thứ tự kênh theo kiểu AETR quen thuộc: 1 roll, 2 pitch, 3 ga, 4 yaw, 5 AUX1 (mở khóa động cơ), 6 AUX2 (chế độ bay), 7–8 dự phòng. Mỗi kênh tính bằng micro giây, 1000–2000 µs, giống các tay điều khiển thương mại.
  • ESP-NOW gửi thẳng giữa hai mạch ESP32, không cần router Wi-Fi, độ trễ chỉ vài mili giây.

Giới hạn cần biết: ESP-NOW dùng anten nhỏ trên mạch, tầm phát và khả năng chống nhiễu kém xa hệ thống điều khiển chuyên dụng như ELRS. Bộ tự làm này dùng để học, thử trên bàn và bay flycam nhỏ trong khu vực an toàn, luôn trong tầm mắt, xa người. Khi bay cần tuân thủ quy định về tàu bay không người lái (xem bài quy định bay flycam tại Việt Nam cùng loạt).

2. Linh kiện cần chuẩn bị

  • Hai mạch ESP32 DevKit V1 30 chân: một cho tay điều khiển, một làm bộ thu.
  • Hai module joystick hai trục (loại có chân GND, +5V, VRx, VRy, SW) và hai công tắc gạt.
  • Breadboard 830 lỗ và dây cắm test board để ráp thử.
  • Nguồn cho tay điều khiển khi cầm đi: pin 18650 trong khay pin 18650, sạc qua jack Type-C tích hợp mạch sạc (cách nối ở mục Xem thêm), hoặc đơn giản là sạc dự phòng cắm cổng USB.
  • Hướng dùng với mạch điều khiển bay: một flycam đã có mạch bay chạy Betaflight hoặc INAV.

3. Sơ đồ nối tay điều khiển

Sơ đồ nối tay điều khiển: joystick trái VRy vào D34 làm ga, VRx vào D35 làm yaw; joystick phải VRy vào D32 làm pitch, VRx vào D33 làm roll; chân +5V của hai joystick nối 3V3, GND nối GND; công tắc AUX1 nối D25 và GND, AUX2 nối D26 và GND
Hình minh họa: hai joystick và hai công tắc nối với ESP32.
  • Bốn trục joystick dùng các chân 34, 35, 32, 33. Đây là các chân của bộ ADC1. Bộ ADC2 không đọc được khi Wi-Fi (và ESP-NOW) đang chạy, nên không dùng chân ADC2 cho joystick.
  • Chân +5V của joystick nối vào 3V3: biến trở trong joystick khi đó cho điện áp 0–3,3 V, vừa với dải đo của ESP32.
  • Công tắc nối giữa GPIO và GND. Chương trình bật điện trở kéo lên bên trong, nên công tắc bật thì chân đọc mức thấp.
  • Chân SW (nhấn cần) không dùng trong bài.

4. Gói lệnh

Gói lệnh 19 byte: byte đầu A5, số thứ tự 2A, tám kênh mỗi kênh 2 byte byte thấp trước như 1500 là DC 05 và 1180 là 9C 04, byte cuối là XOR của 18 byte trước bằng F2
Hình minh họa: các byte của một gói lệnh với giá trị kênh ví dụ.

Hai chương trình khai báo cùng một cấu trúc GoiLenh với thuộc tính packed để không có byte đệm: byte đầu cố định 0xA5, số thứ tự tăng dần, tám kênh 16 bit và một byte kiểm tra bằng phép XOR. Bộ thu bỏ qua gói sai độ dài, sai byte đầu, sai byte kiểm tra hoặc có kênh nằm ngoài 900–2100 µs.

5. Chương trình tay điều khiển

Nạp chương trình bộ thu ở mục 6 trước để biết địa chỉ MAC của nó, rồi sửa dòng MAC_BO_THU bên dưới cho đúng trước khi nạp.

/*
  PYWORLD.VN - Tay điều khiển flycam bằng ESP32: 2 joystick, 2 công tắc, gửi 8 kênh qua ESP-NOW 50 lần/giây
  Joystick trái: VRy = GPIO34 (ga), VRx = GPIO35 (yaw); joystick phải: VRy = GPIO32 (pitch), VRx = GPIO33 (roll); nguồn joystick 3V3
  Công tắc AUX1 (mở khóa) = GPIO25, AUX2 (chế độ bay) = GPIO26, nối xuống GND khi bật; sửa MAC_BO_THU theo mạch nhận
*/

#include <WiFi.h>
#include <esp_now.h>

// Địa chỉ MAC của mạch nhận (bộ thu hoặc drone ESP32): sửa theo dòng mạch nhận in ra lúc khởi động
uint8_t MAC_BO_THU[6] = {0x24, 0x6F, 0x28, 0x12, 0x34, 0x56};

#define CHAN_GA 34
#define CHAN_YAW 35
#define CHAN_PITCH 32
#define CHAN_ROLL 33
#define CHAN_AUX1 25
#define CHAN_AUX2 26
#define CHU_KY_GUI_MS 20
#define VUNG_CHET 60               // Số đếm ADC quanh tâm coi như cần ở giữa
#define GA_KIEU_TICH_LUY 1         // 1: đẩy cần lên thì ga tăng dần, buông thì giữ; 0: ga theo vị trí cần
#define TOC_DO_GA 600.0            // us mỗi giây khi đẩy hết cần ở kiểu tích lũy
#define DAO_ROLL 0                 // Đặt 1 nếu gạt cần sang phải mà kênh roll giảm
#define DAO_PITCH 0
#define DAO_YAW 0
#define DAO_GA 0

// Gói lệnh: mạch gửi và mạch nhận khai báo giống hệt nhau
typedef struct __attribute__((packed)) {
  uint8_t dau;          // luôn là 0xA5
  uint8_t soThuTu;      // tăng dần mỗi gói
  uint16_t kenh[8];     // us: roll, pitch, ga, yaw, AUX1, AUX2, AUX3, AUX4
  uint8_t kiemTra;      // XOR của mọi byte phía trước
} GoiLenh;

GoiLenh goi;
int tamRoll, tamPitch, tamYaw, tamGa;
float gaTichLuy = 1000;
volatile uint32_t soGuiDuoc = 0;
uint32_t soGui = 0;
unsigned long lanGuiTruoc = 0, lanInTruoc = 0;

void khiGuiXong(const esp_now_send_info_t *thongTin, esp_now_send_status_t trangThai) {
  if (trangThai == ESP_NOW_SEND_SUCCESS) {
    soGuiDuoc = soGuiDuoc + 1;
  }
}

int docTrungBinh(int chan) {
  long tong = 0;
  for (int i = 0; i < 16; i++) {
    tong += analogRead(chan);
  }
  return tong / 16;
}

// Đổi số đọc ADC (0-4095) thành 1000-2000 us, tâm cần ứng với 1500 us
uint16_t quyDoiCan(int doc, int tam, bool dao) {
  int lech = doc - tam;
  float ti_le;
  if (abs(lech) < VUNG_CHET) {
    ti_le = 0;
  } else if (lech > 0) {
    ti_le = (float)(lech - VUNG_CHET) / (4095 - tam - VUNG_CHET);
  } else {
    ti_le = (float)(lech + VUNG_CHET) / (tam - VUNG_CHET);
  }
  if (dao) {
    ti_le = -ti_le;
  }
  ti_le = constrain(ti_le, -1.0f, 1.0f);
  return (uint16_t)(1500 + 500 * ti_le);
}

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 setup() {
  Serial.begin(115200);
  pinMode(CHAN_AUX1, INPUT_PULLUP);
  pinMode(CHAN_AUX2, INPUT_PULLUP);
  analogReadResolution(12);
  delay(200);
  Serial.println();

  // Đọc vị trí tâm khi các cần đang buông
  tamRoll = docTrungBinh(CHAN_ROLL);
  tamPitch = docTrungBinh(CHAN_PITCH);
  tamYaw = docTrungBinh(CHAN_YAW);
  tamGa = docTrungBinh(CHAN_GA);
  Serial.printf("Tam can: roll %d, pitch %d, yaw %d, ga %d\n", tamRoll, tamPitch, tamYaw, tamGa);

  // Không bắt đầu khi công tắc mở khóa đang bật
  while (digitalRead(CHAN_AUX1) == LOW) {
    Serial.println("Tat cong tac AUX1 (mo khoa) truoc khi bat dau");
    delay(1000);
  }

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    Serial.println("Khoi dong ESP-NOW that bai");
    while (true) {
      delay(1000);
    }
  }
  esp_now_register_send_cb(khiGuiXong);
  esp_now_peer_info_t boThu = {};
  memcpy(boThu.peer_addr, MAC_BO_THU, 6);
  boThu.channel = 0;
  boThu.encrypt = false;
  if (esp_now_add_peer(&boThu) != ESP_OK) {
    Serial.println("Khong them duoc mach nhan");
  }
  goi.dau = 0xA5;
  goi.soThuTu = 0;
  Serial.println("Tay dieu khien san sang");
}

void loop() {
  unsigned long bayGio = millis();
  if (bayGio - lanGuiTruoc < CHU_KY_GUI_MS) {
    return;
  }
  float dt = (bayGio - lanGuiTruoc) / 1000.0;
  lanGuiTruoc = bayGio;

  bool moKhoa = digitalRead(CHAN_AUX1) == LOW;
  int docGa = docTrungBinh(CHAN_GA);
#if GA_KIEU_TICH_LUY
  // Đẩy cần lên trên tâm: ga tăng; kéo xuống: ga giảm; buông: giữ nguyên
  uint16_t canGa = quyDoiCan(docGa, tamGa, DAO_GA);
  gaTichLuy += (canGa - 1500) / 500.0 * TOC_DO_GA * dt;
  gaTichLuy = constrain(gaTichLuy, 1000.0f, 2000.0f);
#else
  // Joystick đã bỏ lò xo kéo về giữa: dưới cùng 1000 us, trên cùng 2000 us
  if (DAO_GA) {
    docGa = 4095 - docGa;
  }
  gaTichLuy = 1000 + docGa * 1000.0 / 4095;
#endif
  if (!moKhoa) {
    gaTichLuy = 1000;                   // Công tắc khóa: ga luôn về thấp nhất
  }

  goi.kenh[0] = quyDoiCan(docTrungBinh(CHAN_ROLL), tamRoll, DAO_ROLL);
  goi.kenh[1] = quyDoiCan(docTrungBinh(CHAN_PITCH), tamPitch, DAO_PITCH);
  goi.kenh[2] = (uint16_t)gaTichLuy;
  goi.kenh[3] = quyDoiCan(docTrungBinh(CHAN_YAW), tamYaw, DAO_YAW);
  goi.kenh[4] = moKhoa ? 2000 : 1000;
  goi.kenh[5] = (digitalRead(CHAN_AUX2) == LOW) ? 2000 : 1000;
  goi.kenh[6] = 1500;
  goi.kenh[7] = 1500;
  goi.soThuTu++;
  goi.kiemTra = tinhKiemTra(goi);
  esp_now_send(MAC_BO_THU, (const uint8_t *)&goi, sizeof(goi));
  soGui++;

  if (bayGio - lanInTruoc >= 500) {
    lanInTruoc = bayGio;
    Serial.printf("Roll %4u Pitch %4u Ga %4u Yaw %4u AUX1 %4u AUX2 %4u | gui duoc %u/%u\n", goi.kenh[0], goi.kenh[1],
                  goi.kenh[2], goi.kenh[3], goi.kenh[4], goi.kenh[5], (unsigned)soGuiDuoc, (unsigned)soGui);
    soGuiDuoc = 0;
    soGui = 0;
  }
}
  • Vị trí tâm của từng cần được đọc lúc khởi động. Buông tay khỏi hai cần khi cấp điện, nếu không tâm sẽ bị lệch.
  • Vùng chết VUNG_CHET: quanh tâm 60 số đếm coi như cần đang ở giữa, để kênh đứng yên ở 1500 µs khi buông tay. Hàm quyDoiCan() đổi phần còn lại thành 1000–2000 µs, tính riêng hai nửa trên và dưới tâm.
  • Ga kiểu tích lũy (mặc định): joystick thường có lò xo kéo về giữa, nên đẩy cần lên trên tâm thì ga tăng dần, kéo xuống thì giảm, buông thì giữ nguyên. Nếu đã tháo lò xo trục dọc của cần trái, đặt GA_KIEU_TICH_LUY 0 để ga theo đúng vị trí cần như tay điều khiển thật.
  • Khóa ga: khi công tắc AUX1 đang tắt, kênh ga luôn bị ép về 1000 µs. Chương trình cũng không chạy tiếp nếu lúc khởi động AUX1 đang bật.
  • Cần gạt sang phải mà kênh lại giảm thì đặt DAO_ROLL 1; các trục khác tương tự.

6. Bộ thu xuất PPM và SBUS

Sơ đồ nối bộ thu ESP32 với mạch điều khiển bay: D27 ra chân PPM, D17 ra chân SBUS, GND nối GND, chân 5V của mạch bay vào VIN của ESP32
Hình minh họa: bộ thu lấy nguồn 5 V từ mạch điều khiển bay.
  • SBUS (khuyên dùng với mạch bay đời mới): GPIO17 nối vào chân dành cho bộ thu SBUS của mạch bay, thường ghi SBUS hoặc là chân RX của một cổng UART. Bộ thu xuất đúng chuẩn SBUS: 100 000 baud, 8 bit dữ liệu, bit chẵn lẻ chẵn, 2 bit dừng, mức tín hiệu đảo, nên nối như nối một bộ thu SBUS thông thường.
  • PPM (mạch bay đời cũ): GPIO27 nối vào chân PPM. Chỉ cần nối một trong hai; muốn tắt hẳn một ngõ ra, đặt XUAT_PPM hoặc XUAT_SBUS bằng 0.
  • Nguồn: chân 5V của mạch điều khiển bay vào VIN của ESP32, GND nối chung. Khi đang cắm USB vào ESP32 để xem Serial Monitor thì không nối dây 5 V này.
Bảng 9 ký hiệu RMT của một khung PPM: mỗi ký hiệu gồm mức 1 trong 300 µs và mức 0 trong phần còn lại của kênh, ví dụ kênh ga 1180 µs có mức 0 dài 880 µs, ký hiệu cuối là khoảng đồng bộ 10520 µs, tổng 22500 µs
Hình minh họa: khung PPM được phát bằng bộ RMT của ESP32.

Khung PPM do bộ RMT của ESP32 phát: chương trình chỉ cần điền 9 ký hiệu (mức và thời gian) rồi gọi rmtWriteAsync(), phần cứng tự tạo xung chính xác tới micro giây, không phụ thuộc vòng lặp. Khung SBUS 25 byte gửi qua Serial2 mỗi 14 ms; tham số cuối của Serial2.begin(..., true) bật chế độ đảo mức tín hiệu.

/*
  PYWORLD.VN - Bộ thu ESP32: nhận 8 kênh qua ESP-NOW, xuất PPM (GPIO27) và SBUS (GPIO17) cho mạch điều khiển bay
  PPM: khung 22,5 ms, xung 300 us; SBUS: 100000 baud 8E2 đảo mức, 25 byte mỗi 14 ms; GND chung với mạch điều khiển bay
  Mất gói quá 500 ms: ngừng PPM, SBUS gửi giá trị an toàn kèm cờ failsafe
*/

#include <WiFi.h>
#include <esp_now.h>
#include <esp_mac.h>

#define CHAN_PPM 27
#define CHAN_SBUS 17
#define XUAT_PPM 1
#define XUAT_SBUS 1
#define PPM_XUNG_CAO 1              // 1: nghỉ mức thấp, xung mức cao; 0: đảo lại (nghỉ mức cao)
#define KHUNG_PPM_US 22500
#define XUNG_PPM_US 300
#define CHU_KY_SBUS_MS 14
#define MAT_SONG_MS 500

typedef struct __attribute__((packed)) {
  uint8_t dau;
  uint8_t soThuTu;
  uint16_t kenh[8];
  uint8_t kiemTra;
} GoiLenh;

const uint16_t KENH_AN_TOAN[8] = {1500, 1500, 1000, 1500, 1000, 1000, 1000, 1000};

portMUX_TYPE khoaDuLieu = portMUX_INITIALIZER_UNLOCKED;
volatile uint16_t kenhNhan[8] = {1500, 1500, 1000, 1500, 1000, 1000, 1000, 1000};
volatile unsigned long lanNhanCuoi = 0;
volatile uint32_t soGoiHopLe = 0, soGoiLoi = 0;
rmt_data_t khungPpm[9];
unsigned long lanSbusTruoc = 0, lanInTruoc = 0;

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)) {
    soGoiLoi = soGoiLoi + 1;
    return;
  }
  GoiLenh g;
  memcpy(&g, duLieu, sizeof(g));
  if (g.dau != 0xA5 || g.kiemTra != tinhKiemTra(g)) {
    soGoiLoi = soGoiLoi + 1;
    return;
  }
  for (int i = 0; i < 8; i++) {
    if (g.kenh[i] < 900 || g.kenh[i] > 2100) {
      soGoiLoi = soGoiLoi + 1;
      return;
    }
  }
  portENTER_CRITICAL(&khoaDuLieu);
  for (int i = 0; i < 8; i++) {
    kenhNhan[i] = g.kenh[i];
  }
  lanNhanCuoi = millis();
  portEXIT_CRITICAL(&khoaDuLieu);
  soGoiHopLe = soGoiHopLe + 1;
}

// Lấy bản sao kênh; trả về true nếu đang mất sóng (đã điền giá trị an toàn)
bool layKenh(uint16_t kenh[8]) {
  portENTER_CRITICAL(&khoaDuLieu);
  unsigned long cuoi = lanNhanCuoi;
  for (int i = 0; i < 8; i++) {
    kenh[i] = kenhNhan[i];
  }
  portEXIT_CRITICAL(&khoaDuLieu);
  bool matSong = (cuoi == 0) || (millis() - cuoi > MAT_SONG_MS);
  if (matSong) {
    memcpy(kenh, KENH_AN_TOAN, sizeof(KENH_AN_TOAN));
  }
  return matSong;
}

void guiKhungPpm(const uint16_t kenh[8]) {
  uint32_t tong = 0;
  for (int i = 0; i < 8; i++) {
    khungPpm[i].level0 = PPM_XUNG_CAO;
    khungPpm[i].duration0 = XUNG_PPM_US;
    khungPpm[i].level1 = !PPM_XUNG_CAO;
    khungPpm[i].duration1 = kenh[i] - XUNG_PPM_US;
    tong += kenh[i];
  }
  // Khe cuối: xung rồi khoảng nghỉ dài để đủ 22,5 ms, mạch điều khiển bay coi đây là tín hiệu đồng bộ
  khungPpm[8].level0 = PPM_XUNG_CAO;
  khungPpm[8].duration0 = XUNG_PPM_US;
  khungPpm[8].level1 = !PPM_XUNG_CAO;
  khungPpm[8].duration1 = KHUNG_PPM_US - tong - XUNG_PPM_US;
  rmtWriteAsync(CHAN_PPM, khungPpm, 9);
}

// Đổi us sang giá trị SBUS: us = 0,625 x giá trị + 880
uint16_t usSangSbus(uint16_t us) {
  int v = ((int)us - 880) * 8 + 2;
  v = v / 5;
  return (uint16_t)constrain(v, 0, 2047);
}

void guiKhungSbus(const uint16_t kenh[8], bool matSong) {
  uint8_t khung[25] = {0};
  uint16_t gia_tri[16];
  for (int i = 0; i < 16; i++) {
    gia_tri[i] = usSangSbus(i < 8 ? kenh[i] : 1500);
  }
  khung[0] = 0x0F;
  // Xếp 16 kênh 11 bit liền nhau, bit thấp trước, vào byte 1-22
  uint32_t dem = 0;
  int bitDem = 0, viTri = 1;
  for (int i = 0; i < 16; i++) {
    dem |= (uint32_t)gia_tri[i] << bitDem;
    bitDem += 11;
    while (bitDem >= 8) {
      khung[viTri++] = dem & 0xFF;
      dem >>= 8;
      bitDem -= 8;
    }
  }
  khung[23] = matSong ? 0x0C : 0x00;   // bit 2 mất khung, bit 3 failsafe
  khung[24] = 0x00;
  Serial2.write(khung, sizeof(khung));
}

void setup() {
  Serial.begin(115200);
  delay(200);
  WiFi.mode(WIFI_STA);
  uint8_t mac[6];
  esp_read_mac(mac, ESP_MAC_WIFI_STA);
  Serial.println();
  Serial.printf("MAC bo thu: {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) {
    Serial.println("Khoi dong ESP-NOW that bai");
    while (true) {
      delay(1000);
    }
  }
  esp_now_register_recv_cb(khiNhan);
#if XUAT_PPM
  rmtInit(CHAN_PPM, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 1000000);   // 1 tick = 1 us
  rmtSetEOT(CHAN_PPM, !PPM_XUNG_CAO);
  digitalWrite(CHAN_PPM, !PPM_XUNG_CAO);
#endif
#if XUAT_SBUS
  Serial2.begin(100000, SERIAL_8E2, -1, CHAN_SBUS, true);   // true: đảo mức tín hiệu
#endif
  Serial.println("Bo thu san sang, cho goi lenh");
}

void loop() {
  uint16_t kenh[8];
  bool matSong = layKenh(kenh);

#if XUAT_PPM
  // Khi mất sóng ngừng phát PPM để mạch điều khiển bay tự vào failsafe
  if (!matSong && rmtTransmitCompleted(CHAN_PPM)) {
    guiKhungPpm(kenh);
  }
#endif
#if XUAT_SBUS
  if (millis() - lanSbusTruoc >= CHU_KY_SBUS_MS) {
    lanSbusTruoc = millis();
    guiKhungSbus(kenh, matSong);
  }
#endif

  if (millis() - lanInTruoc >= 1000) {
    lanInTruoc = millis();
    uint32_t hopLe = soGoiHopLe, loi = soGoiLoi;
    soGoiHopLe = 0;
    soGoiLoi = 0;
    Serial.printf("Goi/giay %2u | loi %u | kenh %u %u %u %u %u %u %u %u | failsafe: %s\n", (unsigned)hopLe, (unsigned)loi,
                  kenh[0], kenh[1], kenh[2], kenh[3], kenh[4], kenh[5], kenh[6], kenh[7], matSong ? "CO" : "khong");
  }
}

Failsafe: nếu quá 500 ms không nhận được gói hợp lệ, bộ thu ngừng phát PPM (mạch bay sẽ báo mất tín hiệu) và gửi khung SBUS có cờ mất khung, cờ failsafe cùng giá trị an toàn: ga 1000 µs, AUX1 tắt. Mạch điều khiển bay nhận cờ này và làm theo cài đặt failsafe của nó (tắt động cơ hoặc tự hạ cánh).

7. Thứ tự lắp và kiểm tra

  1. Nạp chương trình bộ thu, mở Serial Monitor, chép dòng MAC bo thu: {...}.
  2. Dán vào MAC_BO_THU của chương trình tay điều khiển, nạp cho mạch thứ hai.
  3. Cấp điện cả hai, xem Serial Monitor: tay điều khiển báo gui duoc 25/25, bộ thu báo khoảng 50 gói mỗi giây và failsafe: khong.
  4. Tháo cánh quạt, nối bộ thu với mạch bay. Trong phần mềm cấu hình (ví dụ Betaflight Configurator): chọn kiểu bộ thu SBUS trên đúng cổng UART, hoặc chọn PPM; thứ tự kênh AETR.
  5. Mở trang Receiver, gạt từng cần, kiểm tra đúng kênh, đúng chiều, tâm khoảng 1500.
  6. Tắt tay điều khiển: mạch bay phải báo failsafe sau khoảng nửa giây.

8. Kết quả trên Serial Monitor

Hình minh họa hai Serial Monitor: tay điều khiển in tâm cần, các dòng roll pitch ga yaw AUX và số gói gửi được 25 trên 25, khi bộ thu tắt còn 0 trên 25; bộ thu in MAC, số gói mỗi giây 50, các kênh và trạng thái failsafe
Hình minh họa: kết quả in ra ở hai mạch.

Dòng gui duoc 25/25 nghĩa là trong nửa giây vừa qua, cả 25 gói đều được bộ thu xác nhận đã nhận. Số này giảm khi đi xa hoặc có vật cản, và về 0 khi bộ thu tắt hoặc sai địa chỉ MAC.

9. 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ý
Tay điều khiển báo gui duoc 0/25Sai MAC bộ thu; bộ thu chưa bật; hai mạch khác kênh Wi-FiChép lại MAC; không cho mạch nào kết nối router trong chương trình này
Bộ thu báo số loi tăngHai chương trình khai báo gói khác nhau; có thiết bị khác gửi tớiNạp lại cả hai chương trình cùng phiên bản
Kênh dao động khi buông cầnJoystick rung tâm, vùng chết nhỏTăng VUNG_CHET; khởi động lại khi đã buông cần
Đẩy cần mà kênh chạy ngượcJoystick lắp xoay chiều khácĐặt cờ DAO_… tương ứng bằng 1
Mạch bay không nhận SBUSSai cổng UART, chưa bật chế độ bộ thu trên cổng đó, nối nhầm vào TXKiểm tra cổng trong phần mềm cấu hình, nối vào chân RX
Mạch bay không nhận PPMChưa chọn kiểu PPM; mạch bay cần xung ngược mứcChọn PPM; thử PPM_XUNG_CAO 0
Tay điều khiển in mãi dòng "Tat cong tac AUX1"AUX1 đang bật lúc khởi độngGạt AUX1 về tắt

10. Xem thêm

Cùng loạt: tay điều khiển flycam và FPV (kênh, PPM, SBUS, ELRS, failsafe); ESP32 làm mạch điều khiển bay cho flycam mini; quy định bay flycam tại Việt Nam.


Tin tức liên quan

Hướng dẫn dùng màn hình OLED 1.3 inch SH1106 với ESP32
Hướng dẫn dùng màn hình OLED 1.3 inch SH1106 với ESP32

Hướng dẫn nối màn hình OLED 1.3 inch SH1106 với ESP32 DevKit V1 qua I2C, cài thư viện Adafruit SH110X, chạy code hiển thị chữ và xử lý lỗi thường gặp.

Xe tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 trên servo quét, tự chọn đường
Xe tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 trên servo quét, tự chọn đường

Làm xe robot tự tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 gắn trên servo quét trái phải, cầu phân áp cho chân ECHO, thuật toán chọn đường, lưu đồ và thí nghiệm STEM.

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 nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Xe robot ESP32 điều khiển bằng điện thoại qua Wi-Fi: không cần router, không cần cài ứng dụng
Xe robot ESP32 điều khiển bằng điện thoại qua Wi-Fi: không cần router, không cần cài ứng dụng

Làm xe robot hai bánh với ESP32 và L298N, điều khiển bằng trình duyệt điện thoại qua Wi-Fi do ESP32 phát ra: bố trí, nối dây, code web, tự dừng khi mất sóng, lái vi sai, STEM.

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 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.

Giữ tốc độ động cơ DC ổn định bằng PID trên ESP32: encoder, chỉnh Kp Ki Kd, Serial Plotter
Giữ tốc độ động cơ DC ổn định bằng PID trên ESP32: encoder, chỉnh Kp Ki Kd, Serial Plotter

Hướng dẫn điều khiển vòng kín tốc độ động cơ DC bằng PID trên ESP32: đo tốc độ bằng encoder và PCNT, chu kỳ 20 ms, chống tích phân bão hòa, chỉnh Kp Ki Kd và thí nghiệm STEM.


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