4、中断向量表与中断入口:从硬件复位到第一行C代码
中断向量表,说白了就是一张“地址-函数”的映射表。芯片上电后,CPU 从哪条指令开始执行?发生异常时跳到哪里去处理?答案都写在这张表里。我刚开始接触 STM32 时,总觉得这玩意儿是启动文件里自动搞定的,直到有一次自己写裸机程序,发现中断死活不响应……嗯,查了半天,原来是向量表地址配错了。
4.1 中断向量表的结构
ARM Cortex-M 内核规定,向量表必须从地址 0 开始(或者从 VTOR 指向的地址开始)。每个表项占 4 字节,存放的是对应异常或中断的服务函数入口地址。表的前 16 项是系统异常,从第 16 项开始才是外部中断。
| 偏移地址 | 异常/中断号 | 名称 | 说明 |
|---|---|---|---|
| 0x0000 | - | 栈顶地址 | 初始 MSP 值 |
| 0x0004 | 1 | Reset_Handler | 复位入口 |
| 0x0008 | 2 | NMI_Handler | 不可屏蔽中断 |
| 0x000C | 3 | HardFault_Handler | 硬错误 |
| 0x0010 ~ 0x003C | 4 ~ 15 | 其他系统异常 | MemManage、BusFault、UsageFault、SVCall、PendSV、SysTick 等 |
| 0x0040 起 | 16+ | 外部中断 | EXTI、TIM、USART、SPI、DMA 等外设中断 |
关键点:向量表的第 0 项不是函数指针,而是栈顶地址。CPU 复位后先从这里加载 MSP,再从第 1 项取出 Reset_Handler 地址跳转执行。这个顺序搞反了,程序连跑都跑不起来。
4.2 系统异常与外部中断的向量分配
Cortex-M3/M4 支持最多 240 个外部中断,但具体芯片用多少,得看厂家的设计。以 STM32F103 为例,它只用了 60 个外部中断,编号从 16 到 75。每个中断对应一个 IRQn,这个编号在 ST 的 HAL 库头文件里都有定义。
我习惯把系统异常和外部中断的分配关系画成一张图,这样看起来更直观:
4.3 中断向量表重定向(VTOR 寄存器)
默认情况下,向量表固定在地址 0。但实际项目中,我们经常需要把向量表搬到 RAM 里,或者放在不同的 Flash 段。比如做 IAP(在应用编程)时,Bootloader 和 App 各有各的向量表,必须通过 VTOR 寄存器来切换。
VTOR 是 Cortex-M3/M4 内核里的一个寄存器,地址在 0xE000ED08。它的作用就是告诉 CPU:「喂,向量表现在搬家了,新地址在这儿。」
注意:VTOR 寄存器的低 9 位是保留位,必须清零。也就是说,向量表地址必须 512 字节对齐。我见过有人把地址写成 0x08000100,结果中断全乱套了——因为 0x100 不是 512 的倍数。
设置 VTOR 的代码很简单,几行汇编或直接写寄存器就行:
/* 将向量表重定向到 0x08010000 */
SCB->VTOR = 0x08010000;
/* 或者用汇编 */
__ASM volatile("LDR R0, =0xE000ED08 \n"
"LDR R1, =0x08010000 \n"
"STR R1, [R0]");
我的习惯:在系统初始化早期就设置 VTOR,最好在进入 main 函数之前。因为一旦发生中断,CPU 会立刻去查向量表,如果 VTOR 还没配好,就会跳到错误的地方去。
4.4 中断服务函数命名规则:弱定义与强定义
你有没有想过,为什么启动文件里有一堆「弱定义」的中断函数?比如 WEAK 关键字修饰的 HardFault_Handler、PendSV_Handler 等等。这其实是编译器提供的一种「默认兜底」机制。
弱定义(Weak Definition)的意思是:这个符号虽然定义了,但优先级很低。如果你在别的源文件里重新定义了一个同名的强定义(Strong Definition),链接器会优先用你的版本,把弱定义覆盖掉。
举个例子,启动文件里是这样写的:
/* 启动文件中的弱定义 */
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void Default_Handler(void)
{
while(1);
}
如果你在自己的代码里写了一个同名的 HardFault_Handler:
/* 用户代码中的强定义 */
void HardFault_Handler(void)
{
/* 打印错误信息,或者尝试恢复 */
printf("Hard Fault occurred!\n");
while(1);
}
链接器就会用你的版本,而不是那个死循环的默认版本。这就是「弱定义与强定义」的配合逻辑。
| 特性 | 弱定义 | 强定义 |
|---|---|---|
| 关键字 | __attribute__((weak)) |
无特殊关键字 |
| 优先级 | 低,可被覆盖 | 高,覆盖弱定义 |
| 用途 | 提供默认实现,防止链接错误 | 用户自定义中断处理逻辑 |
| 典型场景 | 启动文件中所有中断服务函数 | 用户编写的具体中断处理函数 |
避坑指南:我曾经在项目里遇到一个诡异的问题——明明写了 USART1 的中断处理函数,但中断就是不进去。查了半天,发现函数名写成了 USART1_IRQHandler,而启动文件里定义的是 USART1_IRQHandler……嗯,大小写差了一个字母。所以,中断函数名必须和启动文件里的符号名完全一致,连大小写都不能错。