拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

IAP升级死机排查:中断向量表重映射的机制与实战指南

IAP升级死机排查:中断向量表重映射的机制与实战指南

上周出差回来的路上接到现场电话:客户产线上一批设备做IAP升级,Bootloader跑完,跳转App的一瞬间,三分之一直接黑屏,串口连一个字符都啃不出来。群里七嘴八舌,有人怀疑固件包损坏,有人怀疑Flash擦写超时,最后用调试器拉回来一看——PC指针卡在HardFault_Handler里,SCB->VTOR的值等于0x08000000,而App躺在0x08010000。问题根本不神秘,就是中断向量表重映射(Vector Table Relocation)没做对。这个坑我前后踩了不下五次,每次死法不太一样,根子却惊人地一致。

做IAP最怕的不是升级过程中断电,而是跳转过去之后CPU直接躺平。这篇文章我打算把自己做IAP升级遇到过、帮别人排查过的死机案例从头到尾捋一遍,把中断向量表重映射的机制、错误写法、正确配置和调试套路一次性说清楚。适合正在做Bootloader+App方案、被升级死机折磨得头疼的嵌入式开发朋友参考。

1. 先还原现场:IAP升级后死机都有哪些死法

1.1 三种最经典的死机表现

先描述现象,再聊原理。IAP升级后的死机,我在实际项目里遇到的绝大多数可以归为三类。

第一类是跳转即死:Bootloader打印完"Jump to App...",代码执行完最后一跳,程序就再没醒过来。串口没输出、LED不闪、调试器连接上去看到PC停在异常向量里。这种死法最常见,十有八九是跳转之前向量表、栈指针或者中断状态没有处理干净。

第二类是间歇性死机:十次升级,七八次正常,两三次死机。这种最让人抓狂,因为复现不稳定,客户那边一口咬定你们固件有问题。后来用逻辑分析仪抓了外部复位信号才发现,基本每次都是跳转瞬间、外设中断恰好Pending的时候触发——Bootloader里串口和定时器还开着中断,跳转后App尚未完成初始化,中断一来,CPU拿着Bootloader的向量表去执行App的异常处理,当场翻车。

第三类是"能跑但一开中断就死":App主循环跑得好好的,一旦用户开启一个外设中断,马上HardFault。这种问题几乎都是忘了给App单独设置VTOR,CPU一直拿着Bootloader的向量表地址找中断服务函数,找到的函数地址要么是Bootloader里的旧函数,要么干脆是非法地址。

这个分类很重要,因为很多人一上来就怀疑Flash写入、固件校验,结果折腾一整天,最后发现只是CTRL+C没做。

1.2 一次持续半个月的失效排查经历

去年帮一个做电力终端的朋友排查过类似问题。他们的Bootloader占32KB,App偏移在0x08008000,升级流程本身跑得挺顺,但是现场总有几台设备跳转后没有反应。现象不是必现,大概2%概率,而且集中在某几个批次。

我们最开始怀疑是Flash扇区边界问题,因为0x08008000正好落在F103的某个扇区边界,后来查了手册发现不是。然后是怀疑CRC校验,最后实在没辙,把一台死机的设备拆开,用JLINK连上读内存,才发现SCB->VTOR还是0x08000000,App自己的向量表在0x08008000。也就是说Bootloader跳转代码里写没写VTOR,或者写了但被后续配置覆盖了。朋友翻了源码,发现他的跳转逻辑是把SCB->VTOR放在了外设初始化和中断使能之后,而那段代码里恰好有一条DMA中断使能的语句,跳转顺序一乱,中断就把整个流程带崩了。

这个案例后面章节会展开拆解。这里先给一个结论性的判断:凡是IAP跳转后死机,第一怀疑对象永远是向量表重映射,不要先去翻Flash写入代码。

2. 为什么重映射出错必死:向量表取址机制与VTOR的对齐规则

2.1 中断向量表就是CPU的"紧急联系簿"

要理解向量表重映射为什么有关键作用,得先理解Cortex-M内核是怎么处理中断的。在Cortex-M3/M4/M7处理器里,Flash最前面放着一张中断向量表(Vector Table),这张表本质上是地址数组:第一个word是初始MSP栈指针,第二个word是Reset_Handler入口地址,再往后是NMI、HardFault、MemManage、BusFault、UsageFault等异常入口,然后是一长串外设中断入口。

