第四章 设备端升级流程:下载管理、存储分区规划与校验完整性检查
好,咱们今天聊聊设备端升级的核心流程。说实话,这部分是OTA升级里最容易出幺蛾子的地方。我做了这么多年嵌入式,见过太多升级到一半变砖的案例,十有八九都是这几个环节没处理好。
设备端升级,说白了就三件事:把固件包下载下来、找个地方存好、确保它没坏。听起来简单?嗯,每个环节都有坑。
4.1 下载管理:别让网络断了你的命
下载管理,我习惯把它分成两个阶段:连接建立和数据接收。
先说说连接。设备端发起HTTP/HTTPS请求,服务器返回固件包。这里有个细节——断点续传。我在项目中遇到过,设备下载到80%突然断网,结果从头再来。用户骂娘,我也头疼。
核心要点:一定要支持断点续传。用HTTP的Range头,记录已下载的偏移量。每次重连后从断点继续,而不是重新下载。
代码示例,一个简单的断点续传逻辑:
// 伪代码:断点续传实现
typedef struct {
uint32_t offset; // 已下载字节数
uint32_t total_size; // 固件总大小
uint8_t md5[16]; // 固件MD5校验值
} download_context_t;
int ota_download_resume(download_context_t *ctx) {
// 设置HTTP Range头
char range_header[64];
snprintf(range_header, sizeof(range_header),
"Range: bytes=%u-%u", ctx->offset, ctx->total_size - 1);
// 发起请求
http_request_t req;
http_set_header(&req, "Range", range_header);
// 接收数据,从offset位置开始写入flash
return http_receive_to_flash(&req, ctx->offset);
}
你想想看,如果设备在信号不好的地方,比如地下室、电梯里,网络随时可能断开。没有断点续传,升级成功率会低得可怜。
我的经验:下载过程中,每接收一个数据块(比如4KB),就更新一次进度到非易失存储。这样即使掉电重启,也能从最近的断点恢复。我曾经见过一个产品,因为没做这个,升级失败率高达30%。
4.2 存储分区规划:A/B系统和recovery分区
存储分区,这是OTA升级的基石。我见过太多人在这上面栽跟头。咱们重点说两种方案:A/B系统和recovery分区。
4.2.1 A/B系统:双保险方案
A/B系统,说白了就是准备两份固件。一份在运行(A分区),一份用来升级(B分区)。升级时,把新固件写到B分区,然后切换启动。
为什么这么做?因为万一升级失败,你还能从A分区启动。设备不会变砖。
| 分区 | 用途 | 大小建议 |
|---|---|---|
| bootloader | 引导加载程序 | 64KB - 128KB |
| 分区表 | 记录分区布局 | 4KB - 8KB |
| slot A (系统) | 当前运行的系统 | 根据固件大小定 |
| slot B (系统) | 备用系统/升级目标 | 与slot A相同 |
| 数据分区 | 用户数据、配置 | 剩余空间 |
我记得有个项目,flash只有8MB,但固件有3MB。做A/B系统需要6MB,剩下2MB给数据和配置,勉强够用。如果flash再小一点,A/B系统就做不了了。
注意:A/B系统会占用双倍存储空间。如果flash容量紧张,可以考虑recovery分区方案。
4.2.2 Recovery分区:轻量级方案
Recovery分区,是一个独立的、最小化的系统。它的任务只有一个:升级主系统。
流程是这样的:设备正常启动时,从主系统分区运行。需要升级时,重启进入recovery模式。Recovery系统下载新固件,写入主系统分区,然后重启。
这样做的好处是节省空间。Recovery分区可以很小,比如1-2MB。但缺点是,如果recovery本身坏了,设备就真的变砖了。
我的建议:如果flash大于16MB,优先考虑A/B系统。如果小于8MB,用recovery分区方案。8-16MB之间,看具体需求。
4.3 校验与完整性检查:别让坏数据害了你
校验,这是最后一道防线。我见过一个案例,固件下载过程中,因为内存比特翻转,一个字节写错了。结果设备升级后,WiFi模块死活连不上。查了三天,才发现是校验没做全。
校验分三个层次:
- 传输层校验:TCP/IP自带校验和,但不够。我习惯在应用层再加一层CRC32。
- 文件层校验:固件包本身有MD5或SHA256签名。下载完成后,计算整个文件的哈希值,和服务器返回的对比。
- 写入层校验:每写一个扇区,读回来验证。确保flash写入没有错误。
代码示例,一个完整的校验流程:
// 伪代码:固件完整性校验
int ota_verify_firmware(firmware_t *fw) {
// 1. 计算整个固件的MD5
uint8_t computed_md5[16];
md5_calc(fw->data, fw->size, computed_md5);
// 2. 对比服务器下发的MD5
if (memcmp(computed_md5, fw->expected_md5, 16) != 0) {
log_error("MD5 mismatch!");
return -1;
}
// 3. 逐扇区验证写入正确性
for (uint32_t sector = 0; sector < fw->num_sectors; sector++) {
uint8_t buffer[FLASH_SECTOR_SIZE];
flash_read(sector, buffer);
uint32_t crc = crc32_calc(buffer, FLASH_SECTOR_SIZE);
if (crc != fw->sector_crcs[sector]) {
log_error("Sector %u CRC mismatch!", sector);
return -2;
}
}
log_info("Firmware verification passed!");
return 0;
}
你可能会问,为什么需要三层校验?嗯,因为每一层都可能出问题。传输层可能丢包,文件层可能被篡改,写入层可能因为flash老化而写错。三层校验,层层把关,才能确保万无一失。
避坑指南:我曾经遇到过一个bug,flash在高温下写入会偶尔出错。后来加了写入后读回验证,才把问题揪出来。所以,写入后读回验证这一步,千万别省。
4.4 实战中的坑与对策
最后,分享几个我踩过的坑:
- 坑一:下载过程中断电,flash写了一半。对策:用事务机制,要么全部写完,要么回滚。
- 坑二:A/B系统切换时,bootloader没处理好,导致两个分区都启动不了。对策:bootloader里加一个「启动计数」逻辑,连续失败3次就回滚。
- 坑三:校验通过,但固件版本号写错了。对策:版本号单独校验,和固件哈希绑定。
说实话,OTA升级的坑远不止这些。但只要你把下载管理、存储分区、校验检查这三个环节做扎实了,80%的问题都能避免。剩下的20%,嗯,那就靠经验积累了。
好,这一章就到这里。下一章咱们聊聊升级过程中的异常处理,比如断电、网络中断、flash损坏这些情况该怎么应对。