消息结构深度解析:起始标志、负载长度、序列号、系统ID、组件ID、消息ID、负载、校验码
上一章我们聊了MAVLink是什么,能干什么。今天咱们把袖子撸起来,直接拆解一条MAVLink消息的每一个字节。
你想想看,无人机在天上飞,地面站发一条指令下去,这条消息到底长什么样?我当年第一次用串口抓包看MAVLink数据时,满屏的十六进制数看得我头皮发麻。后来把结构拆开一看,其实就那么回事。
核心认知:MAVLink v1 消息帧 = 8字节头部 + 0~255字节负载 + 2字节校验码。总共不超过263字节。
2.1 消息帧全景图
一条完整的MAVLink消息,说白了就是下面这个结构。我习惯把它分成三段:头部、负载、尾巴。
字节偏移 字段名 说明
0 STX 起始标志,v1固定为0xFE
1 LEN 负载长度(Payload长度)
2 SEQ 序列号(0~255循环)
3 SYS 系统ID
4 COMP 组件ID
5-6 MSGID 消息ID(低字节在前)
7~N PAYLOAD 负载数据(长度由LEN决定)
N+1~N+2 CKA/CKB 校验码(CRC16低字节+高字节)
嗯,这里要注意:MAVLink v2的头部更长,但咱们课程主要基于v1展开,v2的差异后面会单独讲。
2.2 起始标志(STX)—— 帧同步的哨兵
起始标志就一个字节,v1固定是0xFE。它的作用很简单:告诉接收端「一条新消息来了」。
我在项目中遇到过一个问题:无人机飞远了,无线链路偶尔会丢字节。如果没有STX,接收端根本不知道从哪里开始解析。有了这个固定值,接收端就能不断扫描字节流,直到找到0xFE才尝试解析。
避坑指南:我曾经在调试时发现地面站频繁丢包,抓了半天发现是串口缓冲区里残留了上一个设备的0xFE数据。解决方案很简单:每次上电后先发一条空消息清空缓冲区,或者让接收端连续收到3个无效帧后才重新同步。
2.3 负载长度(LEN)—— 告诉你要读多少字节
LEN字段占1个字节,取值范围0~255。它表示后面跟着的PAYLOAD有多少个字节。
你可能会问:为什么需要这个字段?直接固定长度不行吗?
不行。因为不同的消息类型,负载长度不一样。比如HEARTBEAT只有9字节,而GPS_RAW_INT有30字节。LEN字段让接收端知道:读完这么多字节后,就该取校验码了。
| 消息类型 | 消息ID | 负载长度(字节) |
|---|---|---|
| HEARTBEAT | 0 | 9 |
| SYS_STATUS | 1 | 31 |
| GPS_RAW_INT | 24 | 30 |
| ATTITUDE | 30 | 28 |
2.4 序列号(SEQ)—— 防丢包、防乱序
SEQ占1个字节,从0开始计数,每发一条消息就加1,到255后回绕到0。
它的核心作用有两个:
- 检测丢包:如果接收端收到的SEQ从5直接跳到7,说明第6条丢了。
- 防止重复:如果收到两条SEQ相同的消息,说明是重传,直接丢弃。
我记得有一次在测试中,飞控突然失控。抓包发现地面站连续发了3条SEQ=12的同一指令,飞控以为是3次独立指令,执行了3次。从那以后,我要求所有地面站在重传时必须使用新的SEQ。
注意:SEQ是每个系统(SYS)独立维护的,不是全局的。也就是说,飞控的SEQ和地面站的SEQ是两套计数器,互不影响。
2.5 系统ID(SYS)与组件ID(COMP)—— 谁在说话?
这两个字段合起来,唯一标识一个通信节点。
- SYS(系统ID):1个字节,标识一个独立的系统。比如飞控是1,地面站是255。
- COMP(组件ID):1个字节,标识系统内的一个组件。比如飞控的自动驾驶仪是1,摄像头是100。
你想想看,如果一架无人机上有飞控、摄像头、机械臂三个组件,它们都通过同一个串口发数据。没有COMP字段,地面站根本分不清这条消息是飞控的姿态数据,还是摄像头的画面信息。
| 组件 | 典型COMP值 |
|---|---|
| 自动驾驶仪 | 1 |
| 摄像头 | 100 |
| 机械臂 | 200 |
| 地面站 | 0 |
2.6 消息ID(MSGID)—— 这条消息是什么?
MSGID占2个字节(v1),低字节在前(小端序)。它告诉接收端:这条消息是心跳包、姿态数据、还是GPS坐标?
接收端根据MSGID,就知道该用哪个结构体来解析后面的PAYLOAD。比如MSGID=0,就用mavlink_heartbeat_t来解析;MSGID=24,就用mavlink_gps_raw_int_t。
关键点:MSGID决定了PAYLOAD的解析方式。如果MSGID和PAYLOAD不匹配,解析出来的数据就是一堆垃圾。我在项目中见过有人把GPS数据当成姿态数据解析,结果飞控直接翻了个跟头。
2.7 负载(PAYLOAD)—— 真正的数据
PAYLOAD就是消息要携带的实际数据,长度由LEN字段决定。它按照MAVLink定义的XML消息格式,把各个字段紧密排列在一起。
举个例子,HEARTBEAT消息的PAYLOAD只有9个字节:
字节0: type(系统类型,如四旋翼)
字节1: autopilot(飞控类型,如PX4)
字节2~3: base_mode(位掩码,表示是否已解锁、是否在引导等)
字节4~7: custom_mode(自定义模式,如任务模式、悬停模式)
字节8: system_status(系统状态,如已就绪、正在飞行)
嗯,这里要注意:MAVLink的负载是紧密排列的,没有对齐填充。所以不同平台(ARM、x86)解析时要注意字节对齐问题。我踩过这个坑,后面会专门讲。
2.8 校验码(CKA/CKB)—— 数据完整性的最后防线
校验码占2个字节,使用CRC16算法计算。计算范围包括:LEN、SEQ、SYS、COMP、MSGID、PAYLOAD。注意,不包括STX。
接收端收到消息后,会用同样的算法重新计算一遍CRC,然后跟收到的CKA/CKB对比。如果不一样,说明数据在传输过程中被篡改或损坏了,直接丢弃。
个人经验:我曾经在项目中遇到一个诡异的问题:地面站偶尔会收到校验错误的消息。排查了三天,最后发现是无线模块的FIFO缓冲区溢出,导致数据被截断。从那以后,我养成了一个习惯:在接收端不仅要校验CRC,还要检查LEN是否在合理范围内(比如不超过255)。
2.9 完整消息帧结构图
下面这张图把整个消息帧的字节布局画出来了,你可以对照着看。
2.10 实战:手写一个消息解析器
光说不练假把式。下面我用C语言写一个最简单的MAVLink v1消息解析函数,你感受一下。
#include <stdint.h>
#include <stdbool.h>
#define MAVLINK_STX 0xFE
typedef struct {
uint8_t stx;
uint8_t len;
uint8_t seq;
uint8_t sys;
uint8_t comp;
uint16_t msgid;
uint8_t payload[255];
uint16_t crc;
} mavlink_message_t;
bool parse_mavlink_frame(uint8_t *buffer, int length, mavlink_message_t *msg) {
// 检查最小长度
if (length < 10) return false;
// 检查起始标志
if (buffer[0] != MAVLINK_STX) return false;
// 提取头部字段
msg->stx = buffer[0];
msg->len = buffer[1];
msg->seq = buffer[2];
msg->sys = buffer[3];
msg->comp = buffer[4];
msg->msgid = buffer[5] | (buffer[6] << 8); // 小端序
// 检查负载长度是否合理
if (msg->len > 255) return false;
if (length < (8 + msg->len + 2)) return false;
// 复制负载
for (int i = 0; i < msg->len; i++) {
msg->payload[i] = buffer[7 + i];
}
// 提取校验码
msg->crc = buffer[7 + msg->len] | (buffer[8 + msg->len] << 8);
// 这里应该调用CRC16校验函数,省略具体实现
// uint16_t calc_crc = crc16(buffer+1, 6 + msg->len);
// if (calc_crc != msg->crc) return false;
return true;
}
这段代码虽然简单,但包含了核心逻辑:先检查STX,再提取头部,然后根据LEN复制负载,最后取CRC。实际项目中,你还需要把CRC校验加上,并且要考虑多帧缓冲和帧同步的问题。
重要提醒:上面的代码为了演示清晰,省略了CRC校验。实际产品中,必须做CRC校验,否则数据损坏可能导致灾难性后果。我见过有人为了省几个CPU周期跳过CRC,结果飞控收到了被干扰的指令,直接切到了手动模式——还好当时无人机在地上。
2.11 本章小结
一条MAVLink消息,说白了就是8字节头部 + 可变长度负载 + 2字节校验。每个字段都有明确的职责:STX负责同步,LEN告诉长度,SEQ防丢包,SYS/COMP标识来源,MSGID告诉类型,PAYLOAD装数据,CRC保安全。