每当一个中断到来,CPU会从向量表里取出对应的入口地址,然后跳过去执行。这就像人手边的一本紧急联系簿,CPU并不背每个ISR的地址,它只查这本册子。册子放在哪,就是Vector Table Relocation要回答的问题。

默认情况下,Cortex-M复位后从地址0x00000000读取最初的MSP和Reset_Handler开始执行。在STM32上,0x00000000可以由硬件映射到Flash、SRAM或系统存储器,由BOOT引脚决定。芯片从Flash启动时,0x00000000映射到0x08000000,所以向量表默认在0x08000000。

问题来了:IAP方案里Bootloader占据0x08000000开始的Flash段,App要被安排在后面的偏移地址,比如0x08008000或0x08010000。App编译链接的时候,它的向量表跟着App的链接地址一起挪到了偏移位置。但CPU不知道这件事——如果没有人告诉它向量表搬家了,它发生中断时依然去0x00000000(映射到的0x08000000)查册子,查到的是Bootloader的中断服务入口,拿到的函数地址属于Bootloader,而这段代码可能早就被App覆盖,或者本来就不是当前场景下的服务函数。于是CPU像拿着过期的通讯录去找一个搬走的人,结果自然是要么敲错门、要么掉坑里。

2.2 VTOR不是想写就能写:对齐和映射边界

解决"CPU不知道向量表搬家"问题的官方途径,是设置Cortex-M内核里的VTOR寄存器(Vector Table Offset Register,地址0xE000ED08)。这个寄存器存放向量表的偏移地址,CPU每次响应异常和中断时,会先读取VTOR,以它为基准加上中断号对应的索引取向量。

写VTOR有两个特别容易坑人的地方。

第一个是对齐要求。VTOR的低位字段不是所有位都有效,它要求偏移量对齐到向量表大小的整数倍。ARM给出的规则是:偏移量必须能被向量表大小整除,而向量表大小本身要向上取成2的幂。举例来说,STM32F407有大约86个中断向量,向量表大小超过344字节,向上取2的幂就是512字节,VTOR的地址就必须512字节对齐。如果你的App偏移量是0x08008000(对齐没问题),但如果有人自作聪明把App放在0x08008200这种偏移上,VTOR的某些低位会被强制清零,实际生效的偏移和你预期不一样,中断全部错位,表现出来就是千奇百怪的跑飞、死机。用"绝对禁忌"这个词来强调一点都不过分。

第二个是设置时机。VTOR的更新最好在全局中断关闭、且没有异常在处理器内部排队的情况下进行。如果在中断服务函数里直接改VTOR,或者跳转前后有中断悬起,CPU的取指流水线可能已经按旧向量表预取了地址,新的VTOR要等流水线冲刷后才能完全生效。工程上常见的做法是跳转前关闭全局中断,做完整套迁移操作,再跳进App让App自己重新开启中断。

2.3 没有VTOR的芯片怎么办

说到这必须提一个老芯片的坑:不是所有Cortex-M内核版本都有VTOR。Cortex-M3刚出来的时候,r1p0版本的处理器设计里没有VTOR寄存器,一直到r1p1才加入。STM32F1系列搭载的M3内核版本参差不齐,虽然绝大多数F103后续批次都是r1p1以上,但某些早期型号和部分国产兼容芯片,VTOR位段可能只读或者行为不完全符合预期。遇到这种芯片,就需要用另一套思路:把0x00000000这扇门直接指向SRAM,然后把App向量表原样复制到SRAM起始地址。这其实是利用芯片系统控制块的物理重映射机制(比如STM32F2/F4上的SYSCFG->CFGR1的MEM_MODE字段),把SRAM映射到地址0x00000000,再让VTOR指向SRAM基址。

这种方式比单纯写VTOR多两步:第一步是把SRAM起始处的向量表内容准备好;第二步是保证SRAM首块区域在整个运行期间不被App的初始化代码改掉。很多RTOS的启动代码里都会保留首块SRAM用来放向量表,就是这个原因。

对大多数工程师来说,写SCB->VTOR = APP_FLASH_BASE就够了,但你要清楚自己的芯片内核版本,遇到老内核或者行为异常时,物理重映射是兜底方案。

3. 四个典型死机案例复盘:从错误代码看因果链

这一节我挑了自己实际遇到、也被问过最多遍的四个案例,每个都附上当时的错误写法。对照着看,会比自己闷头查快很多。

