MCUboot 在 RTOS 下串口升级部分的设计分析

摘要

MCUboot 提供了成熟的镜像管理、签名校验与交换回滚机制,本文基讲解适配架构、串口恢复实现、Flash 抽象、镜像签名与完整性校验

核心架构与文件组织

MCUboot 并非单一的 Bootloader,而是一套镜像管理规范。RT‑Thread 的适配采用"插件式"结构:上游 MCUboot 保持最小改动,平台相关能力通过回调与宏注入。

目录结构深度解析
复制代码
rt-thread/packages/system/mcuboot/
├── boot/
│   └── rtthread/
│       ├── main.c              # 引导入口,负责 RT-Thread 基础组件初始化
│       ├── drv_uart.c          # 适配 RT-Thread 串口设备模型
│       ├── serial_adapter.c    # 串口流控与字节读取适配层
│       └── flash_map_extended.c# 将 RT-Thread 分区表映射给 MCUboot
├── ext/
│   └── tinycbor/               # 极简 CBOR 编解码,SMP 协议的基石
└── boot/
    └── boot_serial/
        └── src/
            ├── serial_recovery.c # 串口恢复模式核心逻辑
            └── boot_serial.c     # SMP 协议命令分发器
设计哲学
  • 解耦与注入 :通过实现一组平台回调(如 flash_area_readflash_area_writeflash_area_eraseconsole_readconsole_write),将 RT‑Thread 的设备模型注入 MCUboot。
  • 最小侵入:尽量不修改上游核心逻辑,便于后续合并与升级。
  • 模块化:串口、Flash、加密、CBOR 各自独立,便于替换或在不同硬件上复用。

硬件适配层 深度实现

在 RT‑Thread 平台上,直接操作寄存器会降低可移植性。适配层采用 rt_device 抽象并结合信号量实现高效、可控的串口 I/O。

串口适配与信号量机制
c 复制代码
struct rt_serial_adapter {
    rt_device_t dev;
    struct rt_semaphore rx_sem;
};

static struct rt_serial_adapter _adapter;

// 串口接收回调
static rt_err_t uart_rx_ind(rt_device_t dev, rt_size_t size) {
    rt_sem_release(&_adapter.rx_sem); // 唤醒等待读取的线程
    return RT_EOK;
}

// 带超时的阻塞读取,确保状态机不会永久挂起
int serial_adapter_read(void *data, int len, int timeout_ms) {
    rt_size_t read_len = 0;
    rt_tick_t start_tick = rt_tick_get();

    while (read_len < len) {
        // 尝试从设备读取
        rt_size_t rc = rt_device_read(_adapter.dev, 0, (uint8_t *)data + read_len, len - read_len);
        if (rc > 0) {
            read_len += rc;
            continue;
        }

        // 计算剩余时间并等待信号量
        rt_tick_t elapsed = rt_tick_get() - start_tick;
        if (elapsed >= rt_tick_from_millisecond(timeout_ms)) break;
        rt_sem_take(&_adapter.rx_sem, rt_tick_from_millisecond(timeout_ms) - elapsed);
    }
    return read_len;
}
关键实现要点
  • 回调注册 :在 drv_uart.c 中注册接收回调,回调仅做信号量释放,避免在中断上下文做复杂处理。
  • 超时控制:读取函数支持超时参数,防止状态机在等待数据时永久阻塞。
  • DMA 与中断兼容:适配层应检测串口驱动是否使用 DMA,针对不同模式调整读取策略与缓冲管理。

协议层 SMP 与 CBOR 解析

MCUboot 的串口恢复模式使用 SMP 协议,负载采用 CBOR 编码。协议层负责报文解析、命令分发与响应生成。

为什么选择 CBOR

  • 体积小:比 JSON 更紧凑,节省传输时间。
  • 流式解析:支持边接收边解析,降低内存峰值占用。
  • 类型安全:解析器能严格校验类型,降低注入风险。

SMP 报文结构与解析流程

  • 报文结构:8 字节 SMP 头部 + 可变长度 CBOR 负载。头部包含操作码、序列号、负载长度等。
  • 解析流程:读取头部 → 校验长度 → 读取负载 → 使用 TinyCBOR 解析 → 调用命令处理函数 → 生成响应。
处理示例(伪代码)
c 复制代码
static int handle_upload(struct serial_recovery_ctx *ctx) {
    struct boot_serial_upload_req req;
    int rc;

    rc = boot_serial_parse_upload(ctx->payload, ctx->payload_len, &req);
    if (rc != 0) return BOOT_SERIAL_ERR_BAD_PAYLOAD;

    if (req.off != ctx->write_offset) {
        return BOOT_SERIAL_ERR_INVALID_OFFSET;
    }

    rc = flash_area_write(ctx->flash_area, req.off, req.data, req.len);
    if (rc == 0) {
        ctx->write_offset += req.len;
        return send_upload_response(ctx, ctx->write_offset);
    }
    return BOOT_SERIAL_ERR_FLASH_ERROR;
}

鲁棒状态机 设计与实现

