Arm Coretex-M核MCU做IAP/OTA升级时候为什么要做中断向量表地址偏移?

一句话:

STM32 单 Bootloader + 双 App 区域时,做中断向量表偏移,是为了让 CPU 在 App 运行后,发生中断/异常时去 App 自己的中断函数,而不是继续去 Bootloader 的中断函数。

不做的话,App 可能能跳起来,但一开中断就容易跑飞、进 HardFault、进 Bootloader 的 Default_Handler、FreeRTOS 不调度、串口/定时器/DMA 中断全异常。


假设 Flash 布局是这样:

text 复制代码
0x08000000  ┌────────────────────┐
            │ Bootloader         │
            │ Bootloader向量表   │
0x08010000  ├────────────────────┤
            │ App1               │
            │ App1向量表         │
0x08050000  ├────────────────────┤
            │ App2               │
            │ App2向量表         │
            └────────────────────┘

STM32 上电后,默认会从 0x08000000 取中断向量表。

也就是说,默认情况下 CPU 认为:

c 复制代码
中断向量表地址 = 0x08000000

这正好是 Bootloader 的地址。

所以 Bootloader 能正常启动。


1. 中断向量表到底是什么?

每个 STM32 程序最开头都有一张表,叫中断向量表

比如 App 的起始地址是 0x08010000,那么 App 的前面一般是:

text 复制代码
0x08010000: 初始 MSP 栈顶地址
0x08010004: Reset_Handler 地址
0x08010008: NMI_Handler 地址
0x0801000C: HardFault_Handler 地址
0x08010010: MemManage_Handler 地址
...
0x0801003C: SysTick_Handler 地址
...
后面还有 USART、TIM、DMA、EXTI 等外设中断入口

CPU 发生中断时,不是靠 C 函数名去找 ISR,而是硬件直接查这张表。

比如发生 SysTick 中断时,CPU 会去当前向量表里找:

text 复制代码
SysTick_Handler 的函数地址

发生 USART 中断时,CPU 会去当前向量表里找:

text 复制代码
USARTx_IRQHandler 的函数地址

所以关键点是:

当前 CPU 认为向量表在哪里,它就从哪里取中断函数入口。


2. Bootloader 跳 App 时,为什么要改 VTOR?

Cortex-M 里面有个寄存器叫:

c 复制代码
SCB->VTOR

它的作用就是告诉 CPU:

text 复制代码
当前中断向量表从哪个地址开始

Bootloader 默认运行时:

c 复制代码
SCB->VTOR = 0x08000000;

如果 Bootloader 要跳到 App1:

c 复制代码
SCB->VTOR = 0x08010000;

如果 Bootloader 要跳到 App2:

c 复制代码
SCB->VTOR = 0x08050000;

这就叫中断向量表偏移

注意,"偏移"不是把代码搬走,而是告诉 CPU:

text 复制代码
你以后找中断入口,不要去 Bootloader 那张表了,要去 App 自己那张表。

3. 不做中断向量表偏移会怎么样?

假设 Bootloader 跳到了 App1,但是你没有执行:

c 复制代码
SCB->VTOR = APP1_ADDR;

那么 CPU 还会认为:

c 复制代码
当前向量表 = 0x08000000

也就是 Bootloader 的向量表。

这时候 App1 运行后,一旦发生中断,问题就来了。

比如 App1 初始化了定时器:

c 复制代码
TIM3_IRQHandler()
{
    // App自己的定时器处理
}

但是 TIM3 中断来了以后,CPU 不会去 App1 的 TIM3_IRQHandler

它会去 Bootloader 的向量表里找 TIM3 的入口。

可能结果是:

text 复制代码
情况1:Bootloader 没有 TIM3_IRQHandler
      -> 进 Default_Handler 死循环

情况2:Bootloader 有 TIM3_IRQHandler
      -> 执行 Bootloader 的中断逻辑,和 App 外设状态完全不匹配

情况3:向量表里地址无效
      -> HardFault

情况4:FreeRTOS 的 SysTick/PendSV/SVC 没跑到 App 的处理函数
      -> RTOS 任务调度异常,系统卡死

所以你会看到一些很怪的现象:

text 复制代码
App main() 能进
外设初始化也能跑
但是一开中断就死
HAL_Delay 卡住
FreeRTOS 不调度
串口中断没反应
DMA 中断跑飞
定时器中断进 Default_Handler
莫名其妙 HardFault

这就是典型的向量表没切到 App导致的问题。


4. 为什么 main 能跑,但中断不行?

因为 Bootloader 跳 App 的时候,通常是手动跳到 App 的复位入口:

c 复制代码
uint32_t app_stack = *(uint32_t *)APP_ADDR;
uint32_t app_reset = *(uint32_t *)(APP_ADDR + 4);

__set_MSP(app_stack);

void (*app_entry)(void) = (void (*)(void))app_reset;
app_entry();

这段代码直接从 App 向量表里取了:

text 复制代码
APP_ADDR + 0:App 的 MSP
APP_ADDR + 4:App 的 Reset_Handler

所以 App 的 Reset_HandlerSystemInit()main() 可以正常进入。

但是后面的中断不是 Bootloader 手动跳的,而是 CPU 硬件自动查 SCB->VTOR

所以会出现:

text 复制代码
main() 能进
但是中断一来就炸

原因就是:

text 复制代码
Reset_Handler 是你手动跳对的;
中断入口是 CPU 根据 VTOR 自动找的。

5. 正确跳转 App 的典型流程

Bootloader 跳 App 前一般要这样做:

c 复制代码
#define APP1_ADDR  0x08010000U

