引言

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 启动流程

  1. Bootloader 读取元数据状态。
  2. 若状态为 STATE_NEW_VERIFIED,则计算 App B 区 CRC,若通过,则跳转至 App B,并将状态改为 STATE_NEW_RUNNING
  3. 若状态为 STATE_NEW_RUNNING,检查应用是否设置了“心跳”标志(见下文)。若超时未确认,则回滚。
  4. 若状态为 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 可靠性。本文提供的分区策略、状态机及代码框架可直接应用于实际项目。记住,嵌入式升级无小事,细节决定成败。