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ì

- 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

- 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

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àmquyDoiCan()đổ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

- 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_PPMhoặcXUAT_SBUSbằ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.

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
- Nạp chương trình bộ thu, mở Serial Monitor, chép dòng
MAC bo thu: {...}. - Dán vào
MAC_BO_THUcủa chương trình tay điều khiển, nạp cho mạch thứ hai. - 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. - 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.
- Mở trang Receiver, gạt từng cần, kiểm tra đúng kênh, đúng chiều, tâm khoảng 1500.
- 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

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
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
Tay điều khiển báo gui duoc 0/25 | Sai MAC bộ thu; bộ thu chưa bật; hai mạch khác kênh Wi-Fi | Ché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ăng | Hai chương trình khai báo gói khác nhau; có thiết bị khác gửi tới | Nạp lại cả hai chương trình cùng phiên bản |
| Kênh dao động khi buông cần | Joystick 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ược | Joystick lắp xoay chiều khác | Đặt cờ DAO_… tương ứng bằng 1 |
| Mạch bay không nhận SBUS | Sai cổng UART, chưa bật chế độ bộ thu trên cổng đó, nối nhầm vào TX | Kiể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 PPM | Chưa chọn kiểu PPM; mạch bay cần xung ngược mức | Chọ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 động | Gạt AUX1 về tắt |
10. Xem thêm
- ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch không cần router
- Điều khiển relay từ xa bằng ESP-NOW có phản hồi trạng thái
- UART là gì: khung truyền, tốc độ baud và Serial2 trên ESP32
- Kiểm tra lỗi truyền dữ liệu: bit chẵn lẻ, checksum và CRC
- Cấp nguồn ESP32 bằng pin 18650 sạc qua jack Type-C
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.
Xem thêm