第八章 STM32启动过程分析
本章给大家分析STM32H7RS的启动过程,这里的启动过程是指从STM32芯片上电复位执行的第一条指令开始,到执行用户编写的main函数这之间的过程。我们编写程序,基本都是用C语言编写,并且以main函数作为程序的入口。但是事实上,main函数并非最 先执行的,在此之前需要做一些准备工作,准备工作通过启动文件的程序来完成。理解STM32启动过程,对今后的学习和分析STM32程序有很大的帮助。
注意:学习本章内容之前,请大家最好先阅读由正点原子团队编写的《STM32 启动文件浅析_V1.2.pdf》和《STM32 MAP文件浅析_V1.1.pdf》这两份文档(路径:A 盘→1,入门资料)。
本章将分为如下几个小节:
8.1 启动模式
8.2 启动文件分析
8.3 map文件分析
我们知道的复位方式有三种:上电复位,硬件复位和软件复位。当产生复位,并且离开复位状态后,CM7内核做的第一件事就是读取下列两个32位整数的值:
(1)从地址 0x0000 0000 处取出堆栈指针MSP 的初始值,该值就是栈顶地址。
(2)从地址 0x0000 0004 处取出程序计数器指针PC 的初始值,该值指向复位后执行的第一条指令。下面用示意图表示,如图8.1.1所示:

图8.1.1 复位序列
上述过程中,内核是从0x0000 0000和0x0000 0004两个的地址获取堆栈指针SP和程序计数器指针PC。事实上,0x0000 0000和0x0000 0004两个的地址可以被重映射到其他的地址空间。例如:我们将0x0800 0000映射到 0x0000 0000,即从内部FLASH启动,那么内核会从地址0x0800 0000处取出堆栈指针MSP 的初始值,从地址0x0800 0004处取出程序计数器指针PC 的初始值。CPU 会从 PC 寄存器指向的地址空间取出的第1条指令开始执行程序,就是开始执行复位中断服务程序 Reset_Handler。
将0x0000 0000和0x0000 0004两个的地址重映射到其他地址空间,就是启动模式选择。
对于STM32H7RS的启动模式(也称自举模式),我们看表8.1.1进行分析。

表8.1.1 启动模式选择表
注:启动引脚(BOOT)的电平:0:低电平;1:高电平。
由表8.1.1可以看到,STM32H7RS的启动模式选择需要一个BOOT0引脚,这个BOOT0引脚对应了0和1两个状态,这两个状态都有自己对应的启动地址选项字节,用于选择自己的启动地址。下面分别来介绍一下:
1, 产品状态和芯片的运行状态都为Open的情况下,BOOT0引脚电平为低时,启动地址为ST出厂默认的启动地址:0x0800 0000,这个情况就是我们最常用的了。
2, 产品状态和芯片的运行状态都为Open的情况下,BOOT0引脚电平为高时,启动地址为ST官方的BootLoader程序开始启动。
3, 产品状态和芯片运行状态都为Closed/Locked的情况下,BOOT0引脚不起作用,当定时器的设置不为0xB4或者不是H7R的情况,程序从RSS(根安全服务)启动,ST出厂默认的启动地址是0x0800 0000。
4, 当产品处于关闭或锁定状态,BOOT0引脚不起作用,若定时器的设置为0xB4时,程序从RSS(根安全服务)启动。
STM32启动文件由ST官方提供,在官方的STM32Cube固件包里,对于STM32H7R7系列芯片的启动文件,我们选用的是startup_stm32h7r7xx.s这个文件。启动文件用汇编编写,是系统上电复位后第一个执行的程序。
启动文件主要做了以下工作:
1、初始化堆栈指针 SP = _initial_sp
2、初始化程序计数器指针PC = Reset_Handler
3、设置堆和栈的大小
4、初始化中断向量表
5、配置外部SRAM作为数据存储器(可选)
6、配置系统时钟,通过调用SystemInit函数(可选)
7、调用 C库中的 _main 函数初始化用户堆栈,最终调用 main 函数

表8.2.1.1 启动文件的汇编指令
上表,列举了STM32启动文件的一些汇编和编译器指令,关于其他更多的ARM汇编指令,我们可以通过MDK的索引搜索工具中搜索找到。打开索引搜索工具的方法:MDK->Help->uVision Help,如图8.2.1.1所示:

