详细解析单片机启动汇编文件:以startup_stm32f407xx.s为例

发布于:2025-07-17 ⋅ 阅读:(11) ⋅ 点赞:(0)

        在嵌入式开发中,启动文件(例如startup_stm32f407xx.s)是单片机系统运行的基础。它负责在单片机上电或复位后,完成最初的硬件和软件环境初始化,包括设置堆栈、建立中断向量表、跳转到主程序入口等关键步骤。

        下面我将以startup_stm32f407xx.s为例子,逐段介绍该启动文件,最后再梳理一下单片机从上电到执行main()函数的整个过程。相信在看完本篇博客后,你对单片机的启动会有一个比较深刻的理解,同时当单片机调试的时候卡死在了汇编.s文件里面,你也能大致知道是什么问题了!

一、堆栈的配置

堆和栈都是用来管理程序运行时的内存空间。

用于存放函数调用的局部变量、参数和返回地址,每次进入函数自动分配空间,退出自动释放,空间较小但访问速度快,适合临时数据和递归调用。在单个任务/线程的局部变量多、递归深度大时,栈需要设置得较大

:用于动态分配内存(如malloc/new),适合存放生命周期较长或大小不确定的数据,空间较大但分配和释放速度较慢,需要手动管理。当需要频繁动态分配大量数据(如动态数组、链表、任务控制块等)时,堆需要设置得较大。


本节用到的汇编代码:

EQU(equal):等于。

AREA :是ARM汇编的伪指令,定义一个内存区域,不实际分配空间

SPACE :在上个AREA定义的区域内,分配指定大小的空间。

EXPORT :导出为全局符号,让链接器和其他文件都能访问它们。

标签:用来标记某个内存地址或代码位置的符号。标签本质上就是一个名字,代表当前位置的地址,方便后续引用或跳转。

1.1 栈区域定义

这段代码完成了栈的配置:

  • EQU 是"等于"的意思,用于定义常量。Stack_Size EQU 0x400 意思是 Stack_Size 这个符号的值等于 0x400(1024字节)
  • AREA STACK, NOINIT, READWRITE, ALIGN=3定义一个名为 STACK 的内存区域,属性为无初始化(NOINIT)、可读写(READWRITE)、8字节对齐(ALIGN=3,即2³=8)
  • Stack_Mem SPACE Stack_Size 在当前区域内分配指定大小的空间,Stack_Mem 是这块空间的起始地址标签
  • __initial_sp 是一个标签,标记堆栈顶部地址,CPU复位时会用它初始化堆栈指针

1.2 堆区域定义

堆区域的配置与栈类似,但增加了起始和结束标签(__heap_base和__heap_limit),便于C库进行动态内存管理。

1.3 其他配置

这两个配置确保了代码在Cortex-M4处理器上的正确运行。

二、中断向量表的构建

中断向量表,是 Cortex-M4 启动时最重要的数据结构之一,位于程序存储器的起始地址(通常是0x00000000)。每一项都是一个32位的地址,告诉CPU发生某种异常或中断时应该跳转到哪个处理函数

单片机上电后,一般默认从中断向量表开始执行,并且把表内第一个值赋值给SP寄存器,第二个值赋值给PC寄存器。


本节用到的汇编代码:

DCD(Define Constant Doubleword):定义32位常量。

EXPORT:导出为全局符号,让链接器和其他文件都能访问它们。

2.1 向量表区域声明

AREA    RESET, DATA, READONLY 定义了名字为RESET区域,是数据段,只读,之后分配的空间就属于这个区域。再次申明,AREA 不会实际分配地址,只是给某一片区域起一个名字。

然后用export导出。EXPORT 指令将这些符号导出为全局符号,让链接器和其他模块可以访问。

2.2 系统异常向量表(前16项)

DCD(Define Constant Doubleword)定义32位常量,每一项都是一个中断处理函数的地址。前16项是ARM Cortex-M内核规定的系统异常,包括:

  • 第0项(__initial_sp):初始堆栈指针,CPU复位时自动加载到SP寄存器,而__initial_sp正好是栈顶指针。
  • 第1项(Reset_Handler):复位处理函数,CPU复位时自动加载到PC寄存器,因此接下来会跳转到Reset_Handler捏~
  • 第2-15项:各种系统异常的处理函数

特别注意: 在32位单片机中,双字应该是64位,但这里DCD定义的双字却是32位常量,这是历史遗留问题,这边困惑了我好久来着,但是网上是这么说的。除此之外还有DCB(Define Constant Byte):定义8位常量。

