# 引言 在嵌入式系统中,OTA 更新是远程维护的核心功能,但网络中断、固件损坏或电源故障都可能导致更新失败。若设备无法启动,将造成严重后果。为此,引入**双分区(A/B)策略**:将 Flash 划分为两个独立区域,分别存储当前固件和备份固件。通过改写 Bootloader,在启动时检测固件有效性,若 OTA 失败则自动回滚到备份分区,从而保证设备永不“变砖”。 # 原理讲解 ## 双分区架构 - **分区 A(Active)**:当前运行的固件,OTA 更新目标。 - **分区 B(Backup)**:出厂固件或上次稳定版本,作为回滚源。 - **元数据区**:存储启动计数、固件状态标志(如有效/无效)。 Bootloader 在每次上电时检查元数据: 1. 若当前分区固件有效(如 CRC 校验通过),则正常启动。 2. 若无效(如 OTA 写入不完整),则切换启动分区到备份,并递增回滚计数。 ## 回滚触发条件 - **启动失败**:固件崩溃或无法通过 CRC 校验。 - **看门狗超时**:若应用未在设定时间内喂狗,Bootloader 判定启动失败。 - **用户手动触发**:通过特定 GPIO 或命令强制回滚。 ## 分区表规划(以 Arduino Mega 2560 为例) - Flash 总容量:256KB。 - Bootloader 区域:8KB(地址 0x00000 - 0x01FFF)。 - 分区 A:120KB(0x02000 - 0x1DFFF)。 - 分区 B:120KB(0x1E000 - 0x3BFFF)。 - 元数据区:1KB(0x3C000 - 0x3FFFF)。 # 配置步骤 ## 1. 修改 Bootloader 使用 Arduino IDE 或 AVR-GCC 编译自定义 Bootloader。核心逻辑: ```c // bootloader.c 片段 #include #include #include #define APP_A_START 0x02000 #define APP_B_START 0x1E000 #define META_ADDR 0x3C000 // 元数据结构 struct meta_t { uint8_t magic; uint8_t active_partition; uint8_t boot_count; uint8_t reserved; }; void read_meta(struct meta_t *meta) { // 从 EEPROM 或 Flash 读取,此处用 EEPROM 简化 eeprom_read_block(meta, (void*)META_ADDR, sizeof(struct meta_t)); } void write_meta(struct meta_t *meta) { eeprom_write_block(meta, (void*)META_ADDR, sizeof(struct meta_t)); } int check_firmware(uint16_t start_addr) { // 简单 CRC 校验:读取前 4 字节作为长度,计算整个固件 CRC uint16_t length = pgm_read_word(start_addr); uint16_t crc = 0; for (uint16_t i = 0; i < length; i++) { crc += pgm_read_byte(start_addr + 2 + i); } return (crc == pgm_read_word(start_addr + length + 2)); } void boot_main() { struct meta_t meta; read_meta(&meta); if (meta.magic != 0xA5) { // 首次启动,初始化元数据 meta.magic = 0xA5; meta.active_partition = 0; // A 分区 meta.boot_count = 0; write_meta(&meta); } uint16_t app_start = (meta.active_partition == 0) ? APP_A_START : APP_B_START; if (check_firmware(app_start)) { // 固件有效,启动应用 meta.boot_count = 0; write_meta(&meta); // 跳转到应用复位向量 void (*app_reset)(void) = (void (*)(void))pgm_read_word(app_start); app_reset(); } else { // 固件无效,回滚 meta.active_partition = (meta.active_partition == 0) ? 1 : 0; meta.boot_count++; write_meta(&meta); // 重置看门狗并重启 wdt_enable(WDTO_15MS); while(1); } } ``` ## 2. 应用固件支持 OTA 应用需包含 OTA 写入函数,将新固件写入非活动分区,并更新元数据。 ```c // OTA 更新示例(Arduino 代码) #include #include #define META_ADDR 0x3C000 #define APP_A_START 0x02000 #define APP_B_START 0x1E000 struct meta_t { uint8_t magic; uint8_t active_partition; uint8_t boot_count; uint8_t reserved; }; void write_firmware_to_flash(uint16_t start_addr, uint8_t *data, uint16_t len) { // 使用 bootloader 的写入函数,此处省略具体实现 // 注意:需要关闭中断,并调用 boot_page_erase/write 等 } void ota_update(uint8_t *new_firmware, uint16_t len) { struct meta_t meta; EEPROM.get(META_ADDR, meta); // 目标分区为当前活动分区的另一个 uint16_t target_start = (meta.active_partition == 0) ? APP_B_START : APP_A_START; // 擦除并写入新固件 write_firmware_to_flash(target_start, new_firmware, len); // 更新元数据:切换活动分区 meta.active_partition = (meta.active_partition == 0) ? 1 : 0; EEPROM.put(META_ADDR, meta); // 重启设备 void (*reset)(void) = 0; reset(); } ``` ## 3. 编译与烧录 - 使用 Arduino IDE 编译 Bootloader,并通过 ISP 烧录。 - 应用固件需链接到正确地址(如 0x02000),可在 IDE 中设置 `bootloader` 选项。 - 测试时,可故意写入损坏固件验证回滚。 # 完整代码示例 以下是一个简化的 Bootloader 完整代码(基于 Arduino Mega 2560): ```c // CustomBootloader.c #include #include #include #include #define APP_A_START 0x02000 #define APP_B_START 0x1E000 #define META_ADDR 0x3C000 struct meta_t { uint8_t magic; uint8_t active_partition; uint8_t boot_count; uint8_t reserved; }; void read_meta(struct meta_t *meta) { eeprom_read_block(meta, (void*)META_ADDR, sizeof(struct meta_t)); } void write_meta(struct meta_t *meta) { eeprom_write_block(meta, (void*)META_ADDR, sizeof(struct meta_t)); } uint16_t compute_crc(uint16_t start) { uint16_t len = pgm_read_word(start); uint16_t crc = 0; for (uint16_t i = 0; i < len; i++) { crc += pgm_read_byte(start + 2 + i); } return crc; } int check_firmware(uint16_t start) { uint16_t len = pgm_read_word(start); if (len == 0 || len > 0x1D000) return 0; uint16_t crc = compute_crc(start); return (crc == pgm_read_word(start + len + 2)); } void jump_to_app(uint16_t start) { void (*app_reset)(void) = (void (*)(void))pgm_read_word(start); cli(); // 设置栈指针为应用栈顶 asm volatile("ldi r16, 0x00\n\t" "out 0x3E, r16\n\t" "ldi r16, 0x02\n\t" "out 0x3D, r16"); app_reset(); } int main(void) { struct meta_t meta; read_meta(&meta); if (meta.magic != 0xA5) { meta.magic = 0xA5; meta.active_partition = 0; meta.boot_count = 0; write_meta(&meta); } uint16_t app_start = (meta.active_partition == 0) ? APP_A_START : APP_B_START; if (check_firmware(app_start)) { // 固件有效,正常启动 meta.boot_count = 0; write_meta(&meta); jump_to_app(app_start); } else { // 固件无效,回滚 meta.active_partition = (meta.active_partition == 0) ? 1 : 0; meta.boot_count++; write_meta(&meta); wdt_enable(WDTO_15MS); while(1); } return 0; } ``` # 注意事项 - **Flash 磨损**:频繁回滚会擦写 Flash,建议限制回滚次数(如 3 次),超过则进入安全模式。 - **CRC 校验**:确保固件头部包含长度和 CRC,避免误判。 - **看门狗**:应用启动后需及时初始化看门狗,防止 Bootloader 误判。 - **分区大小**:根据实际 Flash 容量调整,确保应用不超过分区大小。 - **测试**:务必在真实硬件上模拟 OTA 失败场景,验证回滚逻辑。 # 总结 通过 Bootloader 实现双分区回滚,能显著提升 Arduino 设备的 OTA 可靠性。本文提供了完整的原理、配置和代码,开发者可根据具体硬件调整。此策略不仅适用于 Arduino,也可推广到其他嵌入式平台,是构建健壮固件更新机制的重要参考。