typedef void (*pFunction)(void);

void jump_to_app(uint32_t app_addr)
{
    uint32_t app_stack;
    uint32_t app_reset;
    pFunction app_entry;

    // 1. 关闭全局中断,防止跳转过程中来中断
    __disable_irq();

    // 2. 关闭 SysTick,避免 Bootloader 的 SysTick 干扰 App
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL  = 0;

    // 3. 可选:反初始化 Bootloader 用过的外设
    // HAL_RCC_DeInit();
    // HAL_DeInit();

    // 4. 设置 App 的中断向量表地址
    SCB->VTOR = app_addr;

    // 5. 取 App 栈顶地址
    app_stack = *(uint32_t *)app_addr;

    // 6. 取 App Reset_Handler 地址
    app_reset = *(uint32_t *)(app_addr + 4);

    // 7. 设置主栈指针 MSP
    __set_MSP(app_stack);

    // 8. 跳转到 App 的 Reset_Handler
    app_entry = (pFunction)app_reset;
    app_entry();
}

如果是 App2:

c 复制代码
jump_to_app(0x08050000);

核心就是这句:

c 复制代码
SCB->VTOR = app_addr;

6. 双 App 区域时更要注意

单 App 时,Bootloader 只跳一个 App,比如:

text 复制代码
Bootloader -> App1

但双 App 时可能是:

text 复制代码
Bootloader -> App1
Bootloader -> App2

所以 Bootloader 要根据当前有效 App 设置不同的向量表:

c 复制代码
if (active_app == APP1)
{
    SCB->VTOR = APP1_ADDR;
    jump_to_app(APP1_ADDR);
}
else if (active_app == APP2)
{
    SCB->VTOR = APP2_ADDR;
    jump_to_app(APP2_ADDR);
}

否则有一种更危险的情况:

text 复制代码
CPU 正在运行 App2
但是 VTOR 还指向 App1

结果就是:

text 复制代码
App2 的代码在跑,
但是所有中断跑到 App1 的中断函数里。

这会非常混乱。

比如 App2 初始化的是 USART1,App1 的中断函数里处理的是旧版本协议,那就可能直接乱套。


7. App 工程本身也要配对地址

只在 Bootloader 里设置 SCB->VTOR 还不一定够。

因为很多 STM32 工程在 system_stm32xxx.c 里面有类似代码:

c 复制代码
#define VECT_TAB_OFFSET  0x00000000U

SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;

默认情况下:

c 复制代码
FLASH_BASE = 0x08000000
VECT_TAB_OFFSET = 0x00000000

所以它会设置成:

c 复制代码
SCB->VTOR = 0x08000000;

这对 Bootloader 是对的,但是对 App 是错的。

如果 App1 放在 0x08010000,那 App1 里面应该是:

c 复制代码
#define VECT_TAB_OFFSET  0x00010000U

最终:

c 复制代码
SCB->VTOR = 0x08010000;

如果 App2 放在 0x08050000,那就是:

c 复制代码
#define VECT_TAB_OFFSET  0x00050000U

最终:

c 复制代码
SCB->VTOR = 0x08050000;

所以有两个地方要一致:

text 复制代码
1. App 的链接地址
2. App 的中断向量表地址

例如 App1:

text 复制代码
链接地址:0x08010000
VTOR地址:0x08010000

App2:

text 复制代码
链接地址:0x08050000
VTOR地址:0x08050000

8. 链接地址和向量表偏移是两回事

很多人容易混。

链接地址

链接地址决定代码里面的函数、变量、常量最终认为自己在哪里。

比如 App1 链接地址是:

text 复制代码
0x08010000

那么 App1 的 Reset_Handlermain()、各种函数地址都会按 0x08010000 往后排。

向量表偏移

向量表偏移是告诉 CPU:

text 复制代码
中断入口表在哪里

也就是:

c 复制代码
SCB->VTOR = 0x08010000;

所以:

text 复制代码
链接地址不对:程序本身可能跳不起来,函数地址错。
VTOR 不对:程序可能能起来,但中断一来就错。

9. 升级过程中为什么尤其重要?

App 升级一般流程是:

text 复制代码
当前运行 App1
下载新固件到 App2
校验 App2
设置 App2 有效标志
重启
Bootloader 启动
Bootloader 检查 App2 有效
跳转 App2

这时 Bootloader 必须做:

c 复制代码
SCB->VTOR = APP2_ADDR;

否则虽然跳到了 App2,但是中断还可能找 Bootloader 或 App1。

还有一种情况更危险:

text 复制代码
App1 正在升级 App2
App2 区域正在被擦除/写入
如果 VTOR 错误地指向 App2
这时候来了中断
CPU 从正在擦写的 Flash 区域取中断入口

可能取到:

text 复制代码
0xFFFFFFFF

然后直接 HardFault 或跑飞。

所以升级/擦写 Flash 时通常还要注意:

text 复制代码
1. 不要执行正在擦写区域的代码
2. Flash 擦写期间谨慎处理中断
3. Bootloader 跳 App 前重新设置 VTOR
4. App 启动后也最好设置自己的 VTOR

10. 总结成一句工程经验

你的结构是:

text 复制代码
Bootloader + App1 + App2

那么每次 Bootloader 决定运行哪个 App,就必须让 CPU 的中断向量表指向哪个 App:

c 复制代码
运行 Bootloader:SCB->VTOR = 0x08000000

运行 App1:      SCB->VTOR = 0x08010000

运行 App2:      SCB->VTOR = 0x08050000

否则就是:

text 复制代码
代码跑在 App,
中断却跑到 Bootloader 或另一个 App。

这就是为什么必须做中断向量表偏移。




一句话:

STM32 单 Bootloader + 双 App 区域时,做中断向量表偏移,是为了让 CPU 在 App 运行后,发生中断/异常时去 App 自己的中断函数,而不是继续去 Bootloader 的中断函数。

不做的话,App 可能能跳起来,但一开中断就容易跑飞、进 HardFault、进 Bootloader 的 Default_Handler、FreeRTOS 不调度、串口/定时器/DMA 中断全异常。


假设 Flash 布局是这样:

text 复制代码
0x08000000  ┌────────────────────┐
            │ Bootloader         │
            │ Bootloader向量表   │
0x08010000  ├────────────────────┤
            │ App1               │
            │ App1向量表         │
0x08050000  ├────────────────────┤
            │ App2               │
            │ App2向量表         │
            └────────────────────┘

STM32 上电后,默认会从 0x08000000 取中断向量表。

也就是说,默认情况下 CPU 认为:

c 复制代码
中断向量表地址 = 0x08000000

这正好是 Bootloader 的地址。

所以 Bootloader 能正常启动。


1. 中断向量表到底是什么?

每个 STM32 程序最开头都有一张表,叫中断向量表

比如 App 的起始地址是 0x08010000,那么 App 的前面一般是:

text 复制代码
0x08010000: 初始 MSP 栈顶地址
0x08010004: Reset_Handler 地址
0x08010008: NMI_Handler 地址
0x0801000C: HardFault_Handler 地址
0x08010010: MemManage_Handler 地址
...
0x0801003C: SysTick_Handler 地址
...
后面还有 USART、TIM、DMA、EXTI 等外设中断入口

CPU 发生中断时,不是靠 C 函数名去找 ISR,而是硬件直接查这张表。

比如发生 SysTick 中断时,CPU 会去当前向量表里找:

text 复制代码
SysTick_Handler 的函数地址

发生 USART 中断时,CPU 会去当前向量表里找:

text 复制代码
USARTx_IRQHandler 的函数地址

所以关键点是:

当前 CPU 认为向量表在哪里,它就从哪里取中断函数入口。


2. Bootloader 跳 App 时,为什么要改 VTOR?

Cortex-M 里面有个寄存器叫:

c 复制代码
SCB->VTOR

它的作用就是告诉 CPU:

text 复制代码
当前中断向量表从哪个地址开始

Bootloader 默认运行时:

c 复制代码
SCB->VTOR = 0x08000000;

如果 Bootloader 要跳到 App1:

c 复制代码
SCB->VTOR = 0x08010000;

如果 Bootloader 要跳到 App2:

c 复制代码
SCB->VTOR = 0x08050000;

这就叫中断向量表偏移

注意,"偏移"不是把代码搬走,而是告诉 CPU:

text 复制代码
你以后找中断入口,不要去 Bootloader 那张表了,要去 App 自己那张表。

3. 不做中断向量表偏移会怎么样?

假设 Bootloader 跳到了 App1,但是你没有执行:

c 复制代码
SCB->VTOR = APP1_ADDR;

那么 CPU 还会认为:

c 复制代码
当前向量表 = 0x08000000

也就是 Bootloader 的向量表。

这时候 App1 运行后,一旦发生中断,问题就来了。

比如 App1 初始化了定时器:

c 复制代码
TIM3_IRQHandler()
{
    // App自己的定时器处理
}

但是 TIM3 中断来了以后,CPU 不会去 App1 的 TIM3_IRQHandler

它会去 Bootloader 的向量表里找 TIM3 的入口。

可能结果是:

text 复制代码
情况1:Bootloader 没有 TIM3_IRQHandler
      -> 进 Default_Handler 死循环

情况2:Bootloader 有 TIM3_IRQHandler
      -> 执行 Bootloader 的中断逻辑,和 App 外设状态完全不匹配

情况3:向量表里地址无效
      -> HardFault

情况4:FreeRTOS 的 SysTick/PendSV/SVC 没跑到 App 的处理函数
      -> RTOS 任务调度异常,系统卡死

所以你会看到一些很怪的现象:

text 复制代码
App main() 能进
外设初始化也能跑
但是一开中断就死
HAL_Delay 卡住
FreeRTOS 不调度
串口中断没反应
DMA 中断跑飞
定时器中断进 Default_Handler
莫名其妙 HardFault

这就是典型的向量表没切到 App导致的问题。


4. 为什么 main 能跑,但中断不行?

因为 Bootloader 跳 App 的时候,通常是手动跳到 App 的复位入口:

c 复制代码
uint32_t app_stack = *(uint32_t *)APP_ADDR;
uint32_t app_reset = *(uint32_t *)(APP_ADDR + 4);

__set_MSP(app_stack);

void (*app_entry)(void) = (void (*)(void))app_reset;
app_entry();

这段代码直接从 App 向量表里取了:

text 复制代码
APP_ADDR + 0:App 的 MSP
APP_ADDR + 4:App 的 Reset_Handler

所以 App 的 Reset_HandlerSystemInit()main() 可以正常进入。

但是后面的中断不是 Bootloader 手动跳的,而是 CPU 硬件自动查 SCB->VTOR

所以会出现:

text 复制代码
main() 能进
但是中断一来就炸

原因就是:

text 复制代码
Reset_Handler 是你手动跳对的;
中断入口是 CPU 根据 VTOR 自动找的。

5. 正确跳转 App 的典型流程

Bootloader 跳 App 前一般要这样做:

