4. 升级包存储与校验:分区规划、下载策略与完整性校验
升级包怎么存、怎么下、怎么保证没坏——这三个问题,是OTA落地时最容易翻车的地方。我见过不少团队,算法写得漂亮,协议栈调得顺溜,结果栽在存储规划和校验逻辑上。今天咱们就把这块掰开揉碎了讲。
4.1 分区规划:给升级包找个靠谱的家
分区规划说白了就是:你的MCU/SoC上那点Flash,到底怎么切?切几块?每块多大?
我个人习惯把OTA相关的存储空间分成三个区域:
- Bootloader区:引导加载程序,一般64KB~128KB,固定不变
- 活动应用区(A区):当前运行的应用固件
- 备份应用区(B区):升级时写入的新固件,升级失败时回滚用
- 升级包暂存区:存放下载中的升级包,校验通过后再写入B区
- 参数/标志区:存升级状态、版本号、校验结果等元数据
这里有个关键点:升级包暂存区和备份应用区不能共用同一块Flash。为什么?
我在项目中遇到过一例:某团队为了省空间,把下载的升级包直接写到B区。结果下载到一半断电了,B区数据不完整,系统再也起不来——因为Bootloader发现B区校验失败,又没法回滚到A区(A区已经被标记为旧版本)。嗯,这就是典型的“省空间丢命案”。
推荐分区方案(以2MB Flash为例)
| 分区 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 128KB | 引导、校验、回滚逻辑 |
| 参数区 | 0x08020000 | 16KB | 升级标志、版本号、CRC |
| 应用A区 | 0x08024000 | 768KB | 当前运行固件 |
| 应用B区 | 0x080F4000 | 768KB | 备份/新固件 |
| 升级包暂存区 | 0x081C4000 | 256KB | 下载中的升级包 |
你想想看,如果Flash只有1MB怎么办?那就得牺牲一些功能:比如升级包暂存区压缩到128KB,或者采用差分升级(只下载变化的部分)。但无论如何,暂存区和B区必须物理隔离,这是底线。
4.2 升级包下载策略:别让网络拖垮你
下载策略的核心就一句话:怎么在不可靠的网络环境下,把升级包完整地拿下来。
我常用的策略分三种场景:
- 全量下载:适用于局域网或高速网络,一次性拉取整个包。简单粗暴,但失败就得重来。
- 分块下载:把升级包切成N个块(比如每块16KB),逐块下载并校验。某块失败只重传该块。
- 流式下载+边下边写:一边从网络接收数据,一边写入Flash暂存区。适合内存小的设备。
我个人最推荐分块下载。原因很简单:
// 伪代码:分块下载逻辑
#define BLOCK_SIZE 16384 // 16KB
#define TOTAL_BLOCKS (upgrade_size / BLOCK_SIZE)
for (block_id = 0; block_id < TOTAL_BLOCKS; block_id++) {
ret = download_block(block_id, buffer, BLOCK_SIZE);
if (ret != SUCCESS) {
// 重试3次
for (retry = 0; retry < 3; retry++) {
ret = download_block(block_id, buffer, BLOCK_SIZE);
if (ret == SUCCESS) break;
}
if (ret != SUCCESS) {
save_progress(block_id); // 记录断点
return ERROR;
}
}
flash_write(block_id * BLOCK_SIZE, buffer, BLOCK_SIZE);
save_progress(block_id + 1); // 更新进度
}
这段代码里有个细节:每写完一块就保存进度。这样即使中途断电,重启后也能从断点继续,而不是从头再来。
小技巧:进度信息不要只存一个“当前块号”。我习惯存一个位图(bitmap),每块对应一个bit,下载成功就置1。这样即使进度记录被写坏,也能通过位图恢复出哪些块已完成。
4.3 断点续传:别让用户等第二次
断点续传听起来高大上,其实核心就两个问题:
- 怎么知道上次下载到哪了?
- 怎么从断点继续而不破坏已下载的数据?
第一个问题靠持久化进度标志解决。我一般在参数区划出4个字节存“已完成的块数”,再加4个字节存“升级包总大小”。每次下载完一块,就更新这个计数器。
第二个问题稍微复杂点。你想想看,如果上次下载到第50块时断电了,第50块的数据可能只写了一半。重启后从第50块继续,但Flash里已经有一半的脏数据了——怎么办?
我的做法是:每块写入前先擦除该块所在的Flash扇区。这样即使上次写了一半,重新擦除后再写,数据就是完整的。代价是多了擦除操作,但换来的是可靠性。
曾经踩过的坑:有一次我在某款Nor Flash上做断点续传,发现擦除后直接写入会概率性失败。排查了半天,原来是擦除后需要等待内部充电完成(tW时间)。后来我在擦除和写入之间加了5ms延时,问题解决。所以,别相信Flash手册上的典型值,留余量。
4.4 完整性校验:MD5还是SHA256?
升级包下载完了,怎么确认它没被篡改、没丢数据?校验。
常用的校验方式有三种:
| 算法 | 输出长度 | 安全性 | 计算速度 | 适用场景 |
|---|---|---|---|---|
| CRC32 | 4字节 | 低(可碰撞) | 极快 | 传输层校验,防随机错误 |
| MD5 | 16字节 | 中(已知碰撞) | 快 | 非安全场景的完整性校验 |
| SHA256 | 32字节 | 高(无已知碰撞) | 较慢 | 安全场景,防篡改 |
我个人建议:传输层用CRC32,应用层用SHA256。为什么?
- CRC32硬件支持好,很多MCU有CRC外设,算起来飞快。适合每块下载完后快速检查。
- SHA256虽然慢,但安全性高。升级包整体校验用SHA256,能防止恶意篡改。
具体流程是这样的:
// 升级包校验流程
1. 服务器下发升级包时,附带SHA256摘要值(32字节)
2. 设备逐块下载,每块用CRC32校验(防传输错误)
3. 所有块下载完成后,对整个升级包计算SHA256
4. 将计算结果与服务器下发的摘要值比对
5. 一致则校验通过,不一致则丢弃升级包,重新下载
这里有个容易被忽略的点:SHA256计算时,要包含升级包的头部信息。头部里通常有版本号、设备型号、签名等字段。如果不包含头部,攻击者可以替换头部内容(比如把版本号改低),诱导设备降级到有漏洞的版本。
校验失败后的处理策略
- CRC32失败:只重传该块,最多重试3次
- SHA256失败:丢弃整个升级包,清空暂存区,重新下载
- 连续3次SHA256失败:上报服务器异常,停止升级,保持当前版本运行
4.5 整体流程:一张图说清楚
下面这张图把分区、下载、校验串起来了。你看一遍就能明白整个链路。
从图里你能看到,整个流程是分层校验的:传输层用CRC32兜底,应用层用SHA256把关。两层校验各司其职,既不浪费算力,也不放过问题。
4.6 避坑指南:我踩过的那些雷
最后分享几个实战中容易忽略的细节:
- Flash磨损均衡:参数区频繁写入会磨损Flash。我习惯在参数区用“备份+校验”的方式,每次写入前先擦除,写完后校验。如果校验失败,就用备份区恢复。
- 升级包版本号校验:下载前先比对版本号,禁止降级(除非服务器明确允许)。我曾经遇到过设备反复升级到旧版本,就是因为没做版本号检查。
- 超时处理:下载过程中如果网络断开超过30秒,自动暂停并保存进度。等网络恢复后,用户手动或自动续传。
- 校验算法选择:如果MCU没有硬件CRC,用软件CRC32会占用大量CPU。这时候可以考虑用Adler-32,计算量小很多,安全性也够用。
嗯,关于升级包存储与校验,核心就这些。分区规划是骨架,下载策略是血肉,校验是免疫系统——三者缺一不可。下次你设计OTA方案时,不妨从这三个维度先画一张图,再动手写代码。