沉浸式远航固定翼无人机系统-Part 5-遥控增程&4G转发
本文最后更新于 2025-12-23,文章内容可能已经过时。
天地飞09SⅡ 无线中继地面站 & ELRS 全套自制指南
一、 系统架构图 (无线中继版)
本项目构建一个独立的地面中继站。天地飞遥控器通过原装 2.4G 链路无线连接到地面上的接收机。地面站接收 PPM 信号后,通过 915M ELRS 和 WiFi 网络双路并行发送控制指令。
输入源: 天地飞原装接收机 (输出 PPM)。
发射链路 A (主): ESP32-A + E22-900M30S (ELRS 1W TX),带 OLED 屏幕与按键。
发射链路 B (备): ESP32-B (WiFi),UDP 转发至云服务器。
接收端 (机载): 自制或成品 ELRS 915M 接收机。
二、 硬件采购清单 (BOM)
1. 地面中继站 (发射端)
2. 机载接收机 (DIY RX)
如果你决定自制接收机,请采购以下元件;否则建议购买 HappyModel ES900RX。
三、 硬件制作与电路连接
1. 地面站总线连接
将 地面接收机 (天地飞) 的 PPM 信号引脚分叉,同时连接到两个 ESP32:
接收机 PPM -> ESP32-A (GPIO 4)
接收机 PPM -> ESP32-B (GPIO 4)
接收机 VCC/GND -> UBEC 5V / GND (共地至关重要!)
2. ESP32-A (ELRS 发射机) 详细接线
固件目标:DIY 900 MHz TX ESP32 SX126x
3. ESP32-B (WiFi 网桥) 接线
PPM 输入 -> GPIO 4
VCC/GND -> 5V/GND
4. 自制接收机 (DIY RX) 接线
固件目标:DIY 900 MHz RX ESP8285 SX126x
四、 固件编译与烧录 (完整步骤)
1. ELRS 发射机 (ESP32-A)
下载安装 ExpressLRS Configurator。
Category:
DIY 900 MHzDevice:
DIY 900 MHz TX ESP32 SX126xFlashing Method:
UART(首次刷写需用 USB 线)User Defines (复制以下内容):
-D REGULATORY_DOMAIN_ISM_2400 -D MY_BINDING_PHRASE="11223344" // 必须修改为你的密码 -D LOCK_ON_FIRST_CONNECTION -D HAS_OLED -D OLED_SDA=21 -D OLED_SCL=22 -D OLED_ADDR=0x3C -D GPIO_PIN_BUTTON_UP=25 -D GPIO_PIN_BUTTON_DOWN=26 -D GPIO_PIN_BUTTON_ENTER=27 -D BUTTON_PRESSED_STATE=0 // 强制使用 PPM 输入 -D RC_SIGNAL_PIN=4点击 Build and Flash。
刷写完成后,断电重启。进入 Web 后台 (10.0.0.1),在 Model 选项卡确认为 PPM 模式。
2. ELRS 接收机 (DIY RX)
Category:
DIY 900 MHzDevice:
DIY 900 MHz RX ESP8285 SX126xUser Defines:
-D MY_BINDING_PHRASE="11223344" // 密码必须与 TX 一致刷写方法: 使用 USB-TTL 模块。
ESP8285 的 TX -> TTL RX
ESP8285 的 RX -> TTL TX
关键: 上电前将 ESP8285 的 GPIO 0 接地 进入刷机模式。
点击 Build and Flash。
五、 地面端 ESP32-B 完整代码
此代码用于读取 PPM 并通过 UDP 发送 MAVLink 包。(增加后续ATT用的飞控数据打印,ok)
使用方法:
安装 Arduino IDE。
库管理器搜索并安装
MAVLink库 (by Arduino MAVLink)。选择开发板
DOIT ESP32 DEVKIT V1。修改代码中的 WiFi 和服务器地址,点击上传。
#include <WiFi.h>
#include <WiFiUdp.h>
#include <MAVLink.h>
// ================= 用户配置区 =================
const char* ssid = "UAV402";
const char* password = "11223344";
const char* server_ip = "www.cclddk007.top";
const int server_port = 51175; // 对应服务器 PORT_GND_RC
const int ppmPin = 4; // PPM 输入引脚
const int SYSTEM_ID = 255; // 设为 255 以匹配飞控主地面站权限
const int TARGET_SYSTEM = 1; // 目标飞控 ID
// =============================================
WiFiUDP udp;
mavlink_message_t msg_send;
uint8_t buf[MAVLINK_MAX_PACKET_LEN];
// PPM 解码变量
volatile unsigned long lastPulseTime = 0;
volatile int currentChannel = 0;
volatile uint16_t channelValues[8] = {1500, 1500, 1000, 1500, 1500, 1500, 1500, 1500};
// --- AAT 遥测数据存储变量 ---
struct TelemetryData {
int32_t lat; // 经度 1E7
int32_t lon; // 纬度 1E7
int32_t alt; // 海拔高度 mm
float roll; // 翻滚角 rad
float pitch; // 俯仰角 rad
float yaw; // 偏航角 rad
uint16_t heading; // 机头指向 deg
float groundspeed; // 地速 m/s
uint8_t satellites; // 卫星数
uint16_t eph; // 水平定位精度
uint8_t battery_remaining; // 剩余电量 %
uint32_t last_packet_ms; // 最后收到包的时间
} telem;
// MAVLink 解析状态机
mavlink_message_t msg_receive;
mavlink_status_t status_receive;
// PPM 中断解码逻辑
void IRAM_ATTR ppmInterrupt() {
unsigned long now = micros();
unsigned long duration = now - lastPulseTime;
lastPulseTime = now;
if (duration > 3000) {
currentChannel = 0;
} else {
if (currentChannel < 8) {
if (duration > 800 && duration < 2200) channelValues[currentChannel] = duration;
currentChannel++;
}
}
}
void setup() {
Serial.begin(115200);
pinMode(ppmPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ppmPin), ppmInterrupt, RISING);
Serial.printf("\nConnecting to %s", ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi Connected!");
udp.begin(51175);
}
// 解析 MAVLink 消息
void handle_mavlink_packet(mavlink_message_t* msg) {
telem.last_packet_ms = millis();
switch (msg->msgid) {
case MAVLINK_MSG_ID_GPS_RAW_INT: {
mavlink_gps_raw_int_t packet;
mavlink_msg_gps_raw_int_decode(msg, &packet);
telem.lat = packet.lat;
telem.lon = packet.lon;
telem.alt = packet.alt;
telem.satellites = packet.satellites_visible;
telem.eph = packet.eph;
break;
}
case MAVLINK_MSG_ID_ATTITUDE: {
mavlink_attitude_t packet;
mavlink_msg_attitude_decode(msg, &packet);
telem.roll = packet.roll;
telem.pitch = packet.pitch;
telem.yaw = packet.yaw;
break;
}
case MAVLINK_MSG_ID_VFR_HUD: {
mavlink_vfr_hud_t packet;
mavlink_msg_vfr_hud_decode(msg, &packet);
telem.heading = packet.heading;
telem.groundspeed = packet.groundspeed;
break;
}
case MAVLINK_MSG_ID_SYS_STATUS: {
mavlink_sys_status_t packet;
mavlink_msg_sys_status_decode(msg, &packet);
telem.battery_remaining = packet.battery_remaining;
break;
}
case MAVLINK_MSG_ID_GLOBAL_POSITION_INT: {
mavlink_global_position_int_t packet;
mavlink_msg_global_position_int_decode(msg, &packet);
break;
}
}
}
void loop() {
unsigned long now = millis();
static unsigned long lastSend = 0;
static unsigned long lastHB = 0;
static unsigned long lastPrint = 0;
// 1. 发送 RC 覆盖指令 (约 40Hz)
if (now - lastSend >= 25) {
lastSend = now;
uint16_t v[8];
noInterrupts();
for(int i=0; i<8; i++) v[i] = channelValues[i];
interrupts();
mavlink_msg_rc_channels_override_pack(SYSTEM_ID, 190, &msg_send, TARGET_SYSTEM, 1,
v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7],
0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
uint16_t len = mavlink_msg_to_send_buffer(buf, &msg_send);
udp.beginPacket(server_ip, server_port);
udp.write(buf, len);
udp.endPacket();
}
// 2. 发送心跳包 (1Hz)
if (now - lastHB >= 1000) {
lastHB = now;
mavlink_msg_heartbeat_pack(SYSTEM_ID, 190, &msg_send, MAV_TYPE_GCS, MAV_AUTOPILOT_INVALID, MAV_MODE_MANUAL_ARMED, 0, MAV_STATE_ACTIVE);
uint16_t len = mavlink_msg_to_send_buffer(buf, &msg_send);
udp.beginPacket(server_ip, server_port);
udp.write(buf, len);
udp.endPacket();
}
// 3. 接收并解析回传数据
while (udp.parsePacket() > 0) {
while (udp.available()) {
uint8_t byte_received = udp.read();
if (mavlink_parse_char(MAVLINK_COMM_0, byte_received, &msg_receive, &status_receive)) {
handle_mavlink_packet(&msg_receive);
}
}
}
// 4. 定时打印状态 (0.5秒一次)
if (now - lastPrint >= 500) {
lastPrint = now;
long link_age = now - telem.last_packet_ms;
bool link_active = (link_age < 3500); // 略微放宽判断标准,减少误报
if (link_active) {
// 将弧度转换为角度以便观察
float roll_deg = telem.roll * 57.2958f;
float pitch_deg = telem.pitch * 57.2958f;
float yaw_deg = telem.yaw * 57.2958f;
Serial.printf("[Live] Age:%ldms | R:%.1f P:%.1f Y:%.1f deg | GS:%.1f | Bat:%d%%\n",
link_age, roll_deg, pitch_deg, yaw_deg, telem.groundspeed, telem.battery_remaining);
} else {
Serial.printf("[Loss] Link Down for %ld ms - Re-Syncing...\n", link_age);
}
}
}六、 飞控端设置与最终联调
飞控参数设置 (ArduPilot):
SERIALx_PROTOCOL= 2 (MAVLink2) - 针对连接 4G 模块的串口FS_THR_ENABLE= 1 (开启油门失控保护)关键: 确保 ELRS 接收机失控模式为 Cut/No Pulses。
联调步骤:
步骤 1: 开启天地飞遥控器和地面中继站。
步骤 2: 观察 ESP32-A 的 OLED 屏幕,应显示连接状态。
步骤 3: 观察 ESP32-B 的串口输出,应显示
WiFi Connected和Sending RC...。步骤 4: 连接飞控地面站 (Mission Planner),查看
RC_CHANNELS。正常情况下,数值应跟随摇杆变化 (此时走的是 ELRS)。
测试切换: 拔掉 ELRS 接收机 (模拟失控),数值应继续跟随摇杆变化 (此时已自动无缝切换到 4G/WiFi 链路)。
七、 制作注意事项
散热: 1W 的 E22 模块发热极其严重,必须使用导热硅胶将其粘贴在散热片上,并用风扇强制散热,否则几分钟内就会热保护降频。
天线: 地面站建议使用高增益的 915M 莫克森 (Moxon) 天线 或 八木天线 指向飞机方向,可轻松突破 30km+。
屏蔽: 由于 ESP32-B (WiFi) 和 天地飞接收机 (2.4G) 距离很近,建议用铝箔胶带包裹 ESP32-B 的背面 (注意绝缘),防止 WiFi 信号淹没接收机的灵敏度。