必须用双备份分区而非覆盖写入,因断电、中断或校验失败易致变砖;双分区(如app_a/app_b)交替升级并校验后切换启动入口,确保可回退;分区需等大且对齐Flash擦除粒度;Bootloader通过固件末尾的firmware_header(含version、crc32、status magic number)安全选启;需禁用/喂狗、清Cache、跳转至向量表首址,并实现二次回退。

为什么必须用双备份分区,而不是覆盖写入
直接覆盖旧固件会极大增加变砖风险:擦除或写入中途断电、网络中断、校验失败,都会让设备无法启动。双备份的本质是把固件存放在两个独立的 Flash 分区(比如 app_a 和 app_b),每次升级只更新非当前运行的分区,校验通过后再切换启动入口——这样即使新固件损坏,Bootloader 仍能回退到上一版正常固件。
关键点在于:两个分区大小需严格一致,且必须对齐 Flash 的擦除粒度(如 STM32 的 2KB 或 4KB 扇区)。否则擦除失败或写入越界会导致整个分区不可用。
Bootloader 如何安全判断该启动哪个分区
不能靠文件系统或时间戳——嵌入式 Flash 通常无 FS,且 RTC 可能不准确。可靠做法是在每个固件末尾固定偏移处写入一个结构体,包含版本号、CRC32 校验值和状态标志(VALID/INVALID);Bootloader 启动时依次读取两个分区末尾的这个结构体,优先选择 VALID 且 CRC 正确的最新版本。
struct firmware_header {
uint32_t version; // 如 0x010200 表示 v1.2.0
uint32_t crc32; // 覆盖整个固件正文(不含 header 自身)
uint32_t status; // 0x5A5A5A5A = VALID, 其他 = INVALID
};
- 务必在写入新固件后、设置
status前,先完整计算并写入crc32;否则设为VALID却校验失败,Bootloader 会跳过该分区 - 状态标志不要用布尔值——Flash 擦除后默认全 0xFF,用 magic number 可避免误判
- Bootloader 必须禁用看门狗或在检查阶段喂狗,防止因 Flash 读取慢触发复位
OTA 下载阶段怎么避免内存溢出和校验失效
裸机环境 RAM 极其有限(常
立即学习“C++免费学习笔记(深入)”;
示例关键逻辑:
while (receiving) {
recv_chunk(buf, len);
update_crc32(&crc_ctx, buf, len); // 累加 CRC
write_to_flash(target_sector, buf, len); // 直接写入目标扇区
erase_if_needed(target_sector); // 写前确保已擦除
}
// 收完后,将 crc_ctx 结果写入 firmware_header.crc32
// 最后写入 firmware_header.status = 0x5A5A5A5A
- HTTP/CoAP 下载需处理分块传输(
Content-Range)和重传,建议用固定长度 chunk(如 512B),避免动态分配 - CRC 计算必须与 Flash 写入顺序严格一致——如果固件被压缩或加密,CRC 必须在解密/解压后的明文数据流上计算
- 写入前未调用
flash_erase_sector()就直接flash_write(),多数 MCU 会静默失败
切换启动分区时最危险的操作是什么
不是改寄存器,而是改启动跳转地址后没清指令缓存或没同步内存屏障。例如在 Cortex-M 上,从 Bootloader 跳转到新固件前,必须执行:
SCB->ICSR = SCB_ICSR_SYSTICKCAL_Msk; // 清 I-Cache(若使能) __DSB(); __ISB(); // 数据/指令同步屏障 ((void (*)(void))APP_START_ADDR)(); // 绝对跳转,勿用函数指针数组或虚表
- 跳转地址必须是向量表首地址(即新固件的
__Vectors起始位置),不是 main 入口——否则中断向量错乱,第一次中断就死机 - 若使用外部 Flash(如 QSPI),切换前需重新初始化其控制器,否则新固件的代码读取会总线错误
- 切分区后首次启动若崩溃,Bootloader 应检测到(如看门狗超时重启),自动尝试另一分区——这个“二次回退”逻辑容易被忽略,但恰恰是防永久变砖的最后一道防线


