3.1 案例一:跳转成功但一开中断就HardFault

现象:Bootloader顺利跳到App,App的串口打印、按键扫描都正常,但只要调用HAL_TIM_Base_Start_IT()开启定时器中断,程序立刻进HardFault。

错误代码:

// Bootloader跳转逻辑(错误示范) void jump_to_app(uint32_t app_addr) { uint32_t app_sp = *(volatile uint32_t *)app_addr; pFunction app_entry = (pFunction)*(volatile uint32_t *)(app_addr + 4); __set_MSP(app_sp); // 注意:这里漏了SCB->VTOR的配置 app_entry(); }

这个坑的本质就是CPU查了旧向量表。Bootloader自己占着0x08000000,App在0x08010000。定时器中断使能后,CPU去0x08000000查定时器向量,得到的是Bootloader里的定时器服务函数入口。可Bootloader在跳转前已经把外设反初始化得七七八八,函数指针指向的代码段可能已被App覆盖,或者根本不是为这个外设服务的,一进去就是非法访问。当时的现象是开串口中断也死,开定时器中断也死,不开中断一切正常,排查方向一开始还怀疑中断优先级分组,折腾了半天才想到向量表。

3.2 案例二:函数指针跳转前忘了设MSP

现象:每次跳转,App的Reset_Handler都不执行,卡在奇怪的位置,甚至直接复位。

错误代码:

typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { // 从向量表第二个word取Reset_Handler地址 pFunction app_entry = (pFunction)(*(volatile uint32_t *)(app_addr + 4)); // 错误:没有设置主栈指针,直接跳 app_entry(); }

Cortex-M复位后做的第一件事,是从向量表第一个word加载MSP,然后才跳Reset_Handler。如果你跳转前没有设置MSP,CPU会继续使用Bootloader当前的栈,而Bootloader的栈底可能是App链接脚本里定义的某个RAM地址段,两边RAM布局不一致,App一进Reset_Handler就开始压栈写数据,压到谁的地盘都不一定。轻则局部变量全乱、全局变量被踩,重则一启动就触发HardFault或者在切换栈时彻底翻车。正确写法必须先从app_addr + 0处读出App自己定义的栈顶值,用__set_MSP()写进去,再取app_addr + 4的Reset_Handler跳转。

3.3 案例三:VTOR设置了但App链接地址没对上

现象:跳转没死,App也能跑主循环,但某些中断的行为异常——按一下按键,触发的好像是另外一个外设的中断,甚至回调函数里拿到的是完全莫名其妙的数据。

这种问题的排查难度比"一开中断就死"更高,因为程序没死,只是行为错乱。我当时遇到的情况是:App的链接脚本里,FLASH起始地址写错了。Bootloader实际占用0x08000000到0x0800FFFF,App代码和向量表被要求放在0x08010000,但链接脚本里只写了偏移64KB,也就是0x08010000,这个人没写错;错的是另一个同事在SystemInit或者某段配置代码里又写了一次SCB->VTOR = 0x08008000——那是上一版App的地址。

结果就是:向量表里存的实际是0x08010000这套函数地址,CPU却去了0x08008000查表。0x08008000里的内容是不完整的、反复擦写后残留的东西,读出来的"中断入口"全是废地址,程序神出鬼没地跑飞。从行为上完全看不出是向量表问题。后来用内存窗口直接看0x08008000和0x08010000两处向量表内容,对照MAP文件里的中断函数地址,才把问题钉死。

引申出一个工程规范:整个工程里设置VTOR的地方必须唯一,最好收敛到跳转函数里,并且加编译期断言或者地址校验。避免"某个配置模块里也设了一次"这种隐患。

3.4 案例四:中断挂在跳转窗口期,App全灭

现象:偶尔死机,死的时候PC停在HardFault,复位后重升一次又可能正常。

这个案例就是开头提到的电力终端问题。Bootloader的跳转函数里,原先的顺序是这么写的:

void jump_to_app(uint32_t app_addr) { __disable_irq(); // 外设反初始化、DMA去初始化省略 ... SCB->VTOR = app_addr; __set_MSP(*(volatile uint32_t *)app_addr); // 错误:外部中断NVIC的Pending位没有清 ((pFunction)(*(volatile uint32_t *)(app_addr + 4)))(); while(1); }

问题出在:Bootloader主循环里用的串口、SysTick、DMA、外部按键中断,在跳转前确实被DeInit了,但DeInit只是把外设寄存器复位,如果正好有一个外部按键中断在跳转瞬间被触发,NVIC里的Pending位还挂着。跳转到App后,App的SystemInit、外设初始化、中断使能是一步一步来的,如果App还没把对应的外部中断处理逻辑准备好,CPU一开中断,那个悬起的中断立刻被响应。如果此时App已经改了VTOR但不会导致立即死,但如果中断服务代码里访问了尚未初始化完成的外设寄存器,结果就是硬件错误。

修复方式是在跳转前清一次NVIC挂起位,把每组ICPR寄存器写一遍:

NVIC->ICPR[0] = 0xFFFFFFFF; NVIC->ICPR[1] = 0xFFFFFFFF; NVIC->ICPR[2] = 0xFFFFFFFF;

清完再做__set_MSP和跳转。这个细节看起来不起眼,却是"IAP死机偶发性"的最大元凶。

3.5 这些案例背后的共同点:绝对禁忌清单

把上面四个案例归纳一下,中断向量表重映射的"绝对禁忌"有下列五条,每个做IAP的工程师都应该刻在板子上:

禁忌行为后果
跳转前不关闭全局中断跳转期间窗口期中断抢占,App初始化未完成前触发ISR
只跳转不重映射VTORCPU沿用Bootloader向量表,中断映射完全错位
写VTOR时地址不对齐低地址位被内核忽略,向量表实际定位偏移与预期不符
只设MSP和Reset_Handler,却不校验App起始地址合法性空Flash、错误地址会导致从非法内存取值,跳转即复位
跳转过程中不清NVIC Pending位、不及时关外设悬起中断在App使能中断瞬间触发,死机时机随机化

4. 正确的中断向量表重映射实战配置

4.1 启动文件与链接脚本:从源头保证向量表位置

先说结论:向量表重映射不是只在C代码里写一行SCB->VTOR = ...就完事的,它还取决于链接脚本把向量表放到了哪里、启动文件是否保留了向量表区。很多人写跳转函数时地址没问题,但App自己编译出来根本没有把向量表放到指定偏移处,后面一切白搭。

以MDK+STM32为例,App工程的分散加载文件(sct文件)里,你需要指定一个region让向量表落在App的Flash基址:

LR_IROM1 0x08010000 0x00010000 { ER_IROM1 0x08010000 0x00010000 { *startup.o (RESET, +First) * (InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) } }

看到*startup.o (RESET, +First)没有?它保证startup.s里的那个中断向量表段被放到region的第一个位置。如果你用GCC/CMake,链接脚本(.ld)里同样要有类似的布局,至少确保__Vectors符号出现在App Flash区的起始位置。

用GCC时,启动文件通常长这样:

// 简化示意 __attribute__((section(".isr_vector"))) void (*const g_pfnVectors[])(void) = { (void *)&__initial_sp, Reset_Handler, NMI_Handler, HardFault_Handler, ... };

对应的ld脚本里:

MEMORY { FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 64K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K } SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } > FLASH ... }

这里我要多说一句:向量表要在生成固件时被安排到App基址,如果你用OTA传输固件包,下载完成后务必做一次"向量表头校验"——检查App起始第1个word是不是落在RAM范围内,第2个word是不是落在Flash范围内。这样能从入口处挡住大部分跳转死机。

4.2 标准跳转函数实现:关中断、验地址、设VTOR、置MSP、再跳转

下面给出一份可以直接抄的跳转函数,关键顺序和原因都写在注释里。

