基于 RT-Thread 的 OTA 升级中 Bootloader 与 App 分区校验的容错设计

1. 为什么需要分区校验?

OTA(Over-The-Air)升级是物联网设备的关键能力,但升级过程可能因断电、传输错误或写入异常导致固件损坏。若缺乏校验机制,设备将变砖。因此,Bootloader 在跳转 App 前必须验证 App 分区的完整性,同时 App 在运行中也要校验自身或备份分区,以支持回滚。RT-Thread 提供了灵活的 Flash 分区管理(FAL)和软件包(如 OTA 组件),但容错设计仍需开发者根据硬件和应用场景定制。

2. 容错设计核心原则

  • 双分区备份:至少保留两个 App 分区(A/B 槽),当前运行一个,另一个用于接收新固件。
  • 校验机制:使用 CRC32 或 SHA256 对固件镜像进行校验,确保数据完整。
  • 状态标记:在 Flash 中记录升级状态(如待升级、升级完成、校验失败),便于 Bootloader 决策。
  • 回滚策略:若新固件启动失败(如看门狗超时),自动回退到旧版本。
  • 异常处理:任何校验失败或超时均需安全退出,不破坏现有固件。

3. 基于 RT-Thread 的实现方案

3.1 硬件与软件环境

  • MCU:STM32F407(Cortex-M4,2MB Flash)
  • RT-Thread 版本:4.1.0
  • 使用 FAL 管理 Flash 分区,配置如下(rtconfig.h 或 fal_cfg.h):
// fal_cfg.h
#define FAL_PART_TABLE \
{                            \
    {FAL_PART_MAGIC_WROD, "bootloader", "onchip_flash", 0, 128*1024, 0}, \
    {FAL_PART_MAGIC_WROD, "app",        "onchip_flash", 128*1024, 1024*1024, 0}, \
    {FAL_PART_MAGIC_WROD, "app_backup", "onchip_flash", 1152*1024, 1024*1024, 0}, \
    {FAL_PART_MAGIC_WROD, "download",   "onchip_flash", 2176*1024, 512*1024, 0}, \
}

3.2 Bootloader 设计

Bootloader 负责启动流程,核心逻辑:

  1. 检查升级标志(如某个 Flash 地址的魔数)。
  2. 若存在待升级固件,校验其 CRC;若通过,则擦除 app 分区并写入新固件,更新标志。
  3. 校验 app 分区当前固件(CRC 或版本号),若失败则尝试从 backup 分区恢复。
  4. 跳转至 App 入口。

关键代码示例(基于 RT-Thread 的 FAL 和 CRC 软件包):

#include <rtthread.h>
#include <fal.h>
#include <crc32.h>

#define APP_PART_NAME "app"
#define BACKUP_PART_NAME "app_backup"
#define DOWNLOAD_PART_NAME "download"
#define FLAG_ADDR 0x08000000  // 假设在 bootloader 分区末尾存储标志

static int check_partition_crc(const char* part_name) {
    struct fal_part *part = fal_part_find(part_name);
    if (!part) return -1;
    // 读取分区数据,计算 CRC(此处简化,实际需分段读取)
    rt_uint32_t crc = 0;
    rt_uint8_t buf[1024];
    rt_uint32_t offset = 0;
    rt_uint32_t len = part->len;
    while (offset < len) {
        rt_uint32_t read_len = (len - offset) > sizeof(buf) ? sizeof(buf) : (len - offset);
        if (fal_part_read(part, offset, buf, read_len) != read_len) return -1;
        crc = crc32_update(crc, buf, read_len);
        offset += read_len;
    }
    // 假设固件末尾 4 字节存储 CRC 值
    rt_uint32_t stored_crc;
    fal_part_read(part, len - 4, &stored_crc, 4);
    return (crc == stored_crc) ? 0 : -1;
}

