4、本地升级方案(UART/I2C/SPI):串口升级协议设计(XMODEM/YMODEM),I2C/SPI从机升级实现,校验机制(CRC32/MD5)

本地升级,说白了就是通过板子上的物理接口把固件灌进去。UART、I2C、SPI 这三种接口,是嵌入式系统里最常见的本地升级通道。我做了这么多年嵌入式,遇到的项目十有八九都离不开它们。今天咱们就掰开揉碎了聊聊,每种接口怎么设计升级协议,怎么保证数据不出错。

4.1 串口升级协议设计:XMODEM 与 YMODEM

串口升级是最古老、也是最通用的方式。你想想看,几乎每块 MCU 都有 UART,哪怕是最便宜的 8 位机。但串口本身只负责字节流传输,没有帧结构,没有校验。所以我们需要在串口之上跑一个文件传输协议。

XMODEM 协议,我个人的最爱。它简单、可靠,128 字节一个数据包,加上序号、校验和,来回握手。流程大致是这样:

  1. 接收方发送 NAK(0x15),表示「我准备好了,开始发吧」。
  2. 发送方收到 NAK 后,发送第一个数据包:SOH + 序号 + 序号取反 + 128 字节数据 + 校验和。
  3. 接收方校验通过,回复 ACK(0x06);校验失败,回复 NAK。
  4. 发送方收到 ACK 后,发下一个包;收到 NAK 则重传当前包。
  5. 传输完毕,发送方发 EOT(0x04),接收方回复 ACK。

关键点:XMODEM 的校验和只有 1 个字节(8 位),对于大文件来说,碰撞概率其实不低。后来有了 XMODEM-CRC,改用 CRC16,可靠性才上来。我建议新项目直接用 XMODEM-CRC 或 YMODEM。

YMODEM 协议,是 XMODEM 的升级版。它最大的改进是支持批量文件传输和更长的文件名。YMODEM 的第一个数据包是文件头,包含文件名、文件大小、时间戳等信息。后续数据包可以选 128 字节或 1024 字节,根据信道质量动态切换。

我在项目中遇到过一个问题:用 XMODEM 传一个 512KB 的固件,串口波特率 115200,每次 128 字节,来回握手开销很大。后来换成 YMODEM 的 1024 字节模式,传输时间直接砍掉 60%。所以,如果你的串口信道比较干净(比如板内短距离),大胆用大包模式。

特性 XMODEM XMODEM-CRC YMODEM
数据包大小 128 字节 128 字节 128 / 1024 字节
校验方式 8 位校验和 CRC16 CRC16
批量传输 不支持 不支持 支持
文件名支持 不支持 不支持 支持
适用场景 极小系统 一般嵌入式 大文件、多文件

我的习惯:在 Bootloader 里同时实现 XMODEM-CRC 和 YMODEM。默认用 YMODEM,如果上位机工具不支持,自动降级到 XMODEM-CRC。这样兼容性最好。

4.2 I2C/SPI 从机升级实现

串口升级虽然通用,但速度有限。I2C 和 SPI 就不一样了,尤其是 SPI,跑个几十 MHz 轻轻松松。很多设备是作为 I2C 或 SPI 从机存在的,比如传感器、显示屏、无线模块。这些设备怎么升级?

I2C 从机升级,核心思路是利用 I2C 的寄存器读写机制。我在一个温湿度传感器项目里就是这么干的:

  • 定义一组升级相关的寄存器地址,比如 0xF0 是升级命令寄存器,0xF1 是数据长度寄存器,0xF2 是数据缓冲区。
  • 主机先写 0xF0 寄存器,写入命令「开始升级」。
  • 然后写 0xF1 寄存器,告诉从机这次要传多少字节。
  • 接着通过 0xF2 寄存器,一次一页(比如 32 字节)地写入固件数据。
  • 从机每收到一页,写入内部 Flash,然后通过状态寄存器返回成功或失败。