图8.2.1.1打开索引搜索工具的方法
打开之后,我们以EQU为例,演示一下怎么使用,如图8.2.1.2所示:

图8.2.1.2 搜索EQU汇编指令
搜索到的标题有很多,我们只需要看EQU directive这部分即可。
(1)栈空间的开辟
栈空间的开辟,源码如图8.2.2.1所示:

图8.2.2.1 栈空间的开辟
源码含义:开辟一段大小为0x0000 1000(4KB)的栈空间,段名为STACK,NOINIT 表示不初始化; READWRITE 表示可读可写;ALIGN=3,表示按照 2^3对齐,即 8 字节对齐。
AREA汇编一个新的代码段或者数据段。
SPACE分配内存指令,分配大小为Stack_Size字节连续的存储单元给栈空间。
__initial_sp紧挨着SPACE放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。
栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改Stack_Size的值。如果程序出现了莫名其妙的错误,并进入了HardFault的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。
(2)堆空间的开辟
堆空间的开辟,源码如图8.2.2.2所示:

图8.2.2.4 为中断向量表定义一个数据段
源码含义:定义一个数据段,名字为RESET, READONLY表示只读。EXPORT表示声明一个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End和__Vectors_Size三个标号具有全局性,可被外部的文件使用。
STM32H7R7的中断向量表定义代码,如图8.2.2.5所示。

图8.2.2.3 PRESERVE8和THUMB指令
PRESERVE8:指示编译器按照8字节对齐。
THUMB:指示编译器之后的指令为THUMB指令。
注意:由于正点原子提供了独立的内存管理实现方式(mymalloc,myfree等),并不需要使用C库的malloc和free等函数,也就用不到堆空间,因此我们可以设置Heap_Size的大小为0,以节省内存空间。
(3)中断向量表定义(简称:向量表)
为中断向量表定义一个数据段,如图8.2.2.4所示:

图8.2.2.4 为中断向量表定义一个数据段
源码含义:定义一个数据段,名字为RESET, READONLY表示只读。EXPORT表示声明一个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End和__Vectors_Size三个标号具有全局性,可被外部的文件使用。
STM32H7R7的中断向量表定义代码,如图8.2.2.5所示。

图8.2.2.5中断向量表定义代码
__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,__Vectors_Size为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
中断向量表被放置在代码段的最前面。例如:当我们的程序在FLASH运行时,那么向量表的起始地址是:0x0800 0000。结合图8.2.2.5可以知道,地址0x0800 0000存放的是栈顶地址。DCD:以四字节对齐分配内存,也就是下个地址是0x0800 0004,存放的是Reset_Handler中断函数入口地址。
从代码上看,向量表中存放的都是中断服务函数的函数名,所以 C 语言中的函数名对芯片来说实际上就是一个地址。
STM32H7R7的中断向量表可以在《STM32H7Rx参考手册_V6(英文版).pdf》的第19章的19.1.2小节找到,与中断向量表定义代码是对应的。
(4)复位程序
接下来是定义只读代码段,如图8.2.2.6所示:

图8.2.2.6 定义只读代码段
定义一个段命为.text,只读的代码段,在CODE区。
复位子程序代码,如图8.2.2.7所示:

图8.2.2.7 复位子程序代码
利用PROC、ENDP这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
复位子程序是复位后第一个被执行的程序,主要是调用SystemInit函数配置系统时钟、还有就是初始化FSMC/FMC总线上外挂的SRAM(可选)。然后在调用C 库函数__main,最终调用 main 函数去到 C 的世界。
EXPORT声明复位中断向量Reset_Handler为全局属性,这样外部文件就可以调用此复位中断服务。
WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。
IMPORT表示该标号来自外部文件。这里表示SystemInit 和__main 这两个函数均来自外部的文件。
LDR、BLX、BX 是内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到。
LDR表示从存储器中加载字到一个存储器中。
BLX表示跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR。
BX表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main地址,最终调用main函数,不返回,进入C的世界。
(5)中断服务程序
接下来就是中断服务程序了,如图8.2.2.8所示:

