3、消息帧结构深度解析:起始标志、载荷长度、序列号、系统ID、组件ID、消息ID、载荷、校验
各位同学,今天我们来啃一块硬骨头——MAVLink的消息帧结构。说实话,我刚接触MAVLink那会儿,看着这一串字节流,脑子里全是问号。但后来我发现,只要你把每个字段的「脾气」摸透了,这东西其实挺可爱的。
先给你看一个完整的帧结构长什么样。嗯,我习惯把它想象成一封挂号信:信封上有邮戳、收件人地址,里面才是真正的内容。
| 起始标志 | 载荷长度 | 序列号 | 系统ID | 组件ID | 消息ID | 载荷 | 校验 |
| 0xFE | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | N字节 | 2字节 |
一共8个字段,加起来最少8个字节(载荷为0时),最多263个字节。别被这个数字吓到,我们一个一个拆开看。
3.1 起始标志:0xFE 的执念
起始标志固定为 0xFE,也就是十进制的254。为什么是254?不是255也不是253?
我个人猜测,设计者选这个值是为了避免和常见的串口数据冲突。你想想看,串口通信里0x00和0xFF经常被用作特殊控制字符,0xFE刚好是个「中间值」,误触发的概率低。
我在项目中遇到过一个坑:有个同事为了省事,把起始标志写成了0xFF。结果无人机在天上飞着飞着,地面站突然收到一堆乱码。排查了半天,发现是串口缓冲区里残留的0xFF被当成了帧头。从那以后,我团队里谁改起始标志我跟谁急。
3.2 载荷长度:别小看这1个字节
载荷长度字段占1个字节,取值范围0~255。它表示后面跟着的「载荷」有多少个字节。
这里有个细节:载荷长度不包括校验字段。也就是说,如果你发送一个空消息(载荷长度为0),帧结构里依然会有2个字节的校验。
我刚开始做的时候,总以为载荷长度是整个帧的长度。结果算校验和的时候,怎么都对不上。后来翻源码才发现,这个长度只算载荷部分。说白了,就是告诉接收端:「嘿,后面N个字节是真正的数据,再往后2个字节是校验,别搞混了。」
| 载荷长度值 | 实际载荷字节数 | 整帧长度(不含起始标志) |
|---|---|---|
| 0 | 0字节 | 6字节 |
| 255 | 255字节 | 261字节 |
| 典型值(如HEARTBEAT) | 9字节 | 15字节 |
3.3 序列号:防丢包的第一道防线
序列号占1个字节,从0开始,每发送一条消息就加1。加到255后归零,循环使用。
你可能会问:1个字节够用吗?255个序号循环,万一丢包了怎么办?
嗯,这里要说明一下:MAVLink的序列号不是用来做可靠传输的。它更像一个「心跳检测器」。接收端通过检查序列号是否连续,可以判断链路是否丢包。但MAVLink本身不重传,丢了就丢了。
我记得有一次做编队飞行,3架无人机同时发数据,地面站收到的序列号乱成一锅粥。后来发现是串口缓冲区溢出,导致部分帧被丢弃。通过序列号跳变,我们很快定位到了问题。所以,别看它小,关键时刻能救命。
3.4 系统ID与组件ID:谁在说话?
这两个字段放在一起讲,因为它们是一对「组合拳」。
- 系统ID(1字节):标识一个独立的系统。比如一架无人机、一个地面站、一个摄像头。取值范围1~255,0保留。
- 组件ID(1字节):标识系统内部的某个组件。比如飞控、GPS模块、云台。取值范围0~255。
举个例子:系统ID=1,组件ID=1,通常表示「1号无人机的飞控」。系统ID=1,组件ID=5,可能表示「1号无人机的GPS模块」。
我个人的习惯是:系统ID用固定编号,比如1号机、2号机。组件ID则按照MAVLink官方定义来,别自己瞎编。否则地面站解析的时候会一脸懵。
3.5 消息ID:消息的「身份证号」
消息ID占1个字节,范围0~255。每个消息ID对应一种消息类型。比如:
- 0:HEARTBEAT(心跳包)
- 1:SYS_STATUS(系统状态)
- 2:SYSTEM_TIME(系统时间)
- ……
- 255:自定义消息(通常从255开始往下用)
消息ID决定了接收端如何解析后面的载荷。说白了,接收端看到消息ID=0,就知道后面9个字节是HEARTBEAT的数据。看到消息ID=255,就知道这是自定义消息,得按你定义的格式来解析。
我在项目中遇到过一个经典问题:消息ID冲突。团队里两个人各自定义了自定义消息,都用了ID=255。结果地面站收到数据后,用A的解析器去解析B的消息,直接崩溃。从那以后,我们规定自定义消息ID必须登记在案,谁用谁签字。
3.6 载荷:真正的数据
载荷就是消息的「正文」。长度由前面的「载荷长度」字段指定,最大255字节。
载荷的格式完全由消息ID决定。比如HEARTBEAT的载荷是9个字节,包含:系统类型、飞控类型、模式、状态等。而GPS_RAW_INT的载荷是30个字节,包含经纬度、海拔、速度等。
这里有个设计哲学:MAVLink的载荷是紧凑排列的,没有对齐填充。也就是说,一个uint8_t后面紧跟着一个uint32_t,不会为了对齐而插入空字节。这样做的好处是节省带宽,坏处是某些MCU上访问未对齐的数据会触发异常。
3.7 校验:数据完整性的守护神
校验字段占2个字节,使用CRC-16-CCITT算法。它覆盖的范围是:从「载荷长度」字段开始,一直到载荷结束。起始标志不参与校验。
为什么起始标志不参与校验?因为接收端先靠起始标志找到帧头,然后才开始校验。如果起始标志也参与校验,那就成了「先有鸡还是先有蛋」的问题了。
校验的计算公式如下(我直接给你代码,省得你再去翻源码):
uint16_t mavlink_crc_calculate(const uint8_t* buffer, uint16_t length) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < length; i++) {
crc ^= buffer[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0x8408;
} else {
crc >>= 1;
}
}
}
return crc;
}
接收端收到一帧数据后,会重新计算校验和,然后和帧尾的2个字节比对。如果一致,说明数据没被篡改。如果不一致,直接丢弃整帧。
我记得有一次做野外测试,无人机飞到500米外,地面站突然开始丢帧。排查了一圈,发现是电机产生的电磁干扰导致串口数据出错。幸亏有CRC校验,地面站自动丢弃了错误帧,没有把错误数据传给飞控。要是没有校验,飞机可能就自己乱飞了。
3.8 整体流程:一帧数据的「一生」
说了这么多,我们来走一遍完整流程。假设你要发送一条HEARTBEAT消息:
- 填充载荷:9个字节的HEARTBEAT数据
- 设置载荷长度:9
- 设置序列号:当前发送计数,比如42
- 设置系统ID:1
- 设置组件ID:1
- 设置消息ID:0
- 计算校验:从载荷长度到载荷结束,共1+1+1+1+1+9=14个字节,算出CRC
- 拼接帧:0xFE + 9 + 42 + 1 + 1 + 0 + [9字节载荷] + [2字节CRC]
- 发送到串口
接收端收到后,先找0xFE,然后按顺序解析各个字段,最后校验CRC。全部通过,才算收到一条有效消息。
好了,帧结构就讲到这里。下一节我们聊聊如何用代码实现一个MAVLink解析器。嗯,到时候我会给你看一个我自己写的轻量级解析库,只有200行代码,但该有的功能一个不少。
公众号:蓝海资料掘金营,微信deep3321