
05_STM32_启动篇
本篇章主要对于启动文件进行讲解,并且纯理论内容,第一次听可能会很多地方看不懂, 但是没关系,当我们学完之后整个内容之后,再回头来学习这一篇章,你会有不同的体验以及感受。
第一章 基础概念
1.1 启动文件的作用
启动文件由汇编编写,是系统上电复位后第一个执行的程序,负责初始化硬件环境并引导进入用户代码,负责为系统运行搭建基础环境。
主要做了以下工作:
初始化堆栈和堆
- 栈的作用及初始化过程
- 堆的作用及配置
设置中断向量表
- 向量表的结构和位置
- 中断服务函数的关联
执行系统初始化和引导主程序
- 复位处理流程
- 系统时钟配置
- 跳转到主程序
1.2 汇编指令解释
在我们进行启动文件的学习中,不可避免的会会涉及到 ARM 的汇编指令和 Cortex 内核的指令,所以我们需要对一些汇编指令先进行了解。
首先是需要了解关于如何去查询汇编指令。
1.2.1 汇编指令的查询
1、查询 Cortex
汇编指令集
如果我们想了解关于 ST 公司的 Cortex-M4系列的汇编指令集,可以通过 《ARM Cortex-M3与Cortex-M4权威指南》 以及 《Cortex M3与M4权威指南》 进行了解。
2、查询 ARM
汇编指令集
关于 ARM
的汇编指令集可以通过 Keil 软件自带的帮助文档, 通过Help -> uVision Help
可以打开帮助文档,如下所示:
当打开文档后 就可以对需要了解的汇编指令进行检索,虽然检索出来的结果会有很多,但是只需要选择Assembler User Guide
如下以 EQU
指令所示:
1.2.1 汇编指令常用集合
指令名称 | 作用 | 备注 |
---|---|---|
EQU | 给数字常量取一个符号名,相当于 C 语言中的 #define | 用于定义常量符号 |
AREA | 定义一个新的代码段或数据段 | 如 AREA STACK, NOINIT, READWRITE, ALIGN=3 定义堆栈区域 |
SPACE | 分配内存空间 | 例如 Stack_Mem SPACE 0x800 分配 2KB 的栈空间 |
PRESERVES | 当前文件堆栈需按照 8 字节对齐 | 可能与实际指令名存在差异(标准汇编中通常为 PRESERVE8 ) |
EXPORT | 声明一个标号具有全局属性,可被外部文件使用 | 类似 C 语言的 extern |
DCD | 以字(4 字节)为单位分配内存,要求 4 字节对齐,并初始化内存 | 例如 DCD Reset_Handler 定义中断向量表 |
PROC | 定义子程序,与 ENDP 成对使用,表示子程序结束 | 如 Reset_Handler PROC 定义复位处理函数 |
WEAK | 弱定义:如果外部文件声明了标号,则优先使用外部定义;若未定义也不报错 | 编译器特有指令,非 ARM 原生指令 |
IMPORT | 声明标号来自外部文件,类似 C 语言的 extern | 如 IMPORT SystemInit 引入外部函数 |
B | 跳转到一个标号 | 如 B Reset_Handler 跳转到复位处理函数 |
ALIGN | 对指令或数据的存放地址进行对齐(默认 4 字节对齐) | 编译器特有指令,需配合立即数使用(如 ALIGN 3 表示 8 字节对齐) |
END | 表示文件结束 | 汇编文件末尾必须包含此指令 |
IF, ELSE, ENDIF | 汇编条件分支语句,类似 C 语言的 if-else | 用于条件编译,例如根据芯片型号选择代码段 |
1.3 内核寄存器组
摘抄自 《ARM Cortex-M3与Cortex-M4权威指南》
Cortex-M3 和 Cortex-M4 处理器包含多个寄存器,用于数据处理和控制。这些寄存器大部分按寄存器组的方式进行管理。数据处理指令通常会指定源寄存器、目标寄存器和操作类型。
Cortex-M 处理器采用“加载-存储架构”(Load-Store Architecture),即数据操作只能在寄存器之间进行,访问存储器时必须使用专门的加载(Load)或存储(Store)指令。寄存器的使用可以提升指令执行效率,减少对存储器的访问。
Cortex-M3 和 Cortex-M4 共有 16 个 32 位寄存器,其中 13 个是通用寄存器(R0~R12),另外 3 个具有特殊用途,如图所示。
1.3.1 通用寄存器(R0~R12)
寄存器 R0~R12 属于通用寄存器,其中:
-
R0~R7:称为低寄存器,大部分 16 位 Thumb 指令只能访问这些寄存器。
-
R8~R12:称为高寄存器,它们可用于 32 位指令,也能被部分 16 位指令访问(如
MOV
指令)。
通用寄存器的初始值未定义,程序启动时需要手动初始化。
1.3.2 堆栈指针(SP, R13)
R13 作为堆栈指针(Stack Pointer, SP),用于管理栈操作。Cortex-M 处理器提供两个物理堆栈指针:
-
主堆栈指针(MSP,Main Stack Pointer):默认使用,在异常处理和特权模式下运行。
-
进程堆栈指针(PSP,Process Stack Pointer):用于线程模式,可以为不同任务提供独立的栈空间(适用于 RTOS)。
堆栈指针的选择由 CONTROL
寄存器的 SPSEL
位控制,MSP 和 PSP 都要求 32 位对齐。
1.3.3 链接寄存器(LR, R14)
R14 也称链接寄存器(Link Register, LR),用于存储子程序返回地址:
-
函数调用时,LR 保存返回地址,
BL
指令会自动更新 LR。 -
异常处理时,LR 存储特殊的
EXC_RETURN
值,指示异常返回时如何恢复上下文。
如果函数嵌套调用,必须将 LR 的值保存到栈,否则调用新函数时 LR 会被覆盖,导致返回地址丢失。
1.3.4 程序计数器(PC, R15)
R15 也称程序计数器(Program Counter, PC),用于存储当前执行的指令地址。
-
读取 PC 时,得到的是当前指令的下一条指令地址(由于流水线机制)。
-
修改 PC 可以实现跳转,例如
BX
指令用于返回LR
存储的地址。
由于 Thumb 指令集的对齐要求:
-
PC 的最低位(LSB)始终为 0。
-
在某些跳转操作中,需要将目标地址的 LSB 置 1,指示 Thumb 状态,否则会触发异常。
5. 寄存器名称对照表
不同的汇编工具允许使用不同的寄存器名称,如下表所示:
标准寄存器名 | 其他可用名称 | 说明 |
---|---|---|
R0~R12 | - | 通用寄存器 |
R13 | SP, sp | 堆栈指针(可切换 MSP/PSP) |
R14 | LR, lr | 链接寄存器(存储返回地址) |
R15 | PC, pc | 程序计数器(存储指令地址) |
第二章 启动模式
2.1 Flash启动
原理
-
地址映射:STM32F407的Flash起始地址为
0x08000000
,但芯片启动时会将该地址映射到0x00000000
,使程序从Flash直接运行。 -
执行流程:上电后,内核从
0x00000000
(实际是0x08000000
)读取栈顶地址和复位向量,跳转到Reset_Handler
初始化系统。
配置方法
-
硬件配置:BOOT0引脚接地(BOOT0=0),BOOT1无关(默认启动Flash)。
-
代码位置:用户程序需烧录到
0x08000000
开始的Flash区域。
应用场景
-
常规开发:程序固化在Flash中,断电后不丢失。
-
比喻:类似电脑从硬盘启动操作系统。
2.2 SRAM启动
原理
- 地址映射:SRAM起始地址为
0x20000000
,启动时将该地址映射到0x00000000
,程序直接在SRAM中运行。 - 临时性:SRAM内容断电丢失,需通过调试器(如J-Link)将代码加载到SRAM。
配置方法
- 硬件配置:BOOT0=1,BOOT1=1。
- 调试配置:在IDE(如Keil)中设置下载目标为SRAM,并指定起始地址。
应用场景
- 快速调试:无需擦写Flash,适合反复修改代码的场景。
- 比喻:类似电脑从U盘启动临时系统,用于故障排查。
2.3 系统存储器启动
原理
- 内置BootLoader:芯片出厂时固化在系统存储区(地址
0x1FFF0000
),支持通过串口、USB等接口烧录程序。 - 映射机制:启动时系统存储区映射到
0x00000000
,运行BootLoader。
配置方法
- 硬件配置:BOOT0=0,BOOT1=1。
- 烧录工具:使用STM32CubeProgrammer或串口工具(如FlyMcu)。
应用场景
- 恢复模式:当Flash中的程序损坏时,通过BootLoader重新烧录。
- 无调试器环境:仅需串口线即可更新程序。
- 比喻:类似手机进入Recovery模式刷机。
第三章 启动文件详解
3.1 启动流程
在启动文件中的第一段注释中其实已经对STM32F407启动文件的启动流程进行了说明,具体内容如下:
;******************** (C) COPYRIGHT 2016 STMicroelectronics ********************
;* File Name : startup_stm32f40_41xxx.s
;* Author : MCD Application Team
;* @version : V1.8.0
;* @date : 09-November-2016
;* Description : STM32F40xxx/41xxx devices vector table for MDK-ARM toolchain.
;* This module performs:
;* - Set the initial SP
;* - Set the initial PC == Reset_Handler
;* - Set the vector table entries with the exceptions ISR address
;* - Configure the system clock and the external SRAM mounted on
;* STM324xG-EVAL board to be used as data memory (optional,
;* to be enabled by user)
;* - Branches to __main in the C library (which eventually
;* calls main()).
;* After Reset the CortexM4 processor is in Thread mode,
;* priority is Privileged, and the Stack is set to Main.
;* <<< Use Configuration Wizard in Context Menu >>>
;*******************************************************************************
;
; Licensed under MCD-ST Liberty SW License Agreement V2, (the "License");
; You may not use this file except in compliance with the License.
; You may obtain a copy of the License at:
;
; http://www.st.com/software_license_agreement_liberty_v2
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
;
;*******************************************************************************
经过翻译后,内容如下:
;******************** (C) 版权所有 2016 STMicroelectronics ********************
;* 文件名 : startup_stm32f40_41xxx.s
;* 作者 : MCD 应用团队
;* @version : V1.8.0
;* @date : 2016年11月09日
;* 描述 : STM32F40xxx/41xxx 设备的中断向量表,用于 MDK-ARM 工具链。
;* 本模块执行以下操作:
;* - 设置初始堆栈指针(SP)
;* - 设置初始程序计数器(PC)为 Reset_Handler
;* - 用异常中断服务例程(ISR)地址填充向量表条目
;* - 配置系统时钟和外部SRAM(挂载在STM324xG-EVAL开发板上,
;* 可选功能,需用户手动启用)作为数据存储器
;* - 跳转到C库中的 __main(最终调用 main() 函数)
;* 复位后,Cortex-M4 处理器处于线程模式,
;* 优先级为特权级,堆栈设置为Main类型。
;* <<< 使用右键菜单中的配置向导(Configuration Wizard)>>>
;*******************************************************************************
;
; 遵循 MCD-ST Liberty SW 许可证协议 V2 授权("许可证")
; 除非遵守许可证,否则不得使用此文件。
; 您可以在以下网址获取许可证副本:
;
; http://www.st.com/software_license_agreement_liberty_v2
;
; 除非适用法律要求或书面同意,本软件按"原样"分发,
; 无任何明示或暗示的担保或条件。详见许可证中的特定语言条款和限制。
;
;*******************************************************************************
对于启动流程的详细解释如下:
初始化堆栈指针 SP=_initial_sp
初始化 PC 指针 =Reset_Handler
初始化中断向量表
配置系统时钟
调用 C 库函数 _main 初始化用户堆栈,从而最终调用 main 函数
3.2 Stack 栈
代码段
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
; 翻译如下
; 为堆栈分配的内存大小(单位:字节)
; 根据应用需求调整此值
; <h> 堆栈配置
; <o> 栈大小(单位:字节) <0x0-0xFFFFFFFF:8>
; </h>
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
本段代码的配置主要是为单片机开辟栈空间。在 C 语言中,我们已经学习了栈的作用,主要用于局部变量、函数调用、函数形参等内容。
如果忘记了内存分区的内容,也可以参考下表:
区域 | 生命周期 | 分配方式 | 典型用途 |
---|---|---|---|
堆栈 | 函数调用期 | 自动分配 | 局部变量、函数调用链 |
堆 | 动态分配 | 手动分配 | 动态数据结构 |
全局区 | 整个程序 | 静态分配 | 全局变量、静态变量 |
代码区 | 只读 | 固定分配 | 程序指令 |
需要注意的是,如果程序中定义的变量较多时,就需要调整栈空间的大小,即在启动文件中进行修改。常见的错误是程序运行过程中突然进入硬件错误 (Hard Fault),这可能是由于栈空间不足导致的内存管理错误。
此外,栈空间的大小必须小于单片机的实际 SRAM 容量,否则会导致程序崩溃。
3.2.1 代码解释
1、栈空间定义
; EQU 指令用于定义常量,这里将 Stack_Size 设为 0x400(1KB)。
; 该值可根据应用需求进行调整。
Stack_Size EQU 0x00000400 ; 1KB堆栈空间
2、内存段声明
; AREA 用于定义一个新的内存区域。
; STACK 是该区域的名称。
; NOINIT 表示不进行初始化。
; READWRITE 允许读写。
; ALIGN=3 设定 8 字节对齐(`2^3 = 8`)。
AREA STACK, NOINIT, READWRITE, ALIGN=3
3、栈内存分配
; SPACE 用于分配指定大小的内存。
; 这里分配 Stack_Size(1KB)。
Stack_Mem SPACE Stack_Size
4、栈顶指针定义
; 这是栈顶指针的标签。
; 处理器启动时,栈指针 `SP` 将初始化为 `__initial_sp` 的地址。
__initial_sp
标号 __initial_sp
紧挨着 SPACE 语句放置,表示栈的栈顶地址。
栈的生长方向通常是向下(地址递减),__initial_sp
代表栈顶,SP
(栈指针)初始化后会从此处向下增长。
访问栈时,PUSH
指令会减少 SP
,POP
指令会增加 SP
。
如果栈空间不足,可能会触发 Hard Fault
。
3.2.2 指令解释
1、EQU
汇编伪指令
-
功能:定义符号常量,类似C语言的
#define
-
语法:
symbol EQU value
-
特点:不占用内存空间,仅在编译阶段替换
2、AREA
段定义指令
-
功能:用于定义一个新的数据段或代码段。
-
语法:
AREA 名称, [属性]
。 -
示例:以前面的代码为示例
AREA STACK, NOINIT, READWRITE, ALIGN=3
-
STACK
只是命名,可以自定义。 -
NOINIT
表示不初始化数据。 -
READWRITE
允许读写。 -
ALIGN=n
:对齐方式。
-
3、NOINIT
段属性修饰符
-
功能:表示该区域不会被初始化,适用于不需要在启动时清零的数据,如栈空间。
-
特点:
- 内存区域不会被初始化(不生成初始化数据)
- 上电时内容随机,适合栈、堆等不需要初始值的区域
补充指令INIT
, 需要进行初始化的区域。
4、READWRITE
-
内存访问权限定义
-
权限组合:
-
READONLY
:代码段默认属性 -
READWRITE
:数据段默认属性 -
部分架构支持
EXECUTE
属性
-
5、 ALIGN
内存对齐控制
-
语法:
ALIGN=n
设定数据对齐方式。ALIGN=3
表示 8 字节对齐(2^3 = 8)。
-
对齐原则:
-
Cortex-M系列要求栈指针必须8字节对齐(ALIGN=3)
-
对齐错误可能导致Hard Fault
-
对齐能提高内存访问效率
-
6、SPACE
-
功能:在内存中分配指定大小的未初始化空间(相当于C语言中的未初始化数组)
-
语法:
label SPACE expression
-
label
:可选标签,标记分配空间的起始地址 -
expression
:要分配的字节数(必须是常数表达式)
-
-
关键特性:
-
分配的空间内容在启动时是未定义的(随机值)
-
通常用于定义堆、栈等动态内存区域
-
不生成实际代码,仅影响内存布局
-
3.3 Heap 堆
代码段
; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
; <h> 堆配置
; <o> 堆大小(以字节为单位)<0x0-0xFFFFFFFF:8>
; </h>
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
PRESERVE8
THUMB
本段代码的配置主要是为单片机开辟堆空间。在 C 语言中,堆用于动态分配内存(如 malloc
、calloc
等函数申请的内存),需手动管理其分配与释放。若使用不当可能导致内存泄漏或碎片。
需特别注意:
堆大小调整:动态分配需求超过堆容量时,malloc
返回 NULL
导致分配失败
内存容量限制:堆总大小需小于单片机 SRAM 容量,且不得与其他内存区域重叠
生长方向:堆通常向高地址增长(__heap_base
起始,__heap_limit
结束)
对齐要求:ALIGN=3
和 PRESERVE8
确保8字节对齐,避免硬件异常
3.3.1 代码解释
1、堆空间定义
; EQU 指令用于定义常量,这里将 Heap_Size 设为 0x200(512B)。
Heap_Size EQU 0x00000200 ; 512B 堆空间
-
Heap_Size
设定了堆的大小,可以根据实际需求调整。 -
在嵌入式系统中,堆的大小一般较小,应根据内存容量合理分配。
2、内存段声明
; AREA 指令定义了一个新的内存区域。
; HEAP 是该区域的名称。
; NOINIT 表示不进行初始化。
; READWRITE 允许读写。
; ALIGN=3 设定 8 字节对齐(`2^3 = 8`)。
AREA HEAP, NOINIT, READWRITE, ALIGN=3
-
HEAP
只是一个名称,可自定义,但通常使用HEAP
表示堆区。 -
NOINIT
使该区域不会在启动时初始化,适用于动态分配的内存空间。 -
READWRITE
允许程序在运行时对堆区进行读写操作。 -
ALIGN=3
设定 8 字节对齐,以满足某些架构对动态内存管理的要求。
3、堆起始地址
__heap_base
-
__heap_base
作为堆的起始地址,用于指示堆空间的起始位置。 -
堆的增长方向通常是向上(地址递增),与栈相反。
4、堆内存分配
Heap_Mem SPACE Heap_Size
-
SPACE
指令用于分配指定大小的内存,这里分配Heap_Size
(512B)。 -
该内存区域可用于动态分配,例如
malloc()
和free()
。
5、堆结束地址
__heap_limit
-
__heap_limit
作为堆的结束地址,用于指示堆的边界。 -
在动态内存管理中,
__heap_limit
可能用于检测内存溢出或堆区溢出。
4、指令修饰
PRESERVE8
THUMB
-
PRESERVE8
:确保栈指针 (SP
) 始终是 8 字节对齐的,以满足 Cortex-M 处理器的要求。 -
THUMB
:指定使用 Thumb 指令集,以确保 ARM 处理器正确解析指令。
3.3.1 指令解释
1、PRESERVE8
-
功能:确保栈指针 (
SP
) 始终是 8 字节对齐的,强制当前数据段/代码段保持8字节对齐。 -
必要性:
- Cortex-M 处理器要求栈指针必须 8 字节对齐,否则会触发
Hard Fault
。 - Cortex-M 架构要求栈指针必须8字节对齐,否则触发硬件异常
- Cortex-M 处理器要求栈指针必须 8 字节对齐,否则会触发
-
典型应用:堆、栈等需要严格对齐的内存区域
2、THUMB
-
功能:声明后续代码使用 Thumb 指令集,以确保 ARM 处理器正确解析指令。
-
架构特性:Thumb 以16位指令为主,代码密度高,适合资源受限的嵌入式系统
-
强制要求:Cortex-M 全系列仅支持 Thumb 模式
3、堆边界符号
-
__heap_base
:堆起始地址,动态分配从此处开始 -
__heap_limit
:堆结束地址,内存分配不可越过此地址 -
管理机制:通过
sbrk()
系统调用维护堆指针,详见标准库实现
3.4 中断向量表
中断向量表是单片机启动时的核心数据结构,用于存储系统异常和外部中断的入口地址。关键特性:
-
物理定位:复位时被映射到地址0x00000000(可通过重映射修改)
-
严格顺序:每个条目对应特定中断源,位置不可更改
-
地址存储:每个条目存储4字节地址(DCD指令)
-
自动跳转:发生中断时,CPU自动从向量表获取处理函数地址
3.4.1 初始化部分-代码解释
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
-
AREA RESET, DATA, READONLY
:定义一个数据区域为RESET
,该区域可读可使用。 -
EXPORT __Vectors
:将__Vectors
该转向表输出,使其可以被外部调用。 -
EXPORT __Vectors_End
与EXPORT __Vectors_Size
为该表的结束点以及大小。
3.4.2 向量表部分-代码解释
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
DCD RTC_WKUP_IRQHandler ; RTC Wakeup through the EXTI line
; 中间部分省略
DCD OTG_HS_IRQHandler ; USB OTG HS
DCD DCMI_IRQHandler ; DCMI
DCD CRYP_IRQHandler ; CRYP crypto
DCD HASH_RNG_IRQHandler ; Hash and Rng
DCD FPU_IRQHandler ; FPU
__Vectors_End
当内核响应了一个发生的异常后,对应的异常服务例程 (ESR) 就会执行。为了决定 ESR 的入口地址,内核使用了“向量表查表机制”。这里使用一张向量表。
向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。
在复位后,该寄存器的值为 0。
因此,在地址 0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。
要注意的是这里有个另类:0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。
可以通过查询 《STM32F4xx中文参考手册》 的 234 页查询下表。
3.4.3 结束部分-代码解释
__Vectors_Size EQU __Vectors_End - __Vectors
AREA |.text|, CODE, READONLY
-
__Vectors_Size EQU __Vectors_End - __Vectors
计算向量表大小。 -
AREA |.text|, CODE, READONLY
定义一个可执行的可读代码区域。
3.4.4 指令解释
1、 EXPORT
-
功能:用于向链接器导出符号,使其可以在其他模块中访问。
-
示例:
EXPORT __Vectors
__Vectors
是中断向量表的起始地址,导出后可被外部引用。
2、 DCD
-
功能:定义 32 位数据并占用32位存储。
-
示例:
__Vectors DCD __initial_sp
__initial_sp
是栈顶指针的初始值。
3.5 复位程序
复位程序(Reset Handler)是系统上电或复位时执行的第一段代码,负责初始化系统环境,并最终跳转到主程序入口。
代码段
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
3.5.1 代码解释
1. 复位处理程序声明
Reset_Handler PROC
-
功能:声明一个名为
Reset_Handler
的过程(函数),作为复位中断服务程序。 -
复位入口:根据向量表定义,复位后 CPU 自动跳转到此地址执行。
2. 弱定义导出符号
EXPORT Reset_Handler [WEAK]
-
EXPORT
:将该符号导出,允许其他文件引用。 -
[WEAK]
:弱定义属性,若用户自定义同名函数,则优先使用用户版本。
3. 引入外部符号
IMPORT SystemInit
IMPORT __main
-
SystemInit
:系统初始化函数(通常由芯片厂商提供),用于配置时钟、内存控制器等。 -
__main
:C 标准库入口函数,负责初始化全局变量、堆栈等 C 语言运行环境。
4. 系统初始化调用
LDR R0, =SystemInit ; 加载 SystemInit 地址到 R0
BLX R0 ; 跳转执行 SystemInit
-
LDR
:将SystemInit
的函数地址加载到寄存器 R0。 -
BLX
:带链接(返回地址保存到 LR)的跳转指令,执行SystemInit
后返回。
5. 跳转至 C 环境
LDR R0, =__main ; 加载 __main 地址到 R0
BX R0 ; 跳转到 __main
ENDP
-
__main
:并非用户编写的main
函数,而是 C 库的初始化入口。 -
BX
:直接跳转指令,不保存返回地址(程序不再返回复位处理程序)。
3.5.2 指令解释
1. PROC
/ ENDP
-
功能:定义函数过程的开始(
PROC
)与结束(ENDP
)。 -
语法:
函数名 PROC ; 代码 函数名 ENDP
-
作用:明确函数范围,便于调试器和链接器处理。
2. EXPORT
-
功能:导出符号,使其他文件可通过该符号访问当前代码段。
-
弱属性(
[WEAK]
):允许用户覆盖默认实现。若用户未定义Reset_Handler
,则使用此默认版本。
3. IMPORT
-
功能:声明外部符号(如函数或变量),告知汇编器该符号在其他文件中定义。
-
示例:
-
SystemInit
:通常位于system_<芯片型号>.c
文件中。 -
__main
:由 C 标准库提供,负责初始化运行时环境。
-
4. LDR
-
功能:将内存地址或常量加载到寄存器。
-
语法:
LDR Rd, =Label
-
Rd
:目标寄存器(如 R0)。 -
Label
:要加载的符号地址(如SystemInit
)。
-
-
作用:此处用于获取函数入口地址。
5. BLX
-
功能:带链接(Link)的跳转指令,支持切换指令集(ARM/Thumb)。
-
行为:
-
将返回地址(下一条指令地址)存入
LR
寄存器。 -
跳转到目标地址执行。
-
用于函数调用(如
SystemInit
)。
-
6. BX
-
功能:跳转指令,支持切换指令集。
-
语法:
BX Rn
Rn
:寄存器,存储目标地址。
-
特点:不保存返回地址,用于单向跳转(如进入
__main
后不再返回)。
3.5.3 执行流程
-
复位触发:上电或复位后,CPU 从向量表第二个条目获取
Reset_Handler
地址。 -
硬件初始化:执行
SystemInit
,配置时钟、电源、内存控制器等。 -
C 环境初始化:跳转至
__main
,完成以下操作:-
将
.data
段从 FLASH 复制到 RAM(初始化全局变量)。 -
清零
.bss
段(未初始化的全局变量)。 -
调用用户
main()
函数。
-
-
程序运行:用户
main()
开始执行。
3.5.4 注意事项
1、__main
与 main
的区别:
符号 | 来源 | 功能 |
---|---|---|
__main | C 标准库 | 初始化运行时环境,调用用户 main |
main | 用户代码 | 用户程序入口 |
2、 自定义复位处理程序
-
若需修改初始化流程,可重新实现
Reset_Handler
函数(覆盖弱符号)。 -
示例:
void Reset_Handler(void) {
Custom_Init(); // 自定义初始化
SystemInit(); // 保留原有初始化
__main(); // 进入 C 环境
}
3、调试问题
- 若未实现 `SystemInit`,程序可能运行在默认低速时钟下。
- 若 `__main` 未正确初始化堆栈,可能导致 `HardFault`。
3.6 中断复位函数
中断复位函数用于处理异常和中断,确保系统能够在出现错误时执行适当的处理,防止程序崩溃。以下是相关的中断处理函数定义。
; Dummy Exception Handlers (infinite loops which can be modified)
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
Default_Handler PROC
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
EXPORT TAMP_STAMP_IRQHandler [WEAK]
EXPORT RTC_WKUP_IRQHandler [WEAK]
EXPORT FLASH_IRQHandler [WEAK]
EXPORT RCC_IRQHandler [WEAK]
EXPORT EXTI0_IRQHandler [WEAK]
EXPORT EXTI1_IRQHandler [WEAK]
EXPORT EXTI2_IRQHandler [WEAK]
EXPORT EXTI3_IRQHandler [WEAK]
EXPORT EXTI4_IRQHandler [WEAK]
EXPORT DMA1_Stream0_IRQHandler [WEAK]
EXPORT DMA1_Stream1_IRQHandler [WEAK]
EXPORT DMA1_Stream2_IRQHandler [WEAK]
EXPORT DMA1_Stream3_IRQHandler [WEAK]
EXPORT DMA1_Stream4_IRQHandler [WEAK]
EXPORT DMA1_Stream5_IRQHandler [WEAK]
EXPORT DMA1_Stream6_IRQHandler [WEAK]
EXPORT ADC_IRQHandler [WEAK]
EXPORT CAN1_TX_IRQHandler [WEAK]
EXPORT CAN1_RX0_IRQHandler [WEAK]
EXPORT CAN1_RX1_IRQHandler [WEAK]
EXPORT CAN1_SCE_IRQHandler [WEAK]
EXPORT EXTI9_5_IRQHandler [WEAK]
EXPORT TIM1_BRK_TIM9_IRQHandler [WEAK]
EXPORT TIM1_UP_TIM10_IRQHandler [WEAK]
EXPORT TIM1_TRG_COM_TIM11_IRQHandler [WEAK]
EXPORT TIM1_CC_IRQHandler [WEAK]
EXPORT TIM2_IRQHandler [WEAK]
EXPORT TIM3_IRQHandler [WEAK]
EXPORT TIM4_IRQHandler [WEAK]
EXPORT I2C1_EV_IRQHandler [WEAK]
EXPORT I2C1_ER_IRQHandler [WEAK]
EXPORT I2C2_EV_IRQHandler [WEAK]
EXPORT I2C2_ER_IRQHandler [WEAK]
EXPORT SPI1_IRQHandler [WEAK]
EXPORT SPI2_IRQHandler [WEAK]
EXPORT USART1_IRQHandler [WEAK]
EXPORT USART2_IRQHandler [WEAK]
EXPORT USART3_IRQHandler [WEAK]
EXPORT EXTI15_10_IRQHandler [WEAK]
EXPORT RTC_Alarm_IRQHandler [WEAK]
EXPORT OTG_FS_WKUP_IRQHandler [WEAK]
EXPORT TIM8_BRK_TIM12_IRQHandler [WEAK]
EXPORT TIM8_UP_TIM13_IRQHandler [WEAK]
EXPORT TIM8_TRG_COM_TIM14_IRQHandler [WEAK]
EXPORT TIM8_CC_IRQHandler [WEAK]
EXPORT DMA1_Stream7_IRQHandler [WEAK]
EXPORT FSMC_IRQHandler [WEAK]
EXPORT SDIO_IRQHandler [WEAK]
EXPORT TIM5_IRQHandler [WEAK]
EXPORT SPI3_IRQHandler [WEAK]
EXPORT UART4_IRQHandler [WEAK]
EXPORT UART5_IRQHandler [WEAK]
EXPORT TIM6_DAC_IRQHandler [WEAK]
EXPORT TIM7_IRQHandler [WEAK]
EXPORT DMA2_Stream0_IRQHandler [WEAK]
EXPORT DMA2_Stream1_IRQHandler [WEAK]
EXPORT DMA2_Stream2_IRQHandler [WEAK]
EXPORT DMA2_Stream3_IRQHandler [WEAK]
EXPORT DMA2_Stream4_IRQHandler [WEAK]
EXPORT ETH_IRQHandler [WEAK]
EXPORT ETH_WKUP_IRQHandler [WEAK]
EXPORT CAN2_TX_IRQHandler [WEAK]
EXPORT CAN2_RX0_IRQHandler [WEAK]
EXPORT CAN2_RX1_IRQHandler [WEAK]
EXPORT CAN2_SCE_IRQHandler [WEAK]
EXPORT OTG_FS_IRQHandler [WEAK]
EXPORT DMA2_Stream5_IRQHandler [WEAK]
EXPORT DMA2_Stream6_IRQHandler [WEAK]
EXPORT DMA2_Stream7_IRQHandler [WEAK]
EXPORT USART6_IRQHandler [WEAK]
EXPORT I2C3_EV_IRQHandler [WEAK]
EXPORT I2C3_ER_IRQHandler [WEAK]
EXPORT OTG_HS_EP1_OUT_IRQHandler [WEAK]
EXPORT OTG_HS_EP1_IN_IRQHandler [WEAK]
EXPORT OTG_HS_WKUP_IRQHandler [WEAK]
EXPORT OTG_HS_IRQHandler [WEAK]
EXPORT DCMI_IRQHandler [WEAK]
EXPORT CRYP_IRQHandler [WEAK]
EXPORT HASH_RNG_IRQHandler [WEAK]
EXPORT FPU_IRQHandler [WEAK]
WWDG_IRQHandler
PVD_IRQHandler
TAMP_STAMP_IRQHandler
RTC_WKUP_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
DMA1_Stream0_IRQHandler
DMA1_Stream1_IRQHandler
DMA1_Stream2_IRQHandler
DMA1_Stream3_IRQHandler
DMA1_Stream4_IRQHandler
DMA1_Stream5_IRQHandler
DMA1_Stream6_IRQHandler
ADC_IRQHandler
CAN1_TX_IRQHandler
CAN1_RX0_IRQHandler
CAN1_RX1_IRQHandler
CAN1_SCE_IRQHandler
EXTI9_5_IRQHandler
TIM1_BRK_TIM9_IRQHandler
TIM1_UP_TIM10_IRQHandler
TIM1_TRG_COM_TIM11_IRQHandler
TIM1_CC_IRQHandler
TIM2_IRQHandler
TIM3_IRQHandler
TIM4_IRQHandler
I2C1_EV_IRQHandler
I2C1_ER_IRQHandler
I2C2_EV_IRQHandler
I2C2_ER_IRQHandler
SPI1_IRQHandler
SPI2_IRQHandler
USART1_IRQHandler
USART2_IRQHandler
USART3_IRQHandler
EXTI15_10_IRQHandler
RTC_Alarm_IRQHandler
OTG_FS_WKUP_IRQHandler
TIM8_BRK_TIM12_IRQHandler
TIM8_UP_TIM13_IRQHandler
TIM8_TRG_COM_TIM14_IRQHandler
TIM8_CC_IRQHandler
DMA1_Stream7_IRQHandler
FSMC_IRQHandler
SDIO_IRQHandler
TIM5_IRQHandler
SPI3_IRQHandler
UART4_IRQHandler
UART5_IRQHandler
TIM6_DAC_IRQHandler
TIM7_IRQHandler
DMA2_Stream0_IRQHandler
DMA2_Stream1_IRQHandler
DMA2_Stream2_IRQHandler
DMA2_Stream3_IRQHandler
DMA2_Stream4_IRQHandler
ETH_IRQHandler
ETH_WKUP_IRQHandler
CAN2_TX_IRQHandler
CAN2_RX0_IRQHandler
CAN2_RX1_IRQHandler
CAN2_SCE_IRQHandler
OTG_FS_IRQHandler
DMA2_Stream5_IRQHandler
DMA2_Stream6_IRQHandler
DMA2_Stream7_IRQHandler
USART6_IRQHandler
I2C3_EV_IRQHandler
I2C3_ER_IRQHandler
OTG_HS_EP1_OUT_IRQHandler
OTG_HS_EP1_IN_IRQHandler
OTG_HS_WKUP_IRQHandler
OTG_HS_IRQHandler
DCMI_IRQHandler
CRYP_IRQHandler
HASH_RNG_IRQHandler
FPU_IRQHandler
B .
ENDP
ALIGN
; Dummy Exception Handlers (infinite loops which can be modified)
; 默认异常处理程序(可修改的无限循环)
; 系统异常处理程序
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
; ...其他系统异常处理程序(同上格式)
; 外部中断处理程序
Default_Handler PROC
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
; ...其他外部中断导出声明
B .
ENDP
; 外部中断处理程序别名定义
WWDG_IRQHandler
PVD_IRQHandler
; ...其他外部中断别名(共108个)
B .
ENDP
ALIGN
3.6.1 代码解释
1、系统异常默认处理
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
-
功能:定义不可屏蔽中断(NMI)的默认处理程序
-
结构特征:
-
使用
PROC/ENDP
定义函数体 -
[WEAK]
允许用户自定义同名函数覆盖 -
B .
表示无限循环(原地跳转)
-
2、外部中断统一处理
Default_Handler PROC
EXPORT WWDG_IRQHandler [WEAK]
; ...其他中断导出
B .
ENDP
-
设计原理:
-
所有外部中断共用同一个默认处理程序
-
通过别名机制(EQU)将各中断名指向同一地址
-
减少重复代码,节省ROM空间
-
3、 中断处理程序别名
WWDG_IRQHandler
PVD_IRQHandler
; ...其他中断名
-
等效关系:
-
每个中断名都是
Default_Handler
的别名 -
类似C语言的
#define WWDG_IRQHandler Default_Handler
-
4、 对齐控制
ALIGN
-
作用:确保后续代码按指定对齐方式
-
必要性:满足ARM架构的指令对齐要求
3.6.2 指令解释
1、 PROC/ENDP
-
功能:定义函数作用域 开始和结束。
-
语法:
函数名 PROC ; 代码 函数名 ENDP
-
特点:在汇编层面明确函数边界
2、 EXPORT [WEAK]
- 弱定义特性:
使用场景 | 行为 |
---|---|
用户未定义处理程序 | 使用默认无限循环 |
用户定义同名函数 | 自动替换为自定义实现 |
3、 B .
-
机器码:0xE7FE(Thumb指令集)
-
执行效果:
B
:无条件跳转指令.
:表示当前地址(形成死循环)
-
调试意义:触发该中断时程序挂起,便于定位未处理的中断
3.7用户堆栈初始
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
IF :DEF:__MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF
END
;************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE*****
3.7 用户堆栈初始化
代码段
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
IF :DEF:__MICROLIB ; 条件编译判断是否使用MICROLIB
EXPORT __initial_sp ; 导出栈顶指针
EXPORT __heap_base ; 导出堆起始地址
EXPORT __heap_limit ; 导出堆结束地址
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 ; 结束条件编译
END ; 文件结束
3.7 用户堆栈初始化
代码段
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
IF :DEF:__MICROLIB ; 条件编译判断是否使用MICROLIB
EXPORT __initial_sp ; 导出栈顶指针
EXPORT __heap_base ; 导出堆起始地址
EXPORT __heap_limit ; 导出堆结束地址
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 ; 结束条件编译
END ; 文件结束
3.7.1 代码解释
1、 MICROLIB模式(简化C库)
IF :DEF:__MICROLIB
EXPORT __initial_sp ; 栈顶地址(由启动文件已定义)
EXPORT __heap_base ; 堆起始地址(HEAP段起始)
EXPORT __heap_limit ; 堆结束地址(HEAP段结束)
适用场景:使用ARM精简库时(通过MDK勾选Use MicroLIB
启用)
特点:
-
不需要用户手动初始化堆栈
-
堆栈范围由链接脚本自动管理
-
仅需导出预定义符号供库函数使用
小谭老师的提醒:一定要记得勾选
Use MicroLIB
否则容易出现调试中进入硬件错误的问题,因为__MICROLIB
这个宏是定义在Use MicroLIB
中。
2、 标准C库模式
ELSE
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 ; 返回
寄存器参数约定:
寄存器 | 含义 | 用途 |
---|---|---|
R0 | 堆起始地址(heap_base) | 供malloc等函数使用 |
R1 | 栈结束地址(栈底) | 用于栈溢出检测 |
R2 | 堆结束地址(heap_limit) | 限制堆扩展边界 |
R3 | 栈起始地址(栈顶) | 初始化栈指针 |
- 双区内存模型:
- 栈和堆使用独立的内存区域
- 栈向下生长(高地址→低地址)
- 堆向上生长(低地址→高地址)
3、 通用控制指令
ALIGN ; 确保地址按4字节对齐
END ; 结束汇编文件
3.7.2 指令解释
1、 条件编译指令
指令 | 功能 | 等效C语法 |
---|---|---|
IF :DEF:XXX | 判断符号XXX是否已定义 | #ifdef XXX |
ELSE | 条件分支 | #else |
ENDIF | 结束条件编译块 | #endif |
2、 IMPORT/EXPORT
-
IMPORT __use_two_region_memory
:-
声明使用标准C库的双区内存管理模式
-
必须与
__user_initial_stackheap
配合使用
-
-
EXPORT __user_initial_stackheap
:-
标准C库要求的初始化函数
-
在程序启动时由C库自动调用
-
3、 LDR
地址加载
-
语法变体:
LDR Rn, =Label ; 加载Label的32位地址到寄存器 LDR Rn, [Rm] ; 从Rm指向的内存加载数据
-
代码中的用途:
-
将预定义的堆栈符号地址加载到指定寄存器
-
地址值由链接阶段确定(参见启动文件中的
HEAP
/STACK
定义)
-
4、 BX LR
-
功能:跳转到链接寄存器存储的地址(函数返回)
-
特点:
-
不进行栈操作(与
BL
不同) -
此处用于传递堆栈参数后立即返回
-
3.7.3 内存布局示意图
graph TD
RAM起始地址 --> Stack_Mem
Stack_Mem --> |Stack_Size| Stack_Mem+Stack_Size
Stack_Mem+Stack_Size --> Heap_Mem
Heap_Mem --> |Heap_Size| Heap_Mem+Heap_Size
Heap_Mem+Heap_Size --> RAM结束地址
classDef stack fill:#f9d,stroke:#333;
classDef heap fill:#8df,stroke:#333;
class Stack_Mem,Stack_Mem+Stack_Size stack
class Heap_Mem,Heap_Mem+Heap_Size heap
3.7.4 注意事项
1、 堆栈溢出检测
-
标准库会根据R1/R3检测栈溢出(若
SP
超过R3) -
堆分配超过R2会导致
malloc
返回NULL
2、 内存对齐要求
区域 | 最小对齐 | 实现方式 |
---|---|---|
栈顶 | 8字节 | 启动文件中的ALIGN=3 |
堆起始 | 4字节 | 链接脚本控制 |
3、常见错误
-
未实现
__user_initial_stackheap
:链接时报undefined symbol
错误 -
堆栈区域重叠:导致数据覆盖,需在链接脚本中检查
HEAP
/STACK
定义
4、性能优化
-
MICROLIB模式节省约2KB代码空间,但缺失部分标准库功能
-
双区模式适合需要独立堆栈保护的应用
5、 调试方法:
-
在
__user_initial_stackheap
设置断点,观察寄存器值 -
使用
__heap_limit
和__initial_sp
监控堆栈使用情况
- 感谢你赐予我前进的力量