4、本地升级方案(UART/I2C/SPI):串口升级协议设计(XMODEM/YMODEM),I2C/SPI从机升级实现,校验机制(CRC32/MD5)
本地升级,说白了就是通过板子上的物理接口把固件灌进去。UART、I2C、SPI 这三种接口,是嵌入式系统里最常见的本地升级通道。我做了这么多年嵌入式,遇到的项目十有八九都离不开它们。今天咱们就掰开揉碎了聊聊,每种接口怎么设计升级协议,怎么保证数据不出错。
4.1 串口升级协议设计:XMODEM 与 YMODEM
串口升级是最古老、也是最通用的方式。你想想看,几乎每块 MCU 都有 UART,哪怕是最便宜的 8 位机。但串口本身只负责字节流传输,没有帧结构,没有校验。所以我们需要在串口之上跑一个文件传输协议。
XMODEM 协议,我个人的最爱。它简单、可靠,128 字节一个数据包,加上序号、校验和,来回握手。流程大致是这样:
- 接收方发送 NAK(0x15),表示「我准备好了,开始发吧」。
- 发送方收到 NAK 后,发送第一个数据包:SOH + 序号 + 序号取反 + 128 字节数据 + 校验和。
- 接收方校验通过,回复 ACK(0x06);校验失败,回复 NAK。
- 发送方收到 ACK 后,发下一个包;收到 NAK 则重传当前包。
- 传输完毕,发送方发 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 从机升级的通用流程:
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 字节 | 极强 | 慢 | 最终完整性确认 |
最后说一句,校验机制不是万能的。它只能保证数据在传输过程中没出错,但保证不了固件本身是正确版本。所以升级前最好还要做版本号检查、芯片型号匹配检查。嗯,这些内容后面的章节会细聊。