4. OTA系统总体架构
终端升级这件事,说白了就是一套「检测→下载→校验→备份→激活」的组合拳。每个环节都可能翻车,我这些年踩过的坑,基本都集中在这几个节点上。
1. 升级检测:什么时候该升级?
终端不会无缘无故去检查升级。触发方式通常有三种:
- 定时轮询:设备每隔一段时间(比如24小时)向服务器询问是否有新版本。我见过有些产品设成1小时轮询一次,结果服务器被请求打爆了。
- 被动推送:服务器主动下发升级通知,终端收到后开始检测。这种方式实时性好,但需要终端保持长连接。
- 用户触发:用户在设置页面点击「检查更新」。嗯,这个最直接,但用户不点你就没办法。
检测时,终端会带上当前版本号、硬件型号、区域信息等参数。服务器返回的升级包信息通常包含:
| 字段 | 说明 | 我的建议 |
|---|---|---|
| version | 目标版本号 | 用语义化版本,别搞什么v1.2.3_beta_final |
| url | 升级包下载地址 | 用CDN地址,别直接挂内网IP |
| md5/sha256 | 校验值 | 必须用SHA256,MD5已经不够安全了 |
| size | 包体大小 | 用于进度条和存储空间预检 |
| force | 是否强制升级 | 关键安全更新建议强制,功能更新可选 |
2. 下载策略:断点续传与静默下载
下载是整个流程中最耗时的环节,也是最容易出问题的。我遇到过下载到90%突然断网,然后从头开始下的情况——用户差点把设备砸了。
断点续传
断点续传的原理很简单:把大文件切成小块,记录已经下载的块偏移量。断网后重新连接,从断点继续下载,而不是重头开始。
实现时需要注意:
- 服务端必须支持
Range请求头 - 本地要持久化下载进度(写到Flash或文件系统)
- 下载完成后做完整性校验,防止拼接错误
// 伪代码:断点续传核心逻辑
typedef struct {
uint32_t total_size;
uint32_t downloaded_size;
uint8_t chunk_hash[32]; // 每块的SHA256
} DownloadProgress;
// 恢复下载时
DownloadProgress progress = read_progress_from_flash();
http_set_header("Range", "bytes=%d-", progress.downloaded_size);
// 继续接收剩余数据
静默下载
静默下载就是在用户无感知的情况下把升级包拉下来。听起来很美好,但坑不少:
- 带宽控制:不能把用户正在用的网络占满。我一般限制在总带宽的30%以内。
- 时机选择:设备空闲时下载,比如深夜、充电时、WiFi环境下。
- 电量考虑:电池供电的设备,电量低于20%时暂停下载。
3. 校验与解密:信任是从代码到硬件的
下载完成后,不能直接拿来用。你得先确认这包是完整的、没被篡改的、而且是合法的。
完整性校验
用SHA256对比本地计算值和服务器下发的值。如果对不上,说明下载过程中数据损坏了,需要重新下载。
// 校验示例
uint8_t expected_hash[32];
uint8_t actual_hash[32];
sha256_compute(download_buffer, buffer_size, actual_hash);
if (memcmp(expected_hash, actual_hash, 32) != 0) {
// 校验失败,重新下载
goto retry_download;
}
签名验证
完整性校验只能防传输错误,防不了恶意篡改。签名验证才是安全的核心:
- 服务器用私钥对升级包签名
- 终端用预置的公钥验证签名
- 签名算法推荐ECDSA或RSA-2048以上
解密
如果升级包是加密的,终端需要用密钥解密。密钥管理是个大话题,这里只说几个原则:
- 每个设备的密钥不同(基于设备唯一ID派生)
- 密钥不在Flash中明文存储
- 解密过程在安全环境(如TEE或SE)中执行
4. 备份与回滚:给自己留条后路
升级失败怎么办?没有备份机制,设备就变砖了。我见过太多产品因为没做回滚,售后成本直接翻倍。
备份策略
常见的备份方式有两种:
| 方案 | 原理 | 优缺点 |
|---|---|---|
| A/B分区 | 两个系统分区轮流使用 | 切换快,但Flash占用翻倍 |
| 备份分区 | 单独一个备份分区存旧版本 | 节省空间,但恢复时需拷贝 |
我个人更推荐A/B分区方案。虽然多占一倍空间,但切换是原子操作,几乎不会失败。
回滚触发条件
- 新版本启动后,看门狗超时(系统卡死)
- 关键服务注册失败(比如网络连不上)
- 用户手动选择回滚
- 连续重启超过N次(我一般设3次)
5. 激活与切换:最后一哆嗦
所有准备工作就绪,终于到了激活新版本的环节。这一步要保证「要么成功,要么回滚」。
激活流程
- 设置新分区为「待激活」状态
- 写入启动标记(比如Bootloader读取的flag)
- 执行系统复位
- Bootloader检查标记,加载新分区
- 新系统启动,执行自检
- 自检通过后,标记为「已激活」
切换的原子性
写入启动标记这一步必须是原子的。如果写了一半掉电,Bootloader可能读到脏数据,不知道该启动哪个分区。
解决办法:
- 使用双备份标记(写两次,比较一致才生效)
- 使用Flash的原子写操作(比如STM32的选项字节)
- 标记写入后立即读回验证
// 原子切换示例
void switch_to_new_partition(void) {
// 先写主标记
write_boot_flag(BOOT_FLAG_NEW);
// 读回验证
if (read_boot_flag() != BOOT_FLAG_NEW) {
// 写入失败,保持当前分区
return;
}
// 写备份标记
write_boot_flag_backup(BOOT_FLAG_NEW);
// 执行复位
system_reset();
}
整体流程一览
下面这张图把整个升级流程串起来了。你可以看到,每个环节都有分支,不是一条直线走到底。
你看,整个流程中「回滚」这条线是保命的。没有它,升级失败就是灾难。我参与过的项目,凡是认真做了回滚机制的,售后问题都少了一大半。