3、启动模式与向量表:ARM/STM32启动模式、中断向量表重映射、启动文件(startup.s)解析
大家好,我是你们的嵌入式系统讲师。今天我们来聊聊Bootloader开发中最基础、也最关键的一环——启动模式与向量表。说白了,就是芯片上电后,它怎么知道该从哪里开始跑代码?中断来了又该找谁?
我个人习惯,在开始任何Bootloader项目之前,都会先把这几件事理清楚。因为一旦这里出问题,后面所有的安全刷新、固件升级都是空中楼阁。
3.1 STM32的三种启动模式
STM32芯片上电后,会先读一个叫BOOT0和BOOT1的引脚电平。这两个引脚的不同组合,决定了芯片从哪个物理地址开始执行。我刚开始做项目时,有一次焊板子不小心把BOOT0拉高了,结果程序死活跑不起来,查了半天才发现是启动模式选错了。
| BOOT1 | BOOT0 | 启动模式 | 说明 |
|---|---|---|---|
| X | 0 | 主Flash启动 | 从0x08000000开始执行,正常模式 |
| 0 | 1 | 系统存储器启动 | 从0x1FFF0000开始,内置Bootloader |
| 1 | 1 | SRAM启动 | 从0x20000000开始,调试用 |
这里有个关键点:主Flash的起始地址是0x08000000,但CPU实际上是从0x00000000取向量表的。嗯,这里有点绕,我解释一下。
芯片内部有个地址重映射机制。当BOOT0=0时,0x00000000被映射到0x08000000。所以CPU读0x00000000,实际上读的是Flash里的内容。你想想看,这样设计的好处是什么?
- 主Flash启动:正常运行时用,Bootloader和App都放在这里
- 系统存储器启动:出厂固化的Bootloader,用于串口/USB下载
- SRAM启动:调试阶段用,可以快速验证代码
3.2 中断向量表:CPU的紧急通讯录
中断向量表,说白了就是一张地址表。每个中断源对应一个入口地址,CPU收到中断信号后,就去这张表里查该跳到哪里去处理。
ARM Cortex-M系列的向量表长这样:
; 向量表起始地址(0x00000000 或 0x08000000)
0x00000000: 栈顶地址(MSP初始值)
0x00000004: 复位向量(Reset_Handler)
0x00000008: NMI中断入口
0x0000000C: HardFault入口
0x00000010: MemManage入口
...
0x000000XX: 外设中断入口(USART、TIM、DMA等)
这里有个非常重要的细节:向量表的第一个字是栈顶地址,第二个字才是复位向量。CPU上电后,先读第一个字到MSP寄存器,再读第二个字作为PC的初始值。我见过有新手自己写启动文件时把这两个搞反了,结果程序直接跑飞。
3.3 向量表重映射:Bootloader的核心技术
做Bootloader时,我们面临一个现实问题:Flash里既有Bootloader,又有App,它们各自有自己的中断向量表。怎么让它们互不干扰?
答案就是向量表重映射。Cortex-M3/M4提供了一个叫VTOR(Vector Table Offset Register)的寄存器,位于SCB(系统控制块)中,地址是0xE000ED08。
工作原理很简单:
- Bootloader启动时,VTOR指向Bootloader自己的向量表(比如0x08000000)
- Bootloader完成初始化后,跳转到App之前,把VTOR改成App的向量表地址(比如0x08020000)
- App运行后,所有中断都从新的向量表查找入口
代码实现也很直接:
/* 设置向量表偏移 */
#define SCB_VTOR (volatile uint32_t*)0xE000ED08
#define APP_VECTOR_ADDR 0x08020000 /* App的起始地址 */
void jump_to_app(void)
{
uint32_t msp_value;
void (*app_entry)(void);
/* 1. 关闭全局中断 */
__disable_irq();
/* 2. 重映射向量表 */
*SCB_VTOR = APP_VECTOR_ADDR;
/* 3. 从App向量表取栈顶和入口地址 */
msp_value = *(volatile uint32_t*)APP_VECTOR_ADDR;
app_entry = (void (*)(void))(*(volatile uint32_t*)(APP_VECTOR_ADDR + 4));
/* 4. 设置主栈指针 */
__set_MSP(msp_value);
/* 5. 跳转到App */
app_entry();
}
3.4 启动文件(startup.s)深度解析
启动文件是芯片上电后执行的第一段代码。虽然现在很多IDE都自动生成了,但我建议你还是亲手读一遍。我记得第一次看启动文件时,感觉像在读天书,但看懂之后,对芯片的理解直接上了一个台阶。
一个典型的STM32启动文件包含以下几个部分:
3.4.1 向量表定义
__Vectors DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位向量
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; 硬错误
DCD MemManage_Handler ; 内存管理
DCD BusFault_Handler ; 总线错误
DCD UsageFault_Handler ; 用法错误
; ... 其他中断向量 ...
每个DCD指令分配4字节,存放一个32位地址。注意看,第一个是栈顶,第二个才是复位入口。这个顺序是ARM设计好的,不能改。
3.4.2 复位处理函数
复位后,CPU会跳到这里。它负责三件大事:
- 初始化全局变量:把.data段从Flash拷贝到SRAM,把.bss段清零
- 初始化系统时钟:调用SystemInit()设置时钟树
- 跳转到main():调用__libc_init_array()后进入主程序
Reset_Handler PROC
EXPORT Reset_Handler
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
这里有个细节:为什么用BX跳转而不是BL?因为__main是C库的入口,它最终会调用main(),而且不会再返回。用BL的话,如果__main返回了,程序就不知道跑哪去了。
3.4.3 弱定义中断处理
NMI_Handler PROC
EXPORT NMI_Handler
B .
ENDP
这个B .是死循环。所有未实现的中断处理函数默认都指向这个死循环。如果你在代码里实现了某个中断函数,链接器会用你的实现覆盖这个弱定义。
3.5 知识结构总览
下面这张图把本章的核心知识点串起来了,你可以对照着理解:
3.6 避坑指南
最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路:
- 向量表对齐问题:VTOR寄存器要求偏移量是512字节的整数倍(Cortex-M3/M4)。如果你把App放在0x08020000,没问题;但如果放在0x08020100,VTOR就设不了,因为不是512的倍数。
- 中断优先级分组:跳转到App前,记得把中断优先级分组也恢复成默认值。否则App里设置优先级时可能会乱掉。
- 外设状态清理:Bootloader用过的外设(比如串口、定时器),跳转前要彻底复位。不然App初始化时可能遇到意外状态。