《嵌入式 - 深入剖析STM32》深入理解STM32内存管理

Posted Bruceoxl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《嵌入式 - 深入剖析STM32》深入理解STM32内存管理相关的知识,希望对你有一定的参考价值。

在讨论STM32的内存之前,先来看看STM32的存储器系统,我们知道,STM32大都属于Cortex-M系列的处理器,可以对32的存储器进行寻址,因此存储器的寻址空间能够达到4G,这就意味着指定和数据共用相同的地址空间,也就是将程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。数据字节以小端格式存放在存储器中。一个字里的最低地址字节被认为是该字的最低有效字节,而最高地址字节是最高有效字节。

1 STM32存储器架构

4G的地址空间就是地址编码的范围。所谓编码就是对每一个程序存储器、数据存储器、寄存器和输入输出端口(一个字节)分配一个唯一的地址号码,这个过程又叫做“编址”或者“地址映射”。这个过程就好像在日常生活中我们给每家每户分配一个地址门牌号。与编码相对应的是“寻址”过程——分配一个地址号码给一个存储单元的目的是为了便于找到它,完成数据的读写,这就是“寻址”,因此地址空间有时候又被称作“寻址空间”。

有了4G的可寻址空间,我们就可通过寻址来操作相应的地址对象。这就需要将程序存储器、数据存储器、寄存器和输入输出端口进行统一编号,也就是存储器映射。

存储器映射是指把芯片中或芯片外的FLASH,RAM,外设,BOOTBLOCK等进行统一编址。即用地址来表示对象。这个地址绝大多数是由厂家规定好的,用户只能用而不能改。用户只能在挂外部RAM或FLASH的情况下可进行自定义。

如下图,是Cortex-M3存储器映射结构图。

Cortex-M3是32位的内核,因此其PC指针可以指向2^32=4G的地址空间,也就是0x0000_0000——0xFFFF_FFFF这一大块空间。根据图中描述,Cortex-M3内核将0x0000_0000——0xFFFF_FFFF这块4G大小的空间分成8大块:代码、SRAM、外设、外部RAM、外部设备、专用外设总线-内部、专用外设总线-外部、特定厂商等,因此使用该内核的设计者必须按照这个进行各自芯片的存储器结构设计。

首先,我们对比一下Cortex-M3存储器结构和STM32存储器结构:

图中可以很清晰的看到,STM32的存储器结构和Cortex-M3的很相似,不同的是,STM32加入了很多实际的东西,如:Flash、SRAM等。只有加入了这些东西,才能成为一个拥有实际意义的、可以工作的处理芯片——STM32。

STM32的存储器地址空间被划分为大小相等的8块区域,每块区域大小为512MB。

对STM32存储器知识的掌握,实际上就是对Flash和SRAM这两个区域知识的掌握。由STM32的系统结构可以看出,Flash和SRAM这两个区域分别由ICode总线和DCode总线与处理器通信,以此完成相应的数据交换。

下面将重点描述Flash和SRAM的知识。

1.1 STM32的SRAM

不同类型的STM32单片机的SRAM大小是不一样的,但起始地址都是0x2000 0000,终止地址都是0x2000 0000+其固定的容量大小。

SRAM的理解比较简单,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据。设备断电后,SRAM中存储的数据就会丢失。

1.2 STM32的Flash

STM32的Flash,严格说,应该是Flash模块。该Flash模块包括:Flash主存储区(Main memory)、Flash信息区(Information block),以及Flash存储接口寄存器区(Flash memory interface)。三个组成部分分别在0x0000 0000——0xFFFF FFFF不同的区域,如下表所示。

STM32的闪存模块由:主存储器、信息块和闪存储器块3部分组成。

主存储器,该部分用来存放代码和数据常数(如加const类型的数据)。对于大容量产品,其被划分为256页,每页2K,注意,小容量和中容量产品则每页只有1K字节。主存储起的起始地址为0X08000000,B0、B1都接GND的时候,就从0X08000000开始运行代码。

信息块,该部分分为2个部分,其中启动程序代码,是用来存储ST自带的启动程序,用于串口下载,当B0接3.3V,B1接GND时,运行的就这部分代码,用户选择字节,则一般用于配置保护等功能。

闪存储器块,该部分用于控制闪存储器读取等,是整个闪存储器的控制机构。

对于主存储器和信息块的写入有内嵌的闪存编程管理;编程与擦除的高压由内部产生。

在执行闪存写操作时,任何对闪存的读操作都会锁定总线,在写完成后才能正确进行,在进行读取或擦除操作时,不能进行代码或者数据的读取操作。

2 STM32内存分析