图8.2.2.8 中断服务程序
可以看到这些中断服务函数都被[WEAK]声明为弱定义函数,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错。
这些中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。B指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环。
在启动文件代码中,已经把我们所有中断的中断服务函数写好了,但都是声明为弱定义,所以真正的中断服务函数需要我们在外部实现。
如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在B指令作用下跳转到一个‘.’中,无限循环。
这里的系统异常中断部分是内核的,外部中断部分是外设的。
(6)用户堆栈初始化
ALIGN指令,如图8.2.2.9所示:

图8.2.2.9 ALIGN指令
ALIGN表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4字节对齐。要注意的是,这个不是ARM的指令,是编译器的。
接下就是启动文件最后一部分代码,用户堆栈初始化代码,如图8.2.2.10所示:

图8.2.2.10 用户堆栈初始化代码
IF, ELSE, ENDIF是汇编的条件分支语句。
587行判断是否定义了__MICROLIB。关于__MICROLIB这个宏定义,我们是在Keil里面配置,具体方法如图8.2.2.11所示:

图8.2.2.11 __MICROLIB定义方法
勾选了Use MicroLIB就代表定义了__MICROLIB这个宏。
如果定义__MICROLIB,声明__initial_sp、__heap_base和__heap_limit这三个标号具有全局属性,可被外部的文件使用。__initial_sp表示栈顶地址,__heap_base表示堆起始地址,__heap_limit表示堆结束地址。
如果没有定义__MICROLIB,实际的情况就是我们没有定义__MICROLIB,所以使用默认的C库运行。那么堆栈的初始化由C库函数__main来完成。
IMPORT声明__use_two_region_memory标号来自外部文件。
EXPORT声明__user_initial_stackheap具有全局属性,可被外部的文件使用。
598行标号__user_initial_stackheap,表示用户堆栈初始化程序入口。
接下来进行堆栈空间初始化,堆是从低到高生长,栈是从高到低生长,是两个互相独立的数据段,并且不能交叉使用。
600堆起始地址。
601栈大小。
602堆大小。
603栈顶指针。
604到LR标号给出的地址,不用返回。
610的END表示到达文件的末尾,文件结束。
Use MicroLIB
MicroLIB是MDK自带的微库,是缺省C库的备选库,MicroLIB进行了高度优化使得其代码变得很小,功能比缺省C库少。MicroLIB是没有源码的,只有库。
关于MicroLIB更多知识可以看官方介绍 http://www.keil.com/arm/microlib.asp 。
我们知道启动模式不同,启动的起始地址是不一样的,下面我们以代码下载到内部FLASH的情况举例,即代码从地址0x0800 0000开始被执行。
当产生复位,并且离开复位状态后,CM7核做的第一件事就是读取下列两个32位整数的值:
(1)从地址 0x0800 0000 处取出堆栈指针MSP 的初始值,该值就是栈顶地址。
(2)从地址 0x0800 0004 处取出程序计数器指针PC 的初始值,该值指向中断服务程序 Reset_Handler。下面用示意图表示,如图8.2.3.1所示:

图8.2.3.1 复位序列
我们看看H7R7 HAL库例程的实验1 跑马灯实验中,取出的MPS和PC的值是多少,方法如图8.2.3.2所示:

图8.2.3.2 取出的MPS和PC的值
由图8.2.3.2可以知道地址0x0800 0000的值是0x2000 1218,地址0x0800 0004的值是0x0800 02C5,即堆栈指针 SP =0x2000 1218,程序计数器指针PC =0x0x0800 02C5(即复位中断服务程序Reset_Handler的入口地址),即。因为CM7核是小端模式,所以倒着读。
请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。而在 CM3内核中,0 地址处提供 MSP 的初始值,然后就是向量表(向量表在以后还可以被移至其它位置)。向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是Reset_Handler这个函数。下面继续以正点原子STM32H7R7开发板HAL库例程的实验1跑马灯实验为例,代码从地址0x0800 0000开始被执行,讲解一下系统启动,初始化堆栈、MSP和PC后的内存情况。