#include "stm32f4xx.h" #define APP_FLASH_BASE 0x08010000u #define APP_SECTOR_SIZE 0x10000u typedef void (*pFunction)(void); /* 将具体地址转换为函数指针调用 */ static void jump_to_address(uint32_t address) { pFunction jump = (pFunction)address; jump(); } void boot_jump_to_app(void) { uint32_t app_sp = *(volatile uint32_t *)APP_FLASH_BASE; uint32_t app_reset = *(volatile uint32_t *)(APP_FLASH_BASE + 4u); uint32_t ram_limit = (uint32_t)&__RAM_START_LIMIT__; /* 从链接脚本获得 */ uint32_t flash_limit = (uint32_t)&__FLASH_START_LIMIT__; /* 1. 校验栈顶指针在RAM范围内 */ if ((app_sp < 0x20000000u) || (app_sp >= ram_limit)) { return; /* 非法地址,不能跳转 */ } /* 2. 校验复位地址在Flash应用区范围内 */ if ((app_reset < APP_FLASH_BASE) || (app_reset >= flash_limit)) { return; /* 非法复位地址,不能跳转 */ } /* 3. 关闭全局中断 */ __disable_irq(); /* 4. 关闭外设、定时器、DMA等,并清Pending位 */ deinit_peripherals(); NVIC->ICPR[0] = 0xFFFFFFFF; NVIC->ICPR[1] = 0xFFFFFFFF; NVIC->ICPR[2] = 0xFFFFFFFF; /* 5. 重映射中断向量表到App区 */ SCB->VTOR = APP_FLASH_BASE; /* 6. 设置MSP指向App的栈顶 */ __set_MSP(app_sp); /* 7. 跳转到App的复位向量 */ jump_to_address(app_reset); while (1) { /* 正常情况下不会走到这里 */ } }

关中断这步我提过不止一次,它不是为了"礼貌",而是为了防止在改VTOR和MSP这两个关键寄存器的瞬间,有任何中断插入。想象一下你正在改VTOR,然后一个SysTick中断过来,CPU带着旧的流水线状态去取值——新VTOR还没来得及在总线层面生效,这种触发窗口极小,但一旦踩中就是偶发死机的锅。你不想在现场跟它较劲,就老实关中断。

清Pending位那几行,我见过很多人省略,理由是"我们Bootloader很简单,没开几个中断"。但外部中断、RTC闹钟、串口空闲中断等都能在跳转前被电平触发挂到NVIC里。多写三行,省下的是随机死机的排查时间。

4.3 三种重映射方案选型对比

除了最常用的SCB->VTOR = APP_FLASH_BASE,实际项目里还有两种方案,各有适用场景。我把三种方案放在一起对比:

方案适用芯片优点缺点
直接设置SCB->VTOR所有带Cortex-M3 r1p1+ / M4 / M7的芯片简单、灵活、不用搬移数据老内核(M3 r1p0)可能不可用
向量表复制到SRAM,VTOR指向SRAM需要动态修改中断服务函数、Bootloader与App共享中断向量表在RAM里可以运行时改写占用RAM首区域,需要保证该区域不被App覆盖
物理重映射(SYSCFG/AFIO)早期F1、无完整VTOR的芯片不依赖VTOR,硬件映射到0地址配置复杂、需同步保证SRAM向量表内容正确

方案一不用多解释,属于默认选项。方案二比较典型的使用场景是:App里用了AREA_RAM向量表重映射技术,中断服务函数放在RAM执行以提高响应速度,或者Bootloader要同时处理自己的中断。方案三在你用到的国产替代MCU上偶尔会遇到——有些厂商的Cortex-M3内核实现比较原始,SCB->VTOR写入被忽略,那就只能通过系统控制块把SRAM映射到0x00000000,然后将App向量表放在SRAM起始处,同时保证启动阶段这段空间不被清零覆盖。具体怎么做,不同芯片寄存器差异较大,要用的话务必查对应参考手册。

我个人在绝大多数量产方案里只用方案一,因为它把"向量表"和"可执行代码"放在同一块Flash区域,程序行为最一致。方案二看起来高大上,但RAM向量表一旦被DMA误写或者被初始化代码清掉,排查难度指数上升。

5. 死机后的完整排查链路:从PC指针到向量表内容

5.1 第一步:读SCB寄存器组定位故障类型

死机之后不要急着改代码,先用调试器读取关键寄存器。Cortex-M内核出异常后,SCB->CFSR(配置故障状态寄存器)、SCB->HFSR(硬故障状态寄存器)、SCB->MMFAR和SCB->BFAR会带着指向出问题地址的信息。

我给自己总结了一套固定读取顺序:

  1. 看HFSR的FORCED位是不是置1,如果是,说明HardFault是由下级异常升级来的。
  2. 看CFSR里MMARVALID、BFARVALID这些位,判断到底发生的是总线错误(BusFault)还是访存错误(MemManage)。
  3. 读BFAR/MMFAR拿到出错的访存地址。如果地址落在0x00000000附近,那基本就是CPU读向量表失败,或者函数指针指向了低地址。

这套信息比盲猜快得多。之前排查一片STM32L4的跳转死机,跑上去发现BFAR = 0x080002D4——这个地址落在Bootloader区域,那说明CPU正在读Bootloader区域的某个函数入口。一瞬间就知道是向量表没重映射。