在C/C++程序中,编译的程序占用内存分为5个区,分别为栈区、堆区、全局/静态存储区、常量存储区、代码区。

1.Text段(Code Segment/Text Segment,代码段):通常是指用来存放程序执行代码的一块内存区域,也就是存放CPU执行的机器指令(machine instructions)。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

2.全局初始化数据区/静态数据区(Initialized data segment/Data segment):该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。数据段属于静态内存分配。static声明的变量放在data段。

3.BSS段(Block Started by Symbol):BSS段通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS段属于静态内存分配。

4.堆(heap):堆是用于存放程序运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。也就是常说的用malloc,calloc, realloc 等函数分配的变量空间是在堆上。当程序调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

5.栈(stack):栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

一个程序本质上都是由 bss段、data段、text段三个组成的。
在C/C++程序编译完成之后,已初始化的全局变量保存在data 段中,未初始化的全局变量保存在bss 段中。

text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。

2.1 STM32程序占用ROM(FLASH)和RAM的大小分析

为例调试方便,这里使用一个裸机串口例子,关于串口的使用请参看笔者博文:

串口通信

使用Keil编译代码,编译信息如下:

其中:

  • Code 代表执行的代码,程序中所有的函数都位于此处。即上述的text段。
  • RO-data(Read Only) 代表只读数据,程序中所定义的全局常量数据和字符串都位于此处,如const型。
  • RW-data(Read Write) 代表已初始化的读写数据,程序中定义并且初始化的全局变量和静态变量位于此处。
  • ZI-data(Zero Initialize) 代表未初始化的读写数据,程序中定义了但没有初始化的全局变量和静态变量位于此处。Keil编译器默认是把你没有初始化的变量都赋值为例0。即上述的bss段。

值得注意的是,这些参数的单位是Byte。

Code和RO-Data两个段统称为RO段,它们和RW段,需要烧录到FLASH等非易失性器件中。
RW段需要烧录到FLASH中,而ZI段则不用,但在运行时,它们都必须装载到可读可写的RAM中。

因此我们可以计算出FLASH和RAM的大小:

Flash = Code + RO Data + RW Data
RAM = RW-data + ZI-data

2.2 STM32程序的内存分配

程序后成功编译后,通过编译信息可以查看程序空间分配情况,而map文件更加详细的描述了程序编译编译信息。

map文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,它可以在任何地方、任何时候使用,不需要有额外的程序进行支持。

在MDK5中,在项目中双击Target就能自动打开.map文件。

在xxx.map文件的最后也会有不同数据段的信息统计。

我们知道,程序运行需要占用的大小是RAM = RW-data + ZI-data,而堆栈的大小是程序开始运行后才能确定的,堆栈的内存占用就是在上面RAM分配给RW-data + ZI-data之后的地址开始分配的。

那么堆和栈到底能占用多大呢,堆栈的大小是在startup_stm32fxxx.s中设置的,这里以STM32F103ZET6为例进行分析,其内部栈的大小为1KB,堆的大小为0.5KB。

startup_stm32fxxx.s文件是系统的启动文件,主要包括堆和栈的初始化配置、中断向量表的配置以及将程序引导到main( )函数等。

startup_stm32fxxx.s主要完成三个工作:栈和堆的初始化、定位中断向量表、调用Reset Handler

避免产生这类错误的产生,程序设计时就应该考虑变量大小和堆栈大小是否合适。一个是减少过大的临时变量和动态申请内存,另一个是在SRAM空间允许的情况下增大堆栈大小,如上图中栈大小是1024字节,堆大小是512字节。

我们知道,堆栈的设置是在startup_stm32fxxx.s中设置的,但是startup_stm32fxxx.s文件是只读的,无法修改,只需要设置一下该文件的属性,把只读取消即可修改。

另外,FLASH和SRAM起始地址在Options中可以查看:

还是在xxx.mp中,我们可以看到SRAM的分配,如下图所示。

从上图中可以看出SRAM空间用来存放:1.各个文件中声明和定义的全局变量、静态数据和常量;2.未初始化的全局变量;3.HEAP区;4.STACK区

堆在使用时会从低地址往上加,而栈是从__initial_sp开始往下减。以上图中的堆栈地址为例,malloc会从0x20000030开始往上加,局部变量的分配会从0x20000230开始往下减。如果入栈元素过大,使得入栈元素的地址访问到了0x20000260之后的内容,就发生了栈溢出,首先会改变堆中的元素值,如果入栈元素够大,可能会直接改变HEAP后面的全局变量。同理,当动态申请的内存过大时,堆中变量越界到栈中,此时就发送堆溢出。

