单片机存储分配

前言:
  RAM和Flash是单片机重要的数据存储介质,在使用MDK编译时我们也会看到编译结果中会显示’Code、RO-data、RW-data、ZI-data’的使用大小。单片机到底是怎么划分这些存储区域,变量和代码是怎么存储的呢?

1 单片机存储区域的划分

  单片机对地址的划分规则(按照地址排序)一般如下:
高地址

地址划分区域 含义
命令行及环境参数(高地址) main函数是可以有参数的,而传入的参数值将被存储在该区域。
STACK(栈) 由编译器决定分配和释放,存放函数的参数变量及局部非静态变量等。属于动态内存分配。
HEAP(堆) 通常由程序员;申请和释放,若忘记释放则在程序结束时释放。属于动态内存分配。
.bss段(未初始化数据段) 通常用来存放程序中未初始化的全局变量和静态变量,也称为.bss段(Block Started by Symbol),属于静态内存分配。
.data段(初始化数据段) 通常用来存放程序中已初始化的全局变量和静态变量,属于静态内存分配。
Code段 对应的存储介质为ROM。存储二进制程序执行源代码的区域,占用空间在程序运行前就已确定,通常是只读的防止程序被篡改,但也有某些单片机架构允许代码段可写来修改程序。程序常量也存储在该区域。
中断向量(低地址) 向量表通常存储在存储空间的首地址,包含了系统中可用异常(中断)的异常(中断)向量,以及主栈指针(MSP)的初始值。

MCU_memory_allocation.png

1.1 命令行及环境参数

  main函数是可以接受参数输入的,我们可以将main函数的参数分为两类:

  • 命令行参数
    • main函数的第一个参数,常命名为argc或ac,表示命令行参数的数量。
    • main函数的第二个参数,常命名为argv或av,是指向命令行(c字符串)的指针数组。
  • 环境变量
    • main函数的第三个参数,常命名为envp或env,是指向环境变量(c字符串)的指针数组。

1.2 栈(又称堆栈)和堆

1.2.1 栈

  由编译器决定分配和释放,用于存放函数局部非静态变量和函数参数变量等,是动态的内存分配。与栈操作是对应的,当函数被调用时,分配局部非静态变量和函数参数变量对应了入栈操作,而这些变量的释放对应了退栈操作。因此局部非静态变量和函数参数变量的访问涉及到入栈和退栈操作,相对全局或静态变量会更低效。
  还需要解释下为何局部非全局变量和函数参数变量的内存分配和释放是由编译器决定的。程序运行时要使用的栈空间,在编译时就已经由编译器决定好了要分配多少,何时分配和销毁,这部分栈空间的分配和释放的代码在编译后就已经生成好了,只是在运行时执行了它而已。

1.2.2 堆

  堆是用于存放进程运行中被动态的分配内存段,它也是动态的内存分配。当进程调用malloc、free等内存分配和释放函数时,这部分内存就处于堆上。

1.2.3 栈和堆的增长

  栈和堆的内存分配都是动态的,也即实际的空间大小随着程序的运行是会变化的。一些程序对栈的消耗较大,一些程序对堆的消耗较大,为了节省空间,往往将栈设计为地址向低地址增长,堆则地址向高地址增长。也即在物理上栈和堆其实共享一块存储区域,只是首地址不同,地址增长方向相反。

Stack_and_heap_memory_growth.png

1.3 .bss段(未初始化数据段)

  未初始化数据段也即.bss段,通常是指用来存放程序中未初始化的全局变量和静态变量。.bss段为静态内存分配。

