Arduino OTA 升级失败后的安全回滚:CRC 校验与 Bootloader 设计实战
👁 1 阅读 · 2026-08-27 · 嵌入式
在 Arduino 等嵌入式平台上,OTA 升级虽便捷,但传输错误或校验失败可能导致设备变砖。本文深入探讨如何在自定义 bootloader 中实现 CRC 校验,并设计一套可靠的失败回滚机制。通过双分区存储与标志位管理,确保升级失败时系统能自动恢复至旧固件,保障设备持续可用。内容涵盖原理剖析、具体配置步骤、完整代码示例及关键注意事项,助你构建健壮的 OTA 系统。
# 引言
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。记住,健壮的升级机制是产品稳定性的基石。