STM32内存管理的那些事

1,370 阅读8分钟

一、认识Cortex-M3

一个集成芯片最苦逼最累的当属CPU莫属

而我们常见的STM32单片机的CPU就是Cortex‐M3处理器内核,Cortex‐M3 是一个32位处理器内核,内部的数据路径是32位的, 寄存器是32位的,存储器接口也是32位的,采用了哈佛结构(程序指令储存和数据储存分开的存储器结构\color{red}{程序指令储存和数据储存分开的存储器结构}),拥有独立的指令总线和数据总线,可以让取指与数据访问并行不悖。 内核是由ARM公司设计,只提供相应的内核架构,其他的总线、存储器、外设、时钟、I/O等都是芯片制造商设计开发的。下图就可以看明白内核与芯片的关系: 一个小小的CPU里面包括寄存器组(通用寄存器、程序计数寄存器、特殊功能寄存器等)、中断控制器、总线接口、MPU等。大致了解一下就行,如果感兴趣可以去看Cortex-M3权威指南。

重头戏:存储器映射

既然是32位的内核,理论上可以操作4G大小的存储器。可是,并不是所有的芯片制造商都买这个账,他们都有自己的想法,于是ARM就制造出来了一个存储器映射,就是说我把图纸给你画好,具体你怎么盖楼就看你了。

Cortex-M3支持4GB的存储空间,它的存储系统采用统一编址的方式; 程序存储器、数据存储器、寄存器被组织在4GB的线性地址空间内,以小端格式(little-endian)存放。由于Cortex-M3是32位的内核,因此其PC指针可以指向2^32=4G的地址空间,也就是0x0000_0000——0xFFFF_FFFF这一大块空间。见下图:

二、STM32的设计图

麻雀虽小,五脏俱全

ST大叔接过地图一看,嗯,不错,开始干吧!于是给老板一商量,先造出一个 STM32F103RCT6板看看吧,里面都有啥呢? *48KB SRAM、 256KB FLASH、 2 个基本定时器、 4 个通用定时器、 2 个高级定时器、 2 个 DMA 控制器(共 12 个通道)、 3 个 SPI、 2 个 IIC、 5 个串口、 1 个 USB、 1 个 CAN、 3 个 12 位 ADC、 1 个 12 位 DAC、 1 个 SDIO 接口及 51 个通用 IO 口。 *

三、STM32系统架构

CPU已经准备好了,存储器也准备好了,接下来就开始干活了。 小王:淦,这是啥啊。

ST大叔:设计的系统架构图呗!

小王:好复杂看不懂啊!

ST大叔:不要你看懂啊!你不是了解了Cortex-M3吗,再给你说说我这256KB的FLASH主要是存储程序代码的,48KB的SRAM主要是存储运行程序中的数据,然后其他的都是一些定时器外设什么,你后面开发的时候就了解了。

小王:那这些线是什么啊?

ST大叔:这些线就是CPU对这些外设通话的的电话线啊,要不然我这个CPU是干嘛的啊,难道你要教我做事?ARM老爸都把内核给我了,我还不好好用一下。你看,通过ICode连接FLASH取程序指令呗,通过Dcode连接SRAM存放中间数据呗,AHB系统总线连接其他外设呗,这个和时钟有关,后面给你讲解。还有系统矩阵调节DMA和CPU,DMA设计见下一篇博客。

小王:嗯,有点清楚了。那具体CPU和内存是怎么工作的呢?

ST大叔:。。。。。。。。。

四、存储器映像

ST大叔根据ARM老爸的图纸,把自己的内存按照一定的尺寸映射了进去。主要说明SRAM和FLASH的映射地址。

如图是一张stm32的内存映射图,其中代码区是从0x0800 0000开始的,他的结束地址是0x0800 0000加上实际芯片的flash大小,他的ram的起始地址是0x2000 0000,然后结束地址依然是加上芯片实际的ram大小。注意:代码区是在rom(flash)中,数据区(.bss段和.data段)在ram区.

一个程序本质上都是由 bss段、data段、text段三个组成的。

  • bss段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。bss段属于静态内存分配。

  • 数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

数据段+BSS段其实也就是全局区与静态区

  • 代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

  • 栈(stack):是用户存放程序临时创建的局部变量,自动创建的。

  • 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。程序员手动创建的。

  • 0X20000000是RAM开始地址。可以看到各段的分布。从低地址到高地址,分别为:data段,bss段,heap段,stack段。

  • 0X08000000是ROM(flash)开始地址。可以看到constdata(常量),和函数代码 下面一段话写的挺好就写了下来:

其中,data指的是初始化不为0的全局或静态(static)变量。bss指的是没有初始化,或者初始化为0的全局或静态变量。一开始我也不理解什么初始化之类,感觉莫名其妙甚至自相矛盾。其实知道为什么这么安排就很容易理解了。 因为全局与静态变量的初始值,是需要保存下来,其基本可以分为三大类,一种是等于0的,一种是不等于0,还有一种是没有初始值的。这些都是需要记录下来保存在镜像文件里面,再烧录到单片机的flash里(当然非要写到ram也可以)。为了节省空间,就把等于0的变量和没有初始值的变量归为一类,都当做初始值等于0的变量处理。因为这些变量的初始值都为0,所以记录也方便,节省不少空间。比如定义一个500字节的全局数组,要是初始值已经定义,那么镜像文件也需要相应大小的空间记录下来。但是如果全部为0,或者没有定义初始值。那么,从原理上来说,只需记录数组长度,赋予BSS“属性”,就OK得。 程序在开始运行的时候,一般要做内存搬运的操作,比如将data段,bss段,heap段,stack段初始化为对应的值。 由此也可知,在keil mdk中,没有初始值的全局变量或者静态变量,其实等于0。但局部普通变量并不是,一定要初始化再使用。

ST大叔:大体理解内存分配是怎么回事了吧

小王:嗯嗯,那MDK显示的内存又是什么意思啊?

五、MDK内存分配

MDK下Code, RO-data,RW-data,ZI-data这几个段:

  • Code是存储程序代码的。

  • RO-data是存储const常量和指令。

  • RW-data是存储初始化值不为0的全局变量。

  • ZI-data是存储未初始化的全局变量或初始化值为0的全局变量。

Flash=Code + RO-Data + RW-Data;

RAM= RW-data+ZI-data;

这个是MDK编译之后能够得到的每个段的大小,也就能得到占用相应的FLASH和RAM的大小,但是还有两个数据段也会占用RAM,但是是在程序运行的时候,才会占用,那就是堆和栈。在stm32的启动文件.s文件里面,就有堆栈的设置,其实这个堆栈的内存占用就是在上面RAM分配给RW-data+ZI-data之后的地址开始分配的。

堆:是编译器调用动态内存分配的内存区域。

栈:是程序运行的时候局部变量的地方,所以局部变量用数组太大了都有可能造成栈溢出。

堆栈的大小在编译器编译之后是不知道的,只有运行的时候才知道,所以需要注意一点,就是别造成堆栈溢出了。。。不然就等着hardfault找你吧。

六、总结

后面还会写一下操作系统的内存分配