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还要额外处理一件事:两个核的应用代码怎么放?
我个人习惯的做法是:
- Flash分区:把Flash分成Bootloader区、M7应用区、M4应用区、共享参数区。
- 统一校验:Bootloader启动后,先校验M7应用,再校验M4应用。任何一个校验失败,都进入固件升级模式。
- 分步加载: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 双核启动流程图
下面这张图,把整个双核启动流程串起来了。你可以对照着看,哪个环节容易出问题。
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接棒。
每一棒交接时,都要确保:
- 代码完整(校验通过)
- 地址正确(向量表、栈指针)
- 状态干净(中断关闭、外设复位)
做到这三点,双核启动基本不会出大问题。如果真出了问题,按我上面给的表格排查,很快就能定位。