首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >STM32 IAP 跳转 HardFault 排查清单:VTOR 偏移、4 个跳转条件与三重 CRC32

STM32 IAP 跳转 HardFault 排查清单:VTOR 偏移、4 个跳转条件与三重 CRC32

原创
作者头像
用户11831219
修改于 2026-11-09 09:23:00
修改于 2026-11-09 09:23:00
1400
举报

上周又有人问我:Bootloader 跳 App 之后,程序能跑,但一开串口或定时器就 HardFault;或者更糟,上电完全没反应,连 Boot 的串口打印都没有。

这两类现象看着不一样,根因基本集中在两处:跳转前的判断太松,或者向量表没搬。下面是我固定用的排查顺序,按这个顺序走,基本能定位到具体哪一步。

一、先分清内核:M0 根本没有 VTOR

这是最容易搞错的一步。很多教程一句「设置 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 系列

SCB->VTOR = APP_ADDR

Cortex-M4

F4 / L4 系列

SCB->VTOR = APP_ADDR

Cortex-M0

F0 系列

无 VTOR,改 SYSCFG 重映射

Cortex-M0+

L0 系列

无 VTOR,改 SYSCFG 重映射

M3 / M4 上三行就够:

代码语言:c
复制
SCB->VTOR = APP_ADDR;
__DSB();
__ISB();

M0 / M0+ 上只能把向量表拷到 SRAM 起始,再把 SRAM 映射到 0x0:

代码语言:c
复制
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 扇区本来就是这个量级。

二、第一个条件:App 工程真的按 APP_ADDR 链接了吗

最常见的错误是:Boot 里写着跳转到 0x08008000,App 工程却还按 0x08000000 链接。烧进去以后,App 里所有函数地址都是按 0 偏移算的,一跳过去就飞。

要改两处,缺一不可:

  • Keil 的 IROM1 起始地址(GCC 则是 .ld 里的 FLASH ORIGIN)
  • 系统启动后设置向量表(见上一节)

第二处的顺序有个坑:不能放在 HAL_Init() 之后。HAL_Init 里会配 SysTick,SysTick 一开就可能进中断,那时向量表还没搬,直接 HardFault。要放在 main 的第一行、任何中断使能之前。

三、第二个条件:跳转前必须有四道门

教科书上的跳转代码只有五行:取栈顶、取 Reset_Handler、设 MSP、跳。

代码语言:c
复制
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 跳进了一片空地。

我现在固定四道门,缺一道都不跳:

  1. 信息区升级标志 == APP_VALID
  2. App 长度 > 0,且不超出 Flash 剩余空间
  3. 整包 CRC32 校验通过
  4. 栈顶落在 SRAM 区间内

第四道门性价比最高,判据一行就够:

代码语言:c
复制
if ((sp & 0x2FFE0000) != 0x20000000) return;

它把「刷砖」降级成「还能停在 Boot 里用串口救回来」。我第一批板子就是缺这一条,砖了三块,最后靠 SWD 拆机才救回来。

这个掩码只适用于 SRAM 起始 0x20000000、容量不超过 128KB 的型号。SRAM 更大的片子要按实际区间改,别直接抄。

四、第三个条件:跳转前把现场清干净

Boot 里用过的外设、中断、SysTick,跳之前要收干净。否则 App 初始化时会被残留中断打断,表现是偶发死机,而且单步调试时往往不复现。

我的顺序:

代码语言:c
复制
__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 重新初始化时拿到的是脏状态,表现为「第一次收发正常,第二次就卡死」。

五、第四个条件:三重 CRC32 是分层止损,不是冗余

单算一个整包 CRC 最省事,但最难用:传一半断线,整包 CRC 必然失败,只能整包重传。几百 KB 的固件走 4G 模块重传一次就是几分钟。

拆成三层之后,每层管一件事:

层级

粒度

作用

失败代价

帧级

64 字节

错了只重发这一帧

一帧

块级

4KB(一个扇区)

防丢帧、乱序

一个扇区

整包

全部写完

防「写成功但内容错」

整包

第三层不是多余的。Flash 写入失败在电压跌落时不一定报错,函数返回成功、数据却是错的,整包 CRC 是最后一道兜底。

算法用 CRC32 的 MPEG-2 变体:多项式 0x04C11DB7,初值 0xFFFFFFFF,输入输出不反转,末尾不异或。

这里踩过坑:PC 端图省事用了 zlib 的 crc32()(那是反转的 IEEE 变体),两边算出来永远对不上,查了两天。选 MPEG-2 还有个实际好处——它和 STM32 硬件 CRC 单元的默认行为更接近,后续想用硬件加速,软件侧只需要对齐初值和位序。

六、STM32 各系列的 Flash 差异

同一套 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 升级用得上

两条通用建议:

  • App 起始地址按 4K 对齐,别贴着扇区边界放
  • 信息区至少占一个完整擦除单元。定义成 16 字节它照样占一整页,不如显式声明成 1KB

七、完整例程

完整实现(分区表、四道校验门、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 删除。

目录
  • 一、先分清内核:M0 根本没有 VTOR
  • 二、第一个条件:App 工程真的按 APP_ADDR 链接了吗
  • 三、第二个条件:跳转前必须有四道门
  • 四、第三个条件:跳转前把现场清干净
  • 五、第四个条件:三重 CRC32 是分层止损,不是冗余
  • 六、STM32 各系列的 Flash 差异
  • 七、完整例程
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档