在不可靠的串口环境下,状态机的鲁棒性决定了升级流程的稳定性。MCUboot 在 serial_recovery.c 中实现了四态状态机。

状态机图示

匹配到起始字节 0x06 0x09
长度合法
长度超限
数据完整接收
接收超时
处理命令并回传 ACK
CRC 错误
STATE_INIT
STATE_HEADER
STATE_PAYLOAD
STATE_CRC

状态机实现要点

  • 起始标识检测:在 INIT 阶段使用滑动窗口检测起始字节,避免误匹配。
  • 长度校验 :在 HEADER 阶段立即校验 expected_len,超过 CONFIG_BOOT_MAX_LINE_BUF 则丢弃并复位。
  • 超时与重试:在 PAYLOAD 阶段设置合理超时,超时后回到 INIT 并记录错误次数,连续多次失败可触发更严格的重同步策略。
  • CRC 与完整性:在 CRC 阶段做完整性校验,校验通过才交付业务处理。

Flash 抽象与镜像管理

MCUboot 的镜像管理依赖于对 Flash 的抽象与槽位设计。RT‑Thread 适配层需要将平台分区表映射给 MCUboot。

分区布局建议

  • Primary Slot:当前运行镜像。
  • Secondary Slot:待升级镜像。
  • Scratch:用于 swap 临时存放(在 swap-using-scratch 模式下)。
  • Metadata 区 :存放 swap_info、image trailer、TLV 等元数据。

写入原子性与交换策略

  • swap_info 记录进度:每次交换一个 sector 或 page 后更新进度标志,保证断电恢复能力。
  • swap 模式选择 :根据 Flash 特性选择 swap-using-scratchswap-using-move
  • 写入对齐与擦除管理:处理跨页写入、对齐与擦除单元,避免写入失败导致数据损坏。
写入示例(伪代码)
c 复制代码
int flash_area_write(struct flash_area *fa, uint32_t off, const void *data, uint32_t len) {
    // 确保擦除并对齐
    if (!is_erased(fa, off, len)) {
        if (flash_erase(fa, off, len) != 0) return -1;
    }
    if (flash_program(fa, off, data, len) != 0) return -1;
    return 0;
}

镜像完整性与安全签名实现(深度)

镜像签名与完整性校验是防止恶意固件与回滚攻击的核心。RT‑Thread 适配需要在资源受限环境下实现高可靠的验签流程。

签名验证完整流程

  1. TLV 定位:读取镜像尾部 TLV,定位签名与证书。
  2. 哈希计算:对镜像 payload 计算 SHA256(或其他哈希算法)。
  3. 验签:使用预埋公钥对签名进行验证(支持 ECDSA‑P256、RSA2048 等)。
  4. 版本检查 :比较 image_version,拒绝低版本镜像以防回滚。
  5. 标记激活 :验签通过后设置 image_okboot_swap_info,等待下一次重启或立即激活。

资源受限平台的优化策略

  • 选择轻量加密库 :在内存受限设备上优先使用 tinycrypt 或硬件加速模块,避免引入完整的 mbedTLS。
  • 分段哈希与增量验签:对大镜像采用分段哈希,边写入边计算哈希,最终合并,避免一次性分配大内存。
  • 硬件加速利用:在支持的 MCU 上启用 HASH、PKA 或专用加密引擎,显著缩短验签时间。
  • 延迟验签策略:在上传过程中做分段完整性校验,最终在交换前做完整验签,平衡用户体验与安全性。

断电恢复与回滚细节

  • 原子标志 :使用 boot_swap_infoimage_ok 等原子标志记录交换状态。
  • 重启检测:启动时检查标志并决定继续交换或回滚。
  • 失败处理:若签名失败或镜像不完整,自动回滚到上一个可用镜像并记录错误码以便诊断。

安全增强建议

  • 证书链与撤销:支持证书链验证与证书撤销列表(CRL)或在线证书状态协议(OCSP)以提升长期安全。
  • 密钥保护:将私钥保存在安全元件或使用硬件密钥存储,避免私钥泄露。
  • 审计与上报:记录升级事件、签名验证结果与错误码,便于事后审计与远程诊断。

CI/CD 与 自动化回归测试

要把 Bootloader 推向生产,必须把测试与验证自动化。CI/CD 能把串口恢复、签名校验、交换回滚等关键路径纳入持续验证。

自动化打包与签名流水线

  • 脚本化镜像生成:CI 脚本自动生成镜像、填充头部元数据、生成 TLV 并签名。
  • 密钥管理:在 CI 中使用受控密钥库或密钥管理服务,避免私钥泄露。
  • 制品管理:将签名镜像上传到制品库并记录版本信息与构建元数据。
示例流水线步骤
  1. 编译固件并生成二进制。
  2. 计算哈希并生成 TLV。
  3. 使用私钥签名并将签名写入镜像。
  4. 将镜像上传到制品库并触发测试套件。

