消息结构深度解析:起始标志、负载长度、序列号、系统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 完整消息帧结构图

下面这张图把整个消息帧的字节布局画出来了,你可以对照着看。

MAVLink v1 消息帧结构 STX 0xFE LEN 0~255 SEQ 0~255 SYS 1~255 COMP 0~255 MSGID 0~65535 头部(8字节) PAYLOAD(负载,长度由LEN决定,0~255字节) 包含具体数据字段,紧密排列,无对齐填充 负载(0~255字节) CKA CRC低字节 CKB CRC高字节 校验码(2字节) 计算CRC的范围 LEN + SEQ + SYS + COMP + MSGID + PAYLOAD 不包含STX 总长度范围 最小:8+0+2 = 10字节 最大:8+255+2 = 265字节

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保安全。