基于 RT-Thread 的 OTA 升级中 Bootloader 与 App 共享 RAM 的边界设计

1. 为什么需要共享 RAM 边界设计?

在 OTA 升级流程中,Bootloader 负责接收固件、写入 Flash,而 App 是实际运行的应用。两者通常独立编译,但共用同一块物理 RAM。若边界设计不当,可能出现以下问题:

  • 栈溢出冲突:Bootloader 的栈指针(SP)在跳转前未复位,App 启动时可能覆盖 Bootloader 的临时数据。
  • 堆区重叠:Bootloader 使用 malloc 分配的内存区域与 App 的静态变量或堆区重叠,导致数据损坏。
  • 中断向量表错位:App 的中断服务函数(ISR)访问的 RAM 地址与 Bootloader 的保留区域冲突。

因此,必须通过链接脚本和运行时约定,明确划分 RAM 的归属区域,确保升级过程中数据安全。

2. 原理讲解:RAM 分区模型

以 STM32F407 为例,其 RAM 总大小为 128KB(0x20000000 - 0x2001FFFF)。我们将其划分为三个区域:

  • Bootloader 专用区:起始地址 0x20000000,大小 16KB,存放 Bootloader 的栈、堆和全局变量。
  • 共享缓冲区:起始地址 0x20004000,大小 32KB,用于升级过程中暂存固件数据(例如从网络接收的数据包)。
  • App 专用区:起始地址 0x2000C000,大小 64KB,存放 App 的栈、堆和全局变量。

边界设计的关键在于:

  • 静态划分:通过链接脚本(.ld 文件)为 Bootloader 和 App 分别指定 RAM 起始地址和长度。
  • 动态约定:在跳转前,Bootloader 必须复位 SP 和 PC,并确保共享缓冲区的内容在 App 启动前被清空或标记为无效。

RT-Thread 作为实时操作系统,其内核对象(如线程栈、信号量)会占用大量 RAM,因此边界设计需考虑 RT-Thread 的内存池配置。

3. 配置步骤:基于 RT-Thread 的实现

3.1 修改链接脚本

Bootloader 的链接脚本(如 link.lds)中,定义 RAM 区域:

MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
    RAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 16K
}

App 的链接脚本中,定义 RAM 区域:

MEMORY
{
    FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 960K
    RAM (xrw)  : ORIGIN = 0x2000C000, LENGTH = 64K
}

注意:App 的 Flash 起始地址需与 Bootloader 的跳转地址匹配,RAM 起始地址必须避开共享缓冲区。

3.2 配置 RT-Thread 内存堆

RT-Thread 使用 rt_system_heap_init 初始化堆,需在启动代码中指定堆的起始和结束地址。在 App 的 board.c 中:

#define HEAP_BEGIN  (0x2000C000 + 0x1000)  // 避开栈顶区域
#define HEAP_END    (0x2001FFFF)

void rt_hw_board_init(void)
{
    rt_system_heap_init((void*)HEAP_BEGIN, (void*)HEAP_END);
    // 其他初始化
}

3.3 Bootloader 跳转前的 RAM 清理

在 Bootloader 中,跳转到 App 前,需要复位 SP 并清理共享缓冲区:

#define APP_FLASH_ADDR  0x08008000
#define SHARED_RAM_ADDR 0x20004000
#define SHARED_RAM_SIZE 0x8000

typedef void (*pFunction)(void);

void jump_to_app(void)
{
    uint32_t app_sp = *(volatile uint32_t*)APP_FLASH_ADDR;
    pFunction app_pc = (pFunction)(*(volatile uint32_t*)(APP_FLASH_ADDR + 4));

    // 清空共享缓冲区,防止残留数据
    memset((void*)SHARED_RAM_ADDR, 0, SHARED_RAM_SIZE);

    // 设置主栈指针
    __set_MSP(app_sp);

    // 跳转
    app_pc();
}

3.4 共享缓冲区的使用约定

在升级过程中,Bootloader 将接收到的固件数据暂存到共享缓冲区,App 启动后可通过约定地址读取元数据(如固件版本、校验值):

// 共享结构体定义(Bootloader 和 App 均需包含)
typedef struct {
    uint32_t magic;      // 魔数,用于验证有效性
    uint32_t version;    // 固件版本
    uint32_t length;     // 固件长度
    uint32_t crc32;      // 校验值
} ota_shared_info_t;

#define SHARED_INFO_ADDR (0x20004000)

App 启动时检查魔数,若有效则进行固件搬移或校验。

4. 完整代码示例

以下是一个简化的 Bootloader 跳转函数,结合 RT-Thread 的线程暂停机制:

#include <rtthread.h>
#include <string.h>

#define APP_FLASH_ADDR  0x08008000
#define SHARED_RAM_ADDR 0x20004000
#define SHARED_RAM_SIZE 0x8000

typedef void (*pFunction)(void);

void bootloader_jump_to_app(void)
{
    uint32_t app_sp;
    pFunction app_pc;

    // 暂停所有 RT-Thread 线程,避免干扰
    rt_enter_critical();

    // 从 App 向量表读取初始 SP 和 PC
    app_sp = *(volatile uint32_t*)APP_FLASH_ADDR;
    app_pc = (pFunction)(*(volatile uint32_t*)(APP_FLASH_ADDR + 4));

    // 清空共享缓冲区
    memset((void*)SHARED_RAM_ADDR, 0, SHARED_RAM_SIZE);

    // 复位 SysTick 和中断,防止中断冲突
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL = 0;

    // 设置主栈指针并跳转
    __set_MSP(app_sp);
    app_pc();

    // 不应到达此处
    rt_exit_critical();
}

App 端在 main 函数中检查共享信息:

#include <rtthread.h>

typedef struct {
    uint32_t magic;
    uint32_t version;
    uint32_t length;
    uint32_t crc32;
} ota_shared_info_t;

#define SHARED_INFO_ADDR (0x20004000)
#define OTA_MAGIC 0x4F544131  // "OTA1"

int app_check_ota_info(void)
{
    ota_shared_info_t *info = (ota_shared_info_t*)SHARED_INFO_ADDR;
    if (info->magic == OTA_MAGIC) {
        rt_kprintf("OTA info: version %d, length %d\n", info->version, info->length);
        // 进行固件搬移或校验
        // ...
        info->magic = 0;  // 清除魔数,防止重复处理
        return 0;
    }
    return -1;
}
INIT_APP_EXPORT(app_check_ota_info);

5. 注意事项

  • 对齐问题:共享缓冲区地址建议按 8 字节对齐,以满足 RT-Thread 内存管理的要求。
  • 栈大小估算:Bootloader 的栈大小需根据实际调用深度设置,避免溢出到共享缓冲区。
  • 中断向量表重定向:App 中需设置 SCB->VTOR 指向 App 的 Flash 起始地址,否则中断服务函数无法正确执行。
  • 编译优化:在跳转函数中,禁止使用局部变量(或使用 volatile),防止编译器优化导致 SP 设置失败。
  • 测试覆盖:建议在升级流程中加入 CRC 校验和看门狗超时机制,防止升级中断导致系统卡死。

6. 总结

通过合理的 RAM 分区和严格的跳转约定,基于 RT-Thread 的 OTA 升级可以避免内存冲突,提升可靠性。本文的边界设计方法适用于大多数 Cortex-M 系列 MCU,开发者可根据实际 RAM 大小调整分区参数。记住:边界设计不仅是技术问题,更是工程规范,需在项目初期就明确约定。