2. MAVLink协议深度解析:v1 vs v2、帧结构、关键消息与ID分配
好,咱们进入正题。MAVLink这玩意儿,说白了就是无人机和地面站之间的“普通话”。你调参、看姿态、收GPS数据,底层全是它在跑。我最早接触MAVLink时也被一堆字节和ID搞晕过,后来啃了几个月源码才理清楚。今天我把这些坑和经验一次性倒给你。
2.1 MAVLink v1 vs v2:到底差在哪?
先看个最直观的对比。MAVLink v1是2010年左右设计的,那时候飞控资源紧张,能省则省。v2是2017年推出的,主要为了解决v1的“不够用”问题。
| 特性 | MAVLink v1 | MAVLink v2 |
|---|---|---|
| 帧头长度 | 6字节(不含CRC) | 10字节(不含CRC) |
| 系统ID/组件ID | 各1字节 | 各1字节(兼容) |
| 消息ID范围 | 0-255(1字节) | 0-16777215(3字节) |
| 载荷长度 | 0-255字节 | 0-255字节 |
| 签名支持 | 无 | 可选(8字节签名) |
| 兼容性标志 | 无 | 有(incompatibility/compatibility flags) |
我个人习惯是:新项目直接用v2。为什么?因为v2的消息ID空间大了太多。我在一个项目中需要自定义几十条消息,v1的255个ID根本不够分,v2的1677万个ID随便用。而且v2的签名功能对安全通信很有用——虽然ArduPilot默认没开,但如果你做商业产品,建议加上。
2.2 消息帧结构:从起始位到校验
咱们拆一个v2的包看看。每个MAVLink消息就是一串字节流,结构如下:
MAVLink v2 帧结构(共12字节头 + 载荷 + 校验):
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+------------------+--------+
| 0xFD | 长度 | 不兼容 | 兼容 | 序列号 | 系统ID | 组件ID | 消息ID(低) | 消息ID(中) | 消息ID(高) | 载荷(0-255字节) | CRC(2字节) |
| (1字节)| (1字节)| 标志 | 标志 | (1字节)| (1字节)| (1字节)| (1字节) | (1字节) | (1字节) | | |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+------------------+--------+
关键字段说明:
- 起始位(0xFD):v2的魔数。v1是0xFE,v2改成0xFD。解析器看到0xFD就知道这是v2包。
- 长度:载荷的实际字节数。注意,这个长度不包括CRC和头。
- 不兼容标志:如果接收方不支持这个标志位,必须丢弃该包。比如签名位(bit 0)就是典型的不兼容标志。
- 兼容标志:接收方如果不支持,可以忽略该标志继续解析。
- 序列号:每发一条消息递增1。用于丢包检测和去重。
- 系统ID / 组件ID:标识发送者。比如飞控系统ID=1,组件ID=1(飞控主控);GPS组件ID=220。
- 消息ID(3字节):这是v2最大的改进。v1只有1字节,v2扩展到3字节。
- CRC(2字节):对整个包(头+载荷)的校验。注意,CRC计算时会把“长度”字段也参与进去,防止长度被篡改。
2.3 关键消息类型:HEARTBEAT、GPS_RAW_INT、ATTITUDE
ArduPilot里跑的消息有上百种,但调参时你只需要盯住几个核心的。我列三个最常用的:
2.3.1 HEARTBEAT(消息ID=0)
这是最基础的消息。飞控每1秒发一次,告诉地面站“我还活着”。它的载荷只有9字节:
typedef struct __mavlink_heartbeat_t {
uint8_t type; // 飞行器类型(1=固定翼,2=多旋翼,3=直升机...)
uint8_t autopilot; // 飞控类型(3=ArduPilot,4=PX4...)
uint8_t base_mode; // 基础模式(bit0=已加锁,bit1=手动,bit2=自动...)
uint32_t custom_mode; // 自定义模式(ArduPilot里对应子模式,比如Loiter、RTL)
uint8_t system_status; // 系统状态(0=未初始化,3=已加锁,4=已解锁...)
} mavlink_heartbeat_t;
调参时,我经常用HEARTBEAT的custom_mode字段来判断当前飞行模式。比如在QGC里看到模式切换不正常,先抓HEARTBEAT包,看custom_mode值对不对。
2.3.2 GPS_RAW_INT(消息ID=24)
GPS数据,每秒10Hz。载荷结构:
typedef struct __mavlink_gps_raw_int_t {
uint64_t time_usec; // 时间戳(微秒)
int32_t lat; // 纬度(度 * 1e7)
int32_t lon; // 经度(度 * 1e7)
int32_t alt; // 海拔高度(毫米)
uint16_t eph; // 水平定位精度(厘米)
uint16_t epv; // 垂直定位精度(厘米)
uint16_t vel; // 地速(厘米/秒)
uint16_t cog; // 航向角(度 * 100)
uint8_t fix_type; // 定位类型(0=无,1=单点,2=差分,3=RTK固定...)
uint8_t satellites_visible; // 可见卫星数
} mavlink_gps_raw_int_t;
注意lat和lon的单位是度乘以1e7。比如北纬39.9度,存的是399000000。我见过有人直接当整数用,结果差了7个数量级。
2.3.3 ATTITUDE(消息ID=30)
姿态数据,通常50Hz。载荷:
typedef struct __mavlink_attitude_t {
uint32_t time_boot_ms; // 启动后时间(毫秒)
float roll; // 横滚角(弧度)
float pitch; // 俯仰角(弧度)
float yaw; // 偏航角(弧度)
float rollspeed; // 横滚角速度(弧度/秒)
float pitchspeed; // 俯仰角速度(弧度/秒)
float yawspeed; // 偏航角速度(弧度/秒)
} mavlink_attitude_t;
调PID时,我习惯把ATTITUDE的roll、pitch和期望值对比,看跟踪误差。如果误差大,说明P增益不够或者I增益太小。
2.4 消息ID分配规则
MAVLink的消息ID不是随便写的。官方有明确的分配规则:
- 0-255:v1时代的“经典”消息,比如HEARTBEAT(0)、SYS_STATUS(1)、GPS_RAW_INT(24)、ATTITUDE(30)。这些ID在v2里也保留,保证向后兼容。
- 256-65535:v2扩展的“标准”消息,比如MISSION_ITEM_INT(73)、COMMAND_INT(75)。这些消息在ArduPilot里也常用。
- 65536-16777215:留给厂商自定义。比如ArduPilot的
ARDU_PILOT_SPECIFIC系列消息就在这个范围。
我建议:如果你要自定义消息,优先用厂商区间(65536以上)。别去抢0-255的ID,那是历史遗产,改了会破坏兼容性。
2.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作MAVLink的“地图”:
嗯,这张图把版本、帧结构、关键消息和ID分配串在了一起。你调参时遇到任何通信问题,都可以沿着这个链路排查:先看版本对不对,再抓包看帧结构,然后检查消息ID和载荷解析。
2.6 实战建议
最后给几个实操建议:
- 抓包工具:我常用MAVLink Inspector(QGC自带)或者mavlink-router的日志。别用串口助手看二进制,会疯的。
- 调试技巧:如果地面站收不到数据,先看HEARTBEAT。HEARTBEAT都不来,说明物理层或波特率有问题。
- 自定义消息:用v2的厂商ID区间(65536+),并在代码里注册新的消息ID和解析函数。ArduPilot的
GCS_MAVLink类里就有例子。