【注】栈:向低地址扩展,堆:向高地址扩展。如果依次定义变量,先定义的栈变量的内存地址比后定义的栈变量的内存地址要大,先定义的堆变量的内存地址比后定义的堆变量的内存地址要小。

当然啦,如果使用J-link调试程序,也能查看堆栈大小,栈顶指针就是使用SRAM的大小。

【Tips】
1、堆栈的大小在编译器编译之后是不知道的,只有运行的时候才知道,所以需要注意一点,就是别造成堆栈溢出了,不然就会发生hardfault错误。

2、所有在处理的函数,包括函数嵌套,递归,等等,都是从这个“栈”里面,来分配的。所以,如果栈大小为2K,一个函数的局部变量过多,比如在函数里面定义一个char buf[512],这一下就占了1/4的栈大小了,再在其他函数里面来搞两下,程序崩溃是很容易的事情,这时候,一般你会进入到hardfault…。

3、 STM32的栈,是向下生长的。事实上,一般CPU的栈增长方向,都是向下的。而堆的生长方向,都是向上的。堆和栈,只是他们各自的起始地址和增长方向不同,他们没有一个固定的界限,所以一旦堆栈冲突,系统就到了崩溃的时候了。

4、程序中的常量,如果没加const也会编译到SRAM里,加了const会被编译到flash中。

2.3实例代码分析

前面分析了那么多,下面通过一个实例来验证前面的分析。

main.c函数代码如下:

/**
  ******************************************************************************
  * @file                main.c
  * @author              BruceOu
  * @lib version         V3.5.0
  * @version             V1.0
  * @date                2021-08-06
  * @blog                https://blog.bruceou.cn/
  * @Official Accounts   嵌入式实验楼
  * @brief               
  ******************************************************************************
  */
/* Includes*********************************************************************/
#include <stdlib.h>
#include "./USART1/stm32f103_usart1.h" 
#include "./LED/stm32f103_led.h"

uint8_t staic_buffer[10] = {0};//声明了一个初始化为0的全局数组,在静态常量区,0x2000 0000开头

void Delay( u32 xms);

/**
  * @brief  mian
  * @param  None
  * @retval int
  */
int main(void)
{
	uint8_t stack_i; //未初始化的局部变量,
	uint8_t stack_j = 1; //初始化的局部变量
	
	uint8_t *pHeap = (uint8_t *)malloc(200);//指针pHeap指向堆区分配了一个uint8_t类型10大小的空间
	
	/* USART1 配置模式为 115200 8-N-1,中断接收 */
	USART1_Config();
	
	NVIC_Configuration();

	/* LED 初始化 */
	LED_GPIO_Config();	 
 
	printf("全局变量 staic_buffer 的首地址:0x%x\\r\\n", staic_buffer);
	printf("未初始化的局部变量 stack_i 的地址:0x%x\\r\\n", &stack_i);
	printf("初始化的局部变量 stack_j 的地址:0x%x\\r\\n", &stack_j);
	
	printf("pHeap 在堆区首地址:0x%x\\r\\n", pHeap);
	
	free(pHeap);
	
	while (1)
	{
		LED1( ON );			  // 亮
		Delay(0xfFfff);
		LED1( OFF );		  // 灭

		LED2( ON );			  // 亮
		Delay(0xfFfff);
		LED2( OFF );		  // 灭

		LED3( ON );			  // 亮
		Delay(0xffFff);
		LED3( OFF );		  // 灭
	}
}

/**
  * @brief  延时函数
  * @param  
            xms 延时长度
  * @retval None
  */
void Delay( u32 xms)	
{
	//for(; nCount != 0; nCount--);(方法一)
	while(xms--);//(方法二)
}

/*********************************END OF FILE******************************/

编译后map文件中内存分配如下:

运行程序,打印信息如下:

data是初始化的全局变量,在.data区;buffer是未初始化的全局变量,在.bss区;pHeap是通过malloc分配的空间,在堆区;局部变量都在栈区。




欢迎访问我的网站

BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书


欢迎订阅我的微信公众号

以上是关于《嵌入式 - 深入剖析STM32》深入理解STM32内存管理的主要内容,如果未能解决你的问题,请参考以下文章

《嵌入式 - 深入剖析STM32》STM32 启动流程详解

《嵌入式 - 深入剖析STM32》详解STM32位带操作

《嵌入式 - 深入剖析STM32》STM32 启动流程详解(GCC)

《嵌入式 - 深入剖析STM32》STM32 启动流程详解(GCC)

《嵌入式 - 深入剖析STM32》STM32 启动流程详解(GCC)

深入理解STM32内存管理