图8.2.3.3 初始化堆栈、MSP和PC后的内存情况
因为CM7使用的是向下生长的满栈,所以MSP的初始值必须是堆栈内存的末地址加1。
举例来说,如果你的栈区域在 0x2000 0E18‐0x2000 1217 (2KB大小)之间,那么 MSP 的初始值就必须是0x2000 1218。
向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。
R15是程序计数器,在汇编代码中,可以使用名字“PC”来访问它。ARM规定:PC最低两位并不表示真实地址,最低位LSB用于表示是ARM指令(0)还是Thumb指令(1),因为 CM3主要执行Thumb指令,所以这些指令的最低位都是1(都是奇数)。因为CM3内部使用了指令流水线,读 PC 时返回的值是当前指令的地址+4。比如说:
0x1000 : MOV R0, PC ; R0 = 0x1004
如果向 PC 写数据,就会引起一次程序的分支(但是不更新 LR 寄存器)。CM3 中的指令至少是半字对齐的,所以 PC 的 LSB 总是读回 0。然而,在分支时,无论是直接写 PC 的值还是使用分支指令,都必须保证加载到 PC 的数值是奇数(即 LSB=1),表明是在Thumb 状态下执行。倘若写了0,则视为转入 ARM 模式,CM7将产生一个fault异常。
正因为上述原因,图8.2.3.3中使用0x0800 02C5来表达地址 0x0800 02C4。当0x0800 02C4 处的指令得到执行后,就正式开始了程序的执行(即去到C的世界)。所以在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没执行就会被 NMI 或是其它 fault 打断。MSP 初始化好后就已经为它们的服务例程准备好了堆栈。
STM32启动文件分析就给大家介绍到这里,更多内容请看 《STM32 启动文件浅析_V1.1》 。
MDK 编译工程,会生成一些中间文件(如.o、.axf、.map 等),最终生成 hex 文件,以便下载到 MCU 上面执行,以STM32H7R7开发板HAL库例程的实验1 跑马灯实验为例(其他开发板类似),编译过程产生的所有文件,都存放在 Output文件夹下,如图 8.3.1.1 所示:

图 8.3.1.1 MDK编译过程生成的文件
可以看到,这里总共生成了 48个文件,共 11 个类型,分别是:.axf、.crf、.d、.dep、
.hex、.lnp、.lst、.o、.htm、bulild_log.htm 和.map。48个文件看着不是很多,但是随着工程的增大,这些文件也会越来越多,大项目编译一次,可以生成几百甚至上千个这种文件,不过文件类型基本就是上面这些。
对于 MDK 工程来说,基本上任何工程在编译过程中都会有这 11 类文件,常见的 MDK编译过程生产文件类型如表8.3.1.1所示:

表 8.3.1.1 常见的中间文件类型说明
注 1,可重定向是指该文件包含数据/代码,但是并没有指定地址,它的地址可由后续链接的时候进行指定。
注 2,不可重定向是指该文件所包含的数据/代码都已经指定地址了,不能再改变。
.map 文件是编译器链接时生成的一个文件,它主要包含了交叉链接信息。通过.map 文件,我们可以知道整个工程的函数调用关系、FLASH 和 RAM 占用情况及其详细汇总信息,能具体到单个源文件(.c/.s)的占用情况,根据这些信息,我们可以对代码进行优化。.map 文件可以分为以下 5 个组成部分:
1, 程序段交叉引用关系(Section Cross References)
2, 删除映像未使用的程序段(Removing Unused input sections from the image)
3, 映像符号表(Image Symbol Table)
4, 映像内存分布图(Memory Map of the image)
5, 映像组件大小(Image component sizes)
要生成 map 文件,我们需要在 MDK 的魔术棒→Listing 选项卡里面,进行相关设置,如图 8.3.2.1.1所示:

图 8.3.2.1.1 .map 文件生成设置
图 8.3.2.1.1中红框框出的部分就是我们需要设置的,默认情况下,MDK 这部分设置就是全勾选的,如果我们想取消掉一些信息的输出,则取消相关勾选即可(一般不建议)。
如图8.3.2.1.1设置好MDK以后,全编译当前工程,当编译完成后(无错误),就会生成.map文件。在 MDK 里面打开.map 文件的方法如图 8.3.2.1.2所示:

图8.3.2.1.2 打开.map 文件
①,先确保工程编译成功(无错误)。
②,双击atk_h7r7,打开.map 文件。
③,map 文件打开成功。
为了更好的分析 map 文件,我们先对需要用到的一些基础概念进行一个简单介绍,相关概念如下:
l Section:描述映像文件的代码或数据块,我们简称程序段
l RO:Read Only 的缩写,包括只读数据(RO data)和代码(RO code)两部分内容,占用 FLASH 空间
l RW:Read Write 的缩写,包含可读写数据(RW data,有初值,且不为 0),占用 FLASH(存储初值)和 RAM(读写操作)
l ZI:Zero initialized 的缩写,包含初始化为 0 的数据(ZI data),占用 RAM 空间
l .text:相当于 RO code
l .constdata:相当于 RO data
l .bss:相当于 ZI data
l .data:相当于 RW data
我们前面说map 文件分为 5 个部分组成,下面以STM32H7R7开发板HAL库例程的实验1 跑马灯实验为例,简要讲解一下。
1. 程序段交叉引用关系(S S ection Cross References s )
这部分内容描述了各个文件(.c/.s 等)之间函数(程序段)的调用关系,举个例子如图8.3.2.3.1所示:

图8.3.2.3.1 程序段交叉引用关系图
上图中,框出部分: main.o(text.main) refers to sys.o(text.sys_stm32_clock_init) for sys_stm32_clock_init表示:main.c 文件中的 main 函数,调用了 sys.c 中的 sys_stm32_clock_init 函数。其中:text.main表示 main 函数的入口地址,同理 text. sys_stm32_clock_init 表示 sys_stm32_clock_init函数的入口地址。
2. 删除映像未使用的程序段(Removing Unused input sections from the image)
这部分内容描述了工程中由于未被调用而被删除的冗余程序段(函数/数据),如图
8.3.2.3.2 所示:

图8.3.2.3.2 删除未用到的程序段
上图中,列出了所有被移除的程序段,比如 usart.c 里面的 usart_init 函数就被移除了,因为该例程没用到 usart_init 函数。
另外,在最后还有一个统计信息:361 unused section(s) (total 43234 bytes) removed from the image. 表示总共移除了361个程序段(函数/数据),大小为43234字节。即给我们的 MCU 节省了 43234 字节的程序空间。
为了更好的节省空间,我们一般在 MDK→魔术棒→C/C++(AC6)选项卡里面勾选:One ELF Section per Function,如图 8.3.2.3.3所示:

图8.3.2.3.3 MDK 勾选 One ELF Section per Function
3. 映像符号表(Image Symbol Table)
映像符号表(Image Symbol Table)描述了被引用的各个符号(程序段/数据)在存储器中的存储地址、类型、大小等信息。映像符号表分为两类:本地符号(Local Symbols)和全局符号(Global Symbols)。
本地符号(Local Symbols)记录了用 static 声明的全局变量地址和大小,c 文件中函数的地址和用 static 声明的函数代码大小,汇编文件中的标号地址(作用域:限本文件)。
全局符号(Global Symbols)记录了全局变量的地址和大小,C 文件中函数的地址及其代码大小,汇编文件中的标号地址(作用域:全工程)。
4. 映像内存分布图(Memory Map of the image)
映像文件分为加载域(Load Region)和运行域(Execution Region),一个加载域必须有至少一个运行域(可以有多个运行域),而一个程序又可以有多个加载域。加载域为映像程序的实际存储区域,而运行域则是 MCU 上电后的运行状态。加载域和运行域的简化关系(这里仅表示一个加载域的情况)图,如图8.3.2.3.4所示:

图8.3.2.3.4 加载域运行域关系
由图可知,RW 区也是存放在 ROM(FLASH)里面的,在执行 main 函数之前,RW(有初值且不为 0 的变量)数据会被拷贝到 RAM 区,同时还会在 RAM 里面创建 ZI 区(初始化为 0 的变量)。
5. 映像组件大小(Image component sizes)
映像组件大小(Image component sizes)给出了整个映像所有代码(.o)占用空间的汇总信息。
由于篇幅较长,更多内容请大家查阅 《STM32 MAP文件浅析_V1.1.pdf》文档的内容。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3092312/ ,如需转载,请注明出处,否则将追究法律责任。