3. BootROM与用户Bootloader:双核场景下的启动代码加载与校验策略

双核启动,说白了就是两个核要先后“醒过来”。

谁先醒?谁后醒?醒了之后各自该干什么?

这些问题,都写在BootROM和用户Bootloader里。我调试过不少双核项目,发现很多故障其实不是应用代码的问题,而是启动阶段就埋下了雷。今天咱们就把这块彻底捋清楚。

3.1 BootROM:芯片出厂自带的“第一段代码”

STM32的BootROM是芯片出厂时固化的一段只读代码,用户改不了。它的任务很简单:

  • 检测启动引脚电平(BOOT0、BOOT1)
  • 从对应介质(Flash、系统存储器、SRAM)加载第一段代码
  • 跳转到用户代码入口

对于双核芯片(比如STM32H7双核系列),BootROM还多了一个活:决定哪个核先启动。

关键点:BootROM只负责加载“第一级启动代码”。它不会帮你校验用户应用,也不会处理双核同步。这些事,得用户Bootloader来做。

3.2 双核启动顺序:谁先跑?

我见过不少工程师以为双核是同时启动的。其实不是。

以STM32H747为例,它的两个核——Cortex-M7(主核)和Cortex-M4(从核)——启动顺序是固定的:

步骤 执行者 动作
1 BootROM 检测启动引脚,从Flash加载M7的向量表
2 Cortex-M7 执行用户Bootloader(或直接跳转应用)
3 Cortex-M7 初始化系统时钟、外设、共享内存
4 Cortex-M7 通过HSEM(硬件信号量)释放M4复位
5 Cortex-M4 从指定地址加载向量表,开始执行

你看,M4是被M7“叫醒”的。M7不干活,M4就只能干等着。

注意:M4的启动地址不是BootROM决定的,而是M7在代码里写死的。我遇到过有人把M4的向量表地址写错了,结果M4一启动就跑飞。这种问题用调试器看PC指针就能发现——它指向了未初始化的内存区域。

3.3 用户Bootloader:双核场景下的加载策略

用户Bootloader是咱们自己写的,放在Flash开头。它的核心任务包括:

  • 校验应用代码的完整性(CRC/SHA)
  • 解密(如果固件加密)
  • 将应用代码复制到运行地址(如果需要)
  • 跳转到应用入口

在双核场景下,Bootloader还要额外处理一件事:两个核的应用代码怎么放?

我个人习惯的做法是:

  1. Flash分区:把Flash分成Bootloader区、M7应用区、M4应用区、共享参数区。
  2. 统一校验:Bootloader启动后,先校验M7应用,再校验M4应用。任何一个校验失败,都进入固件升级模式。
  3. 分步加载:M7先跳转,M4的加载由M7应用代码完成。

这里有个坑——我曾经踩过:

避坑指南:不要把M4的向量表放在M7的代码段里。M7升级时可能会覆盖M4的向量表。我建议把M4的向量表放在独立的Flash扇区,并且加上写保护。

3.4 校验策略:CRC还是SHA?

校验方式的选择,取决于你对安全性和速度的要求。

校验方式 计算速度 安全性 适用场景
CRC32 快(硬件CRC单元) 低(可被碰撞) 普通产品、启动速度敏感
SHA256 慢(软件计算) 高(不可逆) 安全要求高、固件防篡改
HMAC 中等 高(带密钥) 需要身份认证的场景

我一般这么选:

  • 如果只是防意外损坏(比如Flash写错),CRC32就够了。STM32有硬件CRC单元,算128KB的固件也就几毫秒。
  • 如果需要防恶意篡改,必须上SHA256或HMAC。别指望CRC能防攻击——它连故意构造的碰撞都防不住。

实战建议:Bootloader里同时保留CRC和SHA两种校验方式。正常启动用CRC(快),固件升级时用SHA(安全)。这样既保证了启动速度,又不牺牲安全性。

3.5 双核启动的故障定位

启动阶段的故障,往往是最难调的。因为这时候调试器可能还没连上,打印输出也没初始化。

我总结了几种常见故障和定位方法:

现象 可能原因 定位方法
M7启动正常,M4不跑 M4复位未释放、向量表地址错误 检查HSEM寄存器、读M4的VTOR
两个核都跑,但互相干扰 共享内存未初始化、外设冲突 检查MPU配置、外设时钟使能
Bootloader校验失败 CRC值不对、Flash被意外修改 读Flash内容对比、检查写保护
跳转后跑飞 栈指针错误、向量表偏移不对 用调试器看PC和SP值

嗯,这里要注意:M4不启动时,别急着怀疑M4代码。先检查M7有没有释放M4的复位。我遇到过好几次,都是M7代码里忘了调用HAL_HSEM_ReleaseM4Reset()。

3.6 双核启动流程图

下面这张图,把整个双核启动流程串起来了。你可以对照着看,哪个环节容易出问题。

双核启动流程图(STM32H7系列) BootROM:检测启动引脚 从Flash加载M7向量表 M7执行用户Bootloader:校验、解密、加载 校验 通过? 进入升级模式 M7跳转应用 M7初始化系统、外设、共享内存 M7释放M4复位,M4开始执行 M4向量表地址由M7指定 常见错误:地址写错或未对齐

3.7 代码示例:双核Bootloader的校验与跳转

下面这段代码,是我在实际项目中用过的双核Bootloader核心逻辑。它做了三件事:校验M7应用、校验M4应用、跳转。

/* Bootloader主函数 - 双核版本 */
void Bootloader_Main(void)
{
    uint32_t m7_app_addr = 0x08020000;  /* M7应用起始地址 */
    uint32_t m4_app_addr = 0x08040000;  /* M4应用起始地址 */
    uint32_t m7_size = 0x20000;         /* M7应用大小 */
    uint32_t m4_size = 0x20000;         /* M4应用大小 */

    /* 1. 校验M7应用 */
    if (CRC_Verify(m7_app_addr, m7_size) != PASS)
    {
        EnterUpgradeMode();
        return;
    }

    /* 2. 校验M4应用 */
    if (CRC_Verify(m4_app_addr, m4_size) != PASS)
    {
        EnterUpgradeMode();
        return;
    }

    /* 3. 配置M4向量表地址(M7负责设置) */
    *((uint32_t *)0x1FF00000) = m4_app_addr;  /* 写入共享内存 */

    /* 4. 跳转到M7应用 */
    JumpToApplication(m7_app_addr);
}

/* 跳转函数 */
void JumpToApplication(uint32_t addr)
{
    uint32_t sp = *((uint32_t *)addr);       /* 栈指针 */
    uint32_t pc = *((uint32_t *)(addr + 4)); /* 程序计数器 */

    /* 关闭全局中断 */
    __disable_irq();

    /* 设置主栈指针 */
    __set_MSP(sp);

    /* 跳转 */
    ((void (*)(void))pc)();
}

注意:跳转前一定要关闭全局中断。我见过有人忘了关中断,跳转后旧的中断服务函数还在跑,新应用的中断向量表还没生效,结果一进中断就跑飞。

3.8 小结

双核启动,说白了就是一场“接力赛”。BootROM跑第一棒,用户Bootloader跑第二棒,M7跑第三棒,最后M4接棒。

每一棒交接时,都要确保:

  • 代码完整(校验通过)
  • 地址正确(向量表、栈指针)
  • 状态干净(中断关闭、外设复位)

做到这三点,双核启动基本不会出大问题。如果真出了问题,按我上面给的表格排查,很快就能定位。