c 复制代码
#define APP1_ADDR  0x08010000U

typedef void (*pFunction)(void);

void jump_to_app(uint32_t app_addr)
{
    uint32_t app_stack;
    uint32_t app_reset;
    pFunction app_entry;

    // 1. 关闭全局中断,防止跳转过程中来中断
    __disable_irq();

    // 2. 关闭 SysTick,避免 Bootloader 的 SysTick 干扰 App
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL  = 0;

    // 3. 可选:反初始化 Bootloader 用过的外设
    // HAL_RCC_DeInit();
    // HAL_DeInit();

    // 4. 设置 App 的中断向量表地址
    SCB->VTOR = app_addr;

    // 5. 取 App 栈顶地址
    app_stack = *(uint32_t *)app_addr;

    // 6. 取 App Reset_Handler 地址
    app_reset = *(uint32_t *)(app_addr + 4);

    // 7. 设置主栈指针 MSP
    __set_MSP(app_stack);

    // 8. 跳转到 App 的 Reset_Handler
    app_entry = (pFunction)app_reset;
    app_entry();
}

如果是 App2:

c 复制代码
jump_to_app(0x08050000);

核心就是这句:

c 复制代码
SCB->VTOR = app_addr;

6. 双 App 区域时更要注意

单 App 时,Bootloader 只跳一个 App,比如:

text 复制代码
Bootloader -> App1

但双 App 时可能是:

text 复制代码
Bootloader -> App1
Bootloader -> App2

所以 Bootloader 要根据当前有效 App 设置不同的向量表:

c 复制代码
if (active_app == APP1)
{
    SCB->VTOR = APP1_ADDR;
    jump_to_app(APP1_ADDR);
}
else if (active_app == APP2)
{
    SCB->VTOR = APP2_ADDR;
    jump_to_app(APP2_ADDR);
}

否则有一种更危险的情况:

text 复制代码
CPU 正在运行 App2
但是 VTOR 还指向 App1

结果就是:

text 复制代码
App2 的代码在跑,
但是所有中断跑到 App1 的中断函数里。

这会非常混乱。

比如 App2 初始化的是 USART1,App1 的中断函数里处理的是旧版本协议,那就可能直接乱套。


7. App 工程本身也要配对地址

只在 Bootloader 里设置 SCB->VTOR 还不一定够。

因为很多 STM32 工程在 system_stm32xxx.c 里面有类似代码:

c 复制代码
#define VECT_TAB_OFFSET  0x00000000U

SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;

默认情况下:

c 复制代码
FLASH_BASE = 0x08000000
VECT_TAB_OFFSET = 0x00000000

所以它会设置成:

c 复制代码
SCB->VTOR = 0x08000000;

这对 Bootloader 是对的,但是对 App 是错的。

如果 App1 放在 0x08010000,那 App1 里面应该是:

c 复制代码
#define VECT_TAB_OFFSET  0x00010000U

最终:

c 复制代码
SCB->VTOR = 0x08010000;

如果 App2 放在 0x08050000,那就是:

c 复制代码
#define VECT_TAB_OFFSET  0x00050000U

最终:

c 复制代码
SCB->VTOR = 0x08050000;

所以有两个地方要一致:

text 复制代码
1. App 的链接地址
2. App 的中断向量表地址

例如 App1:

text 复制代码
链接地址:0x08010000
VTOR地址:0x08010000

App2:

text 复制代码
链接地址:0x08050000
VTOR地址:0x08050000

8. 链接地址和向量表偏移是两回事

很多人容易混。

链接地址

链接地址决定代码里面的函数、变量、常量最终认为自己在哪里。

比如 App1 链接地址是:

text 复制代码
0x08010000

那么 App1 的 Reset_Handlermain()、各种函数地址都会按 0x08010000 往后排。

向量表偏移

向量表偏移是告诉 CPU:

text 复制代码
中断入口表在哪里

也就是:

c 复制代码
SCB->VTOR = 0x08010000;

所以:

text 复制代码
链接地址不对:程序本身可能跳不起来,函数地址错。
VTOR 不对:程序可能能起来,但中断一来就错。

9. 升级过程中为什么尤其重要?

App 升级一般流程是:

text 复制代码
当前运行 App1
下载新固件到 App2
校验 App2
设置 App2 有效标志
重启
Bootloader 启动
Bootloader 检查 App2 有效
跳转 App2

这时 Bootloader 必须做:

c 复制代码
SCB->VTOR = APP2_ADDR;

否则虽然跳到了 App2,但是中断还可能找 Bootloader 或 App1。

还有一种情况更危险:

text 复制代码
App1 正在升级 App2
App2 区域正在被擦除/写入
如果 VTOR 错误地指向 App2
这时候来了中断
CPU 从正在擦写的 Flash 区域取中断入口

可能取到:

text 复制代码
0xFFFFFFFF

然后直接 HardFault 或跑飞。

所以升级/擦写 Flash 时通常还要注意:

text 复制代码
1. 不要执行正在擦写区域的代码
2. Flash 擦写期间谨慎处理中断
3. Bootloader 跳 App 前重新设置 VTOR
4. App 启动后也最好设置自己的 VTOR

10. 总结成一句工程经验

你的结构是:

text 复制代码
Bootloader + App1 + App2

那么每次 Bootloader 决定运行哪个 App,就必须让 CPU 的中断向量表指向哪个 App:

c 复制代码
运行 Bootloader:SCB->VTOR = 0x08000000

运行 App1:      SCB->VTOR = 0x08010000

运行 App2:      SCB->VTOR = 0x08050000

否则就是:

