启动文件详解
更新于 2026年9月20日
本篇总结
解析 W55MH32 汇编启动文件:栈/堆分配、中断向量表、复位程序与中断服务程序。
启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要完成 5 项工作:初始化堆栈指针 SP、初始化 PC 指针、初始化中断向量表、配置系统时钟、调用 C 库函数 __main 初始化用户堆栈并最终调用 main 函数。
ARM 汇编指令速查
在讲解启动代码时会涉及 ARM 的汇编指令和 Cortex 内核的指令,有关 Cortex 内核的指令可参考《CM3 权威指南CnR2》第四章:指令集。剩下的 ARM 汇编指令可在 MDK → Help → uVision Help 中搜索到,以 EQU 为例检索如下:

下面列出启动文件中使用到的 ARM 汇编指令,全部从 ARM Development Tools 帮助文档检索而来:
| 指令 | 作用 |
|---|---|
| EQU | 给数字常量取符号名,相当于 C 的 #define |
| AREA | 汇编一个新的代码段或数据段 |
| SPACE | 分配内存空间 |
| PRESERVE8 | 当前文件堆栈需 8 字节对齐 |
| EXPORT | 声明标号具有全局属性,可被外部文件使用 |
| DCD | 以字为单位分配内存,4 字节对齐并初始化 |
| PROC/ENDP | 定义子程序,成对使用表示子程序结束 |
| WEAK | 弱定义,外部文件定义则优先使用(编译器功能) |
| IMPORT | 声明标号来自外部文件,类似 C 的 extern |
| B | 跳转到一个标号 |
| ALIGN | 对齐指令/数据地址,缺省 4 字节对齐 |
| IF/ELSE/ENDIF | 汇编条件分支语句 |
栈与堆的空间分配
栈用于局部变量、函数调用、函数形参等开销,由高向低生长。堆用于动态内存分配(malloc),由低向高生长。
; ==================== 栈 ====================
Stack_Size EQU 0x00000400 ; 栈大小 1KB
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size ; 分配 1KB 空间
__initial_sp ; 栈顶地址(栈由高向低生长)
; ==================== 堆 ====================
Heap_Size EQU 0x00000200 ; 堆大小 512B
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base ; 堆起始地址(堆由低向高生长)
Heap_Mem SPACE Heap_Size
__heap_limit ; 堆结束地址
; ==================== 指令集声明 ====================
PRESERVE8 ; 堆栈 8 字节对齐
THUMB ; 兼容 THUMB 指令集
EQU:宏定义的伪指令,相当于等于,类似 C 中的 define。AREA:告诉汇编器汇编一个新的代码段或数据段,STACK 表示段名,NOINIT 表示不初始化,READWRITE 表示可读可写,ALIGN=3 表示 8 字节对齐。SPACE:分配一定大小的内存空间,单位为字节。标号 __initial_sp 紧挨着 SPACE 语句放置,表示栈顶地址。
栈溢出排查
如果程序出现莫名错误并进入 HardFault,首先考虑栈是否溢出。增大 Stack_Size 试试。栈+堆不能超过内部 SRAM 大小(W55MH32 为 96KB)。
中断向量表
当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。内核使用"向量表查表机制"决定 ESR 的入口地址。向量表是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。地址 0x00000000 存放 MSP 初值(不是入口地址),0x00000004 存放复位程序地址。
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; 栈顶地址(地址 0x00)
DCD Reset_Handler ; 复位程序(地址 0x04)
DCD NMI_Handler ; NMI(地址 0x08)
DCD HardFault_Handler ; HardFault(地址 0x0C)
DCD MemManage_Handler ; MemManage(地址 0x10)
DCD BusFault_Handler ; BusFault(地址 0x14)
DCD UsageFault_Handler ; UsageFault(地址 0x18)
DCD 0, 0, 0, 0 ; 保留
DCD SVC_Handler ; SVCall
DCD DebugMon_Handler ; Debug Monitor
DCD 0 ; 保留
DCD PendSV_Handler ; PendSV
DCD SysTick_Handler ; SysTick
; --- 外部中断开始 ---
DCD WWDG_IRQHandler ; 窗口看门狗
DCD PVD_IRQHandler ; PVD
DCD TAMPER_IRQHandler ; 侵入检测
; ... 中间省略 ...
DCD DMA2_Channel4_5_IRQHandler
__Vectors_End
__Vectors_Size EQU __Vectors_End - __VectorsDCD:分配一个或多个以字为单位的内存,4 字节对齐并初始化。向量表中存放的都是中断服务函数的函数名,而 C 语言中的函数名就是一个地址。
| 地址 | 编号 | 名称 | 说明 |
|---|---|---|---|
| 0x0000 0000 | - | MSP | 栈顶地址(非入口地址) |
| 0x0000 0004 | -3 | Reset | 复位 |
| 0x0000 0008 | -2 | NMI | 不可屏蔽中断 |
| 0x0000 000C | -1 | HardFault | 所有类型的错误 |
| 0x0000 003C | 6 | SysTick | 系统嘀嗒定时器 |
| 0x0000 0040+ | 7+ | 外部中断 | WWDG/PVD/TAMPER...DMA2 |
复位程序 Reset_Handler
复位子程序是上电后第一个执行的程序:调用 SystemInit 配置系统时钟,然后调用 C 库函数 __main 初始化堆栈并跳转到 main。
; ==================== 复位程序 ====================
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit ; 外部函数:配置系统时钟
IMPORT __main ; C 库函数:初始化堆栈→main
LDR R0, =SystemInit ; 加载 SystemInit 地址到 R0
BLX R0 ; 跳转到 R0 执行,保存返回地址到 LR
LDR R0, =__main ; 加载 __main 地址到 R0
BX R0 ; 跳转到 R0,不返回
ENDP
; ==================== 中断服务程序(弱定义) ====================
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B . ; 死循环(B . = 跳转到自身)
ENDP
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B .
ENDP
Default_Handler PROC ; 外部中断的默认处理
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
; ... 中间省略 ...
B .
ENDP
WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用,如果外部文件没有声明也不会出错。IMPORT:表示该标号来自外部文件,类似 C 语言的 extern 关键字。SystemInit() 是标准库函数,在 system_w55mh32.c 中定义,主要配置系统时钟。__main 是 C 库函数,初始化用户堆栈并最终调用 main 函数。
WEAK 弱定义的妙用
启动文件中所有中断服务程序都用 [WEAK] 声明,且函数体只有 B .(死循环)。如果用户在 C 文件中定义了同名函数,链接器优先使用用户定义;如果用户忘记定义,中断来临时进入死循环而非跑飞——这是安全网机制。
| 指令 | 作用 |
|---|---|
| LDR | 从存储器中加载字到一个寄存器中 |
| BLX | 跳转到寄存器给出的地址,保存下条指令地址到 LR |
| BX | 跳转到寄存器给出的地址,不返回 |
| B . | 跳转到自身(死循环) |
用户堆栈初始化
根据是否定义 __MICROLIB 宏,有两种堆栈初始化方式。在 Keil 中通过 Options → Target → Use MicroLIB 勾选开启微库:

