2. MAVLink协议基础

好,咱们进入正题。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默认没开,但如果你做商业产品,建议加上。

我的经验:v2兼容v1的载荷格式。也就是说,你写v2的解析器,可以直接解析v1的包。反过来不行。所以地面站尽量用v2库,一劳永逸。

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计算时会把“长度”字段也参与进去,防止长度被篡改。
我曾经踩过的坑:CRC计算时,v2的CRC算法和v1不一样。v2的CRC会额外包含一个“CRC extra”字节,这个字节由消息ID决定。如果你自己写解析器,一定要查MAVLink的CRC extra表,否则校验永远通不过。

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,那是历史遗产,改了会破坏兼容性。

核心要点:消息ID决定了消息的“身份”。解析器根据ID决定用哪个结构体来解码。ID错了,数据就全乱了。

2.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以把它当作MAVLink的“地图”:

MAVLink协议知识体系 MAVLink协议核心知识体系 MAVLink v1 MAVLink v2 兼容升级 消息帧结构 起始位(0xFD) | 长度 | 标志 | 序列号 | 系统ID | 组件ID | 消息ID(3字节) | 载荷(0-255字节) | CRC(2字节) v1: 起始位0xFE, 消息ID 1字节, 无标志位 HEARTBEAT ID=0, 1Hz, 心跳/模式 GPS_RAW_INT ID=24, 10Hz, 位置/速度 ATTITUDE ID=30, 50Hz, 姿态/角速度 消息ID分配规则 0-255: 经典消息 | 256-65535: 标准扩展 | 65536-16777215: 厂商自定义 图:MAVLink协议知识体系 — 从版本选择到消息解析的完整链路

嗯,这张图把版本、帧结构、关键消息和ID分配串在了一起。你调参时遇到任何通信问题,都可以沿着这个链路排查:先看版本对不对,再抓包看帧结构,然后检查消息ID和载荷解析。

2.6 实战建议

最后给几个实操建议:

  • 抓包工具:我常用MAVLink Inspector(QGC自带)或者mavlink-router的日志。别用串口助手看二进制,会疯的。
  • 调试技巧:如果地面站收不到数据,先看HEARTBEAT。HEARTBEAT都不来,说明物理层或波特率有问题。
  • 自定义消息:用v2的厂商ID区间(65536+),并在代码里注册新的消息ID和解析函数。ArduPilot的GCS_MAVLink类里就有例子。
我的习惯:每次调参前,先开MAVLink日志记录。这样出了问题可以回放分析,比现场抓包省心多了。