text 复制代码
代码跑在 App,
中断却跑到 Bootloader 或另一个 App。

这就是为什么必须做中断向量表偏移。




对,你这个理解方向是对的。

如果 App 正在跑,但 SCB->VTOR 还指向 Bootloader,那么 TIM2 中断来了以后,CPU 就会去 Bootloader 的向量表里找 TIM2 的 ISR 入口。

重点是:

c 复制代码
TIM2中断入口地址 = *(uint32_t *)(SCB->VTOR + TIM2_IRQn对应的偏移)

如果:

c 复制代码
SCB->VTOR = 0x08000000;   // Bootloader

那 CPU 查的就是:

text 复制代码
Bootloader 的 TIM2 中断入口

如果:

c 复制代码
SCB->VTOR = 0x08010000;   // App

那 CPU 查的才是:

text 复制代码
App 的 TIM2 中断入口

1. Bootloader 里也配置了 TIM2,会发生什么?

假设 Bootloader 里面真的写了:

c 复制代码
void TIM2_IRQHandler(void)
{
    // Bootloader自己的TIM2中断处理
}

而 App 里面也写了:

c 复制代码
void TIM2_IRQHandler(void)
{
    // App自己的TIM2中断处理
}

但是你没有设置:

c 复制代码
SCB->VTOR = APP_ADDR;

那么 App 跑起来后,TIM2 中断来了,CPU 会执行:

c 复制代码
Bootloader 的 TIM2_IRQHandler()

而不是 App 的。

这时候就会出现很诡异的情况:

text 复制代码
主程序:App 的 main()
中断函数:Bootloader 的 TIM2_IRQHandler()

也就是:

text 复制代码
代码身体在 App,
中断灵魂还在 Bootloader。

2. Bootloader 没配置 TIM2,会发生什么?

这个更常见。

你可能会想:

Bootloader 没配置 TIM2,那是不是 TIM2 中断就不会有?

不是。

因为现在 TIM2 是 App 配置的。

比如 App 里执行了:

c 复制代码
HAL_TIM_Base_Start_IT(&htim2);

或者底层做了这些事:

c 复制代码
TIM2->DIER |= TIM_DIER_UIE;       // 允许 TIM2 更新中断
NVIC_EnableIRQ(TIM2_IRQn);        // 允许 NVIC 响应 TIM2 中断
TIM2->CR1 |= TIM_CR1_CEN;         // 启动 TIM2

那么 TIM2 到时间后,仍然会产生中断。

TIM2 中断是否产生,取决于外设 TIM2 和 NVIC 是否被 App 配置了。

但是:

TIM2 中断发生后跳到哪里执行,取决于 SCB->VTOR

这两个是两回事。


3. Bootloader 没写 TIM2_IRQHandler,向量表里是什么?

STM32 工程里的启动文件通常有类似这种弱定义:

c 复制代码
void TIM2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));

意思是:

text 复制代码
如果你自己写了 TIM2_IRQHandler,就用你写的。
如果你没写 TIM2_IRQHandler,就默认指向 Default_Handler。

所以 Bootloader 如果没用 TIM2,一般就没有自己实现 TIM2_IRQHandler()

那么 Bootloader 的向量表里,TIM2 那一项通常就是:

text 复制代码
TIM2_IRQHandler -> Default_Handler

Default_Handler 通常长这样:

c 复制代码
void Default_Handler(void)
{
    while (1)
    {
    }
}

所以结果就是:

text 复制代码
App 配置 TIM2
TIM2 中断来了
CPU 去 Bootloader 向量表找 TIM2_IRQHandler
发现 Bootloader 没有 TIM2_IRQHandler
于是进入 Bootloader 的 Default_Handler
系统卡死

这就是你调试时可能看到的现象:

text 复制代码
App 明明在跑
一进 TIM2 中断
结果跑到 0x0800xxxx 的 Default_Handler 里死循环

4. 注意:Bootloader 没配置 TIM2,不代表 TIM2 中断不会发生

这个点很关键。

比如当前运行状态是:

text 复制代码
PC  正在 App 代码区
VTOR 还指向 Bootloader

然后 App 初始化了 TIM2:

c 复制代码
void app_timer_init(void)
{
    TIM2->PSC = 1000 - 1;
    TIM2->ARR = 1000 - 1;

    TIM2->DIER |= TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM2_IRQn);

    TIM2->CR1 |= TIM_CR1_CEN;
}

这个时候 TIM2 外设真的被 App 启动了。

时间到了以后:

text 复制代码
TIM2->SR 里的 UIF 置 1
NVIC 发现 TIM2_IRQn pending
CPU 响应 TIM2 中断

然后 CPU 查中断入口:

c 复制代码
handler = *(uint32_t *)(SCB->VTOR + TIM2向量偏移);

如果 SCB->VTOR 还是 Bootloader:

text 复制代码
handler = Bootloader向量表里的TIM2入口

所以不管 Bootloader 有没有配置 TIM2,只要 App 把 TIM2 和 NVIC 打开了,中断就可能来。

问题只是:

text 复制代码
中断来了以后,CPU 去错地方执行。

5. 分几种情况看结果

情况 1:Bootloader 没写 TIM2_IRQHandler

Bootloader 启动文件里默认:

c 复制代码
TIM2_IRQHandler -> Default_Handler

结果:

text 复制代码
App 启动 TIM2
TIM2 中断来了
CPU 进 Bootloader 的 Default_Handler
系统卡死

这是最常见的。


情况 2:Bootloader 写了 TIM2_IRQHandler

比如 Bootloader 里面有:

