3. 消息格式详解:消息帧结构
好,咱们进入第三讲。这一章我打算把 MAVLink 的消息帧结构彻底拆开,一个字节一个字节地讲清楚。
你可能会问:不就是个通信协议吗,有必要抠这么细?
嗯,我当年也这么想。直到有一次在野外调试无人机,地面站死活收不到心跳包,我拿着逻辑分析仪一帧一帧地抓,最后发现是校验位算错了。从那以后,我再也不敢小看这个帧结构了。
3.1 整体帧结构概览
MAVLink 的消息帧,说白了就是一组固定格式的二进制数据包。每个包都遵循同样的骨架:起始标志、长度、序列号、系统 ID、组件 ID、消息 ID、负载、校验。
我习惯把这个结构画成一张图,你一看就明白:
3.2 起始标志:消息的身份证
起始标志是消息的第一个字节。MAVLink v1 固定为 0xFE,v2 固定为 0xFD。
为什么要有这个?说白了,就是让接收方知道「一条新消息开始了」。串口通信是流式的,没有帧边界,接收方必须靠这个标志来同步。
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,然后跟收到的校验值对比。如果一致,说明消息在传输过程中没有被篡改;如果不一致,直接丢弃。
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 协议的基石,你把它吃透了,后面的内容就顺了。