STM32F407_启动文件深度解析
STM32F407 启动文件深度解析(startup_stm32f407xx.s 逐行剖析 — Keil ARMCC 版)
📖
很多嵌入式开发者能熟练使用 STM32 写业务代码,但对启动文件(startup_stm32f407xx.s)的内部机制却一知半解。一旦遇到上电跑飞、HardFault 等问题,往往无从下手。
本文从源码层面逐行剖析 STM32F407 的启动文件,帮你彻底搞懂从上电复位到 main() 函数之间到底发生了什么。
你将学到:
- 向量表的结构与硬件加载机制
- Reset_Handler 的每一条汇编指令在做什么
.data拷贝和.bss清零的原理(Keil 下由链接器自动完成)- 弱符号机制与默认中断服务程序
- 堆栈配置与内存布局
- Boot 引脚与启动模式
- 从上电到 main() 的完整时序
📑 目录
- 一、启动文件概述
- 二、启动文件整体结构
- 三、向量表(Vector Table)详解
- 四、堆栈与内存布局
- 五、Reset Handler 详解
- 六、默认中断服务程序
- 七、从上电到 main() 的完整启动流程
- 八、链接文件与启动过程的关系(.sct 详解)
- 九、启动模式与 Boot 引脚配置
- 十、常见问题与实战经验
- 附录:关键汇编指令速查
一、启动文件概述
1.1 什么是启动文件?
启动文件(Startup File)是嵌入式系统中第一个被执行的代码,由汇编语言编写。它负责在芯片上电复位后、进入用户 main() 函数之前,完成一系列关键的底层初始化工作。
可以把启动文件理解为嵌入式系统的 “第一道工序” —— 没有它,芯片即使上电也无法正常工作。
1.2 为什么需要启动文件?
C 语言程序需要一个运行环境才能执行,而芯片刚上电时这个环境并不存在。启动文件的核心使命就是构建这个运行环境:
| 需要完成的工作 | 说明 |
|---|---|
| 设置堆栈指针(SP) | Cortex-M4 需要合法的 SP 才能执行 C 代码 |
| 初始化向量表 | 告诉 CPU 各种异常和中断的处理入口 |
| 初始化 .data 段 | 将 Flash 中的已初始化全局变量拷贝到 SRAM(Keil 下由链接器自动完成) |
| 清零 .bss 段 | 将未初始化的全局变量区域清零(Keil 下由链接器自动完成) |
| 跳转到 main() | 将控制权交给用户代码 |
1.3 启动文件来源
ST 官方提供的启动文件位于:
- 路径:
STM32CubeF4/Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/arm/startup_stm32f407xx.s - 编译器版本:GCC(arm-none-eabi-gcc)、IAR、Keil 各有对应版本
- 本文以 Keil ARMCC 版本 为主要分析对象(语法为 ARM 汇编)
二、启动文件整体结构
一个典型的 STM32F407 Keil ARMCC 启动文件可以划分为以下逻辑区块:
┌─────────────────────────────────────────────┐
│ 1. 文件头(注释、版权信息) │
├─────────────────────────────────────────────┤
│ 2. 汇编伪指令与宏定义 │
│ - PRESERVE8, THUMB 等 │
├─────────────────────────────────────────────┤
│ 3. 向量表定义(Vector Table) │
│ - 初始 SP 值 │
│ - Reset Handler │
│ - NMI Handler │
│ - HardFault Handler │
│ - MemManage / BusFault / UsageFault │
│ - SVCall / PendSV / SysTick │
│ - 外设中断向量(WWDG ~ RNG ~ FPU) │
├─────────────────────────────────────────────┤
│ 4. 复位处理函数(Reset_Handler) │
│ - 调用 SystemInit() │
│ - 跳转到 __main │
│ (.data/.bss 由 __main 内部自动完成) │
├─────────────────────────────────────────────┤
│ 5. 默认中断服务程序(Default Handlers) │
│ - B . 死循环(跳转到自身) │
├─────────────────────────────────────────────┤
│ 6. 堆栈空间定义 │
│ - Stack_Size / Heap_Size │
└─────────────────────────────────────────────┘
💡 与 GCC 版本的关键区别:Keil ARMCC 版本的 Reset_Handler 非常简洁——不需要手动拷贝
.data段和清零.bss段,这些工作由 Keil 链接器自动生成的__main函数完成。这是 ARMCC 与 GCC 启动文件最显著的区别。
三、向量表(Vector Table)详解
3.1 向量表是什么?
向量表是 ARM Cortex-M 处理器中一个至关重要的数据结构。它本质上是一个函数指针数组,存储在内存的起始地址处(Flash 起始地址 0x08000000)。每个条目是一个 32 位地址,指向对应异常或中断的处理函数。
Cortex-M4 在发生异常/中断时,硬件会自动从向量表中读取对应的处理函数地址并跳转执行——这个过程不需要软件干预,是硬件级别的行为。
3.2 向量表的内存布局
地址 内容 说明
─────────────────────────────────────────────────────
0x08000000 ┌──────────────────┐
│ 初始 SP 值 │ ← 上电后 CPU 自动加载到 MSP
0x08000004 │ Reset_Handler │ ← 复位向量,上电后 PC 指向这里
0x08000008 │ NMI_Handler │ ← 不可屏蔽中断
0x0800000C │ HardFault_Handler│ ← 硬件错误
0x08000010 │ MemManage_Handler│ ← 内存管理错误
0x08000014 │ BusFault_Handler │ ← 总线错误
0x08000018 │ UsageFault_Handler│← 用法错误
0x0800001C │ (保留) │
0x08000020 │ (保留) │
0x08000024 │ (保留) │
0x08000028 │ (保留) │
0x0800002C │ SVC_Handler │ ← 系统服务调用
0x08000030 │ DebugMon_Handler│ ← 调试监控
0x08000034 │ (保留) │
0x08000038 │ PendSV_Handler │ ← 可挂起系统服务
0x0800003C │ SysTick_Handler │ ← 系统滴答定时器
0x08000040 │ WWDG_IRQHandler │ ← 外设中断 #0
0x08000044 │ PVD_IRQHandler │ ← 外设中断 #1
│ ... │
│ FPU_IRQHandler │ ← 外设中断 #81
└──────────────────┘
3.3 向量表源码分析
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors
DCD 0x20020000 ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler
DCD HardFault_Handler
DCD MemManage_Handler
DCD BusFault_Handler
DCD UsageFault_Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler
DCD DebugMon_Handler
DCD 0 ; Reserved
DCD PendSV_Handler
DCD SysTick_Handler
; External Interrupts
DCD WWDG_IRQHandler ; 0: Window WatchDog
DCD PVD_IRQHandler ; 1: PVD through EXTI Line detect
DCD TAMP_STAMP_IRQHandler ; 2: Tamper and TimeStamps
DCD RTC_WKUP_IRQHandler ; 3: RTC Wakeup through EXTI
DCD FLASH_IRQHandler ; 4: FLASH
DCD RCC_IRQHandler ; 5: RCC
DCD EXTI0_IRQHandler ; 6: EXTI Line0
DCD EXTI1_IRQHandler ; 7: EXTI Line1
; ... 省略中间大量中断 ...
DCD FPU_IRQHandler ; 81: FPU
逐行解析:
AREA RESET, DATA, READONLY:定义一个名为RESET的段,属性为DATA(数据段)、READONLY(只读)。这等价于 GCC 中的.section .isr_vector,"a",%progbits。EXPORT __Vectors:将__Vectors符号导出为全局符号,使其可被链接器和其他文件引用。等价于 GCC 中的.global g_pfnVectors。DCD:Define Constant Data,定义一个 32 位(4 字节)的数据常量。等价于 GCC 中的.word。- ARMCC 中不需要
.type和.size伪指令,编译器会自动处理符号的类型和大小信息。
3.4 向量表条目数量
STM32F407 的向量表包含:
- 系统异常:16 个(编号 -16 ~ -1,对应地址 0x00 ~ 0x3C)
- 外部中断:82 个(编号 0 ~ 81,对应地址 0x40 ~ 0xEC)
- 总计:98 个向量条目,占用 98 × 4 = 392 字节(0x188)
3.5 初始 SP 值——向量表的第一个条目
DCD 0x20020000 ; Top of Stack
这是向量表中最容易被忽略但极其关键的条目。
上电复位时,Cortex-M4 硬件自动执行以下操作:
- 从地址
0x00000000(映射到0x08000000)读取值,加载到主堆栈指针 MSP - 从地址
0x00000004(映射到0x08000004)读取值,加载到程序计数器 PC
这意味着:在执行任何一条指令之前,SP 就已经被正确设置了。 这是 ARM Cortex-M 架构的一个优雅设计——不需要专门的指令来初始化 SP。
💡 GCC vs ARMCC 的区别:GCC 版本使用符号
_estack(在链接脚本.ld中定义),而 ARMCC 版本通常直接写入具体的栈顶地址值0x20020000。在 Keil 中,也可以使用链接器生成的符号如Image$$RW_IRAM1$$ZI$$Limit,但 ST 官方模板通常直接写具体值,更加直观。
💡 工程师经验:如果你遇到程序上电后立即跑飞,首先检查向量表第一个条目(栈顶地址)是否正确指向了有效的 RAM 区域。这是新手最常犯的错误之一。
四、堆栈与内存布局
4.1 栈(Stack)的定义
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
逐行解析:
Stack_Size EQU 0x00000400:使用EQU伪指令定义常量Stack_Size为 0x400(1024 字节 = 1KB)。等价于 GCC 中的.equ Stack_Size, 0x400。AREA STACK, NOINIT, READWRITE, ALIGN=3:定义一个名为STACK的段:NOINIT:不需要初始化为 0(启动时不会清零该区域)READWRITE:可读可写ALIGN=3:8 字节对齐(2³ = 8),符合 AAPCS(ARM 过程调用标准)
Stack_Mem SPACE Stack_Size:分配Stack_Size字节的空间。SPACE等价于 GCC 中的.space。__initial_sp:栈顶符号(高地址),位于栈空间之后。Cortex-M4 的栈是向下生长的(Full Descending Stack),__initial_sp就是向量表中第一个条目所引用的栈顶地址。
关键理解:
- Cortex-M4 的栈是向下生长的(Full Descending Stack)
__initial_sp(栈顶)= 高地址,就是向量表中的初始 SP 值Stack_Mem(栈底)= 低地址ALIGN=3表示 8 字节对齐(2³ = 8),符合 AAPCS(ARM 过程调用标准)
4.2 默认栈大小
Stack_Size EQU 0x00000400 ; 1024 字节 = 1 KB
⚠️ 工程师经验:1KB 的默认栈大小在实际项目中几乎总是不够的。如果你使用了:
printf()/sprintf()(内部缓冲区消耗大量栈空间)- 深层函数调用嵌套
- 大量局部变量(尤其是大型结构体)
- FreeRTOS(每个任务有独立栈)
建议将
Stack_Size增大到0x1000(4KB)甚至更大,并通过 Keil 编译后生成的.map文件来分析实际栈使用情况。
4.3 堆(Heap)的定义
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
逐行解析:
Heap_Size EQU 0x00000200:定义堆大小为 0x200(512 字节)。AREA HEAP, NOINIT, READWRITE, ALIGN=3:定义堆段,属性与栈段类似。__heap_base:堆的起始地址(低地址)。Heap_Mem SPACE Heap_Size:分配堆空间。__heap_limit:堆的结束地址(高地址)。
堆用于动态内存分配(malloc()、calloc()、free())。
⚠️ 工程师经验:在嵌入式系统中,强烈建议避免使用动态内存分配。原因包括:
- 内存碎片化——长时间运行后可能导致分配失败
- 不确定性——
malloc()的执行时间不可预测- 内存泄漏风险——C 语言没有垃圾回收
如果必须使用,建议使用内存池(Memory Pool)模式,或者使用 FreeRTOS 提供的
pvPortMalloc()/vPortFree()。
4.4 STM32F407 内存映射
0xFFFFFFFF ┌─────────────────────┐
│ 保留区域 │
0xE0100000 ├─────────────────────┤
│ 外设寄存器区域 │
│ (APB1/APB2/AHB1) │
0x40000000 ├─────────────────────┤
│ 外设寄存器区域 │
│ (AHB2/AHB3) │
0x20000000 ├─────────────────────┤ ← SRAM 起始
│ │
│ 128KB SRAM │
│ (CCM Data Memory) │
│ 0x10000000-0x1001FFFF │
├─────────────────────┤
│ │
│ 192KB 主 SRAM │
│ (0x20000000- │
│ 0x2002FFFF) │
0x08000000 ├─────────────────────┤ ← Flash 起始
│ │
│ 512KB ~ 1MB Flash │
│ │
0x00000000 └─────────────────────┘
注意:STM32F407 有两块 RAM:
- 主 SRAM:128KB,地址
0x20000000(部分型号为 192KB,地址到0x2002FFFF)- CCM RAM:64KB,地址
0x10000000(Core Coupled Memory,紧耦合内存,性能更高,但 DMA 不可访问)
五、Reset Handler 详解
Reset_Handler 是整个启动文件中最核心的函数。芯片上电复位后,PC 指向的就是这个函数。
5.1 完整源码
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
是不是觉得太简洁了? 没错!与 GCC 版本动辄几十行的 Reset_Handler 相比,Keil ARMCC 版本只有区区 6 行有效代码。这正是 ARMCC 工具链的优雅之处——复杂的初始化工作被链接器自动完成了。
5.2 逐行深度解析
第一步:函数声明与符号导入
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
PROC:标记函数(过程)的开始。等价于 GCC 中隐含在标签定义中的函数开始。EXPORT Reset_Handler [WEAK]:将Reset_Handler导出为全局符号,并标记为弱符号([WEAK])。弱符号可以被同名的强符号覆盖。等价于 GCC 中的.weak Reset_Handler。IMPORT SystemInit:声明SystemInit为外部符号(在其他文件中定义)。等价于 GCC 中隐含的引用行为。IMPORT __main:声明__main为外部符号。__main是 ARMCC 链接器自动生成的入口点,不是用户编写的main()函数——它内部会完成分散加载后再跳转到用户的main()。
第二步:调用 SystemInit()
LDR R0, =SystemInit
BLX R0
LDR R0, =SystemInit:将SystemInit函数的地址加载到寄存器R0。这里使用的是伪指令LDR(带=前缀),编译器会将其转换为LDR R0, [PC, #offset]或MOVW/MOVT指令对。BLX R0:跳转到R0中的地址执行(带链接,即保存返回地址到LR),并可以在 ARM 和 Thumb 状态之间切换。由于SystemInit是 C 函数(编译为 Thumb 指令),使用BLX确保正确的指令集切换。
SystemInit() 定义在 system_stm32f4xx.c 中,负责关键的硬件时钟初始化:
void SystemInit(void)
{
/* FPU 配置 */
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2)); /* 启用 CP10, CP11 */
#endif
/* 复位 RCC 时钟配置为默认复位状态 */
RCC->CR |= (uint32_t)0x00000001; /* 开启 HSI */
RCC->CFGR = 0x00000000; /* HSI 作为系统时钟 */
RCC->CR &= (uint32_t)0xFEF6FFFF; /* 关闭 HSE, CSS, PLL */
RCC->PLLCFGR = 0x24003010; /* PLL 默认配置 */
RCC->CIR = 0x00000000; /* 关闭所有中断 */
}
⚠️ 工程师经验:
SystemInit()只将时钟配置为安全的默认状态(HSI 16MHz),并不会配置到最高频率(168MHz)。真正的时钟配置通常在main()中调用SystemClock_Config()完成。这种设计是有意为之的——确保启动过程在一个稳定、保守的时钟下进行。另一个关键点是 FPU 的使能。STM32F407 内置了硬件浮点单元(FPU),但默认是关闭的。必须在
SystemInit()中通过设置CPACR寄存器来启用它,否则使用float类型会导致 HardFault。
第三步:跳转到 __main
LDR R0, =__main
BX R0
LDR R0, =__main:将__main的地址加载到R0。BX R0:跳转到R0中的地址执行(不带链接,即不保存返回地址)。使用BX而不是BLX,因为跳转到__main后不需要返回。
__main 是什么?它与 main() 有什么区别?
这是 Keil ARMCC 版本启动文件最核心的概念。__main 是 ARM 链接器(armlink)自动生成的一段代码,它在跳转到用户 main() 之前,自动完成以下工作:
__main 内部流程:
┌──────────────────────────────────┐
│ __scatterload: │
│ 1. 将 .data 段从 Flash 拷贝到 RAM │
│ 2. 将 .bss 段清零 │
│ 3. 处理分散加载描述(.sct 文件) │
└────┬─────────────────────────────┘
▼
┌──────────────────────────────────┐
│ __rt_entry: │
│ 1. C 运行时库初始化 │
│ 2. 调用 C++ 全局构造函数(如有) │
│ 3. 调用 __attribute__((constructor)) │
│ 标记的函数(如有) │
│ 4. 调用用户 main() │
└──────────────────────────────────┘
5.3 Keil 与 GCC 启动流程的关键差异
| 对比项 | GCC 版本 | Keil ARMCC 版本 |
|---|---|---|
| .data 拷贝 | Reset_Handler 中手动实现 | __main 内的 __scatterload 自动完成 |
| .bss 清零 | Reset_Handler 中手动实现 | __main 内的 __scatterload 自动完成 |
| C++ 构造函数 | __libc_init_array |
__main 内的 __rt_entry 自动完成 |
| 跳转到 main() | bl main |
__main → __rt_entry → main() |
| 链接脚本 | .ld 文件(链接器脚本) |
.sct 文件(分散加载文件) |
| Reset_Handler 复杂度 | ~40 行汇编 | ~6 行汇编 |
💡 工程师经验:Keil 版本 Reset_Handler 如此简洁,是因为 ARMCC 工具链采用了"链接器做更多事情"的设计哲学。分散加载(Scatter Loading)机制让链接器根据
.sct文件自动生成.data拷贝和.bss清零的代码,嵌入在__main中。这减少了出错的可能,但也意味着你对底层细节的控制力不如 GCC 版本直接。如果你需要在 Keil 下自定义
.data拷贝逻辑(例如从外部 Flash 加载),可以通过修改.sct分散加载文件来实现,而不是修改启动文件。
六、默认中断服务程序
6.1 默认 Handler 定义
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
MemManage_Handler\
PROC
EXPORT MemManage_Handler [WEAK]
B .
ENDP
BusFault_Handler\
PROC
EXPORT BusFault_Handler [WEAK]
B .
ENDP
UsageFault_Handler\
PROC
EXPORT UsageFault_Handler [WEAK]
B .
ENDP
SVC_Handler PROC
EXPORT SVC_Handler [WEAK]
B .
ENDP
DebugMon_Handler\
PROC
EXPORT DebugMon_Handler [WEAK]
B .
ENDP
PendSV_Handler PROC
EXPORT PendSV_Handler [WEAK]
B .
ENDP
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B .
ENDP
6.2 关键汇编指令解析
PROC/ENDP:标记函数(过程)的开始和结束。ARMCC 中每个函数都必须用PROC和ENDP包裹。EXPORT xxx [WEAK]:将符号导出并标记为弱符号。弱符号可以被同名的强符号覆盖。- 实际含义:如果你在 C 文件中定义了
void HardFault_Handler(void),链接器会使用你的版本(强符号);如果没有定义,则使用启动文件中的默认版本(弱符号)。
- 实际含义:如果你在 C 文件中定义了
B .:跳转到自身(.表示当前地址),形成无限循环。等价于 GCC 版本中的b Infinite_Loop。
6.3 与 GCC 版本的结构差异
GCC 版本使用 Default_Handler + Infinite_Loop 的方式,所有未实现的 Handler 通过 .thumb_set 别名指向同一个 Default_Handler:
; GCC 版本的做法(供对比)
.weak NMI_Handler
.thumb_set NMI_Handler, Default_Handler
.weak HardFault_Handler
.thumb_set HardFault_Handler, Default_Handler
Default_Handler:
Infinite_Loop:
b Infinite_Loop
ARMCC 版本则每个 Handler 都是独立的函数,各自包含 B . 无限循环。虽然代码看起来重复,但结构更清晰,每个 Handler 都是独立的 PROC/ENDP 块。
为什么是无限循环?
如果某个中断被触发,但用户没有提供对应的处理函数,程序会进入 B . 的无限循环。这是一种安全的失败模式——比让 PC 随意跑飞要好得多。
💡 工程师经验:在开发阶段,建议在 Keil 调试器中利用断点来捕获未预期的中断。你可以在调试时直接在默认 Handler 的
B .处设置断点,当程序停在这里时,查看中断状态寄存器(NVIC)来判断是哪个中断被意外触发了。
6.4 STM32F407 完整中断向量列表
| 编号 | 中断名称 | 外设 | 说明 |
|---|---|---|---|
| 0 | WWDG_IRQHandler | WWDG | 窗口看门狗 |
| 1 | PVD_IRQHandler | PVD | 电源电压检测 |
| 2 | TAMP_STAMP_IRQHandler | RTC | 入侵检测和时间戳 |
| 3 | RTC_WKUP_IRQHandler | RTC | RTC 闹钟唤醒 |
| 4 | FLASH_IRQHandler | FLASH | Flash 操作完成 |
| 5 | RCC_IRQHandler | RCC | 时钟控制系统 |
| 6 | EXTI0_IRQHandler | EXTI | 外部中断线 0 |
| 7 | EXTI1_IRQHandler | EXTI | 外部中断线 1 |
| 8 | EXTI2_IRQHandler | EXTI | 外部中断线 2 |
| 9 | EXTI3_IRQHandler | EXTI | 外部中断线 3 |
| 10 | EXTI4_IRQHandler | EXTI | 外部中断线 4 |
| 11 | DMA1_Stream0_IRQHandler | DMA1 | DMA1 流 0 |
| 12 | DMA1_Stream1_IRQHandler | DMA1 | DMA1 流 1 |
| 13 | DMA1_Stream2_IRQHandler | DMA1 | DMA1 流 2 |
| 14 | DMA1_Stream3_IRQHandler | DMA1 | DMA1 流 3 |
| 15 | DMA1_Stream4_IRQHandler | DMA1 | DMA1 流 4 |
| 16 | DMA1_Stream5_IRQHandler | DMA1 | DMA1 流 5 |
| 17 | DMA1_Stream6_IRQHandler | DMA1 | DMA1 流 6 |
| 18 | ADC_IRQHandler | ADC | ADC1/ADC2/ADC3 |
| 19 | CAN1_TX_IRQHandler | CAN1 | CAN1 发送 |
| 20 | CAN1_RX0_IRQHandler | CAN1 | CAN1 接收 FIFO 0 |
| 21 | CAN1_RX1_IRQHandler | CAN1 | CAN1 接收 FIFO 1 |
| 22 | CAN1_SCE_IRQHandler | CAN1 | CAN1 状态改变错误 |
| 23 | EXTI9_5_IRQHandler | EXTI | 外部中断线 9-5 |
| 24 | TIM1_BRK_TIM9_IRQHandler | TIM1/TIM9 | TIM1 刹断 / TIM9 全局 |
| 25 | TIM1_UP_TIM10_IRQHandler | TIM1/TIM10 | TIM1 更新 / TIM10 全局 |
| 26 | TIM1_TRG_COM_TIM11_IRQHandler | TIM1/TIM11 | TIM1 触发/通信 / TIM11 |
| 27 | TIM1_CC_IRQHandler | TIM1 | TIM1 捕获/比较 |
| 28 | TIM2_IRQHandler | TIM2 | TIM2 全局 |
| 29 | TIM3_IRQHandler | TIM3 | TIM3 全局 |
| 30 | TIM4_IRQHandler | TIM4 | TIM4 全局 |
| 31 | I2C1_EV_IRQHandler | I2C1 | I2C1 事件 |
| 32 | I2C1_ER_IRQHandler | I2C1 | I2C1 错误 |
| 33 | I2C2_EV_IRQHandler | I2C2 | I2C2 事件 |
| 34 | I2C2_ER_IRQHandler | I2C2 | I2C2 错误 |
| 35 | SPI1_IRQHandler | SPI1 | SPI1 全局 |
| 36 | SPI2_IRQHandler | SPI2 | SPI2 全局 |
| 37 | USART1_IRQHandler | USART1 | USART1 全局 |
| 38 | USART2_IRQHandler | USART2 | USART2 全局 |
| 39 | USART3_IRQHandler | USART3 | USART3 全局 |
| 40 | EXTI15_10_IRQHandler | EXTI | 外部中断线 15-10 |
| 41 | RTC_Alarm_IRQHandler | RTC | RTC 闹钟 A/B |
| 42 | OTG_FS_WKUP_IRQHandler | OTG_FS | USB OTG FS 唤醒 |
| 43 | TIM8_BRK_TIM12_IRQHandler | TIM8/TIM12 | TIM8 刹断 / TIM12 |
| 44 | TIM8_UP_TIM13_IRQHandler | TIM8/TIM13 | TIM8 更新 / TIM13 |
| 45 | TIM8_TRG_COM_TIM14_IRQHandler | TIM8/TIM14 | TIM8 触发/通信 / TIM14 |
| 46 | TIM8_CC_IRQHandler | TIM8 | TIM8 捕获/比较 |
| 47 | DMA1_Stream7_IRQHandler | DMA1 | DMA1 流 7 |
| 48 | FSMC_IRQHandler | FSMC | FSMC 全局 |
| 49 | SDIO_IRQHandler | SDIO | SDIO 全局 |
| 50 | TIM5_IRQHandler | TIM5 | TIM5 全局 |
| 51 | SPI3_IRQHandler | SPI3 | SPI3 全局 |
| 52 | UART4_IRQHandler | UART4 | UART4 全局 |
| 53 | UART5_IRQHandler | UART5 | UART5 全局 |
| 54 | TIM6_DAC_IRQHandler | TIM6/DAC | TIM6 全局 / DAC1/2 |
| 55 | TIM7_IRQHandler | TIM7 | TIM7 全局 |
| 56 | DMA2_Stream0_IRQHandler | DMA2 | DMA2 流 0 |
| 57 | DMA2_Stream1_IRQHandler | DMA2 | DMA2 流 1 |
| 58 | DMA2_Stream2_IRQHandler | DMA2 | DMA2 流 2 |
| 59 | DMA2_Stream3_IRQHandler | DMA2 | DMA2 流 3 |
| 60 | DMA2_Stream4_IRQHandler | DMA2 | DMA2 流 4 |
| 61 | ETH_IRQHandler | Ethernet | 以太网全局 |
| 62 | ETH_WKUP_IRQHandler | Ethernet | 以太网唤醒 |
| 63 | CAN2_TX_IRQHandler | CAN2 | CAN2 发送 |
| 64 | CAN2_RX0_IRQHandler | CAN2 | CAN2 接收 FIFO 0 |
| 65 | CAN2_RX1_IRQHandler | CAN2 | CAN2 接收 FIFO 1 |
| 66 | CAN2_SCE_IRQHandler | CAN2 | CAN2 状态改变错误 |
| 67 | OTG_FS_IRQHandler | OTG_FS | USB OTG FS 全局 |
| 68 | DMA2_Stream5_IRQHandler | DMA2 | DMA2 流 5 |
| 69 | DMA2_Stream6_IRQHandler | DMA2 | DMA2 流 6 |
| 70 | DMA2_Stream7_IRQHandler | DMA2 | DMA2 流 7 |
| 71 | USART6_IRQHandler | USART6 | USART6 全局 |
| 72 | I2C3_EV_IRQHandler | I2C3 | I2C3 事件 |
| 73 | I2C3_ER_IRQHandler | I2C3 | I2C3 错误 |
| 74 | OTG_HS_EP1_OUT_IRQHandler | OTG_HS | USB OTG HS 端点 1 OUT |
| 75 | OTG_HS_EP1_IN_IRQHandler | OTG_HS | USB OTG HS 端点 1 IN |
| 76 | OTG_HS_WKUP_IRQHandler | OTG_HS | USB OTG HS 唤醒 |
| 77 | OTG_HS_IRQHandler | OTG_HS | USB OTG HS 全局 |
| 78 | DCMI_IRQHandler | DCMI | 数字摄像头接口 |
| 79 | CRYP_IRQHandler | CRYP | 加密/解密 |
| 80 | HASH_RNG_IRQHandler | HASH/RNG | 哈希/随机数发生器 |
| 81 | FPU_IRQHandler | FPU | 浮点运算单元 |
七、从上电到 main() 的完整启动流程
7.1 时序图
时间 ──────────────────────────────────────────────────────────→
┌──────────┐
│ 上电复位 │
└────┬─────┘
▼
┌──────────────────────────────────┐
│ 硬件自动操作(Cortex-M4 内核): │
│ 1. 从 0x08000000 读取值 → MSP │
│ 2. 从 0x08000004 读取值 → PC │
│ 3. 读取 0x08000004 的值作为 │
│ Reset_Handler 地址并跳转 │
└────┬─────────────────────────────┘
▼
┌──────────────────────────────────┐
│ Reset_Handler: │
│ 1. LDR R0, =SystemInit │
│ 2. BLX R0 (调用 SystemInit) │
│ 3. LDR R0, =__main │
│ 4. BX R0 (跳转到 __main) │
└────┬─────────────────────────────┘
▼
┌──────────────────────────────────┐
│ SystemInit(): │
│ 1. 启用 FPU (CPACR) │
│ 2. 复位 RCC 为默认状态 │
│ 3. 系统时钟 = HSI (16MHz) │
└────┬─────────────────────────────┘
▼
┌──────────────────────────────────┐
│ __main (链接器自动生成): │
│ ┌────────────────────────────┐ │
│ │ __scatterload: │ │
│ │ 1. 拷贝 .data (Flash→RAM) │ │
│ │ 2. 清零 .bss │ │
│ │ 3. 处理分散加载描述(.sct) │ │
│ └────────┬───────────────────┘ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ __rt_entry: │ │
│ │ 1. C 运行时库初始化 │ │
│ │ 2. C++ 构造函数(如有) │ │
│ │ 3. constructor 属性函数 │ │
│ │ 4. 调用 main() │ │
│ └────────────────────────────┘ │
└────┬─────────────────────────────┘
▼
┌──────────────────────────────────┐
│ main(): │
│ 1. HAL_Init() │
│ 2. SystemClock_Config() (168MHz) │
│ 3. 外设初始化 │
│ 4. while(1) { ... } │
└──────────────────────────────────┘
7.2 各阶段耗时估算
| 阶段 | 典型耗时 | 说明 |
|---|---|---|
| 硬件复位 → 开始执行 | ~1-10 μs | 取决于复位电路和时钟稳定时间 |
| Reset_Handler | ~1-5 μs | 仅调用 SystemInit + 跳转 __main |
| __scatterload(.data 拷贝 + .bss 清零) | ~15-70 μs | 取决于 .data/.bss 段大小和 Flash 等待周期 |
| __rt_entry(C 运行时初始化) | ~1-10 μs | 取决于构造函数数量 |
| 总计 | ~20-100 μs | 从上电到进入 main() |
💡 工程师经验:对于大多数应用,启动时间不是关键瓶颈。但在需要快速启动的场景(如汽车安全系统、UPS 控制器),你可能需要优化启动流程:
- 减小 .data 段(减少需要拷贝的数据)
- 使用更快的 Flash 等待周期配置
- 跳过不必要的初始化步骤
- 考虑将关键代码拷贝到 SRAM 中执行(XIP → XIP-from-RAM)
八、链接文件与启动过程的关系(.sct 详解)
8.1 为什么启动过程离不开链接文件?
在前面的章节中,我们分析了启动文件的每一条指令。但你可能会注意到一个关键问题:启动文件中的很多"地址"并不是在启动文件中定义的,而是由链接文件决定的。
例如:
- 向量表中第一个条目
0x20020000(栈顶地址)—— 由链接文件中的 RAM 大小决定 __main函数内部拷贝.data段的源地址和目标地址 —— 由链接文件中的段布局决定.bss段的起始和结束地址 —— 由链接文件计算
启动文件和链接文件是"搭档"关系:
┌─────────────────────────────────────────────────┐
│ 链接文件 (.sct) — "施工图纸" │
│ ┌─────────────────────────────────────────┐ │
│ │ 定义内存布局 → 生成地址信息 │ │
│ │ 告诉链接器:代码放 Flash 哪里 │ │
│ │ 变量放 RAM 哪里、栈多大 │ │
│ └──────────────┬──────────────────────────┘ │
│ │ 地址信息嵌入到可执行文件 │
│ ▼ │
│ 启动文件 (.s) — "第一道工序" │
│ ┌─────────────────────────────────────────┐ │
│ │ Reset_Handler 使用这些地址: │ │
│ │ 栈顶 = 链接文件计算的 RAM 末尾地址 │ │
│ │ __main 使用链接文件生成的分散加载表 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
在 Keil ARMCC 环境下,链接文件就是分散加载文件(Scatter File),扩展名为 .sct。
8.2 Keil 分散加载文件(.sct)详解
8.2.1 什么是分散加载文件?
分散加载文件是 ARM Compiler 专有的链接配置文件,描述程序在物理内存中的布局。它的核心作用是告诉链接器:
- 代码和只读数据放在 Flash 的什么位置
- 可读写变量放在 RAM 的什么位置
- 栈和堆分配在什么位置
- 各段的排列顺序和对齐要求
8.2.2 STM32F407 典型 .sct 文件
; ============================================================
; STM32F407VG Scatter Loading File (Keil MDK)
; 适用于 STM32F407VG (1MB Flash, 128KB RAM, 64KB CCMRAM)
; ============================================================
; 定义加载域(Load Region)— 烧录到 Flash 的整体布局
LR_IROM1 0x08000000 0x00100000 { ; 起始=0x08000000, 大小=1MB
; ──── 第一个执行域:Flash 中的代码和只读数据 ────
ER_IROM1 0x08000000 0x00100000 { ; 起始=0x08000000, 大小=1MB
*.o (RESET, +First) ; 向量表放在最前面
*(InRoot$$Sections) ; C 运行时库根段(__main 等)
.ANY (+RO) ; 所有只读段(代码 + const 数据)
}
; ──── 第二个执行域:RAM 中的可读写数据 ────
RW_IRAM1 0x20000000 0x00020000 { ; 起始=0x20000000, 大小=128KB
.ANY (+RW +ZI) ; 所有可读写段 + 零初始化段
}
}
8.2.3 核心概念解析
加载域(Load Region)vs 执行域(Execution Region):
| 概念 | 关键字 | 说明 |
|---|---|---|
| 加载域 | LR_ 前缀 |
定义程序烧录时在 Flash 中的整体布局 |
| 执行域 | ER_ 前缀 |
定义程序运行时各段在内存中的位置 |
核心区别:加载域描述的是"烧录到 Flash 的样子",执行域描述的是"运行时在内存中的样子"。当两者地址不同时,Keil 链接器会自动生成代码将数据从 Flash 搬运到 RAM。
模块选择规则:
| 模式 | 含义 | 对应段 |
|---|---|---|
+RO |
只读段 | .text(代码)+ .rodata(const 数据) |
+RW |
可读写段 | .data(已初始化全局变量) |
+ZI |
零初始化段 | .bss(未初始化全局变量) |
+First |
放在区域最前面 | 向量表必须放在 Flash 最前面 |
.ANY |
匹配任何未分配的段 | 类似通配符 |
8.2.4 带多个执行域的 .sct(含 CCMRAM)
; 带有 CCMRAM 分配的完整分散加载文件
LR_IROM1 0x08000000 0x00100000 {
; Flash 代码区
ER_IROM1 0x08000000 0x00100000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
; 主 SRAM 数据区
RW_IRAM1 0x20000000 0x00020000 {
.ANY (+RW +ZI)
}
; CCMRAM 数据区(可选)
RW_IRAM2 0x10000000 0x00010000 {
*.o (CCMRAM_SECTION)
}
}
要在 Keil 中将特定变量放到 CCMRAM:
/* 将变量放到 CCMRAM */
__attribute__((section("CCMRAM_SECTION")))
uint32_t fast_buffer[1024];
⚠️ 工程师经验:CCM RAM 的最大陷阱是 DMA 无法访问!使用 CCMRAM 的黄金法则:只放 CPU 直接访问的数据,绝不放 DMA 缓冲区。
8.3 链接文件如何影响启动过程
链接文件通过以下三种方式直接影响启动过程:
8.3.1 决定向量表在 Flash 中的位置
.sct 文件中的 *.o (RESET, +First) 确保了向量表被放在 Flash 的最前面(0x08000000)。Cortex-M4 硬件上电后从这个固定地址读取 SP 和 PC,所以向量表的位置必须与 .sct 中的定义一致。
.sct 文件: *.o (RESET, +First) → 向量表放在 0x08000000
↓
CPU 硬件: 从 0x08000000 读 SP ← 必须匹配!
从 0x08000004 读 PC ← 必须匹配!
8.3.2 决定 __scatterload 的行为
上一章提到 __main 内部会调用 __scatterload 来完成 .data 拷贝和 .bss 清零。但 __scatterload 怎么知道要拷贝哪些数据、从哪里拷到哪里?
答案就是:链接器根据 .sct 文件生成一张"分散加载描述表"(Scatter Loading Description),嵌入到可执行文件中。__scatterload 在运行时读取这张表来执行搬运操作。
编译时:
.sct 文件 ──→ 链接器 ──→ 生成分散加载描述表 ──→ 嵌入 .axf 文件
运行时:
__scatterload ──→ 读取分散加载描述表 ──→ 自动拷贝 .data 和清零 .bss
分散加载描述表包含的信息:
| 信息 | 说明 | 示例 |
|---|---|---|
| 加载地址(LMA) | 数据在 Flash 中的位置 | 0x08005000 |
| 执行地址(VMA) | 数据在 RAM 中的位置 | 0x20000000 |
| 数据大小 | 需要拷贝的字节数 | 0x200 |
| ZI 大小 | 需要清零的字节数 | 0x100 |
8.3.3 决定栈顶地址
启动文件向量表中的栈顶地址 0x20020000,实际上就是 .sct 文件中 RAM 区域的结束地址:
.sct 文件: RW_IRAM1 0x20000000 0x00020000
^^^^^^^^^^
RAM 大小 = 128KB
栈顶 = 0x20000000 + 0x00020000 = 0x20020000
如果修改了 .sct 文件中的 RAM 大小,栈顶地址也会随之改变。这就是为什么启动文件和链接文件必须配套使用。
8.4 内存布局全景图
以 STM32F407VG(1MB Flash, 128KB RAM, 64KB CCMRAM)为例,结合 .sct 文件和启动文件,最终的内存布局如下:
Flash (0x08000000, 1MB) — 由 .sct 的 LR_IROM1 / ER_IROM1 定义
═══════════════════════════════════════════════════════
0x08000000 ┌─────────────────────┐
│ RESET (向量表) │ *.o (RESET, +First) — 启动文件定义
0x08000188 ├─────────────────────┤
│ │
│ +RO (代码+只读数据) │ .ANY (+RO) — 你的程序代码和 const
│ .text + .rodata │
│ │
0x080FFFFF └─────────────────────┘
主 SRAM (0x20000000, 128KB) — 由 .sct 的 RW_IRAM1 定义
═══════════════════════════════════════════════════════
0x20000000 ┌─────────────────────┐
│ +RW (.data) │ 已初始化全局变量
│ ← __scatterload │ 从 Flash 拷贝到这里
│ 自动拷贝 │
├─────────────────────┤
│ +ZI (.bss) │ 未初始化全局变量
│ ← __scatterload │ 自动清零
│ 自动清零 │
├─────────────────────┤
│ Heap │ 堆 (malloc 使用)
│ (向上生长) │
├─────────────────────┤
│ │
│ (空闲区域) │
│ │
├─────────────────────┤
│ Stack │ 栈 (向下生长)
│ │ 栈顶 = 0x20020000
0x2001FFFF └─────────────────────┘
CCM RAM (0x10000000, 64KB) — 由 .sct 的 RW_IRAM2 定义(可选)
═══════════════════════════════════════════════════════
0x10000000 ┌─────────────────────┐
│ CCMRAM_SECTION │ 高性能数据(DMA 不可访问!)
0x1000FFFF └─────────────────────┘
8.5 链接文件与启动文件的符号协作
Keil 链接器会为每个执行域自动生成一组符号,可以在 C 代码中直接使用:
/* 访问 Keil 自动生成的链接符号 */
extern uint32_t Image$$RW_IRAM1$$Base; /* .data 段起始地址 */
extern uint32_t Image$$RW_IRAM1$$Length; /* .data 段长度 */
extern uint32_t Image$$RW_IRAM1$$Limit; /* .data 段结束地址 */
extern uint32_t Image$$RW_IRAM1$$ZI$$Base; /* .bss 段起始地址 */
extern uint32_t Image$$RW_IRAM1$$ZI$$Length;/* .bss 段长度 */
extern uint32_t Image$$RW_IRAM1$$ZI$$Limit; /* .bss 段结束地址 */
启动过程中的符号使用关系:
链接文件 (.sct) 定义 启动/运行时使用
───────────────── ──────────────
LR_IROM1 0x08000000 ──→ 向量表基地址
ER_IROM1 +First ──→ CPU 硬件读取 SP 和 PC
RW_IRAM1 0x20000000 ──→ __scatterload 拷贝 .data 的目标地址
RW_IRAM1 大小 0x00020000 ──→ 栈顶地址 = 0x20020000
RW_IRAM2 0x10000000 ──→ CCMRAM 变量的地址
⚠️ 工程师经验:如果修改了 .sct 文件但忘记同步修改启动文件(比如改变了 RAM 的起始地址或大小),程序上电后很可能会 HardFault。在 Keil 中,启动文件的栈顶地址通常直接写在向量表中(如
DCD 0x20020000),修改 .sct 时需要手动同步这个值。
九、启动模式与 Boot 引脚配置
9.1 Boot 引脚
STM32F407 有两个 Boot 引脚:BOOT0 和 BOOT1。它们在上电复位时的电平决定了芯片从哪里启动。
| BOOT1 | BOOT0 | 启动模式 | 说明 |
|---|---|---|---|
| X | 0 | 主 Flash | 从内置 Flash 启动(最常用) |
| 0 | 1 | 系统存储器 | 从内置 Bootloader 启动(用于串口/USB 下载) |
| 1 | 1 | 内置 SRAM | 从 SRAM 启动(用于调试) |
注意:BOOT1 引脚在 STM32F407 上默认是
0(内部下拉),且通常没有引出到芯片引脚上(取决于具体封装)。BOOT0 则通常有外部引脚。
9.2 不同启动模式下的向量表地址
| 启动模式 | 向量表基地址 | 说明 |
|---|---|---|
| 主 Flash | 0x08000000 |
正常工作模式 |
| 系统存储器 | 0x1FFF0000 |
ST 内置 Bootloader |
| 内置 SRAM | 0x20000000 |
调试模式 |
9.3 VTOR 寄存器
Cortex-M4 提供了 VTOR(Vector Table Offset Register) 寄存器,允许将向量表重定向到其他地址:
SCB->VTOR = FLASH_BASE; /* 0x08000000 */
这在以下场景中非常有用:
- 在线固件升级(OTA/IAP):使用两个 Flash 区域交替存放固件
- 从 SRAM 运行:调试时将代码加载到 SRAM 中运行
- Bootloader + App 架构:Bootloader 和 App 各自有独立的向量表
/* Bootloader 跳转到 App 之前 */
#define APP_ADDRESS 0x08008000
SCB->VTOR = APP_ADDRESS;
__set_MSP(*(uint32_t *)APP_ADDRESS); /* 设置 App 的 SP */
((void (*)(void))(*((uint32_t *)(APP_ADDRESS + 4))))(); /* 跳转到 App */
十、常见问题与实战经验
10.1 程序上电后立即 HardFault
可能原因:
- 栈溢出:
Stack_Size太小,或者局部变量/函数调用层级过多 - FPU 未启用:使用了
float/double但 FPU 没有在SystemInit()中启用 - 向量表地址错误:
VTOR指向了错误的地址 - 分散加载配置错误:
.sct文件中的内存区域配置不正确
排查方法:
void HardFault_Handler(void) {
/* 读取故障状态寄存器 */
volatile uint32_t cfsr = SCB->CFSR;
volatile uint32_t hfsr = SCB->HFSR;
volatile uint32_t mmfar = SCB->MMFAR;
volatile uint32_t bfar = SCB->BFAR;
volatile uint32_t msp = __get_MSP();
volatile uint32_t psp = __get_PSP();
/* 在这里设置断点,查看上述变量的值 */
while (1);
}
10.2 修改启动文件的常见需求
需求 1:增大栈空间
; 修改前
Stack_Size EQU 0x00000400 ; 1 KB
; 修改后
Stack_Size EQU 0x00002000 ; 8 KB
需求 2:自定义 Reset_Handler
在 Keil 中,你可以直接在 C 文件中定义同名函数(强符号覆盖弱符号),也可以在启动文件中修改汇编代码。推荐使用 C 语言方式:
void Reset_Handler(void) {
/* 自定义初始化逻辑 */
// ...
/* 最后调用原始流程 */
SystemInit();
/* 注意:不能直接调用 main(),必须跳转到 __main */
/* __main 会自动完成 .data 拷贝、.bss 清零等操作 */
/* 在 C 文件中无法直接跳转 __main,建议在启动文件中修改 */
}
💡 工程师经验:在 Keil 中更推荐的做法是修改启动文件中的 Reset_Handler,而不是在 C 文件中重新定义。因为 C 函数无法直接跳转到
__main(__main需要特殊的调用约定)。如果你确实需要在 C 中自定义,建议保留启动文件中的 Reset_Handler,在其中调用你的 C 初始化函数:
; 修改后的 Reset_Handler(在启动文件中)
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
IMPORT UserPreInit ; 用户自定义初始化函数
LDR R0, =SystemInit
BLX R0
LDR R0, =UserPreInit
BLX R0
LDR R0, =__main
BX R0
ENDP
需求 3:在进入 main() 前执行自定义代码
使用 __attribute__((constructor))(Keil ARMCC 也支持此语法):
__attribute__((constructor))
void pre_main_init(void) {
/* 这段代码会在 __rt_entry 中被自动调用 */
/* 在 main() 之前执行 */
GPIO_InitTypeDef gpio = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
gpio.Pin = GPIO_PIN_5;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &gpio);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}
10.3 使用 FreeRTOS 时的注意事项
当使用 FreeRTOS 时,启动流程有一些重要的变化:
- FreeRTOS 使用 PSP(进程栈指针),而启动代码使用 MSP(主栈指针)
- 中断仍然使用 MSP,所以
Stack_Size仍然需要足够大(用于中断嵌套) - FreeRTOS 的第一个任务会在
xPortStartScheduler()中启动,此时会切换到 PSP
启动阶段: MSP ← __initial_sp (主栈)
↓
__main: MSP ← __initial_sp (主栈)
__scatterload + __rt_entry
↓
main(): MSP ← __initial_sp (主栈)
↓
xTaskCreate() → 创建任务(每个任务有独立栈)
↓
vTaskStartScheduler() → 切换到第一个任务
↓
任务运行: PSP ← 任务栈
MSP ← 主栈 (仅中断使用)
10.4 调试技巧
查看 map 文件
Keil 编译后生成的 .map 文件包含了所有符号的地址信息,是排查启动问题的利器。在 Keil 中通过 Options for Target → Listing → Linker Listing 勾选生成 map 文件:
Symbol Name Value Ov Type Size Object(Section)
__Vectors 0x08000000 Section 0 startup_stm32f407xx.o(RESET)
Reset_Handler 0x08000188 Section 0x0c startup_stm32f407xx.o(RESET)
SystemInit 0x080001d8 Thumb Code 0x80 system_stm32f4xx.o(i.SystemInit)
__main 0x08000260 Section 0x08 __main.o(!!!main)
main 0x08000300 Thumb Code 0x20 main.o(i.main)
使用 Keil 调试器验证启动流程
在 Keil MDK 中,你可以使用内置调试器(通常配合 ULINK、ST-Link 或 DAP-Link)来验证启动流程:
- 在 Reset_Handler 设置断点:在启动文件中
Reset_Handler PROC处双击设置断点,启动调试后程序会停在这里。 - 查看寄存器窗口:调试模式下,菜单 View → Registers 可以查看 SP、PC、R0-R12 等寄存器的值。
- 查看内存窗口:菜单 View → Memory Windows,输入
0x08000000可以查看 Flash 中的向量表内容;输入0x20000000可以查看 SRAM 中的数据。 - 单步执行:使用 F11(Step Into)逐条执行汇编指令,观察每一步的寄存器变化。
- 查看调用栈:菜单 View → Call Stack 可以查看函数调用关系,确认
__main→__scatterload→main()的调用链。
附录:关键汇编指令速查
ARMCC 伪指令
| 指令 | 含义 | 示例 |
|---|---|---|
AREA |
定义段 | AREA RESET, DATA, READONLY |
EXPORT |
导出符号(全局可见) | EXPORT __Vectors |
IMPORT |
引入外部符号 | IMPORT SystemInit |
PROC / ENDP |
函数(过程)开始 / 结束 | Reset_Handler PROC ... ENDP |
[WEAK] |
弱符号属性 | EXPORT NMI_Handler [WEAK] |
DCD |
定义 4 字节数据常量 | DCD 0x20020000 |
SPACE |
分配字节空间 | SPACE 0x400 |
EQU |
定义常量(等价于 C 的 #define) | Stack_Size EQU 0x400 |
ALIGN |
对齐 | AREA ..., ALIGN=3(8 字节对齐) |
PRESERVE8 |
保持 8 字节栈对齐 | PRESERVE8 |
THUMB |
声明使用 Thumb 指令集 | THUMB |
KEEP |
防止链接器删除未引用的段 | KEEP |
ARM 指令(Thumb)
| 指令 | 含义 | 示例 |
|---|---|---|
LDR Rn, =label |
加载地址/常量到寄存器(伪指令) | LDR R0, =SystemInit |
LDR Rn, [Rm] |
从内存加载 32 位数据 | LDR R4, [R2, R3] |
STR Rn, [Rm] |
存储 32 位数据到内存 | STR R4, [R0, R3] |
MOV / MOVS |
数据传送 | MOVS R3, #0 |
ADD / ADDS |
加法 | ADDS R3, R3, #4 |
CMP |
比较 | CMP R4, R1 |
B label |
无条件跳转 | B .(跳转到自身) |
Bcc label |
条件跳转(cc 为条件码) | BCC CopyDataLoop |
BL label |
带链接的跳转(调用函数) | BL SystemInit |
BLX Rn |
带链接的跳转并切换指令集 | BLX R0 |
BX Rn |
跳转并切换指令集 | BX R0 |
BX lr |
返回到调用者 | BX lr |
总结
STM32F407 的启动文件虽然代码量不大(Keil ARMCC 版约 200-300 行汇编),但它承载了嵌入式系统最关键的初始化逻辑。理解启动文件的工作原理,是成为资深嵌入式工程师的必修课。
核心要点回顾:
- 向量表是启动的核心 —— CPU 硬件自动从向量表读取 SP 和 PC
- Keil 下 .data 拷贝和 .bss 清零由
__main自动完成 —— 这是 ARMCC 与 GCC 最显著的区别 __main是链接器生成的入口 —— 它内部调用__scatterload和__rt_entry,最终跳转到用户main()- SystemInit() —— 安全地初始化时钟和 FPU
- 弱符号机制 —— 允许用户灵活地覆盖默认的中断处理函数
- 分散加载文件协作 ——
.sct文件定义内存布局,链接器据此自动生成初始化代码
掌握了这些知识,你就能够在遇到启动相关的问题时,快速定位根因,而不是盲目地猜测和试错。
参考资源:
- ARM Cortex-M4 Technical Reference Manual (DUI0553)
- STM32F407xx Reference Manual (RM0090)
- STM32F4xx CMSIS Device Documentation
- ARM Compiler armasm User Guide (ARM DUI 0801)
- ARM Architecture Procedure Call Standard (AAPCS)
如果这篇文章对你有帮助,欢迎点赞收藏关注!有问题欢迎在评论区讨论。
📌 系列文章导航:
- 📘 本文:STM32F407 启动文件深度解析
- 📗 下一篇:STM32F407 链接文件深度解析(.ld 与 .sct)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)