c 复制代码
void TIM2_IRQHandler(void)
{
    TIM2->SR &= ~TIM_SR_UIF;
    bootloader_tick++;
}

结果:

text 复制代码
App 启动 TIM2
TIM2 中断来了
CPU 进 Bootloader 的 TIM2_IRQHandler

这时候可能不会马上死,但是逻辑错了。

因为它执行的是 Bootloader 的中断逻辑,不是 App 的。

可能出现:

text 复制代码
App 的定时任务不执行
App 的计数变量不增加
App 的电机控制周期不跑
Bootloader 的变量被错误修改
Bootloader 的中断清标志方式和 App 不一致
系统行为很诡异

情况 3:Bootloader 的 TIM2_IRQHandler 没有正确清 TIM2 中断标志

比如 Bootloader 中断函数没有清除 App 配置产生的 TIM2 更新标志:

c 复制代码
void TIM2_IRQHandler(void)
{
    // 没有清 TIM2->SR 的 UIF
}

那么会出现:

text 复制代码
进入 TIM2_IRQHandler
退出中断
发现 TIM2 中断标志还在
马上再次进入 TIM2_IRQHandler
再退出
又马上进入
...

结果就是:

text 复制代码
CPU 一直卡在 TIM2 中断里
主循环几乎不运行
看起来像死机

情况 4:Bootloader 的 TIM2_IRQHandler 用了 Bootloader 自己的全局变量/句柄

比如 Bootloader 用 HAL:

c 复制代码
TIM_HandleTypeDef htim2;

void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

但是现在实际运行的是 App。

App 里面也有自己的:

c 复制代码
TIM_HandleTypeDef htim2;

注意,这两个 htim2 不是一个东西。

Bootloader 的 htim2 在 Bootloader 的 RAM 区域,App 的 htim2 在 App 的 RAM 区域。

如果 CPU 跑到了 Bootloader 的中断函数,它用的是 Bootloader 的 htim2

这个 htim2 可能已经不是有效状态了。

可能导致:

text 复制代码
HAL 状态错误
回调不执行
访问异常
清标志异常
进入 HardFault

所以这类问题非常隐蔽。


6. 可以把它理解成"中断函数地址拿错了"

App 里面本来应该是:

text 复制代码
TIM2中断 -> App_TIM2_IRQHandler

但因为 VTOR 没改,实际变成:

text 复制代码
TIM2中断 -> Bootloader_TIM2_IRQHandler

或者:

text 复制代码
TIM2中断 -> Bootloader_Default_Handler

所以问题不在于 TIM2 是谁配置的。

问题在于:

text 复制代码
中断来了以后,CPU 去哪张表里取函数地址。

7. 你可以用这个流程记住

正确情况:

text 复制代码
App 配置 TIM2
    ↓
TIM2 到时间产生中断
    ↓
NVIC 通知 CPU
    ↓
CPU 查 App 向量表
    ↓
执行 App 的 TIM2_IRQHandler

错误情况:

text 复制代码
App 配置 TIM2
    ↓
TIM2 到时间产生中断
    ↓
NVIC 通知 CPU
    ↓
CPU 还在查 Bootloader 向量表
    ↓
执行 Bootloader 的 TIM2_IRQHandler
    ↓
如果 Bootloader 没写,就进 Default_Handler

8. 所以 Bootloader 跳 App 时必须做这件事

c 复制代码
SCB->VTOR = APP_ADDR;

例如:

c 复制代码
#define APP1_ADDR  0x08010000U

SCB->VTOR = APP1_ADDR;

这样 TIM2 中断来了以后,CPU 查的是:

text 复制代码
0x08010000 + TIM2向量偏移

也就是 App 自己的向量表。


9. 最直接的调试现象

如果你遇到这种情况:

text 复制代码
App main 能跑
一开定时器中断就死
调试器停在 Default_Handler
Default_Handler 地址在 Bootloader 区域

那基本可以重点怀疑:

text 复制代码
SCB->VTOR 没有切到 App

可以在 App 运行后看一下:

c 复制代码
SCB->VTOR

如果它还是:

text 复制代码
0x08000000

那就是错的。

App1 应该是:

text 复制代码
0x08010000

App2 应该是:

text 复制代码
0x08050000

具体看你的 Flash 分区。


10. 一句话总结

Bootloader 没配置 TIM2,也不代表没事。

因为:

text 复制代码
App 可以配置 TIM2,让 TIM2 中断产生。
但是 CPU 响应中断时,如果 VTOR 还指向 Bootloader,
它就会去 Bootloader 的向量表找 TIM2_IRQHandler。

如果 Bootloader 有 TIM2_IRQHandler:

text 复制代码
执行 Bootloader 的 TIM2 中断函数。

如果 Bootloader 没有 TIM2_IRQHandler:

text 复制代码
通常进入 Bootloader 的 Default_Handler,然后卡死。

所以中断向量表偏移不是可选项,而是 Bootloader 跳 App 的必要步骤。




对,你这个理解方向是对的。

如果 App 正在跑,但 SCB->VTOR 还指向 Bootloader,那么 TIM2 中断来了以后,CPU 就会去 Bootloader 的向量表里找 TIM2 的 ISR 入口。

重点是:

c 复制代码
TIM2中断入口地址 = *(uint32_t *)(SCB->VTOR + TIM2_IRQn对应的偏移)

如果:

c 复制代码
SCB->VTOR = 0x08000000;   // Bootloader

那 CPU 查的就是:

text 复制代码
Bootloader 的 TIM2 中断入口

如果:

c 复制代码
SCB->VTOR = 0x08010000;   // App