DCW(Define Constant Word):定义16位常量。

2.3 外部中断向量表

外部中断向量表定义了STM32F407xx特有的82个外设中断,涵盖了定时器、通信接口、DMA、USB等所有外设的中断处理入口。

很多初学者会疑问:为什么向量表中只是登记了地址,CPU就能跳转执行,正常不应该是PC=PC+4吗?这是因为中断响应是CPU的硬件行为。当中断发生时,CPU会自动查找中断向量表,获取对应的处理函数地址,然后硬件自动将PC(程序计数器)设置为该地址,开始执行中断处理代码。这个过程不需要软件指令参与,完全由硬件完成。

三、复位处理函数:系统启动的核心

复位处理函数是系统启动后执行的第一段用户代码,负责完成从硬件复位到跳转至C程序的整个过程。


本节用到的汇编代码:

PROC/ENDP定义一个汇编过程(函数),类似C语言的函数定义。

EXPORT [WEAK]:将符号导出为弱符号,允许用户在其他文件里面重新定义

IMPORT:声明外部符号,告诉链接器这些函数在其他文件中定义

LDR R0, =symbol(Load Register):将symbol的地址加载到寄存器R0。

BLX(Branch with Link and Exchange)和BX(Branch and Exchange):BLX会保存返回地址到LR寄存器,用于函数调用;BX只是跳转,不保存返回地址。

3.1复位处理函数的完整实现:

执行流程如下:

  1. PROC/ENDP:定义一个汇编过程(函数),名字叫Reset_Handler,这是复位中断的入口函数
  2. EXPORT [WEAK]:把Reset_Handler导出为全局符号,链接器可以找到它。[WEAK]表示弱符号,用户可以在其他文件中重新定义它
  3. IMPORT:声明外部符号,告诉汇编器和链接器,这些符号在其他文件里定义
  4. LDR R0, =SystemInit:把SystemInit的地址加载到寄存器R0。= 表示取SystemInit的地址
  5. BLX R0:跳转并调用SystemInit,同时保存返回地址到LR寄存器,完成系统初始化
  6. BX R0:跳转到__main,不保存返回地址(因为不会返回),进入C库启动流程

注意:__main 不是用户的main函数,而是C库的启动入口,这个由编译器和标准库实现,用户一般是看不到代码的,也无需关心。它负责初始化C运行环境(如全局变量、BSS段清零等),然后自动调用用户的main()函数。双下划线是编译器约定,表示这是一个特殊的、内部使用的符号。

3.2.SystemInit()的实现

LDR R0, =SystemInit会自动调用system_stm32f4xx.c里面的SystemInit(),这个函数主要干了三件事:

1.FPU(浮点运算单元)设置

如果芯片带有 FPU 并且启用了 FPU(由 __FPU_PRESENT__FPU_USED 两个宏控制),SystemInit 会通过设置 SCB->CPACR 寄存器,让协处理器 CP10 和 CP11(即 FPU)拥有完全访问权限。这样后续 C 代码里的浮点运算就能直接用硬件加速,提高运算效率。

#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
   SCB->CPACR |= ((3UL << 10 * 2) | (3UL << 11 * 2)); /* set CP10 and CP11 Full Access */
#endif

2.外部存储器初始化(可选)

如果你定义了 DATA_IN_ExtSRAMDATA_IN_ExtSDRAM,说明你的板子上接了外部 SRAM 或 SDRAM 芯片。此时会调用 SystemInit_ExtMemCtl(),初始化外部存储器控制器(FMC/FSMC),配置相关引脚和时序,让外部 RAM 可用。这样可以突破片内 RAM 的限制,把堆、栈、全局变量等分配到外部存储器,适合需要大量内存的应用场景。

#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM)
  SystemInit_ExtMemCtl(); 
#endif

3.中断向量表重定位(可选)

默认情况下,中断向量表位于 Flash 的起始地址。如果你定义了 USER_VECT_TAB_ADDRESS,则会通过设置 SCB->VTOR 寄存器,把中断向量表重定位到 SRAM 或其他指定位置。这在 Bootloader、IAP(在线升级)、多系统切换等场景下非常有用,可以灵活切换中断入口。

#if defined(USER_VECT_TAB_ADDRESS)
  SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
#endif

四、默认中断处理函数的设计

启动文件为所有中断都提供了默认的处理机制,既保证了系统的稳定性,又为用户提供了灵活的定制空间。


本节用到的汇编代码:

B .:跳转指令,.代表当前指令的地址,因此就是个死循环。

\:表示当前行与下一行是连续的,应该被当作一行来处理。

