3、升级协议与数据包设计:YModem协议详解、自定义私有协议设计、数据包结构

协议,说白了就是升级时双方约定好的“黑话”。MCU这边说“我要开始传了”,上位机那边得听得懂,反过来也一样。我见过不少项目,硬件设计没问题,Flash也够大,偏偏升级老失败——十有八九是协议层面出了岔子。

这一章咱们就掰开揉碎,把YModem协议和私有协议的设计讲清楚。数据包怎么拆、怎么装、怎么校验,这些是固件升级的“基本功”。

3.1 YModem协议详解

YModem是XModem的改进版。它最大的特点是支持批量文件传输,而且每个数据包带CRC校验。很多成熟的Bootloader,比如STM32的System Bootloader,默认就支持YModem。

YModem的核心流程,我习惯把它分成三个阶段:

  1. 起始协商阶段:接收方发送 'C' 字符(0x43),表示我要用CRC模式。发送方收到后,开始发送第一个数据包。
  2. 文件传输阶段:发送方按128字节或1024字节分包,逐包发送。接收方每收到一包,回复ACK确认。
  3. 结束阶段:发送方发送一个空包(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 协议流程与状态机

协议设计好了,还得用状态机把它跑起来。我画了一张流程图,把升级协议的核心逻辑串起来:

升级协议状态机流程图 升级协议状态机流程图 空闲 (IDLE) 等待同步 (SYNC) 接收包头 (HEADER) 接收数据 (DATA) CRC校验 (CHECK) 写入Flash (WRITE) 完成 (DONE) 收到 'C' 检测到帧头 解析成功 数据收齐 CRC通过 写入成功 CRC失败,重试 说明:CRC失败后回到等待同步状态 最多重试3次,超过则报错

这个状态机看起来简单,但实际调试时很容易出问题。我遇到过最典型的情况是:接收方在“等待同步”状态卡死了,因为发送方发的同步头被噪声干扰了。解决办法是加超时,超时后重新发送同步请求。

小技巧:在“接收包头”状态,建议加一个包头超时计时器。如果超过100ms没收到完整包头,就丢弃当前数据,重新同步。这样可以防止半包数据污染后续传输。

3.5 协议测试要点

协议设计完了,怎么测?我列几个必测的点:

  • 正常流程:完整的升级流程,从开始到结束,确保数据100%一致。
  • 丢包测试:模拟中间丢一包,看接收方能不能通过超时重传恢复。
  • 乱序测试:打乱包的顺序发送,看接收方能不能按序号重组。
  • CRC错误测试:故意改一个字节的CRC值,看接收方能不能正确拒绝。
  • 边界测试:固件大小刚好是包长的整数倍、非整数倍、空文件(0字节)。

我记得有一次做压力测试,连续升级100次,第73次失败了。查了半天,发现是接收方的CRC计算在特定数据模式下溢出了。从那以后,我每次设计协议都会做至少100次的压力测试。

好了,协议和数据包这部分就聊到这儿。你把这些搞明白了,MCU固件升级的底层逻辑就通了。接下来就是把这些协议落地到代码里,写成Bootloader和上位机工具。