那 CPU 查的才是:

text 复制代码
App 的 TIM2 中断入口

1. Bootloader 里也配置了 TIM2,会发生什么?

假设 Bootloader 里面真的写了:

c 复制代码
void TIM2_IRQHandler(void)
{
    // Bootloader自己的TIM2中断处理
}

而 App 里面也写了:

c 复制代码
void TIM2_IRQHandler(void)
{
    // App自己的TIM2中断处理
}

但是你没有设置:

c 复制代码
SCB->VTOR = APP_ADDR;

那么 App 跑起来后,TIM2 中断来了,CPU 会执行:

c 复制代码
Bootloader 的 TIM2_IRQHandler()

而不是 App 的。

这时候就会出现很诡异的情况:

text 复制代码
主程序:App 的 main()
中断函数:Bootloader 的 TIM2_IRQHandler()

也就是:

text 复制代码
代码身体在 App,
中断灵魂还在 Bootloader。

2. Bootloader 没配置 TIM2,会发生什么?

这个更常见。

你可能会想:

Bootloader 没配置 TIM2,那是不是 TIM2 中断就不会有?

不是。

因为现在 TIM2 是 App 配置的。

比如 App 里执行了:

c 复制代码
HAL_TIM_Base_Start_IT(&htim2);

或者底层做了这些事:

c 复制代码
TIM2->DIER |= TIM_DIER_UIE;       // 允许 TIM2 更新中断
NVIC_EnableIRQ(TIM2_IRQn);        // 允许 NVIC 响应 TIM2 中断
TIM2->CR1 |= TIM_CR1_CEN;         // 启动 TIM2

那么 TIM2 到时间后,仍然会产生中断。

TIM2 中断是否产生,取决于外设 TIM2 和 NVIC 是否被 App 配置了。

但是:

TIM2 中断发生后跳到哪里执行,取决于 SCB->VTOR

这两个是两回事。


3. Bootloader 没写 TIM2_IRQHandler,向量表里是什么?

STM32 工程里的启动文件通常有类似这种弱定义:

c 复制代码
void TIM2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));

意思是:

text 复制代码
如果你自己写了 TIM2_IRQHandler,就用你写的。
如果你没写 TIM2_IRQHandler,就默认指向 Default_Handler。

所以 Bootloader 如果没用 TIM2,一般就没有自己实现 TIM2_IRQHandler()

那么 Bootloader 的向量表里,TIM2 那一项通常就是:

text 复制代码
TIM2_IRQHandler -> Default_Handler

Default_Handler 通常长这样:

c 复制代码
void Default_Handler(void)
{
    while (1)
    {
    }
}

所以结果就是:

text 复制代码
App 配置 TIM2
TIM2 中断来了
CPU 去 Bootloader 向量表找 TIM2_IRQHandler
发现 Bootloader 没有 TIM2_IRQHandler
于是进入 Bootloader 的 Default_Handler
系统卡死

这就是你调试时可能看到的现象:

text 复制代码
App 明明在跑
一进 TIM2 中断
结果跑到 0x0800xxxx 的 Default_Handler 里死循环

4. 注意:Bootloader 没配置 TIM2,不代表 TIM2 中断不会发生

这个点很关键。

比如当前运行状态是:

text 复制代码
PC  正在 App 代码区
VTOR 还指向 Bootloader

然后 App 初始化了 TIM2:

c 复制代码
void app_timer_init(void)
{
    TIM2->PSC = 1000 - 1;
    TIM2->ARR = 1000 - 1;

    TIM2->DIER |= TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM2_IRQn);

    TIM2->CR1 |= TIM_CR1_CEN;
}

这个时候 TIM2 外设真的被 App 启动了。

时间到了以后:

text 复制代码
TIM2->SR 里的 UIF 置 1
NVIC 发现 TIM2_IRQn pending
CPU 响应 TIM2 中断

然后 CPU 查中断入口:

c 复制代码
handler = *(uint32_t *)(SCB->VTOR + TIM2向量偏移);

如果 SCB->VTOR 还是 Bootloader:

text 复制代码
handler = Bootloader向量表里的TIM2入口

所以不管 Bootloader 有没有配置 TIM2,只要 App 把 TIM2 和 NVIC 打开了,中断就可能来。

问题只是:

text 复制代码
中断来了以后,CPU 去错地方执行。

5. 分几种情况看结果

情况 1:Bootloader 没写 TIM2_IRQHandler

Bootloader 启动文件里默认:

c 复制代码
TIM2_IRQHandler -> Default_Handler

结果:

text 复制代码
App 启动 TIM2
TIM2 中断来了
CPU 进 Bootloader 的 Default_Handler
系统卡死

这是最常见的。


情况 2:Bootloader 写了 TIM2_IRQHandler

比如 Bootloader 里面有:

c 复制代码
void TIM2_IRQHandler(void)
{
    TIM2->SR &= ~TIM_SR_UIF;
    bootloader_tick++;
}

结果:

text 复制代码
App 启动 TIM2
TIM2 中断来了
CPU 进 Bootloader 的 TIM2_IRQHandler

这时候可能不会马上死,但是逻辑错了。

因为它执行的是 Bootloader 的中断逻辑,不是 App 的。

可能出现:

text 复制代码
App 的定时任务不执行
App 的计数变量不增加
App 的电机控制周期不跑
Bootloader 的变量被错误修改
Bootloader 的中断清标志方式和 App 不一致
系统行为很诡异

情况 3:Bootloader 的 TIM2_IRQHandler 没有正确清 TIM2 中断标志