5.2 第二步:核对向量表和跳转目标地址

拿到复位地址、异常地址之后,下一步是打开内存窗口,直接把App基址开始的前8个word调出来核对。以App基址0x08010000为例,正常内容应该是:

内存地址值解释
0x080100000x20020000栈顶指针,应落在RAM区
0x080100040x0801030DReset_Handler地址,应落在Flash区
0x080100080x08010193NMI_Handler地址
0x0801000C0x0801015FHardFault_Handler地址
.........

如果前两个word异常,多半是App固件没烧对,或者校验不严、升级包有问题。如果前两个word正常,接着看VTOR寄存器的实际值是不是0x08010000。调试器通用寄存器窗口里能看到SCB基址0xE000ED08处的内容,或者直接用表达式*(uint32_t *)0xE000ED08。

再往下就是比对向量表里的ISR地址和MAP文件里对应的函数地址。比如看0x0801D04C这个位置,MAP文件里应该是USART1_IRQHandler的入口。如果内存里存的地址和MAP文件对不上,就说明App编译时向量表内容和运行时代码不对应——八成是链接脚本里把多个region搞混了,或者固件包版本不对。

5.3 第三步:利用调试技巧快速锁定是"哪一行代码"引发的

寄存器能告诉你是哪种错,但通常还不能直接定位到代码行。我常用的三板斧:

第一板斧是反汇编看PC。调试器停在HardFault里时,把PC指向的地址切到反汇编窗口,往上翻几行,看是从哪条指令跑过来的。如果是BLX R3这类跳转指令,能直接看到R3里的值——那就是一个"应该被跳过去但根本不是函数入口"的地址。

第二板斧是在关键跳转点打断点。在Bootloader的SCB->VTOR = ...那行、__set_MSP()那行、app_reset()那行各打一个断点。单步走完,每执行一步就记录SP和VTOR的值。这样能清晰看到是不是在设置VTOR时,某个中断插入改变了现场;或者是不是MSP没有被正确写入。

第三板斧是比较法。找一块正常的设备,同样打上断点,对比正常设备和故障设备在跳转前那一刻的NVIC->ICPR、SCB->VTOR、MSP三个寄存器的差异。这个做法特别适合间歇性死机,能让隐藏在时序窗口里的问题暴露出来。

5.4 其他和向量表无关但同样致命的IAP升级隐患

最后这部分算是顺手排雷。有些IAP死机排查到最后,会发现不是向量表重映射的问题,但大部分人一开始都会跑错方向。我把这几类隐患也列出来:

  • 看门狗没喂饱:Bootloader跳转前调用了复杂的Flash擦写,擦写耗时较长,如果IWDG复位时间设置太短,程序可能在跳转前就被看门狗拍死。排查时先看RCC复位标志寄存器,判断复位源是不是IWDG。
  • 栈指针越过RAM边界:App链接脚本里的RAM区域如果和Bootloader重叠,跳转后MSP可能落在无效地址。这个问题怎么查?看__initial_sp的定义和App启动时的MSP值。
  • Flash等待周期配置不对:如果Flash频率提升后没有重新配置FLASH_ACR的等待周期,Flash读取错乱,App从Flash取指令会取到残缺数据。这个症状和向量表问题很像,但PC一般不会停在HardFault,而是跑到随机地址后复位。
  • App里再次初始化了Bootloader占用的外设:比如Bootloader用PA9/PA10做串口,App初始化时需要先关闭复用功能,否则引脚电平冲突,可能导致外设长时间阻塞。

这几类隐患无论哪一类,都有个共同的排查思路:先确认死在哪,再确定为什么死,最后才改代码。不要一上来就怀疑向量表,也别一上来就怀疑固件包,用数据说话。

做IAP升级这几年,我最深刻的体会是:向量表重映射本身不复杂,关中断、设VTOR、置MSP、清Pending、再跳转,五行代码而已,但这些代码的每一行都有自己存在的理由。省略一行,死机的概率就增加一分,而且大概率是在客户的产线上以最难看的方式暴露出来。我建议你在自己的项目里专门维护一份跳转模板代码,关键步骤都写上注释和校验逻辑,每次换平台、换芯片时逐条确认,而不是靠记忆重写。出问题的时候,这份模板会救你很多时间。

返回列表