上周又有人问我:Bootloader 跳 App 之后,程序能跑,但一开串口或定时器就 HardFault;或者更糟,上电完全没反应,连 Boot 的串口打印都没有。
这两类现象看着不一样,根因基本集中在两处:跳转前的判断太松,或者向量表没搬。下面是我固定用的排查顺序,按这个顺序走,基本能定位到具体哪一步。
这是最容易搞错的一步。很多教程一句「设置 SCB->VTOR」带过,但在 Cortex-M0/M0+ 上这行代码根本编译不过。
ARMv6-M(Cortex-M0 / M0+)没有 VTOR 寄存器,向量表固定从地址 0 取;ARMv7-M(M3 / M4 / M7)和 ARMv8-M(M33)才有 SCB->VTOR。
内核 | STM32 型号 | 向量表怎么搬 |
|---|---|---|
Cortex-M3 | F1 系列 |
|
Cortex-M4 | F4 / L4 系列 |
|
Cortex-M0 | F0 系列 | 无 VTOR,改 SYSCFG 重映射 |
Cortex-M0+ | L0 系列 | 无 VTOR,改 SYSCFG 重映射 |
M3 / M4 上三行就够:
SCB->VTOR = APP_ADDR;
__DSB();
__ISB();M0 / M0+ 上只能把向量表拷到 SRAM 起始,再把 SRAM 映射到 0x0:
memcpy((void *)0x20000000, (void *)APP_ADDR, 0xC0);
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
SYSCFG->CFGR1 |= SYSCFG_CFGR1_MEM_MODE_0;三行缺一不可:拷贝、开 SYSCFG 时钟、切映射。只做前两步的现象是「能跑,但一进中断就飞到 0x0 去」。
另外,M3/M4/M33 的 VTOR 低 7 位是保留位,向量表至少 128 字节对齐。实际做的时候直接按 4K 对齐最省事,Flash 扇区本来就是这个量级。
最常见的错误是:Boot 里写着跳转到 0x08008000,App 工程却还按 0x08000000 链接。烧进去以后,App 里所有函数地址都是按 0 偏移算的,一跳过去就飞。
要改两处,缺一不可:
第二处的顺序有个坑:不能放在 HAL_Init() 之后。HAL_Init 里会配 SysTick,SysTick 一开就可能进中断,那时向量表还没搬,直接 HardFault。要放在 main 的第一行、任何中断使能之前。
教科书上的跳转代码只有五行:取栈顶、取 Reset_Handler、设 MSP、跳。
typedef void (*pFunc)(void);
uint32_t sp = *(volatile uint32_t *)APP_ADDR;
uint32_t pc = *(volatile uint32_t *)(APP_ADDR + 4);
__set_MSP(sp);
((pFunc)pc)();代码本身没错,错在没有判断。App 区如果是空的(全 0xFF),栈顶读出来是 0xFFFFFFFF,__set_MSP 一执行就跑飞,现象是「上电完全没反应」——看起来像 Boot 也死了,实际是 Boot 跳进了一片空地。
我现在固定四道门,缺一道都不跳:
第四道门性价比最高,判据一行就够:
if ((sp & 0x2FFE0000) != 0x20000000) return;它把「刷砖」降级成「还能停在 Boot 里用串口救回来」。我第一批板子就是缺这一条,砖了三块,最后靠 SWD 拆机才救回来。
这个掩码只适用于 SRAM 起始 0x20000000、容量不超过 128KB 的型号。SRAM 更大的片子要按实际区间改,别直接抄。
Boot 里用过的外设、中断、SysTick,跳之前要收干净。否则 App 初始化时会被残留中断打断,表现是偶发死机,而且单步调试时往往不复现。
我的顺序:
__disable_irq();
SysTick->CTRL = 0;
for (int i = 0; i < 8; i++) NVIC->ICER[i] = 0xFFFFFFFF;
for (int i = 0; i < 8; i++) NVIC->ICPR[i] = 0xFFFFFFFF;
__enable_irq();ICER 关中断使能,ICPR 清挂起位。两个都要做:只关使能不清挂起的话,重新使能那一刻挂起的中断会立刻触发。
循环上限按内核改:M3/M4 是 8 个寄存器,M0/M0+ 只有 2 个,写成 8 在 M0 上会越界访问。
RCC 里 Boot 开过的时钟不用全关,但串口、DMA 这些要 DeInit。否则 App 重新初始化时拿到的是脏状态,表现为「第一次收发正常,第二次就卡死」。
单算一个整包 CRC 最省事,但最难用:传一半断线,整包 CRC 必然失败,只能整包重传。几百 KB 的固件走 4G 模块重传一次就是几分钟。
拆成三层之后,每层管一件事:
层级 | 粒度 | 作用 | 失败代价 |
|---|---|---|---|
帧级 | 64 字节 | 错了只重发这一帧 | 一帧 |
块级 | 4KB(一个扇区) | 防丢帧、乱序 | 一个扇区 |
整包 | 全部写完 | 防「写成功但内容错」 | 整包 |
第三层不是多余的。Flash 写入失败在电压跌落时不一定报错,函数返回成功、数据却是错的,整包 CRC 是最后一道兜底。
算法用 CRC32 的 MPEG-2 变体:多项式 0x04C11DB7,初值 0xFFFFFFFF,输入输出不反转,末尾不异或。
这里踩过坑:PC 端图省事用了 zlib 的 crc32()(那是反转的 IEEE 变体),两边算出来永远对不上,查了两天。选 MPEG-2 还有个实际好处——它和 STM32 硬件 CRC 单元的默认行为更接近,后续想用硬件加速,软件侧只需要对齐初值和位序。
同一套 Boot 代码在 STM32 各系列上跑,主要差在这几处:
系列 | 内核 | 擦除粒度 | 注意点 |
|---|---|---|---|
F0 | M0 | 1KB 页 | 无 VTOR,需重映射 |
F1 | M3 | 1KB 页(大容量 2KB) | 写前必须解锁 FLASH->KEYR |
F4 | M4 | 16K / 64K / 128K 扇区 | 擦除按扇区号算,不能用地址除 |
L0 | M0+ | 128B 页 | 无 VTOR,页更小,分区要重算 |
L4 | M4 | 2KB 页 | 双 Bank 可选,A/B 升级用得上 |
两条通用建议:
完整实现(分区表、四道校验门、64 字节帧协议、Boot+App 合并工具)放在 Gitee,Apache-2.0:
https://gitee.com/skydiver/mm32-iap-demo
仓库覆盖灵动微 MM32 四个内核共 27 个型号,分区表可以照着改到 STM32。这套方案的另一半——Boot 区怎么切、信息区存什么、要不要做回滚——在同系列的《MM32 IAP Bootloader 设计:4K 分区、四道校验与最小可信运行时》里写得更细。
本文基于 Gitee 开源仓库 mm32-iap-demo(V2.0)整理,源码版权归原作者(Skydiver)所有。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。