弱符号:允许用户在自己的代码中重新定义,编译器会优先使用用户定义的版本。

4.1 系统异常的独立处理

系统异常采用独立定义的方式,每个异常都有自己的处理函数。这是占位符式的异常处理函数,提供最基本的"陷入死循环"功能。B . 指令创建了一个无限循环:

  • B 是跳转指令
  • . 代表当前指令的地址
  • 因此 B . 就是"跳转到自己",形成死循环

这种设计的目的是:

  1. 防止程序跑飞:异常发生时程序停在已知位置
  2. 便于调试:调试器可以准确定位异常类型
  3. 允许重定义:用户可以实现自己的异常处理逻辑

4.2 外设中断的统一处理机制

...省略类似代码

...省略类似代码

外设中断采用了巧妙的多对一设计,所有这些中断处理函数都指向同一个Default_Handler,并被导出为弱符号,所有未实现的中断都执行同一个死循环。

这种设计既节省了代码空间,又保持了完全的可定制性。前面在中断向量表里面已经定义了入口地址,在发生中断后,CPU自动通过中断向量表跳到对应的地址,如果重写了函数就开始执行,不然会进这里的死循环。

五、内存管理的兼容性设计

启动文件通过条件编译支持不同的C库,因为不同的库需要有不同的堆栈初始化过程。


本节用到的汇编代码:

IF :DEF:__MICROLIB / ELSE / ENDIF:条件编译结构,用于适配不同的C库。

LDR R0, =address:将地址加载到寄存器R0。

BX LR:返回到调用者,相当于C语言中的return。

条件编译结构:

如果定义了 __MICROLIB(轻量级库),则直接导出 __initial_sp(栈顶)、__heap_base(堆起始)、__heap_limit(堆结束)这几个符号,供库使用。

如果使用标准库,则导入 __use_two_region_memory(告诉链接器采用两段式内存管理),并导出 __user_initial_stackheap。标准库会调用 __user_initial_stackheap,该函数通过寄存器返回堆和栈的起始与结束地址(R0~R3),用于C库初始化内存分区。

最后用 ALIGN 保证区域对齐,ENDIF 结束条件编译。

六、启动流程的完整梳理

综上所属,整个启动过程可分为如下几个步骤:

1.硬件复位:单片机上电或复位后,CPU自动从向量表的第0项开始执行,即标记为__Vectors的地址。

2.堆栈指针初始化:CPU读取向量表第0项(__initial_sp,即Stack_Mem + Stack_Size,也就是栈空间的最高地址(顶端)),并自动将其设置SP寄存器,这是硬件完成的

3.程序计数器设置:CPU读取向量表第1项(Reset_Handler),设置PC寄存器指向Reset_Handler,因此后面会直接运行Reset_Handler这个过程

4.系统初始化:由于CP跳转到了指向Reset_Handler的地址,接下来会运行Reset_Handler,在这里面执行SystemInit和__main,__main最后跳入用户定义的main()函数。

SystemInit位于system_stm32f4xx.c里面,主要是完成系统级硬件初始化,包括 FPU(浮点运算单元)权限设置、外部存储器(SRAM/SDRAM)初始化(可选)、中断向量表重定位(可选),为后续 C 语言环境和主程序运行做好准备。

__main函数是 C 库的入口,由编译器和标准库实现。__main 会负责初始化 C 运行环境(比如堆栈、全局变量、C库等)。

5.应用程序启动:__main最终会调用用户的main()函数,随后运行你的逻辑。

七、实际应用中的考虑因素

7.1 内存配置的优化

在实际项目中,需要根据应用需求调整内存配置:

; 根据应用需求调整堆栈大小
Stack_Size      EQU     0x800         
Heap_Size       EQU     0x1000         

内存配置建议:

  • 简单应用:堆栈1KB,堆512字节即可
  • 中等复杂度:堆栈2-4KB,堆1-2KB
  • 复杂应用:根据实际需求动态调整,可能需要8KB以上

7.2 中断处理的实现

用户可以在任意地方重新定义任意中断处理函数,只要被编译器编译了就行。编译器会自动使用用户定义的版本:

// 在C文件中实现具体的中断处理,给出两个例子
void WWDG_IRQHandler(void) {
    // 处理窗口看门狗中断
    // 清除中断标志
    // 执行相应的处理逻辑
}

void HardFault_Handler(void) {
    // 保存错误信息
    // 尝试恢复或安全关闭
    // 重启系统
    while(1); // 最后的安全措施
}


网站公告

今日签到

点亮在社区的每一天
去签到