消息帧结构详解:起始标志、负载长度、序列号、系统ID、组件ID、消息ID、负载、校验和

各位同学,咱们今天来聊聊 MAVLink 消息帧的骨头架子。说白了,就是数据在线上跑的时候,到底长什么样。

我记得刚接触 MAVLink 那会儿,看着一堆十六进制数直发懵。后来拆开一条一条看,才发现这结构设计得挺讲究——每个字节都有它的使命。

一、帧结构总览

MAVLink 有两种版本:v1 和 v2。咱们主要讲 v2,因为现在主流飞控基本都用它了。v2 的帧结构是这样的:

字节偏移 字段名 长度(字节) 说明
0 起始标志 1 固定 0xFD(v2)
1 负载长度 1 有效负载的字节数
2 序列号 1 递增计数,防丢包
3 系统 ID 1 标识发送设备
4 组件 ID 1 标识设备内的模块
5 消息 ID 3 v2 扩展到 24 位
8 ~ 8+N-1 负载 N 实际数据内容
8+N ~ 8+N+1 校验和 2 CRC-16

关键点:v2 的消息 ID 从 v1 的 1 字节扩展到了 3 字节,这意味着可以定义 1600 万种消息类型。实际用不了那么多,但给未来留足了空间。

二、起始标志——帧的身份证

起始标志就一个字节,v1 是 0xFE,v2 是 0xFD。为什么选这两个值?

我个人的理解是:它们跟常见的 ASCII 字符不冲突,不容易被误判。你想想看,串口上如果传的是纯文本,0xFD 这种值几乎不会出现,所以接收端看到它就知道“嘿,新消息来了”。

小技巧:调试的时候,如果发现解析器一直找不到帧头,先检查波特率对不对。我曾经在 115200 的链路上误配成 57600,结果收到的全是乱码,起始标志根本对不上。

三、负载长度——告诉你要读多少

这个字段很直白:它告诉你后面跟着的有效负载有多少字节。范围是 0 到 255。

但有个坑:v2 里如果负载长度是 0,表示这是一个“空消息”,只有头部没有数据。有些心跳包就用这个来节省带宽。

实际项目中,我见过有人把负载长度写死了,结果消息体变了长度就出问题。嗯,这里要注意:负载长度必须跟消息定义严格对应,不能偷懒。

四、序列号——防丢包的第一道防线

序列号是一个递增的计数器,每发一条消息就加 1。从 0 开始,到 255 后回绕到 0。

为什么需要它?

无线链路不稳定,丢包是家常便饭。接收端通过检查序列号是否连续,就能判断中间有没有丢消息。如果发现跳号了,可以请求重传,或者至少知道数据不完整。

注意:序列号是每个系统独立维护的,不是全局统一的。也就是说,飞控的序列号和地面站的序列号各算各的,别搞混了。

五、系统 ID 与组件 ID——谁发的?哪个模块发的?

这两个字段一起用,就能唯一确定消息的来源。

  • 系统 ID:标识一个完整的设备,比如飞控是 1,地面站是 255。
  • 组件 ID:标识设备内部的子模块,比如飞控里的 GPS 模块是 220,IMU 是 81。

举个例子:系统 ID=1,组件 ID=220,表示这是“飞控上的 GPS 模块”发来的消息。

我记得有一次调试多机协同,三台飞控同时发数据,地面站全乱了。后来发现是系统 ID 没配好,三台都用的默认值 1。改完之后,数据就分得清清楚楚了。

六、消息 ID——这条消息是干啥的?

消息 ID 是 MAVLink 里最重要的字段之一。它告诉接收端:这条消息是什么类型。

v1 只有 1 字节,最多 256 种消息。v2 扩展到 3 字节,理论上支持 16,777,216 种。常用的消息 ID 有:

消息名 ID(十进制) 说明
HEARTBEAT 0 心跳包,最基本的保活消息
SYS_STATUS 1 系统状态,电池、CPU 负载等
GPS_RAW_INT 24 GPS 原始数据(整数格式)
ATTITUDE 30 姿态数据(滚转、俯仰、偏航)
LOCAL_POSITION_NED 32 局部位置(北东地坐标系)

避坑指南:我曾经在自定义消息时,随手选了一个 ID,结果跟官方消息冲突了。接收端解析出来的数据完全不对。后来我学乖了:自定义消息一定要用 MAVLink 协议里预留的 ID 范围(比如 256~511),别跟官方抢。

七、负载——真正的数据

负载就是消息要传递的实际内容。长度由前面的“负载长度”字段指定,最大 255 字节。

负载的格式由消息 ID 决定。比如 HEARTBEAT 消息的负载只有几个字节,包含飞控类型、状态、模式等。而 GPS_RAW_INT 的负载就长一些,包含经纬度、海拔、速度等。

解析负载的时候,必须严格按照消息定义来。字节顺序、数据类型、对齐方式,一个都不能错。

个人经验:写解析代码的时候,别用手动移位拼接。用结构体或者 protobuf 那样的序列化工具,既快又不容易出错。我早期用手动解析,一个符号写反了,定位花了两天。

八、校验和——数据完整性的守护神

校验和是帧的最后两个字节,用的是 CRC-16 算法。它覆盖的范围包括:

  • 负载长度
  • 序列号
  • 系统 ID
  • 组件 ID
  • 消息 ID
  • 负载

起始标志不参与校验。为什么?因为接收端先靠起始标志找到帧头,然后才开始校验。如果起始标志也参与校验,那就成了“先有鸡还是先有蛋”的问题了。

校验算法是 MAVLink 协议里定义好的,不是标准的 CRC-16-CCITT,而是带了一个额外的密钥字节。这个密钥跟消息 ID 有关,目的是防止恶意篡改。

重要提醒:校验和算不对,接收端会直接丢弃整条消息。如果你发现地面站收不到数据,先检查 CRC 计算对不对。我见过最离谱的 bug 是有人把 CRC 的初始值设成了 0xFFFF,而协议要求是 0x0000。

九、整体结构图

下面这张图把整个帧结构串起来了,方便你理解数据在缓冲区里是怎么排列的:

MAVLink v2 帧结构示意图 MAVLink v2 帧结构 帧头(8 字节) 起始标志 0xFD 负载长度 N 序列号 0~255 系统 ID 1~255 组件 ID 0~255 消息 ID 24 位 负载(N 字节) 实际数据内容,长度由负载长度字段指定 校验和(2 字节) CRC-16,覆盖负载长度到负载结束 说明 • 起始标志不参与校验 • 负载最大 255 字节 • 消息 ID 在 v2 中为 3 字节 • 序列号用于检测丢包 • 系统 ID + 组件 ID 唯一标识源 • 校验和算法带密钥字节 • 接收端校验失败则丢弃整帧

十、实际抓包示例

咱们来看一条真实的心跳包数据(十六进制):

FD 09 01 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00