深入理解Cortex-M内存管理(GCC)
ahcoder 2025-04-23 21:39 9 浏览
在讨论Cortex-M的内存之前,先来看看Cortex-M的存储器系统,我们知道,Cortex-M系列的处理器,大都可以对32的存储器进行寻址,因此存储器的寻址空间能够达到4G,这就意味着指定和数据共用相同的地址空间,也就是将程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内。数据字节以小端格式存放在存储器中。一个字里的最低地址字节被认为是该字的最低有效字节,而最高地址字节是最高有效字节。
1 Cortex-M存储器架构
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。
Table 1-1 存储器地址空间
地址范围 | 描述 |
0x0000 0000 ~ 0x2000 0000 | 根据启动引脚的状态决定哪个存储空间被映射到此处。 片内系统存储区起始地址:0x1fff 0000(2K字节的空间) |
0x2000 0000 ~ 0x4000 0000 | SRAM区,64K,其中位带别名区首地址为:0x2200 0000 |
0x4000 0000 ~ 0x6000 0000 | 用于片内外设,外设寄存器的别名区首地址:0x4200 0000 |
0x6000 0000 ~ 0x8000 0000 | |
0x8000 0000 ~ 0xa000 0000 | 片上flash存储区512M |
0xa000 0000 ~ 0xc000 0000 | |
0xc000 0000 ~ 0xe000 0000 | |
0xe000 0000 ~ 0xffff ffff |
对STM32存储器知识的掌握,实际上就是对Flash和SRAM这两个区域知识的掌握。由STM32的系统结构可以看出,Flash和SRAM这两个区域分别由ICode总线和DCode总线与处理器通信,以此完成相应的数据交换。
当然啦,其他Cortex-M的处理和STM32的也是类似的,比如GD32、CH32等。
下面将重点描述Flash和SRAM的知识。
1.1 Cortex-M的SRAM
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单片机内部的 RAM 为 SRAM。不同类型的Cortex-M单片机的SRAM大小是不一样的,但起始地址都是0x2000 0000,终止地址都是0x2000 0000+其固定的容量大小。SRAM相对容量小,速度快,掉电数据丢失,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据。设备断电后,SRAM中存储的数据就会丢失。
1.2 Cortex-M的Flash
Cortex-M的Flash,严格说,应该是Flash模块。该Flash模块包括:Flash主存储区(Main memory)、Flash信息区(Information block),以及Flash存储接口寄存器区(Flash memory interface)。三个组成部分分别在0x0000 0000——0xFFFF FFFF不同的区域。下面介绍STM32的Flash,如下表所示。
STM32的闪存模块由:主存储器、信息块和闪存储器块3部分组成。
主存储器,该部分用来存放代码和数据常数(如加const类型的数据)。对于大容量产品,其被划分为256页,每页2K,注意,小容量和中容量产品则每页只有1K字节。主存储起的起始地址为0X08000000,B0、B1都接GND的时候,就从0X08000000开始运行代码。
信息块,该部分分为2个部分,其中启动程序代码,是用来存储ST自带的启动程序,用于串口下载,当B0接3.3V,B1接GND时,运行的就这部分代码,用户选择字节,则一般用于配置保护等功能。
闪存储器块,该部分用于控制闪存储器读取等,是整个闪存储器的控制机构。
对于主存储器和信息块的写入有内嵌的闪存编程管理;编程与擦除的高压由内部产生。
在执行闪存写操作时,任何对闪存的读操作都会锁定总线,在写完成后才能正确进行,在进行读取或擦除操作时,不能进行代码或者数据的读取操作。
2 C程序内存分析
在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段不在可执行文件中,由系统初始化。
3 STM32程序的存储分配
3.1 程序所占RAM和Flash大小分析
为例调试方便,这里使用一个裸机串口例子,关于串口的使用请参看笔者博文:
串口通信:
https://bruceou.blog.csdn.net/article/details/79341769
使用GCC编译代码,编译信息如下:
其中:
- text 代表执行的代码,程序中所有的函数都位于此处。当然还包括RO-data(Read Only)代表只读数据,程序中所定义的全局常量数据和字符串都位于此处,如const型。
- data代表已初始化的读写数据,程序中定义并且初始化的全局变量和静态变量位于此处。
- bss代表未初始化的读写数据,程序中定义了但没有初始化的全局变量和静态变量位于此处。GCC编译器默认是把你没有初始化的变量都赋值为例0。即上述的bss段。
值得注意的是,这些参数的单位是Byte。
text和data两个段需要烧录到FLASH等非易失性器件中。
data段需要烧录到FLASH中,而bss段则不用,但在运行时,它们都必须装载到可读可写的RAM中。
因此我们可以计算出FLASH和RAM的大小:
Flash = test + data
RAM = data + bss
这就要涉及到程序的两种状态:加载域和运行域。
加载域:向Flash中下载程序时,其实仅仅下载的是text+data的内容,意思就是说,在掉电情况下,Flash里面的内存仅包含text+data的内容。
运行域:当上电后,程序运行时,首先程序会从特定的地址进行启动,启动时会将data的数据加载到SRAM中,单片机的test区域不需要加载到 SRAM,内核直接从 FLASH 读取指令运行。那bss的数据怎么办呢?对于初始值为0全局变量来说,因为要在Code区要调用该全局变量,所以肯定要对其进行描述,程序运行时就知道了,原来你是初始值为0的全局变量呀,然后就在SRAM区给你分配了一段固定区域的地址;对于局部变量来说,会自动分配大小。bss有统计作用,并且SRAM中一段特定的区域是运行bss数据,data +bss就是程序运行总共会占用SRAM的长度,生成局部变量的栈空间包含在bss区的范围。
3.2 程序堆栈使用分析
我们知道,程序运行需要占用的大小是RAM = data + bss,而堆栈的大小是程序开始运行后才能确定的。
那么堆和栈到底能占用多大呢,堆栈的大小是在STM32F103ZETx_FLASH.ld中设置的,这里以STM32F103ZET6为例进行分析,其内部栈的大小为1KB,堆的大小为0.5KB。
使用objdump查看elf文件:
堆栈段起始地址为0x2000 002c,大小为0x604,这0x4又是怎么来的?这里查看map文件。
堆占用了0x200字节,栈占用了0x400字节,而剩下的0x4字节来自于 ALIGN(0x8),即8字节对齐,因为堆栈段紧跟.bss段之后,那首地址应该是0x2000 0070,但是规定了8字节对齐,所以最小为32,即需要补上4个字节,所以堆栈段起始地址应该是0x2000 00F4。
【注】栈:向低地址扩展,堆:向高地址扩展。如果依次定义变量,先定义的栈变量的内存地址比后定义的栈变量的内存地址要大,先定义的堆变量的内存地址比后定义的堆变量的内存地址要小。
【Tips】
1、堆栈的大小在编译器编译之后是不知道的,只有运行的时候才知道,所以需要注意一点,就是别造成堆栈溢出了,不然就会发生hard fault错误。
2、所有在处理的函数,包括函数嵌套,递归,等等,都是从这个“栈”里面,来分配的。所以,如果栈大小为2K,一个函数的局部变量过多,比如在函数里面定义一个char buf[512],这一下就占了1/4的栈大小了,再在其他函数里面来搞两下,程序崩溃是很容易的事情,这时候,一般你会进入到hardfault…。
3、STM32的栈,是向下生长的。事实上,一般CPU的栈增长方向,都是向下的。而堆的生长方向,都是向上的。堆和栈,只是他们各自的起始地址和增长方向不同,他们没有一个固定的界限,所以一旦堆栈冲突,系统就到了崩溃的时候了。
4、程序中的常量,如果没加const也会编译到SRAM里,加了const会被编译到flash中。
3.3 实例代码分析
前面分析了那么多,下面通过一个实例来验证前面的分析。
main.c函数代码如下:
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <stdlib.h>
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart1;
/* USER CODE BEGIN PV */
uint8_t buffer[10];//声明了一个初始化为0的全局数组
uint8_t data = 1;//初始化的全局变量
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
uint8_t stack_i; //未初始化的局部变量,
uint8_t stack_j = 1; //初始化的局部变量
uint8_t *pHeap1 = (uint8_t *)malloc(10);//指针pHeap指向堆区分配了一个uint8_t类型10大小的空间
uint8_t *pHeap2 = (uint8_t *)malloc(10);
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
printf("First address of uninitialized global variable buffer: %p\r\n", buffer);
printf("Address of initialized global variable data: %p\r\n", &data);
printf("Address of uninitialized local variable stack_i: %p\r\n", &stack_i);
printf("Address of uninitialized local variable stack_j: %p\r\n", &stack_j);
printf("The first address of pHeap1 in the heap: %p\r\n", pHeap1);
printf("The first address of pHeap2 in the heap: %p\r\n", pHeap2);
free(pHeap1);
free(pHeap2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
HAL_Delay(500);
}
/* USER CODE END 3 */
}
编译后内存分配如下:
运行程序,打印信息如下:
data是初始化的全局变量,在.data区;buffer是未初始化的全局变量,在.bss区;pHeap是通过malloc分配的空间,在堆区,逐渐增加;局部变量都在栈区,增加减小。
相关推荐
- Linux 6.15将更新媒体子系统:高通Iris驱动首次亮相
-
IT之家3月24日消息,科技媒体phoronix昨日(3月23日)发布博文,报道称Linux6.15内核将于本周启动合并窗口,首批提交的PullRequest请求之一,就...
- Ubuntu 25.04发行版登场:Linux 6.14内核,带来多项技术革新
-
IT之家4月18日消息,科技媒体linuxiac昨日(4月17日)发布博文,报道称代号为PluckyPuffin的Ubuntu25.04发行版正式上线,搭载最新Linu...
- WordPress 6.8 版本发布:聚焦性能升级,代号“Cecil”
-
WordPress6.8版本发布:聚焦性能升级,代号“Cecil”全球最受欢迎的内容管理系统(CMS)WordPress正式推出最新版本6.8,代号“塞西尔”(Cecil),以此致敬传奇钢琴家...
- Linus Torvalds接受微软Hyper-V升级 下一代Linux启动会更快
-
虽然Windows的粉丝和Linux的粉丝经常喜欢进行激烈的键盘大战,但操作系统的制造商们自己也了解彼此的优缺点。毫无疑问,微软也明白这一点,事实上,它甚至鼓励用户尝试Linux,尽管是使用...
- Debian 12发布:Linux内核升级6.1(debian更新内核)
-
IT之家6月11日消息,Debian是最古老的GNU/Linux发行版之一,也是许多其他基于Linux的操作系统的基础,包括Ubuntu、Kali、MX和树莓派OS等。这...
- Linux Mint预告新功能:升级Nemo搜索、LMDE 7支持OEM安装
-
IT之家4月10日消息,LinuxMint发布了最新月度简讯,宣布增强Nemo文件管理器的文件搜索功能、Cinnamon桌面环境开始支持Wayland、LMDE7(LinuxM...
- Linux 6.2合并大量网络系统更新:推进Wi-Fi 7和800 Gbps网络
-
IT之家12月15日消息,Linux6.2合并窗口期内已经确认将会合并大量网络子系统更新。和以往版本相同,Linux6.2内核更新周期在网络功能上有大量的改进,更多的细节可以访问这条...
- Linux内核升级实践指南(linux内核怎么升级)
-
Linux内核升级是一个需要谨慎操作的过程,但掌握正确方法后可以显著提升系统性能、安全性或硬件兼容性。以下是一份详细的实践指南,涵盖主流方法及注意事项:一、准备工作查看当前内核版本bashuname...
- Linux又将迎来大版本更新 5.20版可能会被称为Linux 6.0
-
如果你错过了昨天Linux5.19的发布公告,那么这一消息需要回顾一下:被称为Linux5.20的开发中的内核很可能会被称为Linux6.0。根据LinusTorvalds对现代版本的划分,L...
- 周六的娱乐就是安装一台OracleLinux虚拟机(一)
-
每个人有每个人的娱乐方式。老父亲可能喜欢摆弄一些机械工具修修补补。我趁着周六安装一套OracleLinux,寓学于乐。在oracle网站上,下载VirtualBox,现在最新版本是7.0.8。在安装过...
- Linux系统下Gaussian 16的安装(linux系统如何安装新软件)
-
Gaussian是目前使用最多的量子化学计算软件,当前最新的版本为Gaussian16C.01。理论上来说Gaussian并不需要安装,因为高斯一般是不出售源代码的,只有二进制文件,以Gaussi...
- 「Linux基础」VMWare虚拟机安装CentOS后配置静态ip
-
在VMWare中安装本地虚拟机CentOS7操作系统,动态IP地址会经常变化,不便于管理与实验。为了便于实验,考虑设置成静态IP地址,目标是本地局域网可以通过NAT网关互相访问,且能连接互联网。由于是...
- Win10虚拟机,Hyper-V安装CentOS,一次成功
-
打开控制面板,选择程序选择启用或关闭Windows功能选中Hyper-V下所有的组件。提示我们需要,重启计算机,保存我们的文件,之后点击“立即重新启动”选择开始菜单,输入Hy,就可以打开虚拟机管理器了...
- 如何在Windows 10中的Hyper-V虚拟机上安装CentOS Linux
-
注意双重引导的一种可行且相当不错的替代方法是在虚拟机上安装各种操作系统。Microsoft正式支持CentOS作为Hyper-V的来宾OS,并且在安装时可以很好地集成。自CentOS6.4版以来,用...
- 安装Linux虚拟机的5个理由,以及不安装的5个原因
-
虚拟机(VM)允许你在不永久更改计算机的情况下探索Linux。如果你对Linux很好奇,但犹豫是否要尝试,VM提供了一个安全、灵活的解决方案。如果这是你第一次听说虚拟机,虚拟机就像“计算机中的计算机”...
- 一周热门
- 最近发表
-
- Linux 6.15将更新媒体子系统:高通Iris驱动首次亮相
- Ubuntu 25.04发行版登场:Linux 6.14内核,带来多项技术革新
- WordPress 6.8 版本发布:聚焦性能升级,代号“Cecil”
- Linus Torvalds接受微软Hyper-V升级 下一代Linux启动会更快
- Debian 12发布:Linux内核升级6.1(debian更新内核)
- Linux Mint预告新功能:升级Nemo搜索、LMDE 7支持OEM安装
- Linux 6.2合并大量网络系统更新:推进Wi-Fi 7和800 Gbps网络
- Linux内核升级实践指南(linux内核怎么升级)
- Linux又将迎来大版本更新 5.20版可能会被称为Linux 6.0
- 周六的娱乐就是安装一台OracleLinux虚拟机(一)
- 标签列表
-
- linux 远程 (37)
- u盘 linux (32)
- linux 登录 (34)
- linux 路径 (33)
- linux 文件命令 (35)
- linux 是什么 (35)
- linux 界面 (34)
- 查看文件 linux (35)
- linux 语言 (33)
- linux代码 (32)
- linux 查看命令 (33)
- 关闭linux (34)
- root linux (33)
- 删除文件 linux (35)
- linux 主机 (34)
- linux与 (33)
- linux 函数 (35)
- linux .ssh (35)
- cpu linux (35)
- 查看linux 系统 (32)
- linux 防火墙 (33)
- linux 手机 (32)
- linux 镜像 (34)
- linux mac (32)
- linux ip地址 (34)