4. 应急恢复策略:双备份机制(A/B分区)、恢复模式(Recovery Mode)、串口救砖
说实话,做嵌入式最怕什么?不是需求改来改去,也不是老板催进度。最怕的是产品已经量产了,OTA升级到一半突然断电,或者网络断了。然后设备变砖了,用户只能返厂。
我早期做智能家居网关时就踩过这个坑。那批设备发出去两千多台,升级失败率不到1%,但就是这1%的返修,差点把项目利润全吃光。从那以后,我对升级容错这件事就特别较真。
这一章,我把三种最实用的救砖策略掰开揉碎了讲。双备份、恢复模式、串口救砖,这三板斧你掌握了,至少能覆盖95%的升级失败场景。
4.1 双备份机制(A/B分区)
双备份,也叫A/B分区、无缝升级。说白了就是Flash里存两份固件,一份跑着,一份待命。
正常工作时,系统从A分区启动。升级时,把新固件写到B分区。写完后校验,没问题就标记B分区为“可启动”。下次重启,BootROM自动切到B分区。如果B分区启动失败,BootROM还能回退到A分区。
这个机制的核心价值在于:升级过程中即使断电、断网,设备也不会变砖。最多就是升级失败,下次重启还是老版本。
关键设计要点:
- 分区表固定:A/B分区的起始地址和大小必须在BootROM里硬编码,不能依赖固件里的分区表。否则分区表坏了,BootROM就找不到备份分区了。
- 启动计数器:每次从新分区启动,BootROM会递增一个计数器。如果连续启动失败(比如3次),就自动回退到旧分区。这个机制能防止“启动-崩溃-重启-再崩溃”的死循环。
- 版本号管理:A/B分区各自保存版本号。BootROM启动时比较两个版本号,优先启动版本号高的那个。但要注意,如果新版本有严重bug,需要强制回退,就得靠启动计数器或外部指令了。
我建议你在设计分区布局时,把A/B分区放在Flash的前半段,BootROM放在最开头。这样即使后半段Flash坏了,BootROM和至少一个固件分区还能正常工作。
下面是一个典型的A/B分区布局,我拿一个16MB的Flash举例:
| 起始地址 | 大小 | 用途 |
|---|---|---|
| 0x000000 | 64KB | BootROM(不可擦除保护区) |
| 0x010000 | 64KB | BootROM配置区(启动计数器、分区状态等) |
| 0x020000 | 6MB | 分区A(固件镜像) |
| 0x080000 | 6MB | 分区B(固件镜像) |
| 0x0E0000 | 2MB | 用户数据区(配置、日志等) |
我的习惯:在BootROM配置区里,我会额外存一个CRC32校验值,覆盖整个配置区。这样如果配置区被意外改写(比如Flash比特翻转),BootROM能检测到并恢复默认值。这个坑我在一个工业项目里遇到过,环境温度高导致Flash数据出错,启动计数器被清零,设备反复回退。加了CRC校验后,问题就解决了。
4.2 恢复模式(Recovery Mode)
双备份虽然好,但有些场景它搞不定。比如:
- Flash空间不够,放不下两份固件
- 两个分区都坏了(概率极低,但确实可能)
- 固件有逻辑bug,启动后马上崩溃,连启动计数器都来不及递增
这时候就需要恢复模式了。说白了,就是BootROM里内置一个小巧的“救援系统”。它不加载主固件,而是进入一个特殊状态,等待外部输入来恢复设备。
恢复模式的触发方式通常有几种:
- 按键检测:上电时按住某个GPIO按键,BootROM检测到后进入恢复模式
- 启动失败计数:连续N次启动失败,自动进入恢复模式
- 外部信号:比如串口收到特定字符序列,或者USB检测到特定设备
进入恢复模式后,BootROM会做以下几件事:
- 初始化最基本的硬件(时钟、串口、Flash控制器)
- 等待外部输入(比如通过串口接收固件)
- 将接收到的固件写入Flash
- 校验通过后重启
注意:恢复模式的代码必须放在BootROM里,不能放在Flash的固件分区里。因为恢复模式就是在固件坏了的时候用的。所以BootROM的大小要预留足够空间给恢复模式代码。我一般会留8-16KB给恢复模式,包括串口驱动、Flash驱动、简单的协议解析。
我曾经在一个物联网项目里,把恢复模式做得特别“轻”。BootROM只有32KB,恢复模式占了12KB。剩下的20KB做启动逻辑和双备份管理。那个项目出货几十万台,恢复模式从来没出过问题。
4.3 串口救砖
串口救砖,是嵌入式工程师最后的底牌。不管设备变砖成什么样,只要BootROM还能跑,串口还能收发数据,你就有机会把它救回来。
串口救砖的核心流程是这样的:
- 设备上电,BootROM检测到固件校验失败(或者检测到救砖按键)
- BootROM进入串口接收模式,等待主机发送固件
- 主机通过串口工具(比如XMODEM、YMODEM协议,或者自定义协议)发送固件
- BootROM逐包接收,写入Flash
- 接收完成后校验,通过则重启
这里我推荐使用XMODEM协议。为什么?因为它简单、可靠、工具多。Windows上有SecureCRT、Tera Term,Linux上有lrzsz,都能直接支持。你不需要自己写上位机,省很多事。
下面是一个简化的XMODEM接收实现,放在BootROM里:
// 简化的XMODEM接收函数,用于BootROM
// 返回0表示成功,-1表示失败
int xmodem_receive(uint32_t flash_addr, uint32_t max_size) {
uint8_t buf[128]; // XMODEM数据包大小
uint8_t seq = 1; // 起始包序号
uint32_t offset = 0;
// 发送NAK,通知主机开始传输
uart_putchar(NAK);
while (1) {
uint8_t c = uart_getchar_timeout(3000); // 3秒超时
if (c == SOH) { // 数据包开始
uint8_t pkt_seq = uart_getchar();
uint8_t pkt_seq_inv = uart_getchar();
// 检查包序号
if (pkt_seq != seq || (pkt_seq ^ pkt_seq_inv) != 0xFF) {
uart_putchar(NAK); // 请求重发
continue;
}
// 接收128字节数据
for (int i = 0; i < 128; i++) {
buf[i] = uart_getchar();
}
// 接收校验和(简单校验)
uint8_t checksum = uart_getchar();
uint8_t calc_checksum = 0;
for (int i = 0; i < 128; i++) {
calc_checksum += buf[i];
}
if (checksum != calc_checksum) {
uart_putchar(NAK);
continue;
}
// 写入Flash
flash_write(flash_addr + offset, buf, 128);
offset += 128;
seq++;
uart_putchar(ACK); // 确认接收
} else if (c == EOT) { // 传输结束
uart_putchar(ACK);
return 0;
} else if (c == CAN) { // 取消传输
return -1;
} else {
uart_putchar(NAK); // 未知字符,请求重发
}
}
}
避坑指南:我曾经在一个项目里,串口救砖总是失败。查了两天才发现,BootROM里的串口初始化代码把波特率设成了115200,但主机那边默认是9600。两边对不上,数据全是乱码。后来我在BootROM里加了一个“波特率自适应”功能:上电后先以9600波特率发一串特定字符,然后等待主机回一个确认。如果没收到,就切换到115200再试。这样不管主机设什么波特率,都能自动匹配上。
4.4 三种策略的对比与选择
这三种策略不是互斥的,而是互补的。我一般建议这样组合:
| 策略 | 适用场景 | Flash开销 | 恢复速度 | 是否需要用户操作 |
|---|---|---|---|---|
| A/B分区 | Flash空间充足,需要无缝升级 | 大(2倍固件大小) | 快(秒级回退) | 不需要 |
| 恢复模式 | Flash空间有限,或需要手动恢复 | 小(8-16KB) | 中等(需手动操作) | 需要(按键或指令触发) |
| 串口救砖 | 最后手段,BootROM还能跑就行 | 极小(2-4KB) | 慢(需连接串口线) | 需要(连接串口、发送固件) |
我的推荐组合是:A/B分区 + 恢复模式 + 串口救砖。A/B分区处理99%的升级失败场景,恢复模式处理A/B都坏了的极端情况,串口救砖作为最后的保险。三管齐下,基本能做到“万无一失”。
下面这张图展示了这三种策略在BootROM中的协作关系:
嗯,这里要注意一点:串口救砖虽然可靠,但它需要用户有串口线、会操作串口工具。对于消费级产品,你不能指望用户会这个。所以消费级产品一般只做A/B分区和恢复模式,串口救砖留给研发和生产测试用。
但对于工业级产品,我强烈建议把串口救砖做进去。工业现场的设备,维护人员通常有技术背景,串口救砖能省很多事。我做过一个电力行业的项目,设备装在偏远变电站,升级失败后,现场工程师用手机连串口转蓝牙模块,远程就把固件刷好了。省了一趟出差,客户高兴得不行。