; 用户栈和堆初始化,由 C 库函数 __main 完成
IF :DEF:__MICROLIB ; 若定义了 __MICROLIB(Keil 中开启微库)
EXPORT __initial_sp ; 导出栈顶地址
EXPORT __heap_base ; 导出堆起始地址
EXPORT __heap_limit ; 导出堆结束地址
; 堆栈初始化由 __main 自动完成
ELSE ; 未定义 __MICROLIB,使用双段存储器模式
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap ; 用户自行初始化堆栈
LDR R0, =Heap_Mem ; R0 = 堆起始
LDR R1, =(Stack_Mem+Stack_Size); R1 = 栈底
LDR R2, =(Heap_Mem+Heap_Size) ; R2 = 堆结束
LDR R3, =Stack_Mem ; R3 = 栈起始
BX LR
ALIGN
ENDIF
ENDMicrolib vs 标准库
Microlib(微库):代码体积小,无文件 I/O,__main 自动初始化堆栈。在 Keil → Options → Target → Use MicroLib 勾选开启。双段存储器模式:标准库模式,需用户实现 __user_initial_stackheap 函数指定堆栈区域。
常见问题
Q: 程序莫名进入 HardFault
首先检查栈是否溢出——增大 Stack_Size。常见原因:大型局部数组、深层递归调用、中断嵌套过深。栈+堆不能超过 SRAM 大小(96KB)。
Q: 中断服务程序写对了但不触发
检查函数名是否与启动文件中向量表定义完全一致。启动文件用 [WEAK] 弱定义了所有 ISR,函数名拼写错误会导致链接器使用启动文件的空函数(死循环),中断看似不触发。
Q: SystemInit 作用是什么
SystemInit() 是标准库函数,定义在 system_w55mh32.c 中,主要配置系统时钟(如 PLL 倍频)。在跳转到 main 之前执行,确保进入 C 世界时系统时钟已正确配置。
Q: __main 和 main 有什么区别
__main 是 C 库函数,负责初始化用户堆栈(RW/ZI 数据搬运),最后调用用户编写的 main 函数。__main 不需要用户实现,而 main 必须由用户实现。
Q: 何时需要修改启动文件
常见场景:① 栈不够用→增大 Stack_Size;② 使用 RTOS→可能需调整堆栈;③ IAP/Bootloader→需修改中断向量表偏移(SCB->VTOR);④ 外部中断数量变化→选择对应容量的启动文件。
总结
- 启动五步:SP 初始化 → PC 指向 Reset_Handler → 中断向量表 → SystemInit 配置时钟 → __main 初始化堆栈 → main
- 栈:EQU 0x400(1KB),高→低生长,用于局部变量/函数调用;堆:EQU 0x200(512B),低→高生长,用于 malloc
- 向量表:DCD 数组,地址 0x00 存 MSP,0x04 存 Reset_Handler,后续依次排列系统异常和外部中断
- WEAK 弱定义:所有 ISR 在启动文件中空实现(B . 死循环),用户定义则优先链接,未定义则安全卡死
- Microlib:微库模式 __main 自动初始化堆栈;标准库模式需用户实现 __user_initial_stackheap