串口仿真与硬件在环测试

  • 虚拟串口(PTY)测试:在 CI 中使用 PTY 模拟串口,上位机脚本通过 PTY 发送分片数据,Bootloader 在模拟环境中运行。
  • QEMU 模拟:对支持的 MCU 使用 QEMU 运行 Bootloader,结合虚拟串口进行端到端测试。
  • 硬件在环(HIL):在关键版本引入真实硬件测试台,模拟电源抖动、丢包、坏块等极端场景。

测试用例建议

  • 正常流程:完整上传、验签、交换并启动新镜像。
  • 丢包重传:随机丢弃分片,验证偏移校验与重传逻辑。
  • 错位与噪声:插入额外字节或错位起始标记,验证状态机恢复能力。
  • 签名失败:上传篡改镜像,验证验签拒绝并回滚。
  • 断电恢复:在交换中模拟断电,重启后验证继续交换或回滚。

覆盖率与质量门禁

  • 关键路径覆盖 :确保 handle_uploadflash_area_writeboot_swap 等函数被自动化测试覆盖。
  • 覆盖率门禁:在 PR 合并前运行完整测试套件,若关键测试失败则阻止合并。
  • 日志与报告:CI 生成详细测试报告并在失败时自动通知相关人员。

附录 代码片段 与 测试示例

串口读取带超时的实现

c 复制代码
int serial_adapter_read(void *data, int len, int timeout_ms) {
    rt_size_t read_len = 0;
    rt_tick_t start_tick = rt_tick_get();

    while (read_len < len) {
        rt_size_t rc = rt_device_read(_adapter.dev, 0, (uint8_t *)data + read_len, len - read_len);
        if (rc > 0) {
            read_len += rc;
            continue;
        }

        rt_tick_t elapsed = rt_tick_get() - start_tick;
        if (elapsed >= rt_tick_from_millisecond(timeout_ms)) break;

        rt_sem_take(&_adapter.rx_sem, rt_tick_from_millisecond(timeout_ms) - elapsed);
    }
    return read_len;
}

SMP 上传处理示例

c 复制代码
static int handle_upload(struct serial_recovery_ctx *ctx) {
    struct boot_serial_upload_req req;
    int rc;

    rc = boot_serial_parse_upload(ctx->payload, ctx->payload_len, &req);
    if (rc != 0) return BOOT_SERIAL_ERR_BAD_PAYLOAD;

    if (req.off != ctx->write_offset) {
        return BOOT_SERIAL_ERR_INVALID_OFFSET;
    }

    rc = flash_area_write(ctx->flash_area, req.off, req.data, req.len);
    if (rc == 0) {
        ctx->write_offset += req.len;
        return send_upload_response(ctx, ctx->write_offset);
    }
    return BOOT_SERIAL_ERR_FLASH_ERROR;
}

状态机主循环示例

c 复制代码
void boot_serial_handler(struct serial_driver *driver) {
    struct serial_recovery_ctx ctx = { .state = STATE_INIT };
    
    while (1) {
        switch (ctx.state) {
            case STATE_INIT:
                if (search_packet_header(driver)) {
                    ctx.state = STATE_HEADER;
                }
                break;

            case STATE_HEADER:
                if (driver->read(ctx.hdr_buf, SMP_HDR_SIZE, 100) == SMP_HDR_SIZE) {
                    ctx.expected_len = decode_payload_len(ctx.hdr_buf);
                    if (ctx.expected_len <= CONFIG_BOOT_MAX_LINE_BUF) {
                        ctx.state = STATE_PAYLOAD;
                    } else {
                        ctx.state = STATE_INIT;
                    }
                }
                break;

            case STATE_PAYLOAD:
                if (driver->read(ctx.payload, ctx.expected_len, 500) == ctx.expected_len) {
                    ctx.state = STATE_CRC;
                }
                break;

            case STATE_CRC:
                if (check_crc(ctx.payload, ctx.expected_len)) {
                    process_smp_command(&ctx);
                }
                ctx.state = STATE_INIT;
                break;
        }
    }
}
相关推荐
coolwaterld8 天前
Matter over Thread 在线更新 固件OTA
ota·matter·nordic
Lester_110115 天前
单片机 IAP(可实现OTA), ISP, ICP
单片机·嵌入式硬件·iap·ota
小曹要微笑1 个月前
ESP32-S3 OTA 解析(纯技术干货版)
esp32·esp32s3·ota
一枝小雨1 个月前
【OTA专题】12 APP中移植EEprom、W25Q驱动
stm32·单片机·嵌入式·freertos·ota·bootloader
一枝小雨1 个月前
【OTA专题】11 进一步优化OTA后台无感下载架构
stm32·单片机·架构·嵌入式·freertos·ota·bootloader
一枝小雨1 个月前
9 更进一步的 bootloader 架构设计
stm32·单片机·嵌入式·软件架构·ota·bootloader·aes加密
一枝小雨1 个月前
7 App代码转AES加密文件生成步骤
stm32·单片机·嵌入式·aes·ota·bootloader·加密升级
一枝小雨2 个月前
【OTA专题】2 初级bootloader架构和基础工程移植
stm32·单片机·嵌入式·ota·bootloader·固件升级·加密升级
天堂陌客6 个月前
RK3562 OTA 方法
qt·ota·rk3562