OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 117|回复: 0

《STM32F103 精英开发指南V1.3》第九章 STM32启动过程分析

[复制链接]

1088

主题

1099

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4514
金钱
4514
注册时间
2019-5-8
在线时间
1204 小时
发表于 2024-6-14 09:39:43 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2024-6-14 09:39 编辑

第九章 STM32启动过程分析
1)实验平台:正点原子 精英STM32F103开发板

2) 章节摘自【正点原子】STM32F103开发指南 V1.3


4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boar ... 103_jingyingV2.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)正点原子STM32技术交流QQ群:672399978

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

本章给大家分析STM32F1的启动过程,这里的启动过程是指从STM32芯片上电复位执行的第一条指令开始,到执行用户编写的main函数这之间的过程。我们编写程序,基本都是用C语言编写,并且以main函数作为程序的入口。但是事实上,main函数并非最先执行的,在此之前需要做一些准备工作,准备工作通过启动文件的程序来完成。理解STM32启动过程,对今后的学习和分析STM32程序有很大的帮助。
注意:学习本章内容之前,请大家最好先阅读由正点原子团队编写的《STM32 启动文件浅析》和《MAP文件浅析》这两份文档(路径:Aà1,入门资料)。
本章将分为如下几个小节:
9.1 启动模式
9.2 启动文件分析
9.3 map文件分析

9.1 启动模式
我们知道的复位方式有三种:上电复位,硬件复位和软件复位。当产生复位,并且离开复位状态后,CM3内核做的第一件事就是读取下列两个32位整数的值:
(1)从地址 0x0000 0000 处取出堆栈指针MSP 的初始值,该值就是栈顶地址。
(2)从地址 0x0000 0004 处取出程序计数器指针PC 的初始值,该值指向复位后执行的第一条指令。下面用示意图表示,如图9.1.1所示。
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image002.jpg
image001.png
图9.1.1 复位序列
上述过程中,内核是从0x0000 0000和0x0000 0004两个的地址获取堆栈指针SP和程序计数器指针PC。事实上,0x0000 0000和0x0000 0004两个的地址可以被重映射到其他的地址空间。例如:我们将0x08000000映射到 0x00000000,即从内部FLASH启动,那么内核会从地址0x0800 0000处取出堆栈指针MSP 的初始值,从地址0x0800 0004处取出程序计数器指针PC的初始值。CPU会从PC寄存器指向的地址空间取出的第1条指令开始执行程序,就是开始执行复位中断服务程序Reset_Handler。将0x0000 0000和0x0000 0004两个地址重映射到其他的地址空间,就是启动模式选择。
对于STM32F1的启动模式(也称自举模式),我们看表9.1.1进行分析。
QQ截图20240613175301.png
表9.1.1 启动模式选择表
注:启动引脚的电平:0:低电平;1:高电平;x:任意电平,即高低电平均可
由表9.1.1可以看到,STM32F1根据BOOT引脚的电平选择启动模式,这两个BOOT引脚根据外部施加的电平来决定芯片的启动地址。(0和1的准确电平范围可以查看F103系列数据手册I/O特性表,但我们最好是设置成GND和VDD的电平值)
(1)内部FLASH启动方式
当芯片上电后采样到BOOT0引脚为低电平时,0x00000000和0x00000004地址被映射到内部FLASH的首地址0x08000000和0x08000004。因此,内核离开复位状态后,读取内部FLASH的0x08000000地址空间存储的内容,赋值给栈指针MSP,作为栈顶地址,再读取内部FLASH的0x08000004地址空间存储的内容,赋值给程序指针PC,作为将要执行的第一条指令所在的地址。完成这两个操作后,内核就可以开始从PC指向的地址中读取指令执行了。
(2)内部SRAM启动方式
类似于内部Flash,当芯片上电后采样到BOOT0和BOOT1引脚均为高电平时,地址0x00000000和0x00000004被映射到内部SRAM的首地址0x20000000和0x20000004,内核从SRAM空间获取内容进行自举。在实际应用中,由启动文件starttup_stm32f103xe.s决定了0x00000000和0x00000004地址存储什么内容,链接时,由分散加载文件(sct)决定这些内容的绝对地址,即分配到内部FLASH还是内部SRAM。
(3)系统存储器启动方式
当芯片上电后采样到 BOOT0 =1,BOOT1=0的组合时,内核将从系统存储器的0x1FFFF000及0x1FFFF004获取MSP及PC值进行自举。系统存储器是一段特殊的空间,用户不能访问,ST公司在芯片出厂前就在系统存储器中固化了一段代码。因而使用系统存储器启动方式时,内
9.2.1 启动文件中的一些指令
QQ截图20240613175327.png
表9.2.1.1 启动文件的汇编指令
上表,列举了STM32启动文件的一些汇编和编译器指令,关于其他更多的ARM汇编指令,我们可以通过MDK的索引搜索工具中搜索找到。打开索引搜索工具的方法:MDK->Help->uVision Help,如图9.2.1.1所示。
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image004.jpg
image003.png
图9.2.1.1打开索引搜索工具的方法
打开之后,我们以EQU为例,演示一下怎么使用,如图9.2.1.2所示。
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image006.jpg
image005.png
图9.2.1.2 搜索EQU汇编指令
搜索到的结果有很多,我们只需要看位置为Assembler User Guide这部分即可。
9.2.2 启动文件代码讲解
       注意:下面的图是经过优化截取,所以后面提及到的行号,请大家对照源文件进行查看。
