写点什么

STM32 的内存管理相关(内存架构,内存管理,map 文件分析)

作者:矜辰所致
  • 2022 年 8 月 08 日
  • 本文字数:7935 字

    阅读完需:约 26 分钟

STM32的内存管理相关(内存架构,内存管理,map文件分析)

把以前看过的做过的笔记,还有网上参考的部分好文章,利用假期好好梳理了一遍,希望对大家也对自己以后查看有帮助

..梳理章节架构,完善内容

..5.1 STM32 启动方式完善

..5.2 用户 bootloader 简要说明

..GCC 环境下部分相关图例补充

..增加第六节 GCC 下生成的.map 文件

..增加 5.4 ARM 和 x86cpu 指令集 RISC 和 CISC 说明

..更新地址说明错误问题 0x0800 0000

..更新部分细节错误


使用一个 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 数据段中)。

除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。

所以栈特别方便用来保存/恢复调用现场。

从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

放在 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

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 个字节,<font color=#0033FF>到了栈部分,需要 8 字节对齐</font>:


4.5 Image component sizes

存储组成大小这部分的内容就比较直观



最后就是我们熟悉的部分:


五、一些相关的补充内容

5.1 STM32 的启动方式


第一种启动方式是最常用的用户 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:



发布于: 刚刚阅读数: 7
用户头像

矜辰所致

关注

不浮夸,不将就,认真对待学知识的我们! 2022.08.02 加入

不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开! 为了活下去的嵌入式工程师,画画板子,敲敲代码,玩玩RTOS,搞搞Linux ...

评论

发布
暂无评论
STM32的内存管理相关(内存架构,内存管理,map文件分析)_内存_矜辰所致_InfoQ写作社区