先扔一个我自己的判断:如果你已经在PX4上折腾过几周,大概率会产生一个困惑——固件改了一堆,QGroundControl也能连上,但真正想让飞机自己做点事(比如自主起飞、按航点飞、识别到目标后自动降落),好像总隔着一层。这个“层”,就是MAVSDK解决的东西。
MAVSDK是PX4官方维护的SDK,它把PX4底层的MAVLink通信协议封装成了Python、C++、Swift、Java这些语言的API。通俗点说,PX4飞控是飞机的“小脑”,负责姿态控制、位置控制、传感器融合;MAVSDK是你写的程序用来命令“小脑”干活的“手柄”。你想让无人机做什么,在PC/机载电脑上写MAVSDK代码发给飞控,飞控负责执行和稳定飞行。
这篇文章就围绕“写MAVSDK/PX4应用”这件事,拆解从环境搭建、第一个Demo、到Offboard模式自主飞行的完整路径,顺手把我踩过的坑也一起说了。适合刚学PX4开发、想在Ubuntu 22.04上跑通仿真、或者准备在树莓派/Jetson上写无人机的读者参考。
1. 写PX4应用前,先搞懂MAVSDK在整个系统里的位置
很多新手一上来就急着编译PX4固件,想把C++代码塞进飞控里。这个方向不是不行,但成本和风险都很高——一个数组越界可能直接让飞机空中断电。而MAVSDK走的是一条轻量得多的路:应用代码跑在飞控之外的电脑或计算模块上,通过无线数传或串口与飞控通信。
1.1 PX4和MAVSDK各干各的活
PX4是运行在飞控硬件(比如Pixhawk系列)上的实时操作系统,它处理的事情包括:
- 姿态解算:融合陀螺仪、加速度计、磁力计,得出当前姿态角
- 位置估计:融合GPS、气压计、视觉里程计,得到位置和速度
- 控制输出:根据目标姿态/位置,输出PWM/Oneshot等信号给电调
- 状态机管理:地面待机(Manual)、自稳(Stabilized)、定高(Altitude)、Offboard等的切换
MAVSDK不碰这些底层逻辑。它只做一件事:把MAVLink协议封装成一系列高级API。比如调takeoff(),SDK会帮你组装好MAVLink消息(MAV_CMD_NAV_TAKEOFF、设定起飞高度、确认命令等),然后通过串口或UDP发送给PX4。PX4收到后执行实际起飞动作。
这样一个分层的好处很明显:你写应用的时候不需要关心MAVLink报文的格式细节,也不用担心某条消息的确认机制怎么处理。MAVSDK把这些都藏起来了。
1.2 为什么说用MAVSDK而不是直接发MAVLink
我在刚开始接触PX4时,也试过自己拼MAVLink报文,用的还是mavlink-router加pymavlink。但搞到后面发现,处理不同消息的超时、确认、重传逻辑非常繁琐。比如你发一个起飞命令,要等ACK确认,还要同时监听飞行模式的切换状态——这些逻辑自己写,至少几百行。
MAVSDK把这类协议交互封装好了,你看到的是同步的takeoff()调用,底层自动完成命令发送、ACK等待、超时重试。这个抽象价值巨大,尤其是在写复杂任务时,你的注意力应该放在逻辑编排上,而不是协议细节上。
注意:MAVSDK的抽象也有代价——它支持的功能比全量MAVLink少。但实际开发中90%的需求(起飞、降落、Offboard控制、任务上传、遥测订阅)它都覆盖了。
2. 在Ubuntu 22.04上搭建MAVSDK/PX4开发环境
网上搜“PX4 编译环境 Ubuntu 22.04”能搜出一堆教程,但很多都没把MAVSDK依赖单独说清楚。这里我给出一份我自己验证过的、可以少走弯路的方案。
2.1 环境准备:不用装完整的PX4固件也能跑MAVSDK
这是很多新手的第一个误区:以为用MAVSDK就必须先把PX4固件源码编译一遍。其实分两种情况:
- 只写MAVSDK应用:你只需要MAVSDK库本身,然后用一个模拟器(如PX4 SITL或MAVSDK自带的仿真)来测试。
- 同时改PX4固件:才需要编译PX4源码。
更重要的认知是:MAVSDK支持通过mavsdk_server连接真实飞控或仿真器。你可以把mavsdk_server当成一个翻译网关——你的Python/C++程序连上mavsdk_server,它再转发给PX4。这样反而更容易排查连接问题。
我的建议是先编译一次PX4 SITL(软件在环仿真),因为这样你能在Gazebo里看到飞机模型动起来,确认整条链路通了,再开始写MAVSDK代码。这一步的价值远大于直接买一块飞控接真机调参。
2.2 编译PX4 SITL的实战操作
我用的就是Ubuntu 22.04,按下面的顺序操作基本一次过。先装基础依赖:
sudo apt update sudo apt install -y \ git zip qtcreator cmake build-essential genromfs ninja-build \ exiftool astyle python3-pip python3-setuptools \ python3-jinja2 python3-yaml python3-numpy \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ libsqlite3-dev libopencv-dev \ protobuf-compiler libprotobuf-dev \ libeigen3-dev libgazebo11-dev \ ros-${ROS_DISTRO}-gazebo-ros-pkgs然后拉PX4源码,这里注意别直接clone最新main分支,选v1.14.3这个稳定版本:
git clone --recursive https://github.com/PX4/PX4-Autopilot.git cd PX4-Autopilot git checkout v1.14.3 git submodule update --init --recursive如果拉子模块特别慢,可以给git配置代理或换镜像源,但不建议跳过--recursive,缺少子模块几乎必然编译失败。
编译SITL仿真固件:
make px4_sitl gazebo-classic第一次编译大约需要15到30分钟,取决于机器配置。如果编译中途报错,不要急着重来,先看具体是哪个模块失败。我在二次编译时遇到过mavlink子模块版本不匹配,解决方式是把子模块更新到和v1.14.3匹配的版本。这个匹配关系一般会在PX4的Tools/setup/ubuntu.sh里自动处理,手动操作时容易忽略。
编译成功后,启动仿真:
make px4_sitl gazebo-classic启动后会在终端里看到pxh>提示符,同时Gazebo窗口里出现一架带旋翼的多旋翼模型。到这一步,你的PX4 SITL已经跑起来了,可以被MAVSDK连接。
重要提示:SITL默认在UDP 14540端口监听MAVLink消息,MAVSDK的
mavsdk_server默认连接的也是这个端口。理解端口号的作用,后面排查连接问题会有帮助。
2.3 安装MAVSDK-Python与验证连接
我推荐新手用Python版本快速上手,因为调试方便。安装很简单:
pip3 install mavsdk验证环境是否通的脚本如下,它能探测到PX4并打印飞控版本:
import asyncio from mavsdk import System async def run(): drone = System() await drone.connect(system_address="udp://:14540") print("Waiting for drone to connect...") async for state in drone.core.connection_state(): if state.is_connected: print("Connected to drone!") break async for version in drone.info.version(): print(f"Firmware version: {version.version_string}") break asyncio.run(run())这个脚本能跑通,说明你的PX4 SITL已经对MAVSDK开放了。后续所有的应用代码,都是在这个连接基础上叠功能。
3. 第一个MAVSDK应用:起飞、悬停、降落
既然环境通了,就来写一个真正能操控飞机的程序。这里用Python示例,C++版本的API设计几乎一样,只是代码结构更繁琐一些。
3.1 完整流程拆解
一个典型的基础飞行程序需要这几步:
- 连接飞控
- 订阅飞控状态,等待连接就绪
- 获取GPS定位信息和健康状态
- 解锁(arm)
- 起飞到指定高度
- 悬停几秒
- 降落(land)
下面这个例子就是完整的起飞-悬停-降落程序,可以直接套用:
import asyncio from mavsdk import System from mavsdk.action import ActionError async def run(): drone = System() await drone.connect(system_address="udp://:14540") print("等待飞控连接...") async for state in drone.core.connection_state(): if state.is_connected: print("飞控已连接") break print("等待定位信息...") async for health in drone.telemetry.health(): if health.is_global_position_ok and health.is_home_position_ok: print("定位正常,准备起飞") break print("解锁...") await drone.action.arm() print("起飞...") await drone.action.takeoff() await asyncio.sleep(10) print("降落...") await drone.action.land() asyncio.run(run())这段代码虽然只有二十多行,但已经把MAVSDK的异步事件模型体现出来了:用async for订阅telemetry,用await等待命令执行完成。理解这个异步模型是写复杂应用的关键。
3.2 为什么必须等待health就绪再起飞
我见过不少初学者把上面的程序简化为“连接后直接起飞”,结果飞控没有报错,但飞机纹丝不动。原因就在于GPS定位和Home点还没准备好,PX4不执行起飞命令。
PX4内部的逻辑是:起飞前必须确认全局位置和Home位置都有效。如果这个条件不满足,MAV_CMD_NAV_TAKEOFF会被飞控直接拒绝。MAVSDK的takeoff()虽然不会抛异常,但命令根本没有被PX4接受。所以:
is_global_position_ok:代表飞控已经拿到了有效的GPS定位is_home_position_ok:代表飞控已经设置了Home点(通常是解锁位置)
在室内测试仿真时,GPS是仿真的,通常几秒内就能满足条件。真机测试时,要等GPS定位星数够多,室内没有GPS信号就飞不了,这是新手踩坑重灾区。
3.3 真机测试时要注意的额外步骤
如果你把这段代码从SITL换到真机上,有几个参数必须提前确认:
- 在QGroundControl里确认安全开关(Safety Switch)状态。有些遥控器需要拨动安全开关才能解锁,否则
arm()会超时或被拒绝 - 确认电池电压正常,低电量保护会阻止起飞
- 确认飞行模式切换通道设置好。如果MAVSDK的
arm()失败,先切到手动模式排查原因 - 建议把
MAV_ARM_AUTH相关配置检查一遍,确认PX4没有启用额外的解锁鉴权
实操心法:我第一次真机测试时,忘了把遥控器的安全开关拨到位,
arm()一直失败,排查了半小时才发现是硬件保护在起作用。所以用MAVSDK测试前,先通过遥控器把飞机解锁一遍,确定硬件链路通畅,再切到MAVSDK控制,能省掉大量无效排错时间。
4. Offboard模式:真正自由写应用的入口
起飞降落只是热身,真正让开发者兴奋的是Offboard模式。这个模式下,你不再只是发单个命令,而是以指定频率持续地向飞控提供位置或速度设定值,飞控会尽力跟随。这几乎是所有自主飞行应用的基座。
4.1 Offboard模式的工作原理
PX4的Offboard模式接受来自外部计算机的设定值,来源通常是MAVLink消息SET_POSITION_TARGET_LOCAL_NED或SET_POSITION_TARGET_GLOBAL_INT。MAVSDK把这两类消息封装成了offboard.set_position_local()和offboard.set_velocity_ned()。
关键点在这里:Offboard模式要求你以持续频率(建议10Hz以上)发送设定值,如果飞控连续500毫秒没有收到新设定值,会自动退出Offboard模式并切换回之前的落定模式(通常是Land或Hold)。这个机制是为了防止通信链路中断后飞机变成“无头苍蝇”。
理解了这个机制,你就能明白为什么MAVSDK的Offboard API长这样——它需要你维护一个send_*循环,而不是只调用一次就结束。
4.2 用MAVSDK实现Offboard位置控制
下面是一段让飞机飞到指定相对位置的代码,这个方法我用来做过自主巡检的雏形:
import asyncio from mavsdk import System from mavsdk.offboard import (OffboardError, PositionNedYaw) async def run(): drone = System() await drone.connect(system_address="udp://:14540") async for state in drone.core.connection_state(): if state.is_connected: break async for health in drone.telemetry.health(): if health.is_global_position_ok and health.is_home_position_ok: break print("-- 起飞到10米高度") await drone.action.arm() await drone.action.takeoff() await asyncio.sleep(10) print("-- 开启Offboard模式") try: await drone.offboard.start() except OffboardError as error: print(f"启动Offboard失败: {error._result.result_str}") return print("-- 飞到当前位置东侧10米") # 相对当前机头方向? 这里用的是NED系, N=北, E=东 await drone.offboard.set_position_local(PositionNedYaw(0.0, 10.0, -10.0, 0.0)) await asyncio.sleep(10) print("-- 回归到Home点上方") await drone.offboard.set_position_local(PositionNedYaw(0.0, 0.0, -10.0, 0.0)) await asyncio.sleep(10) print("-- 退出Offboard并降落") try: await drone.offboard.stop() except OffboardError as error: print(f"停止Offboard失败: {error._result.result_str}") await drone.action.land() asyncio.run(run())这段代码里有两个细节值得展开。
第一个是坐标系的含义。PositionNedYaw里传入的四个值分别是北向位移、东向位移、向下高度、偏航角。NED坐标系下,“向上”是负数,所以-10.0代表10米高度。新手很容易把-10写成10,导致飞机直接往地里钻。我在SITL里试过写反,模拟器里能明显看到飞机先下降再拉升,如果发生在真机上,大概率已经炸鸡了。
第二个是offboard.start()和arm()的先后关系。PX4要求必须先解锁、再进入Offboard,顺序反了会报错。另外,在SITL里如果尝试从地面直接进入Offboard模式,飞控会拒绝,常见表现是start()超时。正确做法是从起飞后的稳定模式(比如Takeoff)切到Offboard,所以我上面的代码里先takeoff()再start()。
4.3 速度控制模式:更适合做动态跟踪
位置控制适合航点巡航,但如果你要做目标跟踪、避障绕行,速度控制会更自然。MAVSDK里对应的API是set_velocity_ned(),它接收北向速度、东向速度、向下速度、偏航速率。
一个简单的悬停速度保持循环如下:
import asyncio from mavsdk import System from mavsdk.offboard import VelocityNedYaw async def run(): drone = System() await drone.connect(system_address="udp://:14540") # ... 省略连接检查 ... await drone.offboard.set_velocity_ned(VelocityNedYaw(0.0, 0.0, 0.0, 0.0)) await drone.offboard.start() # 持续发送1m/s北向速度 for _ in range(50): await drone.offboard.set_velocity_ned(VelocityNedYaw(1.0, 0.0, -0.5, 0.0)) await asyncio.sleep(0.1) # 停止 await drone.offboard.set_velocity_ned(VelocityNedYaw(0.0, 0.0, -0.5, 0.0)) await asyncio.sleep(0.1) await drone.offboard.stop() asyncio.run(run())注意这里我故意没有用死循环,而是发送50次然后停下来。实际应用中,如果你需要连续控制,应该把发送循环放在一个while True里,并加上退出条件,否则通信异常时飞控会因收不到设定值而自动退出Offboard。
我做过一个简单的“键盘控制无人机”项目,就是这个思路,用键盘的WASD更新目标速度,然后以20Hz频率持续发送。整个过程非常直观,飞控对速度指令的响应也很快。
5. 调试MAVSDK程序的几个高频问题与排查方法
写代码总会遇到问题,但MAVSDK的报错信息不算友好,很多时候只是“timeout”或“connection error”,新手容易一头雾水。下面把我见过的高频问题整理成表,附上排查思路:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
connect()后一直卡在等待连接 | 端口不对,或PX4 SITL没启动 | 检查SITL终端是否有pxh>提示符;确认UDP端口是14540 |
arm()超时 | 飞控安全开关锁定,或GPS未就绪 | 检查QGC里是否有解锁报错;确认health检查通过 |
takeoff()没反应 | Home点未设置,或飞行模式不允许 | 确认is_home_position_ok为True;手动起飞一次验证 |
Offboardstart()失败 | 未先解锁,或已处于Offboard模式 | 先arm()再start();检查是否重复调用 |
| 飞行中突然退出Offboard | 设定值发送中断超过500ms | 检查发送频率,建议10-20Hz;检查网络延迟 |
| Python脚本卡死无输出 | 异步事件循环被阻塞 | 避免在async for循环里放入CPU密集计算 |
5.1 连接不上:优先查端口和IP
MAVSDK的connect(system_address="udp://:14540")表示在本机的14540端口监听UDP数据。PX4 SITL默认将MAVLink消息广播到14540端口,所以本机连接没问题。但如果你是在树莓派上运行MAVSDK,PX4在另一台机器上,就要改成:
await drone.connect(system_address="udp://192.168.1.100:14540")这个IP是运行PX4的电脑地址。如果连不上,先在本机测试,再排查防火墙。我遇到过一台Ubuntu机器上防火墙默认拦截UDP,直接导致连接失败,sudo ufw disable才解决(当然,这只在可信局域网里建议这样做)。
5.2 起飞后飞机偏航或漂移
这个问题大多不是MAVSDK代码的锅,而是PX4的位置估计还没收敛。SITL里可以等GPS定位稳定,但真机需要等待磁力计校准完成。另有一个容易被忽略的点:起飞前如果云台或其他负载的磁干扰比较大,PX4的航向估计会漂,导致视觉上飞机“转圈”。
排除方法很简单:在QGC中查看姿态和航向曲线,确认解锁后没有明显的漂移。如果漂移严重,重新校准磁力计,并检查桨叶下方是否有磁铁等干扰源。
5.3 Offboard模式下电机突然抽动一声然后停转
这通常是飞控进入了降落保护或紧急停止。可能的原因是:你发送的速度/位置设定值非常不合理(比如让飞机以极快的速度撞地),PX4的内置保护机制检测到异常,强制切出Offboard并降落。
我自己的排查顺序是:先检查发送的坐标/速度数值方向是否正确,然后看QGC的告警日志,最后再看代码里的循环频率是否稳定。大部分情况都是数值符号搞反,比如NED坐标系的“北向”写成了负值,导致飞机反向飞。
6. 从仿真到真机的几个关键经验
仿真跑通了,不代表真机就能飞。我自己在从SITL到真机切换时,至少踩了三次坑,这里展开说说。
6.1 通信链路:数传和串口的配置差异
仿真用的是UDP,真机一般用串口。MAVSDK连接串口的地址形如:
await drone.connect(system_address="serial:///dev/ttyUSB0:57600")注意波特率,Pixhawk默认的TELEM2口一般是57600,有些飞控是921600。搞错波特率的表现是:连接时不报错,但收不到任何数据或数据乱码。
我建议先用QGroundControl连一次数传,确认串口设备名和波特率,再在MAVSDK里配置。奉行的原则是:先让QGC连上,再让MAVSDK连上,不要两个终端同时抢一个串口。
6.2 安全冗余:别把命压在一根链路上
写自主飞行程序时,最容易忽略的是“如果MAVSDK程序崩溃了怎么办”。PX4有内置的Geofence和RTL(Return to Launch)机制,你的应用一定要在起飞前把RTL触发条件设置好。
MAVSDK里可以通过action.set_return_to_launch_altitude()设置返航高度,也可以通过mavsdk.telemetry监听遥控器信号。如果遥控器信号丢失,PX4会自动触发RTL。但如果你用的是纯数传遥控而没有遥控器接收机,就需要提前设置好Geofence和降落保护。
我的做法是:在MAVSDK的主循环里加一个看门狗,如果状态机连续几秒没有收到健康的遥测数据,就调用
action.land()或action.return_to_launch()。这样即使程序逻辑出错,也有兜底动作。
6.3 Offboard频繁失控:检查RC信号覆盖
PX4有个默认参数COM_RC_OVERRIDE,在遥控器收到RC输入时,会优先响应遥控器信号并退出Offboard。这个机制是好是坏取决于场景。如果飞行过程中有人碰了遥控器的摇杆,Offboard模式会突然失守,飞机跟随遥控器操作,可能会不听程序指挥。
如果你的应用是完全自主飞行,建议把遥控器切换到Hold模式或干脆关掉遥控器开关,或者检查COM_RC_IN_MODE参数,确认为Disabled时不会因RC输入切换模式。更稳妥的方案是在QGC中设置一个物理的紧急降落开关,而不是依赖程序里的逻辑判断。
7. 扩展:MAVSDK能做的远不止起飞降落
写到这里,基础部分已经覆盖了。如果你追求更进一步,MAVSDK还提供了几个我实际用过的能力,值得研究:
- 任务上传:
mission模块支持上传完整的航点任务到飞控,飞控自己执行,不需要外部持续发送设定值。这和Offboard是两种不同思路,前者适合预设航线巡检,后者适合动态决策 - 传感器遥测订阅:GPS、IMU、电池、姿态、速度这些数据都能通过
telemetry模块实时订阅,方便你在机载电脑上做状态监视或日志记录 - 摄像头触发:
camera模块可以触发拍照或录像,用于航测或者数据采集 - 自定义MAVLink消息:如果MAVSDK功能不满足需求,你可以直接使用
send_mavlink_message()发送原始MAVLink消息,相当于“跳过SDK”直接面对协议
我也见过团队用MAVSDK做多机编队控制,一架地面站电脑同时连接多架飞机的MAVSDK实例,通过分配不同端口实现同步控制。这种场景下,MAVSDK的异步模型反而比单线程的逻辑好写很多,因为每架飞机都是一个独立System对象,用asyncio.gather()并发控制就行。
MAVSDK+PX4的组合,短期内不一定会取代底层的MAVLink开发和PX4固件开发,但作为应用层的开发方式,它已经足够稳定和高效。尤其是现在ROS2和PX4的官方接口也在逐渐成熟,MAVSDK补齐了最轻量化的那一环——不需要装ROS,一个Python文件就能驱动飞机,这非常适合快速验证想法和做开发原型。
从我个人经验来说,在还没完全理解PX4内部状态机之前,MAVSDK是一个非常好的进修路径。通过它,你能直观地感受到“命令飞控”和“维护飞控”之间的界线在哪里。等有了这个手感,再回头去读PX4固件源码,理解深度会完全不同。这个顺序,比一开始就钻进制C++堆栈里,要高效得多。