引言
OTA(Over-The-Air)升级在物联网设备中至关重要,但 Arduino 默认的 Bootloader(如 Optiboot)仅支持串口烧录,无法直接处理无线固件更新。自定义 Bootloader 需承担接收固件、校验完整性、切换应用及异常回滚等任务。其中,CRC(循环冗余校验)确保固件数据无损坏,而回滚机制则保证新固件无法启动时能恢复旧版本。本文基于 STM32 或 AVR 类 Arduino 板,聚焦设计细节。
1. 系统分区布局
自定义 Bootloader 将 Flash 划分为四个区域:
- Bootloader 区:存放 Bootloader 代码,通常占用最小空间(如 8KB)。
- App A 区:当前运行的应用固件。
- App B 区:备用应用区,用于存放新下载的固件或作为回滚副本。
- 元数据区:存储固件版本、CRC 值、状态标志等,独立于应用区,避免被覆盖。
分区地址需在链接脚本中定义,例如 STM32F103 的布局:
#define BOOTLOADER_START 0x08000000
#define APP_A_START 0x08002000 // 8KB 后
#define APP_B_START 0x08010000 // 64KB 后
#define META_START 0x0801F800 // 最后 2KB
2. CRC 校验设计
2.1 算法选择
CRC32(如 CRC-32/MPEG-2)比 CRC16 更可靠,但计算开销稍高。对于 Arduino 的 8 位 MCU,CRC16 足够;对于 32 位 MCU,推荐 CRC32。使用查表法可提升速度。
2.2 校验范围与时机
- 下载时:分块接收固件,每块计算 CRC,最后合并,但更简单的是整体计算。
- 启动时:Bootloader 在跳转前计算 App 区 CRC,与元数据中存储的 CRC 比对。
2.3 代码示例(基于 STM32 硬件 CRC)
#include "stm32f1xx_hal.h"
uint32_t calculate_crc(uint32_t *data, uint32_t len) {
__HAL_RCC_CRC_CLK_ENABLE();
CRC->CR = CRC_CR_RESET;
for (uint32_t i = 0; i < len; i++) {
CRC->DR = data[i];
}
return CRC->DR;
}
// 在接收完固件后调用
bool verify_firmware(uint32_t app_start, uint32_t app_size, uint32_t expected_crc) {
uint32_t *fw = (uint32_t *)app_start;
uint32_t real_crc = calculate_crc(fw, app_size / 4);
return (real_crc == expected_crc);
}
注意:若 MCU 无硬件 CRC,需软件实现,例如使用查表法。
3. 回滚机制设计
3.1 状态标志管理
在元数据区定义状态字节,常见状态:
-
STATE_OLD_APP:当前运行旧固件,正常。 -
STATE_NEW_DOWNLOADED:新固件已下载但未验证。 -
STATE_NEW_VERIFIED:新固件通过 CRC 校验,待启动。 -
STATE_NEW_RUNNING:新固件已启动,等待确认。 -
STATE_ROLLBACK:需要回滚。
3.2 启动流程
- Bootloader 读取元数据状态。
- 若状态为
STATE_NEW_VERIFIED,则计算 App B 区 CRC,若通过,则跳转至 App B,并将状态改为STATE_NEW_RUNNING。 - 若状态为
STATE_NEW_RUNNING,检查应用是否设置了“心跳”标志(见下文)。若超时未确认,则回滚。 - 若状态为
STATE_ROLLBACK,则复制 App A 区到 App B 区(或直接跳转 App A),并恢复状态。
3.3 应用确认机制
新应用启动后,应主动向 Bootloader 报告“运行正常”。可通过在元数据区写入一个确认标志,或使用看门狗定时器。
// 在应用代码中,初始化后调用
extern void mark_app_ok(void);
void setup() {
// ... 外设初始化
mark_app_ok(); // 写入 STATE_OLD_APP 或清除待回滚标志
}
Bootloader 侧实现:
void check_and_rollback(void) {
if (meta.state == STATE_NEW_RUNNING) {
if (meta.confirm_flag != 0x5A) {
// 未确认,回滚
rollback_to_old();
} else {
meta.state = STATE_OLD_APP;
save_meta();
}
}
}
4. 完整示例:Bootloader 主逻辑
以下为简化版 Bootloader 主循环,演示 CRC 验证与回滚决策:
#include <string.h>
#include "flash_if.h"
#include "crc32.h"
typedef struct {
uint32_t app_a_crc;
uint32_t app_b_crc;
uint8_t state;
uint8_t confirm_flag;
} meta_t;
meta_t meta;
void bootloader_main(void) {
load_meta(&meta);
if (meta.state == STATE_NEW_DOWNLOADED) {
// 验证 App B
if (verify_firmware(APP_B_START, APP_SIZE, meta.app_b_crc)) {
meta.state = STATE_NEW_VERIFIED;
save_meta(&meta);
} else {
// CRC 失败,丢弃新固件,回滚
meta.state = STATE_ROLLBACK;
save_meta(&meta);
}
}
if (meta.state == STATE_NEW_VERIFIED) {
// 跳转至 App B
jump_to_app(APP_B_START);
} else if (meta.state == STATE_NEW_RUNNING) {
// 检查确认标志
if (meta.confirm_flag != 0xA5) {
rollback_to_app_a();
} else {
meta.state = STATE_OLD_APP;
save_meta(&meta);
jump_to_app(APP_A_START);
}
} else {
// 默认跳转 App A
jump_to_app(APP_A_START);
}
}
void rollback_to_app_a(void) {
// 可选:将 App A 复制到 App B,或直接跳转
meta.state = STATE_OLD_APP;
meta.confirm_flag = 0;
save_meta(&meta);
jump_to_app(APP_A_START);
}
5. 注意事项
- Flash 磨损均衡:频繁写入元数据会损耗 Flash,建议使用双备份或磨损均衡算法。
- 中断向量重映射:应用区起始地址需设置 VTOR(向量表偏移),否则中断异常。
- 固件签名:CRC 仅防随机错误,不防恶意篡改。若需安全,应增加 RSA/ECDSA 签名。
- 看门狗:在跳转前启动独立看门狗,防止应用卡死导致无法回滚。
- 测试回滚:务必在开发阶段模拟固件损坏,验证回滚路径。
结语
通过精心设计的 CRC 校验与回滚机制,自定义 Bootloader 能显著提升 Arduino 设备的 OTA 可靠性。本文提供的分区策略、状态机及代码框架可直接应用于实际项目。记住,嵌入式升级无小事,细节决定成败。