4. 启动文件startup_stm32xxxx.s逐行精读
启动文件是 STM32 上电后执行的第一段代码,通常命名为 startup_stm32f103xb.s(以 F1 中容量为例,其他系列后缀不同)。它由汇编编写,承担四件事:定义栈和堆、建立中断向量表、编写复位处理入口、在进入 main 之前调用系统初始化。理解这个文件,等于掌握了从“芯片复位”到“C 语言世界”的完整交接过程。
一、栈与堆的定义:先给 C 运行环境划出内存
文件开头通常先声明栈大小和堆大小,例如:
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
Stack_Size 为 0x400,即 1 KB;Heap_Size 为 0x200,即 512 B。这两个值不是随便写的:栈用于函数调用、局部变量和中断嵌套,堆用于 malloc 等动态分配。若程序里用了 printf 浮点格式化或递归较深,1 KB 栈可能不够,需要调大;若完全不用动态内存,堆可以设为 0。注意 __initial_sp 标号紧跟在栈空间之后,它正是向量表第一个字要填的“栈顶地址”。
__initial_sp 指向栈区最高地址,作为初始 SP 值。二、中断向量表:一张“异常编号到函数地址”的映射表
向量表放在只读代码段,开头是 DCD 伪指令逐项列出 32 位地址:
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors DCD __initial_sp
DCD Reset_Handler
DCD NMI_Handler
DCD HardFault_Handler
DCD MemManage_Handler
DCD BusFault_Handler
DCD UsageFault_Handler
DCD 0
DCD 0
DCD 0
DCD 0
DCD SVC_Handler
DCD DebugMon_Handler
DCD 0
DCD PendSV_Handler
DCD SysTick_Handler
; 之后是外设中断,如 WWDG_IRQHandler、PVD_IRQHandler ...
第 0 项是初始栈顶,第 1 项是复位入口,第 2 项起依次为 NMI、HardFault 等内核异常,再往后是各外设中断。表中出现 0 的位置表示该异常未使用,一旦触发会跳转到地址 0 导致异常。外设中断的顺序必须与芯片参考手册中的向量表编号严格一致,这也是为什么不同型号的启动文件不能混用。
向量表默认位于 Flash 起始地址 0x08000000。如果程序需要重定位向量表(例如做 Bootloader 跳转或使用 SRAM 运行),会通过 SCB->VTOR 寄存器修改基址,此时启动文件里的表仍是“模板”,实际生效地址由 VTOR 决定。
三、复位处理:从 Reset_Handler 到 main 的交接
复位后 CPU 从向量表第 1 项取出 Reset_Handler 地址并跳转。典型实现如下:
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
这里有两个关键调用:先执行 SystemInit,再跳转到 __main。注意 __main 不是用户写的 main,而是 C 库的入口,它负责把已初始化数据从 Flash 拷贝到 RAM、清零 .bss 段,最后才调用用户的 main。因此 SystemInit 运行在数据段尚未初始化完成的环境中,它内部不能依赖已初始化的全局变量。
四、时钟初始化调用:SystemInit 做了什么
SystemInit 定义在 system_stm32f1xx.c 中,启动文件只负责“调用”它。以 F1 为例,它主要完成:使能外部高速晶振 HSE、配置 PLL 倍频、切换系统时钟源到 PLL、设置 AHB/APB 分频。常见配置是 8 MHz 晶振经 9 倍频得到 72 MHz 系统时钟。若 HSE 起振失败,SystemInit 会停留在等待循环或回退到 HSI,这解释了“程序卡在启动阶段”的常见现象。
需要区分:SystemInit 只配置时钟树,不配置外设。GPIO、USART 等初始化仍由用户在 main 中完成。若使用 HAL 库,HAL_Init() 会配置 SysTick 和中断优先级分组,通常在 main 开头调用。
五、弱定义与用户覆盖
启动文件里所有中断处理函数都用 [WEAK] 声明,并给出一个死循环实现,例如:
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
“弱”意味着:如果用户在 C 文件里定义了同名函数,链接器会优先使用用户的强符号,启动文件里的实现被丢弃。这就是为什么我们能在 stm32f1xx_it.c 中重写 SysTick_Handler 而不冲突。若用户没有定义,则中断触发后停在 B . 死循环,便于调试时定位“哪个中断没写处理函数”。
六、逐行精读的实用检查清单
| 检查项 | 典型值/写法 | 判断依据 |
|---|---|---|
| 栈大小 | 0x400(1KB) | 有浮点 printf 或深递归时需增大 |
| 堆大小 | 0x200(512B) | 不用 malloc 可设为 0 |
| 向量表首项 | __initial_sp | 必须是栈顶地址,不是函数 |
| 复位入口 | Reset_Handler | 向量表第 1 项 |
| 时钟调用 | BL SystemInit | 在 __main 之前执行 |
| 中断顺序 | 与参考手册一致 | 型号不匹配会导致中断错位 |
Reset_Handler 里的 SystemInit 调用删掉,程序仍能运行但时钟停留在 HSI 8 MHz,表现为串口波特率错误、延时变长。另一个错误是向量表外设中断顺序与手册不符,导致“进错中断”。把启动文件读透后,再遇到“程序不进 main”“中断不响应”“时钟不对”这类问题,就能沿着“向量表 → Reset_Handler → SystemInit → __main → main”这条链逐段排查,而不是盲目改代码。