4、启动文件编写:芯片复位流程、堆栈初始化、中断向量表重映射
好,咱们进入第四章。这一章可以说是整个移植工作的地基——启动文件。你想想看,芯片一上电,CPU 从哪开始跑?谁给它指路?谁给它准备运行环境?这些全写在启动文件里。
我个人习惯把启动文件比作「开机引导程序」。它干的事不多,但每件都性命攸关。咱们今天就把这三件事掰开揉碎讲清楚:芯片复位流程、堆栈初始化、中断向量表重映射。
4.1 芯片复位流程——上电后第一行代码在哪?
ARM Cortex-M 内核的芯片,复位后硬件会自动做两件事:
- 从地址
0x00000000读取栈顶指针(MSP 初始值) - 从地址
0x00000004读取复位向量(Reset_Handler 地址)
然后 CPU 直接跳转到 Reset_Handler 去执行。说白了,这就是芯片的「出生证明」——它知道自己该从哪开始。
核心要点:启动文件必须把这两个值放在正确的位置。第一个字是栈顶,第二个字是复位入口。顺序不能错,否则芯片直接跑飞。
我在项目中遇到过一件事:有次帮客户调试一块新板子,焊好之后死活跑不起来。查了半天,发现是链接脚本里把 __initial_sp 放到了 Reset_Handler 后面。嗯,这种低级错误,犯过一次就再也不会忘了。
复位流程大致是这样的:
上电复位 → 硬件加载 MSP → 硬件加载 PC → 跳转 Reset_Handler
↓
Reset_Handler:
1. 关闭全局中断
2. 拷贝 .data 段(从 Flash 到 RAM)
3. 清零 .bss 段
4. 初始化系统时钟
5. 调用 main()
你可能会问:为什么先关中断?因为复位时外设状态不确定,万一哪个中断源已经挂起了,一开中断就乱套。这是规矩,也是经验。
4.2 堆栈初始化——给 C 语言搭好舞台
C 语言跑起来需要两样东西:栈(Stack)和堆(Heap)。栈用来存局部变量、函数调用现场;堆用来动态分配内存。但嵌入式系统里,堆用得少,栈才是主角。
堆栈初始化说白了就是告诉编译器:栈顶在哪?栈有多大?堆从哪开始?
看一个典型的汇编写法:
; 栈大小定义
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; 堆大小定义
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
这里要注意几个细节:
ALIGN=3表示 8 字节对齐。ARM 要求栈必须 8 字节对齐,否则调用printf这类函数时会出问题。__initial_sp就是栈顶地址,它会被放到中断向量表的第一个位置。- 栈大小 1KB(0x400)够不够?看你的任务数量。我一般给每个任务分配 256 字节,再加一个 512 字节的异常栈。牙刷这种简单应用,1KB 绰绰有余。
个人经验:我曾经在一个项目里把栈设得太小,结果函数嵌套一深就栈溢出,程序随机死机。查了三天才找到原因。后来我养成了一个习惯:在启动文件里加一段栈填充模式(比如 0xDEADBEEF),然后在运行时定期检查栈底有没有被踩。这招很土,但很管用。
堆的初始化相对简单。如果你不用 malloc,堆甚至可以设为 0。RTOS 的任务栈通常是从堆里分配的,所以堆大小要预留够。我建议至少 2KB 起步。
4.3 中断向量表重映射——让中断找到正确的门
默认情况下,Cortex-M 的中断向量表放在 Flash 的起始地址(0x00000000)。但有时候我们需要把它搬到 RAM 里,或者搬到 Flash 的其他位置。为什么?
- 运行时修改中断向量:Flash 是只读的,RAM 可写。如果你需要在运行时动态修改某个中断的处理函数,就必须把向量表搬到 RAM。
- Bootloader + 应用程序:Bootloader 占用了 0x00000000,应用程序就得把向量表放到偏移位置,比如 0x00010000。
- RTOS 接管中断:有些 RTOS 需要接管 PendSV 和 SysTick 中断,重映射向量表可以更灵活地管理。
重映射怎么做?Cortex-M3/M4 提供了一个寄存器:SCB->VTOR(Vector Table Offset Register)。你只需要把新的向量表基地址写进去就行。
// 假设新的向量表在 0x20000000(SRAM 起始地址)
#define SCB_VTOR (*(volatile uint32_t *)0xE000ED08)
void vector_table_remap(void)
{
// 1. 先把向量表复制到 SRAM
// 注意:复制前要确保 SRAM 区域已经初始化
memcpy((void *)0x20000000, (void *)0x08000000, 0x100);
// 2. 设置 VTOR 寄存器
SCB_VTOR = 0x20000000;
// 3. 读回确认(调试用)
uint32_t vtor_value = SCB_VTOR;
// 如果 vtor_value == 0x20000000,说明设置成功
}
警告:重映射向量表时,一定要确保新地址是 512 字节对齐的(Cortex-M3/M4 要求)。否则硬件会忽略你的设置,继续用默认的向量表。我见过有人因为对齐问题折腾了一整天。
还有一个坑:如果你把向量表搬到 SRAM,那么 SRAM 的起始地址必须能被 512 整除。比如 0x20000000 没问题,0x20000100 就不行。嗯,这个细节很容易被忽略。
4.4 整合到启动文件中
好了,现在我们把三件事串起来。一个完整的启动文件应该包含:
| 步骤 | 内容 | 说明 |
|---|---|---|
| 1 | 定义栈和堆 | 分配内存空间,定义 __initial_sp、__heap_base、__heap_limit |
| 2 | 中断向量表 | 按顺序列出所有中断入口,第一个是栈顶,第二个是 Reset_Handler |
| 3 | Reset_Handler | 拷贝 .data、清零 .bss、初始化系统时钟、调用 main |
| 4 | 其他中断处理 | 默认指向一个死循环函数(防止意外触发) |
| 5 | 系统初始化 | SystemInit 函数(配置时钟、FPU 等) |
这里我多说一句:很多初学者喜欢在启动文件里直接调用 main(),但忽略了 SystemInit。结果芯片跑在默认的慢速时钟上,性能大打折扣。我建议在 Reset_Handler 里先调 SystemInit,再调 main。顺序不要搞反。
避坑指南:我曾经在一个项目里把 SystemInit 放在了 main() 里面,结果启动文件里直接调了 main()。看起来没问题,但后来发现某些外设在 main() 之前就已经被触发了,时钟没配好,外设直接罢工。从那以后,我坚持在 Reset_Handler 里完成所有初始化,再跳转 main。
4.5 小结
启动文件这东西,写一次就定型了。但每次移植到新芯片,都得重新审视这三个点:
- 复位流程对不对?栈顶和复位向量位置正确吗?
- 栈大小够不够?堆要不要留?
- 中断向量表需不需要重映射?对齐了吗?
下一章咱们开始写链接脚本。链接脚本决定了代码和数据怎么分布到 Flash 和 RAM 里。嗯,那也是个容易踩坑的地方,到时候我把我踩过的坑都告诉你。