ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

飞控二次开发全路径:从外挂树莓派到PX4模块

飞控二次开发全路径:从外挂树莓派到PX4模块 飞控二次开发这件事我见过太多人走弯路。拿到一块 Pixhawk 或者 SpeedyBee F405第一反应就是去 GitHub 拉源码然后对着 ArduPilot 几十万行 C 发呆或者一头扎进 PX4 的模块树里出不来。不是说啃源码不对而是二次开发这四个字的含义远比改固件要宽。以我自己的经验从外挂树莓派到自定义模块中间隔着好几条完全不同的技术路径每一条的工作量、风险、适用场景都不一样。这篇文章就把这些路径一次性讲透帮你在动手之前先想明白你到底需要开发的是什么以及哪条路最适合你。1. 飞控二次开发的地图先知道自己站在哪一层1.1 啃源码只是其中一种还是最难的那种先说一个很反直觉的事实多数实际项目里的飞控二次开发根本不需要改飞控固件源码。航拍无人机要做的目标跟踪、物流无人机的航线规划、巡检无人机的数据采集这些功能全部可以在飞控之外的第二台电脑上完成。飞控在这套架构里只扮演一个稳定飞行和响应指令的执行者真正的业务逻辑放在外挂设备上。改源码只是所有路径里最深入、最灵活、也最难维护的一种不应该成为你起步的默认选项。回想我自己第一次做飞控二次开发是给一台四旋翼加一个视觉降落功能。当时我的第一反应也是去看 PX4 源码想在里面加一个视觉识别模块。结果光是搭建交叉编译环境就折腾了两个晚上还没等我把代码写出来一个前辈提醒我Pixhawk 上还有一个空闲的串口你直接用树莓派跑 OpenCV识别结果通过 MAVLink 发给飞控不就行了那一瞬间我意识到我一直以来都在用最费力的方式解决一个本可以很简单的需求。1.2 从外挂到固件的四层开发路径我把常见的飞控二次开发方式按离飞控核心的远近分成四个层级层级开发方式是否改固件典型工具难度风险第一层地面站参数配置否Mission Planner / QGroundControl极低极低第二层外挂协处理器的 MAVLink 开发否树莓派 / Jetson Nano / 单片机中低第三层在固件内扩展新模块是PX4 模块 / ArduPilot 库高中第四层深度改造飞行控制算法是姿态解算、EKF、控制器极高高注意这四层之间不是互相替代的关系而是递进的关系。一个成熟的行业应用往往是先在第二层把业务逻辑跑通确认算法有效再把最核心、对实时性要求最高的部分下沉到第三层甚至第四层。直接从第四层起步等于在业务验证之前就先啃了最难的技术骨头。1.3 一句话判断自己该走哪条路判断依据可以归结为一个问题你的功能需要多快的响应速度响应速度在几十毫秒到秒级就够用的视觉识别、路径规划、物流投放、航测数据采集走外挂方案用树莓派或者高性能单片机。响应速度要到毫秒级甚至更快、必须和姿态控制强耦合的比如特殊的避障策略、故障保护逻辑、新的控制算法才需要进固件层。我见过一个做电力巡检的团队他们想在无人机碰到电线之前自动刹车。最初打算在 PX4 源码里加一个激光雷达数据处理模块后来发现这套系统的决策周期其实在 100ms 量级就够了最后用树莓派加一个串口转发的方案轻松搞定开发周期从两个月缩短到两周。这就是先想清楚需求边界的好处。2. 外挂树莓派不动固件的旁路开发2.1 这套方案的架构逻辑飞控当执行者树莓派当大脑外挂方案的架构可以这样理解树莓派是大脑飞控是脊髓反射弧。飞控本身依然在干它最擅长的事——姿态解算、电机控制、定高定点树莓派则负责一切需要思考的工作——图像识别、路径规划、传感器融合、任务调度。二者通过串口UART相连用 MAVLink 协议对话。这么做的好处非常明显飞控固件完全不用动稳定性有保障飞控厂商做的所有安全机制原封不动。开发环境友好。树莓派上跑的是完整 LinuxPython、C、ROS 随便用出问题调试起来比嵌入式环境舒服太多。功能迭代快。想改逻辑改代码就行不用重新编译烧录固件也不用担心刷固件把飞机刷成砖。代价是增加了一台外设的重量和功耗而且通信链路上多了一环会有一定的延迟。但绝大多数二次开发场景根本感知不到这点延迟。2.2 树莓派和飞控怎么接一根线的事接线远比想象中简单。以树莓派 4B 和 Pixhawk 为例飞控的 TELEM1 或者 TELEM2 串口 —— 这是飞控对外通信的串口一般是 JST-GH 1.25mm 接口。树莓派的 UART 引脚GPIO 14 的 TXD 和 GPIO 15 的 RXD—— 注意交叉连接飞控 TX 接树莓派 RX飞控 RX 接树莓派 TX。两边必须共地GND 相连否则通信会出现随机乱码。还有几个容易被忽略的细节电平问题。树莓派的 GPIO 是 3.3V 电平Pixhawk 的串口也是 3.3V可以直接连。如果你用的是 Arduino 的 5V 串口就要加电平转换芯片不然有烧毁飞控串口引脚的风险。树莓派默认的串口可能被分配给了 Linux 控制台需要先做两处修改一是用raspi-config关闭串口终端登录二是把/boot/config.txt里的enable_uart1打开。这步不做你会发现树莓派发出去的数据全被系统控制台的日志占用了。飞控这边需要在 Mission Planner 或者 QGroundControl 里把对应的串口协议设置为 MAVLink波特率给到 57600 或者 115200。默认 9600 不是不能用但跑 MAVLink 高频消息时容易丢包。2.3 基于 MAVLink 的核心开发方式飞控与树莓派之间的对话语言是 MAVLink这是一套专门为飞行器设计的轻量级消息协议。它的设计类似一个航空报文系统每一条消息都有固定的编号消息 ID、格式和载荷通信双方只要都会这套报文就能互相理解。开发时你需要重点掌握几组消息功能方向关键消息说明获取状态HEARTBEAT心跳包判断飞控是否在线获取姿态ATTITUDE飞控当前四元数姿态获取位置GLOBAL_POSITION_INT全球经纬度和相对高度手动控制MANUAL_CONTROL/RC_CHANNELS_OVERRIDE直接给通道值模拟遥控器自动控制SET_POSITION_TARGET_LOCAL_NED给定目标位置 / 速度 / 加速度航点任务MISSION_ITEM_INT上传航点任务实现方式上有两个成熟方案一个是用 pymavlink 库直接写 Python 脚本适合快速验证逻辑另一个是用 MAVSDK官方提供的跨语言库API 设计更友好。我自己习惯先用 pymavlink 写一个 demo 跑通链路然后再迁移到 ROS 里做正式开发。给你一个最简的 Python 示例让飞控解锁并在本地坐标系下飞到指定位置from pymavlink import mavutil # 连接树莓派串口波特率与飞控设置保持一致 master mavutil.mavlink_connection(/dev/serial0, baud57600) master.wait_heartbeat() print(飞控已连接) # 切换到 GUIDED 模式自动控制必须在这个模式下 mode GUIDED mode_id master.mode_mapping()[mode] master.set_mode(mode_id) master.arducopter_arm() master.motors_armed_wait() print(电机已解锁) # 发送本地坐标系下的目标位置 master.mav.set_position_target_local_ned_send( 0, 0, 0, mavutil.mavlink.MAV_FRAME_LOCAL_NED, 0b0000111111111000, # 只使用位置分量 5, 0, -3, # x5米, y0米, z-3米注意NED坐标系z轴向下 0, 0, 0, # 速度 0, 0, 0, # 加速度 0, 0 # yaw相关 ) print(目标位置已发送)写这行代码很容易但里面有几个坑值得说一下NED 坐标系里 z 轴是向下的所以往高处飞要传负的 z 值。参数0b0000111111111000是类型掩码它告诉飞控这次只使用位置分量速度加速度都不用。掩码写错飞控可能根本不会执行你的指令或者行为完全不是你想的那样。如果电机解锁失败检查一下飞控是否处于未校准或者报错状态以及地面上是否有足够的安全检测很多飞控默认检测到震动异常会拒绝解锁。2.4 外挂方案的边界什么时候该换路外挂方案不是万能的。遇到下面几种情况你就得认真考虑向固件层下沉你的控制周期必须跑到 1ms 级别树莓派上的 Linux 调度无法保证这种实时性。你要读取的传感器挂在飞控的总线上比如 IMU 的数据通过飞控的 SPI 接口访问树莓派直接在物理上够不到。飞行器对载重和功耗极其敏感没有余量再背一个协处理器。我见过一个做农用无人机的团队想实现断药自动补喷功能。最初外挂树莓派做但每次断药检测到动作执行要经过传感器 → 飞控 → 树莓派 → 飞控 → 电机这条链路累计延迟接近 300ms喷洒不均匀被农户投诉了好几次。后来他们把断药检测逻辑直接写成 PX4 的一个模块延迟降到 20ms 以内问题立刻解决。这就是一个典型的该下沉了的案例。3. 再进一步PX4 Offboard 模式与 uORB 通信3.1 Offboard 模式和普通自稳模式的区别如果你决定往固件方向走第一步不是直接去改源码而是先学会用 PX4 的 Offboard 模式。这个模式是一个介于外挂和改固件之间的桥梁代码跑在树莓派上不碰固件但控制指令通过 MAVLink 进入飞控后直接由飞控内部的控制器执行绕过了用户遥控器的输入。从代码角度看你要做的和第二章展示的很像核心区别在于控制链路的等级。在 GUIDED 模式下飞控按是否收到心跳来运行在 Offboard 模式下飞控每隔一段时间非常严格地盯着外部指令流一旦超过一定时间默认是 500ms没有收到新的目标指令它会认为通信中断立刻触发失控保护。3.2 uORBPX4 内部的消息总线如果你继续走下去迟早会碰到 PX4 的 uORB。可以把 uORB 理解成 PX4 内部的消息总线就像电脑主板上的总线一样把各个模块连接起来。传感器数据、姿态估计、控制指令全部以主题topic的形式在 uORB 上发布和订阅。举几个常见的 uORB 主题主题名数据类型内容sensor_combinedsensor_combined_s融合后的 IMU 数据vehicle_attitudevehicle_attitude_s当前姿态四元数vehicle_local_positionvehicle_local_position_s本地坐标系位置vehicle_commandvehicle_command_s指令解锁、模式切换等actuator_controlsactuator_controls_s归一化的作动器控制量这套机制的精妙之处在于模块解耦。比如你写一个新的避障模块不需要知道避障信息最终怎么变成电机转速的你只需要把前方有障碍物发布到 uORB 上任何订阅这个主题的模块比如任务控制器都能收到。这就像在微信群里发一条消息你不用管谁在看谁需要谁就会响应。3.3 一个能跑的 offboard 控制例子这里给你一个最简的 offboard 控制脚本完整版可以从 MAVSDK 官方示例里找到。核心是用 MAVSDK 的offboard插件定期推送位置指令import asyncio from mavsdk import System from mavsdk.offboard import PositionNedYaw async def run(): drone System() # 连接树莓派串口 await drone.connect(system_addressserial:///dev/serial0:57600) print(等待飞控连接...) async for state in drone.core.connection_state(): if state.is_connected: print(已连接) break # 起飞并切换到 offboard 模式 await drone.offboard.set_position_ned(PositionNedYaw(0.0, 0.0, -2.0, 0.0)) await drone.offboard.start() print(Offboard 模式已启动) # 循环发送目标位置防止超时触发失控保护 for i in range(200): await drone.offboard.set_position_ned( PositionNedYaw(5.0, 0.0, -2.0, 0.0) ) await asyncio.sleep(0.2) asyncio.run(run())注意代码里那个 for 循环不是随便写的。Offboard 模式有指令超时机制飞控如果 500ms 收不到新指令会认为外部的大脑崩溃了自动退出 Offboard 并切换到安全模式。所以真实项目中最好是开一个独立的线程以 10Hz 以上的频率持续推送指令而不是发完一次就完事。这是所有 offboard 开发里最常踩、也最容易在测试中炸机的坑。4. 固件层开发ArduPilot 和 PX4 的两种扩展思路4.1 ArduPilot参数、库函数与 Lua 脚本ArduPilot 和 PX4 体系的设计哲学有明显差异。ArduPilot 更像是一套完整的自动驾驶仪配置 库函数的集合它的二次开发方式有两条温和的路径。第一条是极轻量的参数级开发。ArduPilot 有着极其丰富的参数系统很多功能可以通过在 Mission Planner 地面站里配置参数来实现比如设置 PID、调整混控器输出、配置 RC 通道功能等。一个有趣的现象是很多团队花了几个月做二次开发最后发现只需要修改几个参数的默认值就能满足需求。第二条是Lua 脚本。ArduPilot 从 4.1 版本开始在部分板卡上支持 Lua 脚本尤其是 F405/F427 这类资源较少的飞控新版本对 Lua 的支持也越来越好。Lua 脚本可以在不编译固件的情况下运行在飞控之上操作部分 APM 内部的数据和参数。比如下面这个极简示例读取当前空速并据此改变一个辅助通道的输出-- 每 200ms 运行一次 local PERIOD_MS 200 function update() -- 读取当前空速单位m/s local airspeed ahrs:airspeed_estimate() if airspeed then -- 按比例映射到 PWM 输出这里仅示意 local pwm 1000 math.min(math.max(airspeed*5, 0), 1000) SRV_Channels:set_output_pwm(8, pwm) end return update, PERIOD_MS end return update, PERIOD_MSLua 方案的好处是脚本文件可以放 SD 卡里飞控上电后自动加载想改逻辑直接把 SD 卡拔下来编辑再插回去就行不用重新烧固件。缺点是执行效率和实时性不如 C复杂任务处理不了。4.2 PX4从模块到飞行栈PX4 的思路更接近一个模块化的实时操作系统。它本身基于 NuttX RTOS各个功能传感器驱动、姿态估计、位置估计、任务控制、混控器等都是独立的任务通过 uORB 通信。PX4 的二次开发通常分为两种写一个全新的独立模块。这个模块运行在 PX4 内部订阅 uORB 消息处理业务逻辑再发布新的 uORB 消息。这种方式不需要动飞行控制算法的核心代码只在你自己的模块里做文章。深度修改飞行控制算法。比如你想改 EKF 滤波器、改姿态控制器这就不是加模块那么简单了你需要深入理解整个飞行栈的内部逻辑。这块工作的难度会突然跳升一个数量级因为姿态控制、位置控制的耦合关系极其复杂改一处可能影响全局稳定性。4.3 搞固件层最容易被坑的三个地方在固件层动手之前把下面三个坑提前告诉你能帮你省不少时间版本地狱。PX4 和 ArduPilot 都在快速迭代你写的模块在某个版本上能编译换个版本可能接口就变了。我在 2023 年写的一个 PX4 v1.13 模块到 v1.14 时就需要改不少 API 调用。建议把项目用的固件版本固定下来不要追新。板卡资源差异。同样是 F405不同飞控板的 Flash 和 RAM 差异很大。你写的模块如果在 QGroundControl 里能编译烧到板子上却运行不起来先去看看内存够不够。以前遇到过编译通过、上电直接跑飞的情况最后发现是任务栈分配太小导致溢出。烧录风险。刷固件操作本身有变砖风险尤其在一些 Clone 版飞控上bootloader 可能不兼容。所以固件层开发一开始一定要准备一块便宜的备用飞控并且养成熟练的拿回原厂固件恢复的操作习惯。5. 从零写一个自定义模块的完整路径5.1 模块骨架一个最小可编译的模块下面我用 PX4 为例带你走一遍自定义模块的完整过程。新建一个模块其实是在给 PX4 加一个能编译、能运行、能订阅消息的新任务。在src/examples下建一个目录px4_custom_module里面有下面两个文件。先看CMakeLists.txtpx4_add_module( MODULE modules__px4_custom_module MAIN px4_custom_module_main STACK_MAIN 2048 COMPILE_FLAGS SRCS px4_custom_module.c DEPENDS platform__uorb )再看核心 C 文件px4_custom_module.c的主体结构#include px4_platform_common/px4_config.h #include px4_platform_common/tasks.h #include uORB/uORB.h #include uORB/topics/vehicle_attitude.h static void custom_module_main(int argc, char *argv[]) { // 订阅姿态主题 int att_sub orb_subscribe(ORB_ID(vehicle_attitude)); while (!should_exit()) { struct vehicle_attitude_s att; orb_copy(ORB_ID(vehicle_attitude), att_sub, att); // 打印姿态四元数 PX4_INFO(att quaternion: [%.3f, %.3f, %.3f, %.3f], (double)att.q[0], (double)att.q[1], (double)att.q[2], (double)att.q[3]); px4_usleep(200000); // 5Hz } orb_unsubscribe(att_sub); } int px4_custom_module_main(int argc, char *argv[]) { // 启动任务 return px4_task_spawn_cmd(custom_module, SCHED_DEFAULT, SCHED_PRIORITY_DEFAULT, 2048, custom_module_main, argv); }这段代码的逻辑是模块启动一个任务订阅飞控的姿态消息每 200ms 打印一次姿态。所有 PX4 模块基本长这个样子——订阅输入、处理数据、发布输出只是业务逻辑各有不同。5.2 消息订阅与发布真正干活的代码仅仅打印姿态不算干活真正的二次开发往往要消费数据并产出新数据。PX4 的 uORB 发布机制也非常直接。假设你的模块要发布一个自定义的消息先在msg/目录下定义你的消息类型# custom_data.msg uint64 timestamp # 时间戳 float32 target_x # 目标位置 x float32 target_y # 目标位置 y float32 target_z # 目标位置 z然后重新编译PX4 会自动生成对应的 C 语言结构体和 ORB_ID。在代码里发布消息只要这样#include uORB/topics/custom_data.h struct custom_data_s data; data.timestamp hrt_absolute_time(); data.target_x 1.0f; data.target_y 2.0f; data.target_z -3.0f; // 第一次发布需要先 advertise static struct custom_data_s custom_data; static orb_advert_t data_pub orb_advertise(ORB_ID(custom_data), custom_data); // 后续用 orb_publish 更新 orb_publish(ORB_ID(custom_data), data_pub, data);你发布出去的custom_data主题任何其他模块包括地面站QGroundControl 也可以通过 MAVLink 透传看到都可以订阅。这就是 PX4 模块扩展的套路它不需要你改动飞控原本的核心只需要你在旁边挂一个新的功能块和一个新的群聊频道。5.3 编译、烧录和仿真验证写完代码后下一步是编译。强烈建议先使用 PX4 自带的 SITL软件在环仿真来验证模块逻辑再烧到真机。SITL 的启动流程在 Ubuntu 环境下大概是cd PX4-Autopilot make px4_sitl gazebo启动之后你的自定义模块会自动加载前提是你把它加进了编译列表然后在 QGroundControl 里可以看到飞控已经运行在仿真模式下。此时可以模拟飞行看你的模块是否按预期打印数据、发布消息。SITL 最大的价值是提供了几乎真实的运行环境你可以在这里验证消息订阅、发布、任务调度等所有逻辑。但注意真机上最常见的硬件故障比如总线冲突、引脚复用错误SITL 是模拟不出来的所以第一次上真机前要格外谨慎。真机烧录有两种方式一种是用 QGroundControl 直接刷写生成的.px4固件另一种是用命令行工具。无论哪一种刷完后一定要在地面站上重新执行传感器校准因为固件变化可能导致传感器零漂等参数失效。我自己第一次刷完没校准就起飞结果飞机在地面疯狂晃万幸是系留测试没出事。6. 路径选择建议先做减法再做加法站在今天的视角回头看最能帮你省时间的其实是选路这件事——不是技术难度而是你对自己需求的判断。如果你只是想让现有无人机加点功能——比如给一台成品 FPV 加个简单的定高、加一个自定义的灯效逻辑、或者做一个简单的航点任务——先用地面站参数配置解决解决不了再看外挂方案。如果你要做视觉识别、目标跟踪、自主航线这类有智能的功能——几乎不用犹豫直接上外挂树莓派或 NVIDIA Jetson 方案。这类功能的核心在算法不在飞控。把算法跑在 Linux 环境里开发效率高一个量级。如果你要做的功能必须在飞控内部完成——比如要读取 IMU 原始数据做新算法、要基于飞机的实时状态做特殊控制——再考虑 PX4 模块或 ArduPilot 的 Lua。不过就算是这样也可以先用 SITL 仿真把算法逻辑跑通再下到固件层不要一上来就面对真实飞控的复杂环境。如果你是初学者、刚开始接触飞控开发——请一定先做一遍外挂树莓派 MAVLink的例子。这个过程会让你理解飞控是怎么对外通信的、MAVLink 消息长什么样、解锁和起飞到底需要哪些条件。这些知识后面无论走哪条路都用得上而且是很多啃源码的老手可能都讲不清楚的暗知识。最后分享一个我项目里养成的习惯每接到一个新需求先画一张简单的数据流图——数据从哪里来传感器/遥控器/外部指令经过什么处理最终到哪里去电机/地面站/外部设备然后看这张图经过了飞控的哪个环节。如果飞控只是数据通路就走外挂如果飞控本身就是数据源或执行器才考虑固件层。用这个方法我后来几乎没再做过无用功。希望这篇文章也能帮你少走那些我已经走过的弯路。
返回列表