比如 Bootloader 中断函数没有清除 App 配置产生的 TIM2 更新标志:

c 复制代码
void TIM2_IRQHandler(void)
{
    // 没有清 TIM2->SR 的 UIF
}

那么会出现:

text 复制代码
进入 TIM2_IRQHandler
退出中断
发现 TIM2 中断标志还在
马上再次进入 TIM2_IRQHandler
再退出
又马上进入
...

结果就是:

text 复制代码
CPU 一直卡在 TIM2 中断里
主循环几乎不运行
看起来像死机

情况 4:Bootloader 的 TIM2_IRQHandler 用了 Bootloader 自己的全局变量/句柄

比如 Bootloader 用 HAL:

c 复制代码
TIM_HandleTypeDef htim2;

void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}

但是现在实际运行的是 App。

App 里面也有自己的:

c 复制代码
TIM_HandleTypeDef htim2;

注意,这两个 htim2 不是一个东西。

Bootloader 的 htim2 在 Bootloader 的 RAM 区域,App 的 htim2 在 App 的 RAM 区域。

如果 CPU 跑到了 Bootloader 的中断函数,它用的是 Bootloader 的 htim2

这个 htim2 可能已经不是有效状态了。

可能导致:

text 复制代码
HAL 状态错误
回调不执行
访问异常
清标志异常
进入 HardFault

所以这类问题非常隐蔽。


6. 可以把它理解成"中断函数地址拿错了"

App 里面本来应该是:

text 复制代码
TIM2中断 -> App_TIM2_IRQHandler

但因为 VTOR 没改,实际变成:

text 复制代码
TIM2中断 -> Bootloader_TIM2_IRQHandler

或者:

text 复制代码
TIM2中断 -> Bootloader_Default_Handler

所以问题不在于 TIM2 是谁配置的。

问题在于:

text 复制代码
中断来了以后,CPU 去哪张表里取函数地址。

7. 你可以用这个流程记住

正确情况:

text 复制代码
App 配置 TIM2
    ↓
TIM2 到时间产生中断
    ↓
NVIC 通知 CPU
    ↓
CPU 查 App 向量表
    ↓
执行 App 的 TIM2_IRQHandler

错误情况:

text 复制代码
App 配置 TIM2
    ↓
TIM2 到时间产生中断
    ↓
NVIC 通知 CPU
    ↓
CPU 还在查 Bootloader 向量表
    ↓
执行 Bootloader 的 TIM2_IRQHandler
    ↓
如果 Bootloader 没写,就进 Default_Handler

8. 所以 Bootloader 跳 App 时必须做这件事

c 复制代码
SCB->VTOR = APP_ADDR;

例如:

c 复制代码
#define APP1_ADDR  0x08010000U

SCB->VTOR = APP1_ADDR;

这样 TIM2 中断来了以后,CPU 查的是:

text 复制代码
0x08010000 + TIM2向量偏移

也就是 App 自己的向量表。


9. 最直接的调试现象

如果你遇到这种情况:

text 复制代码
App main 能跑
一开定时器中断就死
调试器停在 Default_Handler
Default_Handler 地址在 Bootloader 区域

那基本可以重点怀疑:

text 复制代码
SCB->VTOR 没有切到 App

可以在 App 运行后看一下:

c 复制代码
SCB->VTOR

如果它还是:

text 复制代码
0x08000000

那就是错的。

App1 应该是:

text 复制代码
0x08010000

App2 应该是:

text 复制代码
0x08050000

具体看你的 Flash 分区。


10. 一句话总结

Bootloader 没配置 TIM2,也不代表没事。

因为:

text 复制代码
App 可以配置 TIM2,让 TIM2 中断产生。
但是 CPU 响应中断时,如果 VTOR 还指向 Bootloader,
它就会去 Bootloader 的向量表找 TIM2_IRQHandler。

如果 Bootloader 有 TIM2_IRQHandler:

text 复制代码
执行 Bootloader 的 TIM2 中断函数。

如果 Bootloader 没有 TIM2_IRQHandler:

text 复制代码
通常进入 Bootloader 的 Default_Handler,然后卡死。

所以中断向量表偏移不是可选项,而是 Bootloader 跳 App 的必要步骤。

相关推荐
不脱发的程序猿2 小时前
MCU升级固件合并和转换工具
单片机·嵌入式硬件
qq_370773092 小时前
OpenOCD 嵌入式调试完全指南:从零开始调试 GD32/STM32 单片机
stm32·单片机·嵌入式硬件·openocd
黑猫学长呀2 小时前
存储宝典第1篇:Nand SCA是什么
arm开发·arm·nand·存储芯片·nandflash·onfi
LCG元2 小时前
STM32实战:基于STM32F103的迷迭香智慧种植系统(自动补光+滴灌)
stm32·单片机·嵌入式硬件
SDAU200511 小时前
CH32V103C8T6的时钟操作
单片机·嵌入式硬件
不做无法实现的梦~11 小时前
SBUS 接收机到 STM32:为什么要做硬件反相、如何解析数据、如何接线与实现代码
stm32·单片机·嵌入式硬件
一路往蓝-Anbo12 小时前
第二章:隔离硬件 —— 利用 CMock 伪造 GPIO 与定时器
stm32·单片机·嵌入式硬件·软件工程·信息与通信·tdd
刘延林.13 小时前
esp32 s3+micpython快速验证ML307R 是否能正常连接4G
单片机·嵌入式硬件
不做无法实现的梦~18 小时前
86步进电机和DM860H驱动器的使用方法和记录
单片机·嵌入式硬件