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 库头文件里都有定义。

我习惯把系统异常和外部中断的分配关系画成一张图,这样看起来更直观:

中断向量表分配结构 系统异常 (Exception) — 前 16 项 偏移 0x0000 ~ 0x003C 包含:Reset、NMI、HardFault、MemManage、BusFault、UsageFault、SVCall、PendSV、SysTick 外部中断 (IRQ) — 第 16 项起 偏移 0x0040 开始,每项 4 字节 包含:EXTI、TIM、USART、SPI、DMA、ADC、I2C 等外设中断 说明: • 系统异常由内核产生,优先级固定或可配置 • 外部中断由外设触发,优先级可编程(0~15) • 具体芯片支持的外部中断数量,请查阅对应参考手册

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……嗯,大小写差了一个字母。所以,中断函数名必须和启动文件里的符号名完全一致,连大小写都不能错。