这里有个坑:I2C 的从机地址通常是 7 位,一个总线上最多挂 127 个设备。但升级过程中,从机不能响应其他命令,否则会乱套。我建议在升级开始前,主机先发一个「锁定」命令,让从机进入升级模式,不再响应应用层请求。

SPI 从机升级,速度比 I2C 快得多。SPI 是全双工,主机发数据的同时从机可以回数据。我常用的做法是:

  • 主机拉低 CS 片选,开始一次 SPI 事务。
  • 第一个字节是命令码,比如 0x01 表示「写固件」,0x02 表示「读状态」。
  • 后续字节是地址和数据。从机在接收数据的同时,通过 MISO 线返回状态字节。
  • 每写完一页(比如 256 字节),从机自动触发 Flash 擦写,完成后在状态寄存器里置位「就绪」标志。

我曾经踩过的坑:SPI 从机升级时,主机发送速度太快,从机来不及处理。结果数据覆盖,Flash 写入了错误内容。后来我在从机端加了一个硬件 FIFO,深度至少 64 字节,并且主机每发完一页要等待从机确认。别贪快,稳才是第一位的。

下面这张图展示了 I2C/SPI 从机升级的通用流程:

I2C/SPI 从机升级流程图 I2C/SPI 从机升级流程 主机发送升级命令 从机进入升级模式 主机发送一页数据 从机写入内部 Flash 从机返回成功/失败状态 所有页传输完成? 是 升级完成 否 继续发送下一页 说明:I2C 使用寄存器读写方式,SPI 使用命令+地址+数据方式,流程本质相同

4.3 校验机制:CRC32 与 MD5

传输过程中数据会不会出错?肯定会。电磁干扰、时钟抖动、电平不稳,随便一个原因就能让一个 bit 翻转。所以校验机制是升级方案的命根子。

CRC32,循环冗余校验,32 位。它用多项式除法计算出一个 4 字节的校验值。CRC32 的检错能力极强,能检测出所有单 bit 错误、双 bit 错误、奇数个错误,以及长度不超过 32 位的突发错误。硬件上很多 MCU 都有 CRC 外设,算起来飞快。

我一般这样用:每传完一整个固件文件,在文件末尾附加 4 字节的 CRC32 值。接收方收完所有数据后,自己算一遍 CRC32,跟收到的值对比。如果一致,说明文件完整。

// CRC32 计算示例(查表法,速度快)
uint32_t crc32(const uint8_t *data, uint32_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc = crc_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
    }
    return crc ^ 0xFFFFFFFF;
}

MD5,消息摘要算法第五版,128 位。它比 CRC32 更安全,但计算量也大得多。MD5 是哈希算法,理论上不同的输入会产生不同的哈希值。虽然 MD5 已经被证明有碰撞漏洞,但在嵌入式升级场景下,攻击者不太可能为了改你的固件去构造碰撞。所以 MD5 仍然够用。

我个人习惯:CRC32 用于实时校验,比如每传完一页就校验一次,发现错误立即重传。MD5 用于最终完整性确认,整个文件传完后算一次 MD5,跟固件发布时公布的 MD5 值对比。这样双重保险。

避坑指南:我曾经在一个项目里只用了 CRC16 做校验,结果有一次传输过程中出现了 3 个 bit 的错误,CRC16 愣是没检出来。从那以后,所有升级方案我至少用 CRC32。如果对安全性有要求,直接上 MD5 或 SHA256。

校验算法 输出长度 检错能力 计算速度 适用场景
CRC8 1 字节 弱 极快 小数据包、简单校验
CRC16 2 字节 中等 快 XMODEM、Modbus
CRC32 4 字节 强 较快 固件文件校验
MD5 16 字节 极强 慢 最终完整性确认

最后说一句,校验机制不是万能的。它只能保证数据在传输过程中没出错,但保证不了固件本身是正确版本。所以升级前最好还要做版本号检查、芯片型号匹配检查。嗯,这些内容后面的章节会细聊。