把以前看过的做过的笔记,还有网上参考的部分好文章,利用十一假期好好梳理了一遍,希望对大家也对自己以后查看有帮助
..梳理章节架构,完善内容 2021/10/6--2021/10/7
..5.1 STM32启动方式完善
..5.2 用户bootloader简要说明
..GCC环境下部分相关图例补充 2021/10/8
..增加第六节GCC下生成的.map文件 2021/11/21
..增加 5.4 ARM 和 x86cpu 指令集RISC和CISC说明 2021/12/10
..更新地址说明错误问题0x0800 0000 2022/6/13
..更新部分细节错误 2022/7/28
STM32的内存管理
使用一个STM32芯片,对于内存而言有两个直观的指标就是 RAM 大小,FLASH大小,比如STM32F103系列(其他系列也是如此):
那么着两个大小意味着什么?怎么去理解这两个内存,那就得从什么是Flash,什么是RAM说起。
一、FLASH 和 RAM基本概念
先来看一张图:
1.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。
Flash 相对容量大,掉电数据不丢失,主要用来存储 代码,以及一些掉电不丢失的用户数据。
1.2 RAM是什么
RAM 属于易失性存储器:
RAM随机存储器(Random Access Memory)表示既可以从中读取数据,也可以写入数据。当机器电源关闭时,存于其中的数据就会丢失。比如电脑的内存条。
RAM有两大类,一种称为静态RAM(Static RAM/SRAM),SRAM速度非常快,是目前读写最快的存储设备了,但是它也非常昂贵,所以只在要求很苛刻的地方使用,譬如CPU的一级缓冲,二级缓冲。另一种称为动态RAM(Dynamic RAM/DRAM),DRAM保留数据的时间很短,速度也比SRAM慢,不过它还是比任何的ROM都要快,但从价格上来说DRAM相比SRAM要便宜很多,计算机内存就是DRAM的。
DRAM分为很多种,常见的主要有FPRAM/FastPage、EDORAM、SDRAM、DDR RAM、RDRAM、SGRAM以及WRAM等,这里介绍其中的一种DDR RAM。
DDR RAM(Date-Rate RAM)也称作DDR SDRAM,这种改进型的RAM和SDRAM是基本一样的,不同之处在于它可以在一个时钟读写两次数据,这样就使得数据传输速度加倍了。这是目前电脑中用得最多的内存,而且它有着成本优势,事实上击败了Intel的另外一种内存标准-Rambus DRAM。在很多高端的显卡上,也配备了高速DDR RAM来提高带宽,这可以大幅度提高3D加速卡的像素渲染能力。
为什么需要RAM,因为相对FlASH而言,RAM的速度快很多,所有数据在FLASH里面读取太慢了,为了加快速度,就把一些需要和CPU交换的数据读到RAM里来执行(注意这里不是全部数据,只是一部分需要的数据,这个在后面介绍STM32的内存管理中会提到)。
STM32单片机内部的 RAM 为 SRAM。
RAM相对容量小,速度快,掉电数据丢失,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据。
二、STM32的内存架构
2.1 Cortex-M3的存储器映射分析
在《ARM Cotrex-M3权威指南》中有关 M3的存储器映射表:
存储器映射 是用 地址来表示 对象,因为Cortex-M3是32位的单片机,因此其PC指针可以指向2^32=4G的地址空间,也就是图中的 0x00000000到0xFFFFFFFF的区间,也就是将程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内,数据字节以小端格式存放在存储器中。
2.2 STM32 的存储器映射分析
STM32存储器映射表(选用的是STM32F103VE的,不同的型号Flash 和 SRAM 的地址空间不同,起始地址都是一样的):
那么我们所需要分析的STM32 内存,就是图中 0X0800 0000开始的 Flash 部分 和 0x2000 0000 开始的SRAM部分,这里还要介绍一个和Flash模块相关的部分:
2.3 STM32的 Flash 组织
参考博文:深入理解STM32内存管理
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时,运行的就这部分代码,用户选择字节,则一般用于配置保护等功能。
闪存储器块,该部分用于控制闪存储器读取等,是整个闪存储器的控制机构。
对于主存储器和信息块的写入有内嵌的闪存编程管理;编程与擦除的高压由内部产生。
在执行闪存写操作时,任何对闪存的读操作都会锁定总线,在写完成后才能正确进行,在进行读取或擦除操作时,不能进行代码或者数据的读取操作。
三、STM32 的内存管理
STM32 的内存管理起始就是对
0X0800 0000 开始的 Flash 部分 和
0x2000 0000 开始的 SRAM 部分使用管理
3.1 C/C++ 程序编译后的存储数据段
参考博文:STM32内存结构介绍
在了解如何使用内存管理之前,先得理解一下 6 个储存数据段 和 3种存储属性区 的概念:
.data
数据段,储存已初始化且不为0的全局变量和静态变量(全局静态变量和局部静态变量)。
static声明的变量放在data段。
数据段属于静态内存分配,所以放在RAM里,准确来说,是在程序运行的时候需要在RAM中运行。
.BSS
Block Started by Symbol。储存未初始化的,或初始化为0的全局变量和静态变量。
BSS段属于静态内存分配,所以放在RAM里。
.text(CodeSegment/Text Segment)
代码段,储存程序代码。也就是存放CPU执行的机器指令(machineinstructions)。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。
在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
放在Flash里。
.constdata
储存只读常量。const修饰的常量,不管是在局部还是全局
放在Flash 里。
所以为了节省 RAM,把常量的字符串,数据等 用const声明
heap(堆)
堆是用于存放进程运行中被动态分配的内存段。他的大小并不固定,可动态扩张或者缩减,由程序员使用malloc()和free()函数进行分配和释放。当调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
放在RAM里
其可用大小定义在启动文件startup_stm32fxx.s中。
stack(栈)
栈又称堆栈,是用户存放程序临时创建的局部变量,由系统自动分配和释放。可存放局部变量、函数的参数和返回值(但不包括static声明的变量,static意味着 放在 data 数据段中)。
除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
???由于栈的先进先出(FIFO)特点???上面这句话正确吗?Cortex-M3/M4的堆栈是向下生长,第一个入栈的元素应该是最后一个才能出来??
所以栈特别方便用来保存/恢复调用现场。
从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
放在RAM里
其大小定义在启动文件startup_stm32fxx.s中。
3.2 STM32 程序编译后的内存占用情况
3.2.1 MDK 编译
MDK编译后的结果:
Code:
程序代码部分。
.text 段
放在ROM里面,就是Flash,需要占用flash空间
RO-data
(Read Only)只读数据
程序定义的常量,只读数据,字符串常量(const修饰的)
.constdata 段
放在flash里面,需要占用flash空间
RW-data
(Read Write)可读可写数据
已经初始化的全局变量和静态变量(就是static修饰的变量);
.data 段
需要在 RAM里面运行,但是起初需要保存在 Flash里面,程序运行后复制到 RAM里面运行,需要占用Flash空间
ZI-data
(Zero Initialize)未初始化的全局变量和静态变量,以及初始化为0的变量;
.BSS段
ZI的数据全部是0,没必要开始就包含,只要程序运行之前将ZI数据所在的区域(RAM里面)一律清 0,不占用Flash,运行时候占用RAM.
heap 和 stack 其实也属于 ZI,只不过他不是程序编译就能确定大小的,必须在运行中才会有大小,而是是变化的
因为RAM掉电丢失,所以 RW-data 数据也得下载到ROM(flash) 中,在运行的时候复制到 RAM中运行,如下图所示(图中的地址也是错的,应该是从0x0800 0000 开始):
由上我们得知:
程序占用 Flash = Code + RO data + RW data
程序运行时候占用 RAM = RW data + ZI data。
Code + RO data + RW data 的大小也是生成的 bin 文件的大小
3.2.1 GCC 编译
GCC编译结果:
GCC编译,
图中红色的部分是占用 Flash 的大小:Flash = text + data 。
蓝色部分是运行时候占用 RAM大小:RAM = data + bss。
上面的 text 22516 是 byte ,是字节 。
换算成 KB 就是 22516 / 1024 = 21.98 KB
3.3 STM32 程序的内存分配
我们前面说到的 stack(栈) 和 heap(堆),程序编译完成以后并不能知道运行时候实际占用RAM 大小。
但是我们可以知道的是 stack(栈) 和 heap(堆)的起始地址,和能够使用的最大空间,我们先看能够使用的空间大小。
3.3.1 MDK 环境
MDK是在 startup_stm32fxxx.s 中定义的:
在 startup_stm32fxxx.s 中我们可以看到关于 Stack_Size 和 Heap_Size的定义,图中的定义就是规定本程序中 栈 的大小为 1K, 堆的大小为 0.5K。
startup_stm32fxxx.s文件是系统的启动文件,startup_stm32fxxx.s主要完成三个工作:栈和堆的初始化、定位中断向量表、调用Reset Handler。
(关于STM32的启动文件,我们有单独的一篇文章,请查看另一篇博文 STM32的启动过程(startup_xxxx.s文件解析))
我们在生成的.map文件可以看到 HEAP 和 STACK的起始地址(不懂的可以先看博文下面一节的内容——四、MDK生成的.map文件简析),我们要注意的是:
堆使用时候从起始地址开始,往上加
栈使用时候从结束地址,就是__initial_sp(栈顶指针的地址)开始,往下减
他们的空间大小定义好了,如果入栈元素过大,使得元素减到了堆的地址范围,就是栈溢出,这会导致改变堆中相应地址元素的值。
同样的,当申请的动态内存过大,使得堆的变量加到了栈的地址范围,就是堆溢出。
在实际项目中,如果程序复杂,中断嵌套太多,栈需要多设置一点空间
如果你使用裸机程序,从来不使用标准库的malloc,heap可以没有,因为永远不会用到,还一直占用着一片RAM区。
所以在我们知道了以上的知识后,我们可以按照自己的工程需求,定义Stack_Size 和 Heap_Size。
3.3.2 GCC 环境
如果是在GCC编译器下面,关于 Stack_Size 和 Heap_Size的定义如下图:
四、MDK生成的.map文件简析
为了更深层次的理解上述内容,我们还有必要分析一下MDK生成的 .map 文件,
那么既然要分析,除了我们关注的flash 和 ram部分的内容,其他的地方也稍微做一下笔记:
4.1 Section Cross References
主要是不同文件中函数的调用关系:
我们查看一下 clock.c 文件下的 rt_tick_increase
函数:
4.2 Removing Unused input sections from the image
被删除的冗余函数:
删除函数功能在MDK的配置中可以设置,勾选以后删除得多,不勾选删除得少,如下图:
在 Removing Unused input sections from the image 的最后会列出删除的冗余函数的大小,如果在MDK上改变上图所示的配置,下图中的删除总代码可以看到变化:
4.3 Image Symbol Table
4.3.1 Local Symbols
局部标号,
用Static声明的全局变量地址和大小,
C文件中函数的地址和用static声明的函数代码大小,
汇编文件中的标号地址(作用域限本文件):
我们找个占用内存地址的地方看一下:
我们在 startup_stm32f103xg.s ,可以看到RESET函数:
我们继续往下看:
上图中的 i.RCC_Delay 下面跟了一个 RCC_Delay,说明这个函数是用static修饰的,我们找到 stm32f1xx_hal_rcc.c下的RCC_Delay
如图:
我们接着看到 SRAM区:
我们继续看到后面的.bss段,包括了 HEAP 和 STACK区域:
通过上图中我们可以看到 HEAP 的起始地址为 0x20002338,
ram从 0x2000 0000开始存放的依次为 .data、.bss、HEAP、STACK。
HEAP在 startup_stm32fxxx.s 中定义过大小为 0x200,
所以结束地址为0x20002538, HEAP 是和 STACK连接在一起的,所以STACK的起始地址为 0x20002538,大小 0X400,结束地址为 0x20002938。
最后我们可以看到 __initial_sp 指向的是 0x20002938,入栈从高地址开始入栈,地址越来越小。
4.3.2 Global Symbols
全局标号,
全局变量的地址和大小,
C文件中函数的地址及其代码大小,
汇编文件中的标号地址(作用域全工程):
我们找到Flash地址开始的部分:
在 startup_stm32fxxx.s 能看到对应的部分:
看到最后的 RAM区,注意下图中标出的两行的功能:
4.4 Memory Map of the image
映像文件可以分为加载域(Load Region)和运行域(Execution Region):加载域反映了ARM可执行映像文件各个段存放在存储器中时的位置关系。
其中还能看出来 Flash中存放的都是 code,和 RO_Data:
我们看看SRAM部分:
需要注意一下,我们前面代码he 数据部分都是4字节对齐,PAD一般都是补充2个字节,到了栈部分,需要8字节对齐:
4.5 Image component sizes
存储组成大小
这部分的内容就比较直观
最后就是我们熟悉的部分:
五、一些相关的补充内容
5.1 STM32 的启动方式
BOOT0 | BOOT1 | 启动模式 |
---|---|---|
0 | X | User Flash memory(从闪存存储器启动) |
1 | 0 | System memory(从系统存储器启动) |
1 | 1 | Embedded SRAM(从内嵌SRAM启动) |
第一种启动方式是最常用的用户FLASH启动,正常工作就在这种模式下,STM32的FLASH可以擦出10万次,所以不用担心芯片哪天会被擦爆!
第二种启动方式是系统存储器启动方式,即我们常说的串口下载方式(ISP),不建议使用这种,速度比较慢。STM32 中自带的BootLoader就是在这种启动方式中,如果出现程序硬件错误的话可以切换BOOT0/1到该模式下重新烧写Flash即可恢复正常。
第三种启动方式是STM32内嵌的SRAM启动。该模式用于调试。 用jlink在线仿真,则是下载到SRAM中。
以上三种启动方式我们都很熟悉,但是他的究竟是如何实现的呢?
我们先来看看《Cortex-M3权威指南》关于CM3复位后的动作:
当选择相应的启动方式时,对应的存储器空间被映射到启动空间(0x00000000)。
从闪存存储器启动:主闪存存储器被映射到启动空间(0x0000 0000) ,也就是0x08000000被映射到0x00000000。
从内嵌SRAM启动 :SRAM起始地址 0x2000 0000 被映射到0x00000000。
从系统存储器启动:系统存储器被映射到启动空间(0x0000 0000),也就是0x1FFF F000被映射到0x00000000。
(为什么是0x1FFF F000 可以查阅上文中的 2.2小节 STM32 的存储器映射分析,STM32互联型产品这个地址不一样,此地址由ST官方写入了一段BootLoader代码,可以通过官方BootLoader升级MCU固件,无法修改)。
5.2 STM32启动地址和 Bootloader 说明
通过上面的内容,我们现在知道STM32 从Flash程序启动以后会从 0X08000000 开始运行,那么他这个地址是否可以修改,答案是当然的!但是单独的改他的启动地址,没有任何意义,一般都是需要使用到 Bootloader 才会使得应用程序的地址发生变化。
在 0x1FFF F000 这个地址上官方写入了一段 BootLoader 用户使用,我们也可以自己写一段 BootLoader 程序方便自己使用,因为是自己写的,他还是用户程序,只是我们自己把程序分成了 BootLoader部分和 应用程序部分,大概的意思如下图所示:
为什么要使用用户 BootLoader :
在有些项目中,可能因为某些原因需要经常更换 程序,如果每次都是重新烧录,特别的麻烦,那么我们就可以自己设计一个 BootLoader,通过 SD卡进行升级:
上电后先运行 BootLoader,BootLoader主要工作是检测是否有SD卡,SD卡中是否有需要的BIn文件,
如果检测到就将其复制到 应用程序区域 使得程序得以更新,更新结束以后跳转到应用程序执行;
如果没检测到相应的SD卡,就说明程序不需要更新,也跳转到应用程序执行;
以上主要是说明使用 BootLoader 的思路与适用场合,至于具体的实现其实网上有很多教程,如果有机会我也会补充或者单独写一篇文章总结一下
(说明:下面是零碎的笔记,还没整理完,仅供参考
stm32 FLASH的起始地址是0x08000000,当然也可以自定义起始地址,不过记得在main函数中定义变量后加一句SCB->VTOR=FLASH_BASE |
OFFSET;OFFSET是想要偏移的量,可宏定义或直接0xXX。
当然也可以调用库函数 NVIC_SetVectorTable()进行偏移,效果一样。IAP升级这样用的多)
5.3 单片机和 x86cpu运行程序的不同
参考博文:cpu运行时程序是在flash中还是在RAM呢?
x86的pc机cpu在运行的时候程序是存储在RAM中的,而单片机等嵌入式系统则是存于flash中
x86cpu和单片机读取程序的具体途径:
pc机在运行程序的时候将程序从外存(硬盘)中,调入到RAM中运行,cpu从RAM中读取程序和数据
而单片机的程序则是固化在flash中,cpu运行时直接从flash中读取程序,从RAM中读取数据
原因分析 :
x86构架的cpu是基于冯.诺依曼体系的,即数据和程序存储在一起,而且pc机的RAM资源相当丰富,从几十M到几百M甚至是几个G,客观上能够承受大量的程序数据。
单片机的构架大多是哈弗体系的,即程序和数据分开存储,而且单片的片内RAM资源是相当有限的,内部的RAM过大会带来成本的大幅度提高。
冯.诺依曼体系与哈佛体系的区别:
二者的区别就是程序空间和数据空间是否是一体的。 早期的微处理器大多采用冯诺依曼结构,典型代表是Intel公司的X86微处理器。取指令和取操作数都在同一总线上,通过分时复用的方式进行的。缺点是在高速运行时,不能达到同时取指令和取操作数,从而形成了传输过程的瓶颈。
哈佛总线技术应用是以DSP和ARM为代表的。采用哈佛总线体系结构的芯片内部程序空间和数据空间是分开的,这就允许同时取指令和取操作数,从而大大提高了运算能力。
5.4 ARM 和 x86cpu 指令集RISC和CISC说明
(此部分是学习了韦东山老师的视频后来记录的,原视频可以在韦东山老师官网里面找到:百问网)
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
① 对内存只有读、写指令
② 对于数据的运算是在CPU内部实现
③ 使用RISC指令的CPU复杂度小一点,易于设计
在ARM架构中,对于乘法运算a = a * b,
在RISC中要使用4条汇编指令:
① 读内存a
② 读内存b
③ 计算a*b
④ 把结果写入内存
x86属于复杂指令集计算机(CISC:Complex Instruction Set Computing),
它所用的指令比较复杂,比如某些复杂的指令,它是通过“微程序”来实现的。
比如执行乘法指令时,实际上会去执行一个“微程序”,
一样是去执行这4步操作:
① 读内存a
② 读内存b
③ 计算a*b
④ 把结果写入内存
RISC和CISC的区别:
- CISC的指令能力强,单多数指令使用率低却增加了CPU的复杂度,指令是可变长格式;
- RISC的指令大部分为单周期指令,指令长度固定,操作寄存器,对于内存只有Load/Store操作
- CISC支持多种寻址方式;RISC支持的寻址方式
- CISC通过微程序控制技术实现;
- RISC增加了通用寄存器,硬布线逻辑控制为主,采用流水线
- CISC的研制周期长
- RISC优化编译,有效支持高级语言
六、 GCC下生成的.map文件
我们知道了MDK下的.map文件,GCC下的.map文件基本上也可以看懂了,这里添加GCC下.map文件关于RAM的部分:
6.1 RAM部分
RAM部分从0x2000 0000开始,首先存放的是.data部分:
接下来是.bss段:
如果使用了FreeRTOS,那么在.bbs段后面会有关于FreeRTOS部分的数据:
最后才到了heap和stack: