STM32F407 启动文件深度解析(startup_stm32f407xx.s 逐行剖析 — Keil ARMCC 版)


📖

很多嵌入式开发者能熟练使用 STM32 写业务代码,但对启动文件(startup_stm32f407xx.s)的内部机制却一知半解。一旦遇到上电跑飞、HardFault 等问题,往往无从下手。

本文从源码层面逐行剖析 STM32F407 的启动文件,帮你彻底搞懂从上电复位到 main() 函数之间到底发生了什么。

你将学到:

  • 向量表的结构与硬件加载机制
  • Reset_Handler 的每一条汇编指令在做什么
  • .data 拷贝和 .bss 清零的原理(Keil 下由链接器自动完成)
  • 弱符号机制与默认中断服务程序
  • 堆栈配置与内存布局
  • Boot 引脚与启动模式
  • 从上电到 main() 的完整时序

📑 目录

一、启动文件概述

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 硬件自动执行以下操作:

  1. 从地址 0x00000000(映射到 0x08000000)读取值,加载到主堆栈指针 MSP
  2. 从地址 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())。

⚠️ 工程师经验:在嵌入式系统中,强烈建议避免使用动态内存分配。原因包括:

  1. 内存碎片化——长时间运行后可能导致分配失败
  2. 不确定性——malloc() 的执行时间不可预测
  3. 内存泄漏风险——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_entrymain()
链接脚本 .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 中每个函数都必须用 PROCENDP 包裹。
  • EXPORT xxx [WEAK]:将符号导出并标记为弱符号。弱符号可以被同名的强符号覆盖。
    • 实际含义:如果你在 C 文件中定义了 void HardFault_Handler(void),链接器会使用你的版本(强符号);如果没有定义,则使用启动文件中的默认版本(弱符号)。
  • 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 控制器),你可能需要优化启动流程:

  1. 减小 .data 段(减少需要拷贝的数据)
  2. 使用更快的 Flash 等待周期配置
  3. 跳过不必要的初始化步骤
  4. 考虑将关键代码拷贝到 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 引脚:BOOT0BOOT1。它们在上电复位时的电平决定了芯片从哪里启动。

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

可能原因:

  1. 栈溢出Stack_Size 太小,或者局部变量/函数调用层级过多
  2. FPU 未启用:使用了 float/double 但 FPU 没有在 SystemInit() 中启用
  3. 向量表地址错误VTOR 指向了错误的地址
  4. 分散加载配置错误.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 时,启动流程有一些重要的变化:

  1. FreeRTOS 使用 PSP(进程栈指针),而启动代码使用 MSP(主栈指针)
  2. 中断仍然使用 MSP,所以 Stack_Size 仍然需要足够大(用于中断嵌套)
  3. 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)来验证启动流程:

  1. 在 Reset_Handler 设置断点:在启动文件中 Reset_Handler PROC 处双击设置断点,启动调试后程序会停在这里。
  2. 查看寄存器窗口:调试模式下,菜单 View → Registers 可以查看 SP、PC、R0-R12 等寄存器的值。
  3. 查看内存窗口:菜单 View → Memory Windows,输入 0x08000000 可以查看 Flash 中的向量表内容;输入 0x20000000 可以查看 SRAM 中的数据。
  4. 单步执行:使用 F11(Step Into)逐条执行汇编指令,观察每一步的寄存器变化。
  5. 查看调用栈:菜单 View → Call Stack 可以查看函数调用关系,确认 __main__scatterloadmain() 的调用链。

附录:关键汇编指令速查

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 行汇编),但它承载了嵌入式系统最关键的初始化逻辑。理解启动文件的工作原理,是成为资深嵌入式工程师的必修课

核心要点回顾:

  1. 向量表是启动的核心 —— CPU 硬件自动从向量表读取 SP 和 PC
  2. Keil 下 .data 拷贝和 .bss 清零由 __main 自动完成 —— 这是 ARMCC 与 GCC 最显著的区别
  3. __main 是链接器生成的入口 —— 它内部调用 __scatterload__rt_entry,最终跳转到用户 main()
  4. SystemInit() —— 安全地初始化时钟和 FPU
  5. 弱符号机制 —— 允许用户灵活地覆盖默认的中断处理函数
  6. 分散加载文件协作 —— .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)

如果这篇文章对你有帮助,欢迎点赞收藏关注!有问题欢迎在评论区讨论。

📌 系列文章导航:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