# 引言 OTA(Over-The-Air)升级是物联网设备的核心功能,但网络不稳定、固件损坏或断电常导致升级失败。若 bootloader 不具备校验与回滚能力,设备将变砖。本文以 Arduino(如 ESP32/STM32)为例,设计一个带 CRC 校验和双分区回滚机制的自定义 bootloader,确保升级失败时自动恢复旧固件。 # 原理:双分区与 CRC 校验 ## 双分区布局 将 Flash 划分为两个应用区: - **App A**(当前运行区) - **App B**(待升级区) Bootloader 根据标志位决定启动哪个分区。升级时,新固件写入 App B,校验通过后切换标志位;若失败,则保持 App A 运行。 ## CRC 校验 CRC(循环冗余校验)用于检测数据完整性。常见 CRC32 算法,计算固件二进制文件的校验值,存储在固件头部或单独区域。Bootloader 在启动或升级后校验,若 CRC 不匹配则触发回滚。 ## 回滚机制 - **标志位**:在 Flash 的独立区域(如 EEPROM 或专用扇区)存储当前启动分区和升级状态。 - **状态机**: - `STATE_NORMAL`:正常启动 App A。 - `STATE_UPDATE_PENDING`:新固件已写入 App B,待校验。 - `STATE_UPDATE_SUCCESS`:校验通过,切换启动分区。 - `STATE_ROLLBACK`:校验失败,回滚至 App A。 # 配置步骤 ## 1. 定义 Flash 分区 以 ESP32 为例,在 `partitions.csv` 中定义: ```csv # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, app_a, app, factory, 0x10000, 0x200000, app_b, app, ota_1, 0x210000, 0x200000, storage, data, 0x01, 0x410000, 0x10000, ``` ## 2. Bootloader 设计 Bootloader 负责: - 读取标志位。 - 校验 App B 的 CRC(若处于更新状态)。 - 根据结果跳转至相应分区。 ## 3. 应用层 OTA 流程 应用层接收新固件,写入 App B,计算 CRC 并存储,然后设置标志位并重启。 # 完整代码示例 以下为简化版 bootloader 核心逻辑(以 ESP32 为例,使用 Arduino 框架): ```c #include #include #include #include // 标志位存储地址(使用 NVS 或自定义分区) #define FLAG_ADDR 0x410000 enum BootState { NORMAL = 0, UPDATE_PENDING = 1, UPDATE_SUCCESS = 2, ROLLBACK = 3 }; void setup() { Serial.begin(115200); delay(100); // 读取标志位 uint32_t flag; esp_partition_read(&boot_partition, FLAG_ADDR, &flag, sizeof(flag)); if (flag == UPDATE_PENDING) { // 校验 App B 的 CRC const esp_partition_t* app_b = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); if (app_b == NULL) { Serial.println("App B not found!"); rollback(); return; } // 读取固件头部中的 CRC 值(假设存储在偏移 0 处) uint32_t stored_crc; esp_partition_read(app_b, 0, &stored_crc, sizeof(stored_crc)); // 计算实际 CRC(跳过头部 4 字节) uint32_t calc_crc = calculate_crc(app_b, 4, app_b->size - 4); if (calc_crc == stored_crc) { Serial.println("CRC OK, switching to App B"); flag = UPDATE_SUCCESS; esp_partition_write(&boot_partition, FLAG_ADDR, &flag, sizeof(flag)); esp_ota_set_boot_partition(app_b); esp_restart(); } else { Serial.println("CRC failed, rolling back"); rollback(); } } else if (flag == UPDATE_SUCCESS) { // 正常启动 App B,但可再次校验(可选) // 此处直接启动 } else { // NORMAL 状态,启动 App A } } void loop() { // Bootloader 不运行 loop } uint32_t calculate_crc(const esp_partition_t* part, size_t offset, size_t len) { CRC32 crc; uint8_t buf[128]; size_t remaining = len; size_t pos = offset; while (remaining > 0) { size_t to_read = min(remaining, sizeof(buf)); esp_partition_read(part, pos, buf, to_read); crc.update(buf, to_read); pos += to_read; remaining -= to_read; } return crc.finalize(); } void rollback() { // 设置回滚标志,并启动 App A uint32_t flag = ROLLBACK; esp_partition_write(&boot_partition, FLAG_ADDR, &flag, sizeof(flag)); const esp_partition_t* app_a = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL); esp_ota_set_boot_partition(app_a); esp_restart(); } ``` # 应用层 OTA 写入示例 ```c // 假设通过 HTTP 接收固件 void perform_ota() { const esp_partition_t* app_b = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); esp_ota_handle_t ota_handle; esp_ota_begin(app_b, OTA_SIZE_UNKNOWN, &ota_handle); // 写入固件数据(循环接收) while (receive_data(buf, len)) { esp_ota_write(ota_handle, buf, len); } // 计算 CRC 并写入固件头部(假设头部预留 4 字节) uint32_t crc = calculate_crc_from_received_data(); esp_partition_write(app_b, 0, &crc, sizeof(crc)); // 设置标志位为 UPDATE_PENDING uint32_t flag = UPDATE_PENDING; esp_partition_write(&boot_partition, FLAG_ADDR, &flag, sizeof(flag)); esp_ota_end(ota_handle); esp_restart(); } ``` # 注意事项 - **分区大小**:App A 和 App B 必须大小一致,且足够容纳最大固件。 - **CRC 存储位置**:建议放在固件头部,但需确保 bootloader 能读取到,且不影响固件启动(可跳过头部)。 - **标志位原子性**:写入标志位时,建议先擦除再写入,并考虑使用双备份或校验和防止标志位损坏。 - **断电保护**:在写入 App B 过程中断电,可能导致分区不完整。可增加“写入完成”标志,bootloader 检测到未完成时直接回滚。 - **测试**:务必在真实硬件上模拟升级失败(如中断传输、篡改数据)验证回滚。 - **启动超时**:若 App B 启动后崩溃,bootloader 应具备看门狗或超时机制,自动回滚至 App A。 # 结语 通过自定义 bootloader 实现 CRC 校验和双分区回滚,能显著提升 OTA 的可靠性。本文提供的方案适用于 Arduino 及类似平台,核心思想可移植至其他 MCU。记住,健壮的升级机制是产品稳定性的基石。