本次给大家分析 STM32F4 的启动过程,这里的启动过程是指从 STM32 芯片上电复位执行的第一条指令开始,到执行用户编写的 main 函数这之间的过程。我们编写程序,基本都是用 C 语言编写,并且以 main 函数作为程序的入口。但是事实上,main 函数并非最先执行的,在此之前需要做一些准备工作,准备工作通过启动文件的程序来完成。理解 STM32 启动过程,对今后的学习和分析 STM32 程序有很大的帮助。本文主要是了解启动文件的作用。
目录
一、内存管理
1.0 内存管理的概念
1、内存管理
是指软件运行时对计算机内存资源的分配和使用的技术。主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种,他们其实最终都是要实现 2 个函数:malloc 和 free;malloc 函数用于内存申请,free 函数用于内存释放。
2、进程内存分区:
不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分成以下4个部分。(1)代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令并执行。
(2)数据区:用于存储全局变量等。
(3)堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配内存和回收内存是堆区的特点。
(4)栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到主调函数中继续执行。
3、进程执行过程
在Windows平台下,高级语言写出的程序经过编译链接,最终会变成所谓的PE文件。当PE文件被装载运行后,就成了所谓的进程。
- PE文件代码段中包含的二进制的机器代码会被装入内存的代码区(.text)
- 处理器将到内存的代码区一条一条地取出指令和操作数,并送入算术逻辑单元进行运算;
- 如果代码中请求开辟动态内存,则会在内存的堆区分配一块大小合适的区域返回给代码区的代码使用;
- 当函数调用发生时,函数的调用关系等信息会动态地保存在内存的栈区,以供处理器在执行完被调用函数的代码时,返回被调函数。
1.1 stmf407内存大小和分布
1.2 Flash和RAM介绍
使用一个STM32芯片,对于内存而言有两个直观的指标就是 RAM 大小,FLASH大小,那么着两个大小意味着什么?怎么去理解这两个内存,那就得从什么是Flash,什么是RAM说起。先来看一张图:
1.2.1 FLASH是什么?
通过上图我们可以知道,FLASH属于 非易失性存储器:扩展一点说,FLASH又称为闪存,不仅具备电子可擦除可编程(EEPROM)的性能,不会因为断电丢失数据同时可以快速读取数据,U盘和MP3里用的就是这种存储器。在以前的嵌入式芯片中,存储设备一直使用ROM(EPROM),随着技术的进步,现在嵌入式中基本都是FLASH,用作存储Bootloader以及操作系统或者程序代码或者直接当硬盘使用(U盘)。然后 Flash 主要有两种NOR Flash和NADN Flash。
NOR Flash的读取和我们常见的SDRAM的读取是一样,用户可以直接运行装载在NOR FLASH里面的代码,这样可以减少SRAM的容量从而节约了成本。
NAND Flash没有采取内存的随机读取技术,它的读取是以一次读取一块的形式来进行的,通常是一次读取512个字节,采用这种技术的Flash比较廉价。用户不能直接运行NAND Flash上的代码,因此好多使用NAND Flash的开发板除了使用NAND Flah以外,还作上了一块小的NOR Flash来运行启动代码。
STM32单片机内部的FLASH为 NOR FLASH,有1M大小。 Flash 相对容量大,掉电数据不丢失,主要用来存储代码,以及一些掉电不丢失的用户数据。
1.2.2 RAM是什么?
RAM 属于易失性存储器:RAM随机存储器(Random Access Memory)表示既可以从中读取数据,也可以写入数据。当机器电源关闭时,存于其中的数据就会丢失。比如电脑的内存条。
RAM有两大类,一种称为静态RAM(Static RAM/SRAM),SRAM速度非常快,是目前读写最快的存储设备了,但是它也非常昂贵,所以只在要求很苛刻的地方使用,譬如CPU的一级缓冲,二级缓冲。另一种称为动态RAM(Dynamic RAM/DRAM),DRAM保留数据的时间很短,速度也比SRAM慢,不过它还是比任何的ROM都要快,但从价格上来说DRAM相比SRAM要便宜很多,计算机内存就是DRAM的。
为什么需要RAM,因为相对FlASH而言,RAM的速度快很多,所有数据在FLASH里面读取太慢了,为了加快速度,就把一些需要和CPU交换的数据读到RAM里来执行(注意这里不是全部数据,只是一部分需要的数据,这个在后面介绍STM32的内存管理中会提到)。STM32单片机内部的 RAM 为 SRAM。 RAM相对容量小,速度快,掉电数据丢失,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据。
在单片机中,RAM主要是做运行时数据存储器,FLASH主要是程序存储器,EEPROM主要是用以在程序运行保存一些需要掉电不丢失的数据。
1.3 STM32的内存管理
我们知道,STM32大都属于Cortex-M系列的处理器,可以对32的存储器进行寻址,因此存储器的寻址空间能够达到4G,这就意味着指令和数据共用相同的地址空间,也就是将程序存储器(FLASH)、数据存储器(SRAM)、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。数据字节以小端格式存放在存储器中。一个字里的最低地址字节被认为是该字的最低有效字节,而最高地址字节是最高有效字节。
1.3.1 STM32存储器架构
4G的地址空间就是地址编码的范围。
所谓编码就是对每一个程序存储器、数据存储器、寄存器和输入输出端口(一个字节)分配一个唯一的地址号码,这个过程又叫做“编址”或者“地址映射”。这个过程就好像在日常生活中我们给每家每户分配一个地址门牌号。与编码相对应的是“寻址”过程——分配一个地址号码给一个存储单元的目的是为了便于找到它,完成数据的读写,这就是“寻址”,因此地址空间有时候又被称作“寻址空间”。
有了4G的可寻址空间,我们就可通过寻址来操作相应的地址对象。这就需要将程序存储器、数据存储器、寄存器和输入输出端口进行统一编号,也就是存储器映射。存储器映射是指把芯片中或芯片外的FLASH,RAM,外设,BOOTBLOCK等进行统一编址。即用地址来表示对象。这个地址绝大多数是由厂家规定好的,用户只能用而不能改。用户只能在挂外部RAM或FLASH的情况下可进行自定义。
1.3.2 STM32的存储器映射分析
ST将所有的存储器及外设资源都映射在一个4GB的地址空间上(8个块),每个块512MB,从而可以通过访问对应的地址,访问具体的外设。其映射关系如图所示:
STM32 的内存管理就是对 0X0800 0000 开始的 Flash 部分 和 0x2000 0000 开始的 SRAM 部分使用管理。
STM32的SRAM
不同类型的STM32单片机的SRAM大小是不一样的,但起始地址都是0x2000 0000,终止地址都是0x2000 0000+其固定的容量大小。SRAM的理解比较简单,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据。设备断电后,SRAM中存储的数据就会丢失。
STM32的Flash
STM32的Flash,严格说,应该是Flash模块。该Flash模块包括:Flash主存储区(Main memory)、Flash信息区(Informationblock),以及Flash存储接口寄存器区(Flash memory interface)。
主存储器,该部分用来存放代码和数据常数(如加const类型的数据)。对于大容量产品,其被划分为256页,每页2K,小容量和中容量产品则每页只有1K字节。主存储起的起始地址为0X08000000,B0、B1都接GND的时候,就从0X08000000开始运行代码。
信息块,该部分分为2个部分,其中启动程序代码,是用来存储ST自带的启动程序,用于下载,当B0接3.3V,B1接GND时,运行的就这部分代码,用户选择字节,则一般用于配置保护等功能。
闪存储器块,该部分用于控制闪存储器读取等,是整个闪存储器的控制机构。
1.3.3 STM32内存分析
C/C++ 程序编译后的存储数据段:
在了解如何使用内存管理之前,先得理解一下c++的内存划分:
(1)data段 (可读可写)
数据段,储存已初始化且不为0的全局变量和静态变量(全局静态变量和局部静态变量)。
(2)BSS段(可读可写)
储存未初始化的,或初始化为0的全局变量和静态变量。
(3)text段(只读)
代码段,储存程序代码。也就是存放CPU执行的机器指令(machineinstructions)。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。 在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
(4)constdata
储存只读常量。const修饰的常量。 把常量的字符串,数据等用const声明
(5)heap(堆)
堆是用于存放进程运行中被动态分配的内存段。他的大小并不固定,可动态扩张或者缩减,由程序员使用malloc()和free()函数进行分配和释放。当调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
(6)stack(栈)
栈又称堆栈,是用户存放程序临时创建的局部变量,由系统自动分配和释放。可存放局部变量、函数的参数和返回值。 除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。中断发生时能保存现场。
对应STM32的内存分析,STM32的内存主要是FLASH区和SRAM区,根据数据的不同属性存放在两个不同的区域,与C++内存划分进行对比,如下图所示:
1.3.4 单片机和 x86CPU运行程序的不同
x86的pc机CPU在运行的时候程序是存储在内存中的,而单片机等嵌入式系统则是存于flash中。x86CPU和单片机读取程序的具体途径: pc机在运行程序的时候将程序从外存(硬盘)中,调入到内存中运行,cpu从内存中读取程序和数据,而单片机的程序则是固化在flash中,CPU运行时直接从flash中读取程序,从RAM中读取数据。
原因分析 : x86构架的CPU是基于冯.诺依曼体系的,即数据和程序存储在一起,而且pc机的内存资源相当丰富,从几十M到几百M甚至是几个G,客观上能够承受大量的程序数据。
单片机的构架大多是哈弗体系的,即程序和数据分开存储,而且单片的片内RAM资源是相当有限的,内部的RAM过大会带来成本的大幅度提高。冯.诺依曼体系与哈佛体系的区别: 二者的区别就是程序空间和数据空间是否是一体的。 早期的微处理器大多采用冯诺依曼结构,典型代表是Intel公司的X86微处理器。取指令和取操作数都在同一总线上,通过分时复用的方式进行的。缺点是在高速运行时,不能达到同时取指令和取操作数,从而形成了传输过程的瓶颈。哈佛总线技术应用是以DSP和ARM为代表的。采用哈佛总线体系结构的芯片内部程序空间和数据空间是分开的,这就允许同时取指令和取操作数,从而大大提高了运算能力。
二、STM32启动分析
2.1 启动模式
我们知道的复位方式有三种:上电复位,硬件复位和软件复位。
复位可以理解为我们的板⼦开始从0x00000000地址处运行了。此处的0x00000000地址就是我们前面讲的内存映射4G地址空间。当产生复位,并且离开复位状态后,CM4 内核做的第一件事就是读取下列两个 32 位整数的值: (1)从地址 0x0000 0000 处取出堆栈指针 MSP 的初始值,该值就是栈顶地址。 (2)从地址 0x0000 0004 处取出程序计数器指针 PC 的初始值,该值指向复位后执行的第一条指令。
下面用示意图表示,如图所示。
上述过程中,内核是从 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 两个的地址重映射到其他地址空间,就是启动模式选择。对于 STM32F4 的启动模式(也称自举模式),我们看表进行分析。
注:启动引脚的电平:0:低电平;1:高电平;x:任意电平,即高低电平均可由表 可以看到,STM32F4 根据 BOOT 引脚的电平选择启动模式,这两个 BOOT 引脚根据外部施加的电平来决定芯片的启动地址。
(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 空间获取内容进行自举。
(3)系统存储器启动方式
当芯片上电后采样到 BOOT0=1,BOOT1=0 的组合时,内核将从系统存储器的 0x1FFFF000 及 0x1FFFF004 获取 MSP 及 PC 值进行自举。系统存储器是一段特殊的空间,用户不能访问, ST 公司在芯片出厂前就在系统存储器中固化了一段代码。因而使用系统存储器启动方式时,内核会执行该代码,该代码运行时,会为 ISP(In System Program)提供支持,在 STM32F4 上最常见的是检测 USART1 传输过来的信息,并根据这些信息更新自己内部 FLASH 的内容,达到升级产品应用程序的目的,因此这种启动方式也称为 ISP 启动方式。
2.2 启动文件分析
2.2.0 什么是启动文件
⾸先,对于嵌入式开发,⼀般使⽤的是C语言开发。⽽我们知道C语言都是从main函数开始 的,因此,对于处理器来说它是如何找到并执行main函数的,就需要用到启动文件,就是各种 startup_xxx.s文件。 对于我们STM32f407来说其启动⽂件为:startup_stm32f40_41xx.s 。启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作:
启动文件主要做了以下工作:
1、初始化堆栈指针 SP = _initial_sp
2、初始化程序计数器指针 PC = Reset_Handler
3、设置堆和栈的大小
4、初始化中断向量表
5、配置外部 SRAM 作为数据存储器(可选)
6、配置系统时钟,通过调用 SystemInit 函数(可选)
7、调用 C 库中的 _main 函数初始化用户堆栈,最终调用 main 函数
2.2.1 启动文件中的一些指令
2.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)堆空间的开辟
堆空间的开辟,源码如图所示:
源码含义:开辟一段大小为 0x0000 0200(512 字节)的堆空间,段名为 HEAP,不初始 化,可读可写,8 字节对齐。 __heap_base 表示堆的起始地址,__heap_limit 表示堆的结束地址。堆和栈的生长方向相反 的,堆是由低向高生长,而栈是从高往低生长。 堆主要用于动态内存的分配,像 malloc()、calloc()和 realloc()等函数申请的内存就在堆上面。 堆中的内存一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。注意:由于正点原子提供了独立的内存管理实现方式(mymalloc,myfree 等),并不需要使用 C 库的 malloc 和 free 等函数,也就用不到堆空间,因此我们可以设置 Heap_Size 的大小为 0, 以节省内存空间。
堆和栈的属性都是可读可写的,执行上述代码会在SRAM区(位于0x2000 0000地址后)开辟相应的空间。
(3)中断向量表定义(简称:向量表)
STM32F407 的中断向量表定义代码,如图所示。
__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,__Vectors_Size 为向量 表大小,__Vectors_Size = __Vectors_End - __Vectors。 DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
中断向量表被放置在代码段的最前面。例如:当我们的程序在 FLASH 运行时,那么向量表的起始地址是:0x0800 0000。结合图可以知道,地址 0x0800 0000 存放的是栈顶地址。 DCD:以四字节对齐分配内存,也就是下个地址是 0x0800 0004,存放的是 Reset_Handler 中断 函数入口地址。 从代码上看,向量表中存放的都是中断服务函数的函数名,所以 C 语言中的函数名对芯片来说实际上就是一个地址。
如何理解向量表?
其实向量表的本质就是一个元素大小为4字节的数组,而这个数组里面的元素就是这些中断服务函数的名字(也就是中断服务函数的地址),而这张向量表默认就放在flash起始地址处,注意向量表的第一个存储的是栈顶指针(栈顶的地址),开发板上电复位时由CPU将这个栈顶地址赋给CPU的栈寄存器SP
重点:向量表有什么用呢?
当中断来临时内核就会去向量表中找出对应的中断服务函数的地址,并将地址赋给PC寄存器,进而程序跳转去执行中断函数,而这个过程就是由硬件帮助我们完成的。
(4)复位程序
Reset handler函数内部会调用_main 函数,而_main调用到main函数进入到C语言的世界,但是_main 在调用main之前做了什么事情呢?初始化堆栈:
反正_main函数的最后会调用main函数,然后你就可以随意的写C代码了
(5) 中断服务程序
在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置而已,以防程序奔溃。
如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名 写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且 在 B 指令作用下跳转到一个‘.’中,无限循环。 这里的系统异常中断部分是内核的,外部中断部分是外设的。
2.3map文件分析
C语⾔的编译链接过程:
MDK 编译工程,也会生成一些中间文件(如.o、.axf、.map 等),最终生成 hex 文件,以 便下载到 MCU 上面执行.
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)
以上便是STM32启动过程分析的全部内容,如有兴趣,感谢点赞、关注、收藏,若有不正地方,还请各位大佬多多指教!