启动文件
0. 它是谁、什么时候跑
-
文件名:
startup_stm32f407xx.s,汇编编写,放在 MDK-ARM 目录下。 -
上电复位后第一个执行 的程序,在
main()之前。 -
干 5 件事:CPU 上电后从向量表第0项取栈指针、从第1项取 Reset_Handler 地址跳转 → 调
SystemInit()→ 调__main(C 库初始化)→ 跳进main()。
1. 栈和堆
Stack_Size EQU 0x00000400 ; 栈 1KB
Heap_Size EQU 0x00000200 ; 堆 512B
| 栈(Stack) | 堆(Heap) | |
|---|---|---|
| 用途 | 局部变量、函数调用、形参 | malloc() 动态分配 |
| 生长方向 | 高地址 → 低地址 | 低地址 → 高地址 |
| 大小默认值 | 1KB | 512B |
| 不够了会怎样 | 溢出 → HardFault | malloc 返回 NULL |
局部变量多了/递归深了,栈不够会莫名其妙进 HardFault。遇到这种事先把
Stack_Size改大。
2. 向量表
向量表就是一个函数地址数组,存在 Flash 0 地址。
__Vectors DCD __initial_sp ; 第0个:栈顶地址(不是函数!)
DCD Reset_Handler ; 第1个:复位
DCD NMI_Handler ; 第2个:不可屏蔽中断
DCD HardFault_Handler ; 第3个:硬错误
...
DCD WWDG_IRQHandler ; 外部中断从这里开始
...
CPU 响应中断的机制:中断号 × 4 → 到向量表里查对应地址 → 跳过去执行。
| 位置 | 内容 | 说明 |
|---|---|---|
| 0x00 | __initial_sp |
栈顶地址,复位后直接加载到 SP |
| 0x04 | Reset_Handler | 复位后第一条要跑的代码 |
| 0x08~0x3C | 系统异常 | NMI / HardFault / SVC / PendSV / SysTick 等 |
| 0x40 起 | 外部中断 | WWDG / PVD / 各种外设中断 |
3. Reset_Handler:复位后干了什么
Reset_Handler PROC
LDR R0, =SystemInit ; ① 调 SystemInit()
BLX R0
LDR R0, =__main ; ② 调 __main(C 库初始化,最终调你的 main)
BX R0
ENDP
-
SystemInit():在system_stm32f4xx.c里。HAL 工程里它不配时钟 ,只做 FPU 使能、向量表地址设置、外部 SRAM 配置。时钟靠你自己写的SystemClock_Config()(见 4. 时钟.md 第6节)。 -
__main:C 库函数,初始化堆和栈,然后跳进你的main()。
4. WEAK(弱定义)------最关键的概念
启动文件里所有中断服务函数都是空实现,标了 [WEAK]:
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B . ; 死循环
ENDP
规则:
-
你在 C 文件里写了同名函数(比如
void SysTick_Handler(void) {})→ 用你的。 -
没写 → 用这个空的死循环。
后果:开了中断但忘了写中断服务函数 / 函数名拼错 → 中断来了直接跳进这个死循环,程序卡死。
你自己写中断函数时,名字必须和向量表里的完全一致(如
USART1_IRQHandler),错一个字母就等于没写。
5. 常用汇编指令速查
| 指令 | 作用 |
|---|---|
EQU |
给常量起名字,相当于 C 的 #define |
SPACE |
分配内存(单位字节) |
DCD |
以字为单位分配并初始化内存(向量表用它存函数地址) |
EXPORT |
把本文件的标号导出给别的模块用,相当于 C 里非 static 的全局定义(对外可见) |
IMPORT |
声明标号来自外部文件,相当于 C 的 extern(引用) |
B |
跳转。B . = 跳到自己 = 死循环 |
BLX |
跳转到函数并保存返回地址 |
WEAK |
弱定义(不是 ARM 指令,是编译器伪指令) |
ALIGN |
地址对齐 |
6. MicroLIB
Keil 里 Target 选项卡勾的 "Use MicroLIB",对应的就是启动文件里的 __MICROLIB 宏:
IF :DEF:__MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
; 不勾 MicroLIB 时需要用户自己实现 __use_two_region_memory
ENDIF
-
勾了:Keil 自带精简 C 库,启动文件直接导出栈顶/堆起止地址,省事。
-
不勾 :用标准 C 库,需要自己实现
__use_two_region_memory。 -
嵌入式开发一般都勾 MicroLIB,代码小、启动快。