1.4 .data段(初始化数据段)

  通常用来存放已初始化的全局变量和静态变量,也属于静态内存分配。
  这里有个疑问,为什么要把全局变量和静态变量按照初始化和未初始化分开存储呢?变量的初始值是由启动代码完成的,为了使启动代码简化,编译器会把全局或静态变量按照是否初始化分段划分,.data段的的映像包含了各个变量的初始值,被保存在“只读数据段”(对应ZO-data,存储在ROM中),这样启动代码就可以简单的直接按照映像复制数据到.data段,这样所有需要初始化的全局或静态变量就都初始化了。.bss段未初始化的变量,启动代码通常会对其清0操作。
  还有一些细节需要了解。一些编译器,对于初始化值为0的全局或静态变量编译器仍然将其分配到.bss段。一些编译器,对于定义但未使用的变量,不会为其分配变量空间。

1.5 代码段

  这部分存储区域用来存放程序执行代码,大小在编译后就已经确定,往往也是占用存储空间最大的部分。通常这部分存储区域是只读的,为了防止程序被篡改。

1.6 中断向量

  单片机的实时处理离不开中断的触发,中断向量通常存储在空间的首地址处。这部分区域还包含了系统中可用异常(中断)的异常(中断)向量,以及主栈指针(MSP)的初始值。

Interrupt_vector_table.png

2 Code、RO-data、RW-data、ZI-data

  MDK编译通过后,在“Build Output”窗口会显示Code、RO-data、RW-data、ZI-data几种类型的大小(单位是Byte)。为了对程序存储空间使用情况有更具体的了解,我们需要能看懂这些编译结果。

2.1 Code

  代码执行程序占用的空间大小,通常是存储空间消耗的“大头”部分。

2.2 RO-data

  Read only data,即只读数据域。它指程序中只读数据占用的存储空间大小,这些数据存储在ROM中(一般为单片机内部Flash)。这些数据包括字符串常量和数值常量,比如C中被const修饰的变量。由于ROM的访问速度比RAM慢,因此常量数据的访问速度要低于变量。

2.3 RW-data

  Read write data,即可读写数据域。指经过初始化(指非0值)的全局或静态变量占用的存储空间大小,程序加载前存储在ROM,加载后存储在RAM,因此可以在程序运行中修改值。

2.4 ZI-data

  Zero initialie data,即0初始化区域。指未经过初始化或初始化值为0的变量占用的存储空间大小,与RW-data类似,加载前存储在ROM中,加载后存储在RAM,因此可以在程序运行中修改值。它们的区别在于是否被初始化或初始值是否为0,这样划分是为了简化启动代码的设计(划分的原因上面讲过了)。

2.5 程序RAM和ROM使用大小

  通过MDK我们可以了解单片机的空间使用情况如:

程序状态与区域 组成
程序执行时的只读区域(RO) Code + RO-data
程序执行时的可读写区域(RW) RW-data + ZI-data
程序存储时占用的ROM区域 Code + RO-data + RW-data

  我们较常关注的是ROM和RAM占用的大小:

空间资源 组成
RAM RW-data + ZI-data
ROM Code + RO-data + RW-data

3 关于运行状态和掉电状态下的存储

  RAM具有高速读写的特性,但由于价格高使得空间资源更加有限,掉电数据丢失。而ROM读写速度比RAM慢,但价格更低空间资源更大,且具有掉电保存的特点。为了更充分的利用资源,程序在运行和掉电状态下的存储是不同的。
  程序是保存在掉电不丢失的ROM中的,通常单片机程序运行是在ROM中运行程序(也有一些单片机能把部分程序加载到RAM运行,加快运行速度,但是掉电后将会丢失而且RAM空间非常有限)。在单片机开始运行时,内核在执行主体程序前会先执行启动代码(启动代码是固化在单片机内部ROM的,一般情况下用户访问不到也修改不了),启动代码会将RW-data从ROM加载到RAM,这时分为有初始化.data段和未初始化的.bss段,对.data段进行初始化加载值,对.bss段统一清零。常量存储的RO-data区也保存在ROM中,程序运行时直接从ROM读取。
  因此就不难理解RW-data的使用大小既要算到RAM里也要算到ROM里,Code和Ro-data的大小包含在ROM中,ZI-data大小包含在RAM中。