3、升级协议与数据包设计:YModem协议详解、自定义私有协议设计、数据包结构
协议,说白了就是升级时双方约定好的“黑话”。MCU这边说“我要开始传了”,上位机那边得听得懂,反过来也一样。我见过不少项目,硬件设计没问题,Flash也够大,偏偏升级老失败——十有八九是协议层面出了岔子。
这一章咱们就掰开揉碎,把YModem协议和私有协议的设计讲清楚。数据包怎么拆、怎么装、怎么校验,这些是固件升级的“基本功”。
3.1 YModem协议详解
YModem是XModem的改进版。它最大的特点是支持批量文件传输,而且每个数据包带CRC校验。很多成熟的Bootloader,比如STM32的System Bootloader,默认就支持YModem。
YModem的核心流程,我习惯把它分成三个阶段:
- 起始协商阶段:接收方发送 'C' 字符(0x43),表示我要用CRC模式。发送方收到后,开始发送第一个数据包。
- 文件传输阶段:发送方按128字节或1024字节分包,逐包发送。接收方每收到一包,回复ACK确认。
- 结束阶段:发送方发送一个空包(SOH + 0x00 + 0xFF + CRC),接收方回复ACK,传输结束。
关键点:YModem的第一个包不是固件数据,而是文件名和文件大小。接收方需要从中解析出这些信息,用来分配存储空间或做校验。
来看一个典型的数据包结构:
// YModem 128字节数据包格式
// SOH + 序号 + 序号取反 + 数据(128字节) + CRC高字节 + CRC低字节
// 示例:发送序号为1的数据包
uint8_t packet[] = {
0x01, // SOH (Start of Header)
0x01, // 序号 (从1开始)
0xFE, // 序号取反 (0xFF - 0x01)
// 128字节数据...
0x12, 0x34 // CRC16校验值
};
这里有个坑,我踩过好几次。YModem的序号是单字节的,范围0~255。如果你传的文件分包数超过255,序号会回绕。接收方必须处理好这个回绕逻辑,否则会丢包或乱序。
注意:YModem支持两种包长:128字节(SOH开头)和1024字节(STX开头)。1024字节模式传输效率更高,但接收方的缓冲区要够大。我建议在资源允许的情况下优先用1024字节模式。
3.2 自定义私有协议设计
YModem虽然通用,但有时候不够灵活。比如你想在升级过程中同时传输版本号、校验密钥、或者做断点续传,YModem就有点力不从心了。这时候,自己设计一套私有协议反而更靠谱。
我参与过一个车载项目,MCU通过CAN总线升级。YModem是基于串口的,根本没法直接用。我们只好自己设计了一套基于CAN的私有协议。
私有协议的设计原则,我总结了三条:
- 轻量:包头别太复杂,MCU资源有限,解析开销要小。
- 可扩展:预留一些保留字段,方便以后加功能。
- 容错:要有超时重传、序号校验、CRC校验这些机制。
下面是我常用的一种私有协议数据包结构:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 帧头 | 2 | 固定值,比如 0xAA 0x55,用于同步 |
| 命令字 | 1 | 0x01=开始升级,0x02=传输数据,0x03=结束升级 |
| 包序号 | 2 | 从0开始,用于去重和排序 |
| 数据长度 | 2 | 有效数据的字节数,最大1024 |
| 数据 | N | 实际固件数据 |
| CRC32 | 4 | 对整个包(从帧头到数据)的校验 |
你想想看,这个结构比YModem灵活多了。命令字可以扩展,比如加一个0x04用来查询版本号。包序号用2字节,65535个包,够用了。
经验之谈:CRC32比CRC16更可靠,但计算开销也大。如果MCU主频不高,或者升级包不大,用CRC16也够。我一般会在协议里同时支持两种校验方式,通过命令字切换。
3.3 数据包结构:包头、数据、CRC校验
不管用YModem还是私有协议,数据包的核心三要素都一样:包头、数据、校验。咱们一个一个说。
3.3.1 包头设计
包头的作用是让接收方知道“我要开始收数据了”。它通常包含:
- 同步头:固定字节序列,比如0xAA 0x55。接收方不断扫描这个序列,找到后才开始解析后续内容。
- 包类型/命令字:告诉接收方这个包是干啥的。
- 包序号:用于排序和去重。
- 数据长度:告诉接收方要收多少字节的数据。
包头长度要适中。太短了,信息不够;太长了,浪费带宽。我一般控制在6~12字节之间。
3.3.2 数据段
数据段就是固件的二进制内容。这里有个细节:对齐。很多MCU的Flash写入要求按页对齐,或者按字对齐。如果数据段长度不是对齐单位的整数倍,接收方要做填充处理。
我曾经遇到过一个Bug:固件大小是1001字节,Flash页大小是1024字节。发送方按1024字节分包,最后一包只有1001字节,但接收方直接写了1024字节,把后面23字节的旧数据也写进去了。结果校验通过,但运行起来偶尔死机。
避坑指南:数据段不足一包时,用固定值(比如0xFF)填充。接收方写入Flash时,只写有效数据长度,不要多写。
3.3.3 CRC校验
CRC校验是最后一道防线。它保证数据在传输过程中没有被篡改或损坏。
常用的CRC算法有:
- CRC8:用于小数据包,校验能力一般。
- CRC16:YModem用的就是它,适合128~1024字节的包。
- CRC32:校验能力强,但计算量大。
下面是一个CRC16的C语言实现,我一直在用:
uint16_t crc16_update(uint16_t crc, uint8_t data) {
crc ^= data;
for (int i = 0; i < 8; i++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
return crc;
}
// 计算整个数据包的CRC
uint16_t calc_packet_crc(uint8_t *buf, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc = crc16_update(crc, buf[i]);
}
return crc;
}
嗯,这里要注意:CRC的初始值和多项式不是随便选的。YModem用的是CRC-CCITT(0x1021),初始值0x0000。如果你自己设计协议,建议用标准的CRC算法,别自己发明。
3.4 协议流程与状态机
协议设计好了,还得用状态机把它跑起来。我画了一张流程图,把升级协议的核心逻辑串起来:
这个状态机看起来简单,但实际调试时很容易出问题。我遇到过最典型的情况是:接收方在“等待同步”状态卡死了,因为发送方发的同步头被噪声干扰了。解决办法是加超时,超时后重新发送同步请求。
小技巧:在“接收包头”状态,建议加一个包头超时计时器。如果超过100ms没收到完整包头,就丢弃当前数据,重新同步。这样可以防止半包数据污染后续传输。
3.5 协议测试要点
协议设计完了,怎么测?我列几个必测的点:
- 正常流程:完整的升级流程,从开始到结束,确保数据100%一致。
- 丢包测试:模拟中间丢一包,看接收方能不能通过超时重传恢复。
- 乱序测试:打乱包的顺序发送,看接收方能不能按序号重组。
- CRC错误测试:故意改一个字节的CRC值,看接收方能不能正确拒绝。
- 边界测试:固件大小刚好是包长的整数倍、非整数倍、空文件(0字节)。
我记得有一次做压力测试,连续升级100次,第73次失败了。查了半天,发现是接收方的CRC计算在特定数据模式下溢出了。从那以后,我每次设计协议都会做至少100次的压力测试。
好了,协议和数据包这部分就聊到这儿。你把这些搞明白了,MCU固件升级的底层逻辑就通了。接下来就是把这些协议落地到代码里,写成Bootloader和上位机工具。