1 什么是启动文件
启动文件是嵌入式系统中必不可少的文件,通常以汇编语言编写,负责初始化硬件和准备运行环境,以便用户的 C 程序能够正常执行。C 语言的程序通常从 main 函数开始执行,但 CPU 在上电后并不知道从哪里开始执行,因此需要启动文件来引导执行过程。
2 汇编指令

伪指令的意思是指这个 “指令” 并不会生产二进制程序代码,也不会引起变量空间分配。
3 启动文件分析
3.1 主要功能
- 初始化堆栈指针 SP=_initial_sp
- 初始化 PC 指针 =Reset_Handler
- 初始化中断向量表
- 配置系统时钟
- 调用C库的 _main 函数,最终调用main函数
3.2 开辟栈空间

- Stack_Size EQU 0x00000400
- 定义栈空间大小为1kb
- 相当于#define Stack_Size 0x00000400
- EQU 是宏定义的伪指令
- AREA STACK, NOINIT, READWRITE, ALIGN=3
- 开辟名为STACK
- 不初始化
- 可读可写
- 8字节对齐的地址空间
- Stack_Mem SPACE Stack_Size
- 告诉汇编器给栈分配Stack_Size 大小的连续内存空间
- __initial_sp
- 一个标号,标号主要用于表示一片内存空间的某个位置
- 在这里,紧接着 SPACE 语句放置,表示了栈顶地址
3.3 开辟堆空间

- Heap_Size EQU 0x00000200
- 定义堆空间大小为0.5kb
- 相当于#define Heap_Size 0x00000400
- EQU 是宏定义的伪指令
- AREA HEAP, NOINIT, READWRITE, ALIGN=3
- 开辟名为HEAP
- 不初始化
- 可读可写
- 8字节对齐的地址空间
- __heap_base
- 标识堆的开始地址
- Heap_Mem SPACE Heap_Size
- 告诉汇编器给堆分配Heap_Size 大小的连续内存空间
- __heap_limit
- 标识堆的结束地址
- PRESERVE8
- 指定当前文件的堆栈按照 8 字节对齐
- THUMB:
- 表示后面指令兼容 THUMB 指令。THUBM 是 ARM 以前的指令集,16bit,现在 Cortex-M系列的都使用 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB的超级。
3.4 中断向量表

- AREA RESET, DATA, READONLY
- 定义一个名为RESET 的只读数据段
- EXPORT
- 声明 __Vectors、__Vectors_End 和 __Vectors_Size 这三个标号具有全局属性,可供外部的文件调用
- __Vectors
- 表示中断向量表入口地址
- __Vectors_End
- 表示中断向量表的结束地址
- __Vectors_Size
- 表示中断向量表的大号

………………..

- DCD
- 表示分配一个4字节的空间
- __Vectors DCD __initial_sp
- 开始建立中断向量表,首地址一定是栈顶指针
- 73行 ~ 87行
- 系统的中断
- ; External Interrupts
- 表示下面90行 ~ 180行是外设的中断
3.5 复位程序

- AREA |.text|, CODE, READONLY
- 定义一个名为.text 的可读代码段
- PROC … ENDP
- 表示程序的开始和结束
- EXPORT Reset_Handler [WEAK]
- 声明带全局属性的WEAK 中断服务程序
- IMPORT
- 告知编译器要是用的标号在其他源文件中定义
- LDR R0, =SystemInit
- 跳转到SystemInit 执行
- LDR R0, =__main
- 跳转到__main 执行
3.6 中断服务程序

……

- B:跳转到一个标号。这里跳转到一个‘.’,即表示无线循环。
- 在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置而已。
- 如果我们在使用某个外设的时候,开启了某个中断,但是又忘记编写配套的中断服务程序或者函数名写错,那当中断来临的时,程序就会跳转到启动文件预先写好的空的中断服务程序中,并且在这个空函数中无线循环,即程序就死在这里。
3.7 初始化堆栈

- __user_initial_stackheap
- 标号,表示用户堆栈初始化程序入口
- LDR
- 分别保存栈顶指针和栈大小,堆始地址和堆大小至R0,R1,R2,R3寄存器
4 系统启动流程
下面这段话引用自《CM3 权威指南 CnR2》3.8—复位序列,CM4 的复位序列跟 CM3 一样。
在离开复位状态后,CM3 做的第一件事就是读取下列两个 32 位整数的值:
- 从地址 0x0000,0000 处取出 MSP 的初始值。
- 从地址 0x0000,0004 处取出 PC 的初始值——这个值是复位向量,LSB 必须是 1。然后从这个值所对应的地址处取指。

请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。在 CM3 中,在 0 地址处提供 MSP 的初始值,然后紧跟着就是向量表。向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是我们刚刚分析的 Reset_Handler
这个函数。

因为 CM3 使用的是向下生长的满栈,所以 MSP(主堆栈指针) 的初始值必须是堆栈内存的末地址加 1。举例来说,如果我们的堆栈区域在 0x20007C00-0x20007FFF 之间,那么 MSP 的初始值就必须是 0x20008000。向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。要注意因为 CM3 是在 Thumb 下执行,所以向量表中的每个数值都必须把 LSB 置 1(也就是奇数)。正是因为这个原因,图中使用 0x101 来表达地址 0x100。当 0x100 处的指令得到执行后,就正式开始了程序的执行(即去到 C 的世界)。在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没来得及执行,就发生了NMI 或是其它 fault。MSP 初始化好后就已经为它们的服务例程准备好了堆栈。现在,程序就进入了我们熟悉的 C 世界,现在我们也应该明白 main 并不是系统执行的第一个程序了。