void bootloader_main(void) {
    // 检查升级标志(例如 0xA5A5 表示有待升级固件)
    if (*(volatile rt_uint32_t *)FLAG_ADDR == 0xA5A5) {
        if (check_partition_crc(DOWNLOAD_PART_NAME) == 0) {
            // 擦除 app 并写入新固件
            fal_part_erase_all(fal_part_find(APP_PART_NAME));
            // 复制 download 分区到 app 分区(省略具体复制代码)
            // 更新标志为 0
            *(volatile rt_uint32_t *)FLAG_ADDR = 0;
        } else {
            // 下载固件损坏,清除标志,继续启动旧版本
            *(volatile rt_uint32_t *)FLAG_ADDR = 0;
        }
    }
    // 校验 app 分区,若失败尝试从 backup 恢复
    if (check_partition_crc(APP_PART_NAME) != 0) {
        if (check_partition_crc(BACKUP_PART_NAME) == 0) {
            // 恢复备份
            fal_part_erase_all(fal_part_find(APP_PART_NAME));
            // 复制 backup 到 app(省略)
        } else {
            // 双分区都坏,进入紧急模式(如串口等待)
            rt_kprintf("Firmware corrupt!\n");
            while(1);
        }
    }
    // 跳转到 App(需获取 app 分区起始地址)
    void (*app_entry)(void) = (void (*)(void))(fal_part_find(APP_PART_NAME)->offset + 0x08000000);
    app_entry();
}

3.3 App 端设计

App 端负责接收新固件并触发升级,同时需具备自校验和回滚触发机制。

  • 升级流程

    1. 从网络或外设接收固件,写入 download 分区。
    2. 计算 CRC 并附加到固件末尾。
    3. 设置升级标志(如写入 bootloader 分区特定地址)。
    4. 软件复位,进入 Bootloader。
  • 自校验与回滚:App 启动后,启动一个看门狗(如 30 秒),若系统正常运行则喂狗,并清除回滚标志;若未及时喂狗,看门狗复位后 Bootloader 检测到回滚标志,自动从 backup 恢复。

代码示例(App 端触发升级):

#include <rtthread.h>
#include <fal.h>
#include <crc32.h>

#define DOWNLOAD_PART_NAME "download"
#define FLAG_ADDR 0x08000000  // 与 Bootloader 一致

extern void system_reset(void);

void ota_upgrade(const rt_uint8_t *fw_data, rt_uint32_t fw_len) {
    struct fal_part *dl_part = fal_part_find(DOWNLOAD_PART_NAME);
    if (!dl_part || fw_len > dl_part->len - 4) {
        rt_kprintf("Invalid firmware size\n");
        return;
    }
    // 写入固件数据
    fal_part_erase_all(dl_part);
    fal_part_write(dl_part, 0, fw_data, fw_len);
    // 计算 CRC 并写入末尾
    rt_uint32_t crc = crc32_calc(fw_data, fw_len);
    fal_part_write(dl_part, fw_len, &crc, 4);
    // 设置升级标志
    *(volatile rt_uint32_t *)FLAG_ADDR = 0xA5A5;
    // 复位
    system_reset();
}

4. 注意事项

  • Flash 磨损均衡:频繁擦写同一分区会缩短 Flash 寿命,建议使用磨损均衡算法或限制升级次数。
  • 原子操作:设置升级标志时,确保写入操作是原子的(如使用 Flash 编程的原子性),避免半写状态。
  • 看门狗:在 Bootloader 中也要启用看门狗,防止升级过程卡死。
  • 版本兼容:App 和 Bootloader 的通信协议(如标志地址)需固定,升级 Bootloader 时需谨慎。
  • 测试覆盖:模拟断电、传输错误、校验失败等场景,验证回滚机制。

5. 总结

通过合理的分区划分、CRC 校验、状态标志和回滚机制,基于 RT-Thread 的 OTA 升级可以做到高可靠性。本文提供的设计思路和代码示例可直接应用于实际项目,但需根据具体硬件调整 Flash 地址和分区大小。容错设计的本质是“假设一切都会出错”,并提前准备应对方案,这样才能保证设备在恶劣环境下依然稳定运行。