(1)栈空间的开辟
栈空间的开辟,源码如图9.2.2.1所示:
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image008.jpg
image007.png
图9.2.2.1 栈空间的开辟
源码含义:开辟一段大小为0x0000 0400(1KB)的栈空间,段名为STACK,NOINIT 表示不初始化; READWRITE 表示可读可写;ALIGN=3,表示按照 2^3对齐,即 8 字节对齐。
AREA汇编一个新的代码段或者数据段。
SPACE分配内存指令,分配大小为Stack_Size字节连续的存储单元给栈空间。
__initial_sp紧挨着SPACE放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。
栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改Stack_Size的值。如果程序出现了莫名其妙的错误,并进入了HardFault的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。
(2)堆空间的开辟
堆空间的开辟,源码如图9.2.2.2所示:
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image010.jpg
image009.png
图9.2.2.2堆空间的开辟
源码含义:开辟一段大小为0x00000200(512字节)的堆空间,段名为HEAP,不初始化,可读可写,8字节对齐。
__heap_base表示堆的起始地址,__heap_limit表示堆的结束地址。堆和栈的生长方向相反的,堆是由低向高生长,而栈是从高往低生长。
堆主要用于动态内存的分配,像malloc()、calloc()和realloc()等函数申请的内存就在堆上面。堆中的内存一般由程序员分配和释放,程序员不释放,程序结束时可能由操作系统回收。
接下来是PRESERVE8和THUMB指令两行代码。如图9.2.2.3所示。
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image012.jpg
image011.png
图9.2.2.3PRESERVE8和THUMB指令
PRESERVE8:指示编译器按照8字节对齐。
THUMB:指示编译器之后的指令为THUMB指令。
注意:由于正点原子提供了独立的内存管理实现方式(mymalloc,myfree等),并不需要使用C库的malloc和free等函数,也就用不到堆空间,因此我们可以设置Heap_Size的大小为0,以节省内存空间。
(3)中断向量表定义(简称:向量表)
为中断向量表定义一个数据段,如图9.2.2.4所示:
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image014.jpg
image013.png
图9.2.2.4 为中断向量表定义一个数据段
源码含义:定义一个数据段,名字为RESET, READONLY表示只读。EXPORT表示声明一个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End和__Vectors_Size三个标号具有全局性,可被外部的文件使用。
STM32F103的中断向量表定义代码,如图9.2.2.5所示。
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image016.jpg
image015.png
图9.2.2.5中断向量表定义代码
__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,__Vectors_Size为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
中断向量表被放置在代码段的最前面。例如:当我们的程序在FLASH运行时,那么向量表的起始地址是:0x0800 0000。结合图9.2.2.5可以知道,地址0x0800 0000存放的是栈顶地址。DCD以四字节对齐分配内存,也就是下个地址是0x0800 0004,存放的是Reset_Handler中断函数入口地址。
从代码上看,向量表中存放的都是中断服务函数的函数名,所以C语言中的函数名对芯片来说实际上就是一个地址。
STM32F103的中断向量表可以在《STM32F10xxx参考手册_V10(中文版).pdf》的第9章的9.1.2小节找到,与中断向量表定义代码是对应的。
(4)复位程序
接下来是定义只读代码段,如图9.2.2.6所示:
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image018.jpg
image017.png
图9.2.2.6 定义只读代码段
定义一个段命为.text,只读的代码段,在CODE区。
复位子程序代码,如图9.2.2.7所示:
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image020.jpg
image019.png
图9.2.2.7 复位子程序代码
利用PROC、ENDP这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
复位子程序是复位后第一个被执行的程序,主要是调用SystemInit函数配置系统时钟、还有就是初始化FSMC/FMC总线上外挂的SRAM(可选)。然后在调用C库函数__main,最终调用main函数去到C的世界。
EXPORT声明复位中断向量Reset_Handler为全局属性,这样外部文件就可以调用此复位中断服务。
WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。
IMPORT表示该标号来自外部文件。这里表示SystemInit和__main这两个函数均来自外部的文件。
LDR、BLX、BX是内核指令,可在《Cortex-M3权威指南》第四章-指令集里面查询到。
LDR表示从存储器中加载字到一个存储器中。
BLX表示跳转到由寄存器给出的地址,并根据寄存器的LSE确定处理器的状态,还要把跳转前的下条指令地址保存到LR。
BX表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main地址,最终调用main函数,不返回,进入C的世界。
(5)中断服务程序
接下来就是中断服务程序了,如图9.2.2.8所示。
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image022.jpg
image021.png
图9.2.2.8 中断服务程序
可以看到这些中断服务函数都被[WEAK]声明为弱定义函数,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错。
这些中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。B指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环。
在启动文件代码中,已经把我们所有中断的中断服务函数写好了,但都是声明为弱定义,所以真正的中断服务函数需要我们在外部实现。
如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在B指令作用下跳转到一个‘.’中,无限循环。
这里的系统异常中断部分是内核的,外部中断部分是外设的。
(6)用户堆栈初始化
ALIGN指令,如图9.2.2.9所示:
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image024.jpg
image023.png
图9.2.2.9 ALIGN指令
ALIGN表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4字节对齐。要注意的是,这个不是ARM的指令,是编译器的。
接下就是启动文件最后一部分代码,用户堆栈初始化代码,如图9.2.2.10所示:
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image026.jpg
image025.png
图9.2.2.10 用户堆栈初始化代码
IF, ELSE, ENDIF是汇编的条件分支语句。
588行判断是否定义了__MICROLIB。关于__MICROLIB这个宏定义,我们是在KEIL里面配置,具体方法如图9.2.2.11所示。
file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image028.jpg
image027.png
图9.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具有全局属性,可被外部的文件使用。
340行标号__user_initial_stackheap,表示用户堆栈初始化程序入口。
接下来进行堆栈空间初始化,堆是从低到高生长,栈是从高到低生长,是两个互相独立的数据段,并且不能交叉使用。
344行保存堆起始地址。345行保存栈大小。346行保存堆大小。347行保存栈顶指针。348行跳转到LR标号给出的地址,不用返回。354行END表示到达文件的末尾,文件结束。
Use MicroLIB
MicroLIB是MDK自带的微库,是缺省C库的备选库,MicroLIB进行了高度优化使得其代码变得很小,功能比缺省C库少。MicroLIB是没有源码的,只有库。
关于MicroLIB更多知识可以看官方介绍http://www.keil.com/arm/microlib.asp
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2024-7-3 03:05

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表