3. 消息格式详解:消息帧结构

好,咱们进入第三讲。这一章我打算把 MAVLink 的消息帧结构彻底拆开,一个字节一个字节地讲清楚。

你可能会问:不就是个通信协议吗,有必要抠这么细?

嗯,我当年也这么想。直到有一次在野外调试无人机,地面站死活收不到心跳包,我拿着逻辑分析仪一帧一帧地抓,最后发现是校验位算错了。从那以后,我再也不敢小看这个帧结构了。

3.1 整体帧结构概览

MAVLink 的消息帧,说白了就是一组固定格式的二进制数据包。每个包都遵循同样的骨架:起始标志、长度、序列号、系统 ID、组件 ID、消息 ID、负载、校验。

我习惯把这个结构画成一张图,你一看就明白:

MAVLink v1 消息帧结构 起始标志 0xFE 长度 1 字节 序列号 1 字节 系统 ID 1 字节 组件 ID 1 字节 消息 ID 1 字节 负载 0-255 字节 校验 2 字节 关键说明: • 起始标志固定为 0xFE(MAVLink v1)或 0xFD(MAVLink v2) • 长度字段表示负载部分的字节数,不包含帧头和校验 • 序列号用于检测丢包,每发一条消息自动加 1 • 系统 ID + 组件 ID 唯一标识一个发送者 • 校验采用 CRC-16-CCITT,覆盖整个消息(不含起始标志)

3.2 起始标志:消息的身份证

起始标志是消息的第一个字节。MAVLink v1 固定为 0xFE,v2 固定为 0xFD。

为什么要有这个?说白了,就是让接收方知道「一条新消息开始了」。串口通信是流式的,没有帧边界,接收方必须靠这个标志来同步。

我的经验: 如果你在调试时发现地面站频繁丢包或解析错误,先检查起始标志对不对。我曾经遇到一个坑:飞控的串口波特率设错了,导致接收方把 0xFE 读成了 0x7F,结果整条消息都废了。

3.3 长度字段:负载有多大?

长度字段占 1 个字节,取值范围 0-255。它告诉接收方:负载部分有多少个字节。

注意,这个长度 不包含帧头、校验和,只算负载。比如一个心跳包(HEARTBEAT)的负载是 9 个字节,那长度字段就是 0x09。

我建议你在写解析代码时,先读长度,再根据长度分配缓冲区。别一次性读 255 个字节,浪费内存。

3.4 序列号:谁丢了包?

序列号也是 1 个字节,范围 0-255。每发一条消息,序列号自动加 1。到 255 后归零,循环使用。

接收方通过检查序列号是否连续,就能判断有没有丢包。比如收到了序列号 5、6、8,那 7 号包肯定丢了。

注意: 序列号是每个系统独立维护的。也就是说,飞控的序列号和地面站的序列号是两套,别搞混了。

3.5 系统 ID 与组件 ID:谁在说话?

这两个字段合起来,唯一标识一个发送者。

  • 系统 ID:标识一个完整的系统,比如飞控、地面站、相机。通常飞控的系统 ID 是 1,地面站是 255。
  • 组件 ID:标识系统内的一个组件。比如飞控的自动驾驶仪是 1,GPS 是 5,摄像头是 100。

举个例子:系统 ID=1、组件 ID=1 表示「飞控的自动驾驶仪」;系统 ID=255、组件 ID=0 表示「地面站的主控」。

我在项目中遇到过一个问题:两个飞控共用一个地面站,结果地面站分不清消息是谁发的。后来我强制给每个飞控分配不同的系统 ID,问题就解决了。

3.6 消息 ID:这条消息是什么?

消息 ID 占 1 个字节(v1)或 3 个字节(v2)。它告诉接收方:这条消息的类型是什么。

比如:

消息名称 消息 ID 说明
HEARTBEAT 0 心跳包,最基础的消息
SYS_STATUS 1 系统状态
GPS_RAW_INT 24 GPS 原始数据
ATTITUDE 30 姿态数据
COMMAND_LONG 76 长命令

接收方拿到消息 ID 后,就知道该用哪个结构体来解析负载了。比如消息 ID=0,就用 HEARTBEAT 的结构体。

3.7 负载:真正的数据

负载就是消息的实际内容。长度由前面的长度字段决定,最大 255 字节。

不同的消息 ID 对应不同的负载格式。比如心跳包的负载是 9 个字节,包含:

  • 类型(1 字节)
  • 自动驾驶仪类型(1 字节)
  • 基础模式(1 字节)
  • 自定义模式(4 字节)
  • 系统状态(1 字节)
  • MAVLink 版本(1 字节)

你想想看,如果没有消息 ID,接收方拿到这 9 个字节根本不知道该怎么解释。所以消息 ID 和负载是配套的。

3.8 校验:数据有没有被篡改?

校验字段占 2 个字节,采用 CRC-16-CCITT 算法。它覆盖整个消息(从长度字段开始,到负载结束),但不包含起始标志。

为什么不算起始标志?因为起始标志是固定的,校验它没有意义。

接收方收到消息后,会重新计算一遍 CRC,然后跟收到的校验值对比。如果一致,说明消息在传输过程中没有被篡改;如果不一致,直接丢弃。

核心要点: 校验是 MAVLink 协议可靠性的最后一道防线。我建议你在实现时,一定要校验通过后才处理消息,否则可能被错误数据坑惨。

3.9 完整消息示例

咱们来看一个实际的心跳包(HEARTBEAT)例子:

起始标志: 0xFE
长度:     0x09  (负载 9 字节)
序列号:   0x01  (第 1 条消息)
系统 ID:  0x01  (飞控)
组件 ID:  0x01  (自动驾驶仪)
消息 ID:  0x00  (HEARTBEAT)
负载:     0x01 0x01 0x01 0x00 0x00 0x00 0x00 0x04 0x03
校验:     0x5C 0x3E  (CRC-16-CCITT 计算结果)

这条消息总共 14 个字节(1 + 1 + 1 + 1 + 1 + 1 + 9 + 2)。接收方收到后,先检查起始标志 0xFE,然后读长度 0x09,接着读序列号、系统 ID、组件 ID、消息 ID,再读 9 个字节的负载,最后校验。全部通过,才把数据交给上层应用。

3.10 避坑指南

最后,我总结几个我在项目中踩过的坑:

  • 起始标志误判: 如果负载里恰好出现了 0xFE,接收方可能会误认为是一条新消息的开始。解决办法是:收到起始标志后,必须等待完整的一帧并校验通过,才确认这是一条有效消息。
  • 序列号溢出: 序列号只有 1 个字节,到 255 就归零。如果你的系统消息量很大,短时间内可能重复使用同一个序列号。这时候丢包检测会失效。我建议在应用层再加一个时间戳或消息计数器。
  • 校验计算错误: CRC-16-CCITT 的初始值、多项式、异或值都有标准。别自己瞎改,否则两边算出来的校验值永远对不上。
  • 负载长度不匹配: 长度字段和实际负载长度不一致,会导致解析错位。我习惯在发送前先检查一下负载长度,确保跟长度字段一致。

嗯,这一章的内容就到这儿。帧结构是 MAVLink 协议的基石,你把它吃透了,后面的内容就顺了。