嵌入式OTA升级原理解密:从下载到刷写,设备如何安全"复活"
导读:OTA升级不是"下载一个bin文件往里写"这么简单。本文聚焦两种主流方案——MCU的双Bank架构与Linux的A/B分区,讲清楚升级过程中"断电不失手,变砖可回滚"的核心机制,并对比 SWUpdate、Mender、RAUC三大开源工具。
一、OTA升级核心原理
无论MCU还是Linux,OTA的本质都是先写备份、再切换、最后擦除的三步走:
flowchart TB
subgraph 下载["🔵 第一步:下载固件到暂存区"]
D1["接收.swu/.bin固件包"]
D2["完整性校验<br/>SHA-256 / CRC32"]
D3["签名验证<br/>RSA / ECDSA"]
D4["写入备用区"]
end
subgraph 重启["🟡 第二步:重启进入Bootloader"]
R1["设置更新标志位"]
R2["跳转至Bootloader"]
R3["Bootloader读取标志位"]
end
subgraph 刷写["🟢 第三步:Bootloader校验并写入目标分区"]
W1["从备用区读取固件"]
W2["验签+验完整性"]
W3["解压(如已压缩)"]
W4["写入目标Flash分区"]
W5["更新启动标记"]
end
subgraph 回滚["🔴 异常处理"]
B1{"启动成功?"}
B2["回滚至旧版本"]
B3["报告升级失败"]
end
下载 --> 重启 --> 刷写
刷写 -->|"重启验证"| B1
B1 -->|"否"| B2
B1 -->|"是"| B3
style 下载 fill:#E3F2FD,stroke:#1976D2
style 重启 fill:#FFF8E1,stroke:#F57C00
style 刷写 fill:#E8F5E9,stroke:#388E3C
style 回滚 fill:#FFEBEE,stroke:#D32F2F
断电不失手的秘密:新固件永远先写入备用区,原固件原地不动。断电发生时Bootloader依然能加载旧系统。
二、MCU方案:双Bank架构
MCU没有文件系统,Flash直接被分成两块——Bank A和Bank B,轮着用。
来自 linuxros.cn · linuxROS
2.1 Flash分区结构
flowchart TB
subgraph Flash布局["MCU Flash 分区布局"]
BL["Bootloader<br/>固定区域<br/>永不改动"]
BA["App Bank A<br/>当前运行固件"]
BB["App Bank B<br/>备用区<br/>接收新固件"]
CFG["Config区<br/>升级标志/版本号"]
end
style BL fill:#F3E5F5,stroke:#7B1FA2
style BA fill:#E8F5E9,stroke:#388E3C
style BB fill:#FFF8E1,stroke:#F57C00
style CFG fill:#E3F2FD,stroke:#1976D2
2.2 双Bank升级流程
flowchart TB
subgraph 正常启动["正常启动路径"]
A1["上电/复位"] --> A2{"Bank A 可用?"}
A2 -->|"是"| A3["从Bank A启动"]
A3 --> A4["运行App"]
end
subgraph 升级流程["OTA升级路径"]
B1["接收新固件"] --> B2["写入Bank B"]
B2 --> B3["Bank B 验签"]
B3 --> B4{"验签通过?"}
B4 -->|"否"| B5["丢弃Bank B<br/>保持Bank A"]
B4 -->|"是"| B6["设置升级标志<br/>bank_to_boot=B"]
B6 --> B7["系统重启"]
B7 --> A2
A2 -->|"标志指向B"| A3B["从Bank B启动"]
A3B --> A4B["运行新固件"]
A4B --> A5["Bank A变为备用区"]
end
style A1 fill:#E3F2FD,stroke:#1976D2
style A2 fill:#FFF8E1,stroke:#F57C00
style BA fill:#E3F2FD,stroke:#1976D2
style BB fill:#FFF8E1,stroke:#F57C00
style B4 fill:#FFF8E1,stroke:#F57C00
style B5 fill:#FFEBEE,stroke:#D32F2F
style A5 fill:#E3F2FD,stroke:#1976D2
2.3 常见MCU OTA开源方案
| 方案 | 特点 | 适用场景 |
|---|---|---|
| MCUBoot | 开源安全Bootloader,支持签名验签、AES加密、双Bank | NXP/i.MX系列 |
| STM32FOTA | ST官方方案,支持X-CUBE-BLE/X-CUBE-SBSFU | STM32全系列 |
| ESP8266/ESP32 OTA | WiFi模块内置,支持HTTP/HTTPS差分升级 | ESP系列 |
| TI UNIFLASH | 德州仪器方案,支持Serial/BLE/WiFi多种方式 | TI CC系列 |
三、Linux方案:A/B分区
Linux设备有文件系统,OTA分区远比MCU复杂。主流方案是用A/B分区实现无缝切换。
3.1 eMMC/SD卡分区结构
flowchart TB
subgraph Boot["Boot分区 A/B"]
K1["Kernel A"]
K2["Kernel B"]
end
subgraph Rootfs["Rootfs分区 A/B"]
R1["rootfs A<br/>当前系统"]
R2["rootfs B<br/>备用系统"]
end
subgraph Data["Data数据分区"]
DAT["数据分区<br/>配置文件/日志/应用数据"]
end
subgraph Config["Config配置区"]
C["UBoot环境变量<br/>bootlimit/bootpart"]
end
style Boot fill:#E3F2FD,stroke:#1976D2
style Rootfs fill:#E8F5E9,stroke:#388E3C
style Data fill:#FFF8E1,stroke:#F57C00
style Config fill:#F3E5F5,stroke:#7B1FA2
3.2 A/B分区升级流程
flowchart TB
subgraph 运行中["当前系统运行中"]
CUR["rootfs A 正在运行<br/>应用持续服务"]
end
subgraph 下载写入["OTA下载写入"]
DL["从服务器下载.swu包"]
VER["sw-description<br/>描述镜像配置"]
WR["写入rootfs B<br/>不触碰rootfs A"]
end
subgraph 切换["重启切换"]
SW["更新UBoot环境变量<br/>bootpart=B"]
RE["重启"]
end
subgraph 验证["新系统启动验证"]
NEW["从rootfs B启动"]
CHECK{"健康检查通过?"}
OK["确认升级成功<br/>rootfs A变为备用"]
FAIL["回滚rootfs A<br/>保持原状"]
end
CUR -->|"后台下载"| DL
DL --> WR --> SW --> RE --> NEW --> CHECK
CHECK -->|"成功"| OK
CHECK -->|"失败"| FAIL
style CUR fill:#E8F5E9,stroke:#388E3C
style DL fill:#E3F2FD,stroke:#1976D2
style WR fill:#FFF8E1,stroke:#F57C00
style SW fill:#FFF8E1,stroke:#F57C00
style NEW fill:#E8F5E9,stroke:#388E3C
style CHECK fill:#FFF8E1,stroke:#F57C00
style OK fill:#E8F5E9,stroke:#388E3C
style FAIL fill:#FFEBEE,stroke:#D32F2F
3.3 UBoot A/B切换原理
flowchart TB
subgraph 环境变量["UBoot 环境变量"]
PART["mmcbootpart=1<br/>当前启动分区"]
PARTB["mmcbootpart=2<br/>备用分区"]
LIMIT["bootlimit=3<br/>允许连续失败次数"]
end
subgraph 切换逻辑["A→B 切换逻辑"]
SET["setenv bootpart 2"]
SAV["saveenv<br/>固化到Flash"]
REBOOT["reset"]
end
subgraph 回滚机制["连续失败回滚"]
CNT["fail_count++"]
CHK{"fail_count > bootlimit?"}
ROLL["回滚至旧分区<br/>fail_count=0"]
end
SET --> SAV --> REBOOT
CNT --> CHK -->|"是"| ROLL
style PART fill:#E3F2FD,stroke:#1976D2
style PARTB fill:#FFF8E1,stroke:#F57C00
style SET fill:#FFF8E1,stroke:#F57C00
style CHK fill:#FFF8E1,stroke:#F57C00
style ROLL fill:#FFEBEE,stroke:#D32F2F
四、开源方案对比
| 方案 | 类型 | 签名验签 | 差分升级 | 断电保护 | 适用平台 |
|---|---|---|---|---|---|
| SWUpdate | 块+文件级 | ✅ RSA/ECDSA | ✅ | ✅ A/B分区 | 嵌入式Linux |
| Mender | 块级 | ✅ RSA | ✅ | ✅ A/B分区 | Yocto/Linux |
| RAUC | 块级 | ✅ RSA/Ed25519 | ✅ | ✅ A/B分区 | 嵌入式Linux |
| balenaOS | 容器化 | ✅ | ✅ delta | ✅ A/B分区 | ARM/Yocto |
核心结论:三者都支持A/B分区和签名验签。SWUpdate最灵活(支持单文件更新),RAUC安全标准最高(Muttus模式),Mender与Yocto集成最深。
五、总结
OTA升级的本质是**"不破坏性写入 + 可回滚切换"**。记住一个口诀:
MCU看Bank,Linux看分区;下载写备用,重启再切换;验签不过关,坚决不刷写。
| 方案 | 核心差异 |
|---|---|
| MCU双Bank | Flash物理分成两块,Bootloader决定从哪块启动 |
| Linux A/B分区 | 文件系统级分区,OTA工具写备用分区,UBoot切换启动目标 |
| 共同点 | 新固件验签通过前,原系统纹丝不动;任何阶段失败都能回滚 |