4、向量表与中断管理:向量表重定向原理、中断服务函数注册、双Bank启动中的中断处理
中断管理,说白了就是MCU的“应急响应系统”。你想想看,CPU正在跑主循环,突然来了个按键、定时器溢出或者串口数据,它得立刻放下手头的事去处理。这个“放下手头事、跳转到指定地址执行”的过程,全靠一张表——向量表。
做Bootloader,最核心的一个动作就是向量表重定向。没有它,你的APP永远跑不起来。
4.1 向量表的结构与默认位置
大部分Cortex-M内核的MCU,向量表默认放在0x00000000地址。这张表里存的是啥?
- 栈顶指针:复位后SP寄存器的初始值
- 复位向量:程序入口地址
- NMI、HardFault、MemManage、BusFault、UsageFault:各种异常入口
- 外设中断向量:TIM、UART、DMA等外设的中断服务函数地址
每个向量占4字节。以STM32为例,前16个是系统异常,后面才是外设中断。整个表的大小取决于芯片支持的中断数量。
关键点:CPU复位后,硬件自动从0x00000000加载栈顶指针,从0x00000004加载PC指针。这就是为什么向量表必须放在起始位置。
4.2 向量表重定向原理
Bootloader在0x08000000,APP在0x08020000。APP的中断向量表当然也在0x08020000。问题是——CPU怎么知道要去那里找向量?
Cortex-M内核提供了一个寄存器:SCB->VTOR(Vector Table Offset Register)。
你只需要把APP的基地址写进VTOR,CPU就会自动从新地址读取向量。就这么简单。
// 以STM32为例,重定向向量表到0x08020000
#define APP_BASE_ADDRESS 0x08020000
SCB->VTOR = APP_BASE_ADDRESS;
嗯,这里要注意:VTOR必须对齐到512字节边界(有些芯片要求更高,比如1024字节)。为什么?因为向量表大小是固定的,对齐是为了硬件查找方便。
我曾经踩过的坑:有一次我把APP地址设成了0x08020080,没对齐。结果一进中断就HardFault。查了两天才发现是VTOR没对齐。从那以后,我每次写VTOR之前都会检查地址的低9位是不是0。
4.3 中断服务函数注册
中断服务函数怎么注册?说白了就是把函数地址填到向量表的对应位置。
在标准库或HAL库中,我们通常用__attribute__((interrupt))或者WEAK声明。但做Bootloader时,有个细节容易被忽略:
- Bootloader的中断服务函数:只在Bootloader阶段有效
- APP的中断服务函数:跳转到APP后,中断向量表已经重定向,Bootloader的中断函数不再被调用
所以,你不需要在Bootloader里实现所有外设的中断函数。只需要实现跳转相关的(比如按键检测、通信超时等)。
我的个人习惯:在Bootloader里只开一个定时器中断做看门狗喂狗,和一个串口中断做固件接收。其他中断全部关闭。这样既简单又安全。
4.4 双Bank启动中的中断处理
双Bank启动,就是芯片内部有两块Flash区域:Bank0和Bank1。一个跑Bootloader,一个跑APP。或者两个都跑APP,做A/B升级。
这时候中断处理就有点意思了。
| 场景 | 向量表位置 | VTOR设置 | 注意事项 |
|---|---|---|---|
| Bootloader运行 | Bank0起始地址 | 默认或Bank0地址 | 只处理必要中断 |
| APP运行(Bank1) | Bank1起始地址 | Bank1地址 | 确保Bank1的向量表完整 |
| 回滚到旧APP | Bank0的APP区 | Bank0的APP区地址 | 注意Bank0可能被Bootloader占用 |
双Bank启动时,中断处理的核心问题只有一个:中断来了,CPU该找哪个向量表?
答案很简单:看VTOR指向哪里。
所以,在跳转到APP之前,你必须把VTOR改成APP的基地址。否则,中断一来,CPU还是去Bootloader的向量表找函数,结果要么找不到,要么跑错地方。
避坑指南:我曾经在一个双Bank项目里,跳转前忘了改VTOR。结果APP跑得挺好,但只要一按按键(外部中断),系统就死机。后来发现是中断向量还在指向Bootloader,而Bootloader里根本没实现这个按键中断函数。嗯,从那以后,我把VTOR重定向写进了跳转函数的第一步。
4.5 中断向量表重定向的完整流程
我总结了一个标准流程,每次做Bootloader都这么干:
- 关闭全局中断:跳转前关掉所有中断,防止跳转过程中被打断
- 清理中断标志:确保没有挂起的中断
- 设置VTOR:写入APP的基地址
- 设置栈指针:从APP向量表加载新的栈顶指针
- 跳转到APP复位向量:从APP向量表加载PC指针
- APP初始化:APP重新配置中断、外设、时钟等
// 标准跳转函数示例
void jump_to_app(uint32_t app_address)
{
uint32_t stack_ptr;
uint32_t reset_vector;
void (*app_entry)(void);
// 1. 关闭全局中断
__disable_irq();
// 2. 清理中断标志(具体实现取决于芯片)
// ...
// 3. 重定向向量表
SCB->VTOR = app_address;
// 4. 从APP向量表加载栈顶指针
stack_ptr = *(volatile uint32_t *)app_address;
// 5. 从APP向量表加载复位向量
reset_vector = *(volatile uint32_t *)(app_address + 4);
// 6. 设置主栈指针
__set_MSP(stack_ptr);
// 7. 跳转到APP
app_entry = (void (*)(void))reset_vector;
app_entry();
}
注意:跳转前一定要确保外设时钟、DMA、中断控制器等已经恢复到复位状态。否则APP初始化时可能会遇到奇怪的问题。
4.6 向量表与中断管理知识图谱
下面这张图是我自己画的,把向量表重定向、中断注册和双Bank启动的关系理清楚了。
4.7 常见问题与避坑
- 中断进不去:检查VTOR是否设置正确,地址是否对齐
- 跳转后死机:检查栈指针是否从APP向量表正确加载
- 双Bank切换后中断混乱:确认两个Bank的向量表都完整,且VTOR指向正确
- 中断优先级配置丢失:APP初始化时要重新配置NVIC
我的经验:调试中断问题,最有效的方法是先关掉所有中断,一个一个开。这样能快速定位是哪个中断出了问题。另外,用逻辑分析仪抓中断引脚波形,比看代码快多了。
向量表重定向是Bootloader的基石。搞懂了它,中断管理就没什么神秘的了。记住一句话:VTOR指向哪里,CPU就去哪里找中断服务函数。