分类: C/C++
2015-10-15 16:03:20
原文地址:写给过去的自己-No.3-内存管理篇-KL25内存结构浅析 作者:ianhom
过去的自己,你好!
又是好久没有写东西了,工作和生活都很忙,总是没空思考。你以后会变得和我一样忙碌,千万不要安逸,因为所谓的“艰难困苦”来的比你想象的快得多。Life is tough, you must be tougher。
今天就基于KL25系列的MCU向你介绍下内存结构。你以后会在工作中会使用模块化、面向对象方式的编程编写的你产品代码,不过那个时候你没有留意过内存的使用情况,只能说你比较幸运没有遇到问题而已。不过很快你就会遇到一个内存不够用的情况,然后你就会来思考内存里都存了什么。
首先我介绍下分析环境:使用的平台是FRDM-KL25Z,128k FLASH +
16K RAM,编译环境为IAR,编译器优化等级为NONE,通过IAR的.icf文件可以看出KL25芯片地址资源的分配情况。如下图所示,flash区域从0x00000000地址开始,大小为128k;RAM区域由两个部分组成,由0x20000000这个地址分开,RAM1区域在0x20000000地址之前,大小为1/4个总RAM大小(16k),即总RAM区域的起始地址为0x1FFFF000;RAM2区域在0x20000000地址之后,大小为3/4个总RAM大小,即总RAM区域的结束地址为0x20003000。这样我们就是知道要分析的RAM的地址空间了(0x1FFFF000~0x20003000),那这个RAM里又是怎样的结构模型呢
RAM的结构模型大致分为7个部分:
RAM |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
下面结合IAR调试环境讲一下各个区域到底存放了什么
中断向量表:
为了快速响应中断操作,原本在0x00000000的中断向量表也复制了一份放到RAM区域中,大小为0x410字节。通过IAR的Symbolic Memory窗口可以观察到0x00000000~0x00000410区域的数据与0x1FFFF000~0x1FFFF410区域的数据是一致。
中断向量表是在startup阶段从FLASH复制到RAM中的,在startup.c文件中有:
void common_startup(void)
{
………………………….
…………………………
#elif (defined(IAR))
/* Addresses for VECTOR_TABLE and VECTOR_RAM come from the linker file */
extern uint32 __VECTOR_TABLE[]; //在ICF文件中定义,地址为0x00000000
extern uint32 __VECTOR_RAM[]; //在ICF文件中定义,地址为0x1FFFF000
……………………….
……………………….
/* Copy the vector table to RAM */
if (__VECTOR_RAM != __VECTOR_TABLE)
{
for (n = 0; n < 0x104; n++)
__VECTOR_RAM[n] = __VECTOR_TABLE[n];
}
………………………
}
这里稍微扩展说一下中断向量表里的几个向量。地址为0x00000000的值为0x20002FF8,这个值其实是栈指针SP,地址为0x00000004的值为0x00000411,这个值其实是程序计数寄存器PC。MCU上电后直接通过这个PC值就可以跳转到执行代码,如图所示:
至于为什么PC值为0x00000411而不是0x00000410,可以参考下M0权威指南。
0x00001F31是default_isr中断向量,若没有提供中断处理函数,但启动并触发了硬件中断,则会转入该default_isr函数中;若需要提供相应的中断处理函数,仅需在ISR.H文件中修改即可。
所以,RAM区域第一块内容为中断向量表,区域为0x1FFFF000~0x1FFF410,大小为0x410字节。
RAM |
中断向量表 |
|
|
|
|
|
|
|
|
|
|
|
|
|
在RAM中运行的代码:
代码一般是储存在FLASH中,但有些场合需要将代码加载到RAM中运行以获得更快的响应。在IAR环境中,在函数之前添加“__ramfunc”即可实现将该函数放到RAM中运行。因为RAM掉电后里面的内容就会丢失,不能永久性存放代码,所以函数其实还是存放在FLASH中,在上电启动的时候将flash中的代码复制到RAM区域中,同样在在startup.c文件的common_startup()函数实现复制:
void common_startup(void)
{
………………….
………………….
/* Get addresses for any code sections that need to be copied from ROM to RAM.
* The IAR tools have a predefined keyword that can be used to mark individual
* functions for execution from RAM. Add "__ramfunc" before the return type in
* the function prototype for any routines you need to execute from RAM instead
* of ROM. ex: __ramfunc void foo(void);
*/
#if (defined(IAR))
uint8* code_relocate_ram = __section_begin("CodeRelocateRam");
uint8* code_relocate = __section_begin("CodeRelocate");
uint8* code_relocate_end = __section_end("CodeRelocate");
/* Copy functions from ROM to RAM */
n = code_relocate_end - code_relocate;
while (n--)
*code_relocate_ram++ = *code_relocate++;
#endif
}
在测试代码中,我使用PrintTest函数作为例子
__ramfunc void PrintTest() //函数在RAM中运行
{
Global_Var1++;
}
在从flash复制代码到RAM的环节中我们可以看到代码存放的位置,如图所示,PrintTest函数存放在地址为0x2078的flash区域中,而复制的目的地址为0x1FFFF410,这正好是RAM里中断向量表之后的RAM区域。
程序继续运行完复制后,即可发现,0x1FFFF410~0x1FFFF420之间存放的就是PrintTest函数的代码。通过仿真观察汇编执行情况就可以看到执行PrintTest时就是在这个RAM区域。
所以,RAM区域第二块内容为运行在RAM上的代码段,区域为0x1FFFF410~0x1FFF420,大小为0x10字节。
RAM |
中断向量表 |
RAM运行代码段 |
|
|
|
|
|
|
|
|
|
|
|
|
已初始化的全局/静态数据区:
全局变量是定义在函数以外的变量,其生命周期是程序运行整个过程,作用域是自定义位置之后有效;
静态变量是通过static修饰的全局变量或局部变量,静态全局变量的和全局变量一样,唯一的区别是静态全局变量只能本文件使用;静态局部变量的作用域与局部变量一样,但生命周期和存放位置和全局变量是一样的;
出于优化目的,由const的修饰的常量一般都被存放在FLASH中,因为这些常量不能修改只能读取,所以放在flash区域可以节约RAM的消耗,若需要将常量定义在RAM上,需要增加volatile修饰。
上述的全局变量、静态全局变量、静态局部变量和RAM中的常量全部都放在RAM的全局/静态数据区。RAM的全局区/静态数据区根据变量/常量的初始化情况分为已初始化全局/静态数据区和未初始化全局/静态数据区。在测试程序中我定义了如下变量和常量:
unsigned char Global_Var1 = 0x55; //在已初始化的全局区域
unsigned char Global_Var2 = 0; //在未初始化的全局区域
const unsigned char Const_Global_Var1 = 0xAA; //在flash区域
const unsigned char Const_Global_Var2 = 0; //在flash区域
volatile const unsigned char Volatile_Const_Global_Var1 = 0xCC; //在已初始化的全局区域
volatile const unsigned char Volatile_Const_Global_Var2 = 0; //在未初始化的全局区域
void TestFn()
{
unsigned char Local_Var1 = 2; //在栈区域
unsigned char Local_Var2 = 0; //在栈区域
static unsigned char Static_Globle_Var1 = 3; //在已初始化的全局区域
static unsigned char Static_Globle_Var2 = 0; //在未初始化的全局区域
……..
}
全局变量Global_Var1的值为0x55,定义在RAM中的常量Volatile_Const_Global_Var1的值为0xCC,定义在子函数中的静态局部变量Static_Globle_Var1的值为0x03;通过仿真观察上述变量/常量的地址为0x1FFF420,紧接着RAM代码段之后
从上图可以看出,虽然全局变量Global_Var1和Global_Var2在程序中是一起定义的,但是因为Global_Var1定义时初始化为非0的值,所以Global_Var2并没有与Global_Var1定义在同一个RAM区域(Global_Var2定义在未初始化全局/静态数据区);对于静态局部变量Static_Globle_Var1虽然是局部变量,但是也定义在了已初始化全局/静态数据区;如前述const修饰的常量定义在了flash区域中(即使编译优化等级为NONE),而RAM中常量Static_Globle_Var1也定义在了已初始化全局/静态数据区。
上图是运行到main以后的RAM视图,可以发现他们的值已经被初始化,而RAM掉电后数据会丢失,那这里的0x55,0xCC,0x03都保存在哪里呢?这里再次回到startup.c文件的common_startup()函数中:
void common_startup(void)
{
……….
#elif (defined(IAR))
data_ram = __section_begin(".data");
data_rom = __section_begin(".data_init");
data_rom_end = __section_end(".data_init");
n = data_rom_end - data_rom;
#endif
……..
/* Copy initialized data from ROM to RAM */
while (n--)
*data_ram++ = *data_rom++;
……..
}
通过仿真该程序可知,初始化的数据存放在0x00002164的flash区域,目的地址为0x1FFFF420的RAM区域,数据内容为0x55,0xCC,0x03。如下图
所以,RAM区域第三块内容为已初始化的全局/静态数据区,区域为0x1FFFF420~0x1FFF424,大小为0x4字节。
RAM |
中断向量表 |
RAM运行代码段 |
|
已初始化的全局/静态数据区 |
|
|
|
|
|
|
|
|
|
|
未初始化的全局/静态数据区:
前面已经介绍了已初始化的全局变量、RAM中的常量、静态局部变量的存放地址。相对的,未初始化的全局变量、RAM中的常量、静态局部变量存放在未初始化的全局/静态数据区。虽说是未初始化,但不代表这些数据就是掉电后RAM中的随机数据,而是在程序运行后全部赋值为0。通过测试程序观察RAM如图:
因为在运行到main函数之前,有部分硬件初始化代码会定义部分全局/静态变量,所以测试程序定义的全局变量Global_Var2、RAM中的常量Volatile_Const_Global_Var2、定义在子函数中的静态局部变量Static_Globle_Var2是定义在非用户定义未初始化全局/静态变量之后的。由上图可以看出这些为初始化的变量/常量地址从0x1FFFF424开始,紧接着已初始化全局/静态数据区之后。
再来看一下数值0是何时被赋值的,再看startup.c文件的common_startup()函数中:
Void common_startup(void)
{
……….
#elif (defined(IAR))
bss_start = __section_begin(".bss");
bss_end = __section_end(".bss");
#endif
/* Clear the zero-initialized data section */
n = bss_end - bss_start;
while(n--)
*bss_start++ = 0;
……..
}
通过仿真可以看到,未初始化的全局/静态数据区地址为0x1FFFF424
所以,RAM区域第四块内容为未初始化的全局/静态数据区,区域为0x1FFFF424~0x1FFF453,大小为0x30字节。
RAM |
中断向量表 |
RAM运行代码段 |
|
已初始化的全局/静态数据区 |
|
未初始化的全局/静态数据区 |
|
|
|
|
|
|
|
|
堆区域:
说到内存大家经常提到堆栈,其实堆和栈是两个东西。平日所说的堆栈其实指的是栈(STACK),而堆(HEAP)是用于用户自定义使用的RAM区域。前面讨论的各种变量都是在编译时由编译器分配内存地址,而如果用户想在程序运行时分配内存地址(内存动态分配)的话,就需要使用到堆区域。堆区域的大小在.ICF中定义(__ICFEDIT_size_heap__ = (2*1024)),使用方法是通过malloc函数进行分配,堆区域的位置可以通过编译后的.MAP文件查看(很多信息都可以在.MAP里查看)。由下图可知,堆区域为0x1FFFF458 ~ 0x1FFFC58,堆起始地址没有紧接着未初始化全局/静态数据区,而是从0x1FFFF458这个8字节对齐的位置开始,是因为在.ICF文件中如如下定义:
define block HEAP with alignment = 8, size = __ICFEDIT_size_heap__ { };
通过测试程序来观察堆的操作:
void TestFn()
{
……..
unsigned char *pHeap_Var = NULL; //pHeap_Var这个指针变量在栈区域
pHeap_Var = malloc(sizeof(unsigned char)); //数据在堆区域
*pHeap_Var = 4;
……..
printf("The Heap_Var = %d, the address is 0x%x, the address of point is 0x%x\n\n " , *pHeap_Var, pHeap_Var,&pHeap_Var);
……..
}
如下图,malloc返回的地址0x1FFFFC48在堆区域中,其值为0x04。你可能会问,为什么是从堆的高地址开始分配内存,这里扩展讲一下:空堆中如何分配内存(所谓的堆增长方向)是由malloc函数决定的,KL25+IAR开发环境中使用的是自己的malloc函数,malloc时是从高地址向低地址分配;而C标准库中的malloc是从低地址向高地址分配的。
所以,RAM区域第五块内容为堆区域,区域为0x1FFFF458~0x1FFFC58,大小为0x800字节。
RAM |
中断向量表 |
RAM运行代码段 |
|
已初始化的全局/静态数据区 |
|
未初始化的全局/静态数据区 |
|
堆区域 |
|
|
|
|
|
|
栈区域:
栈区域是存放局部变量,子函数的返回地址和参数的区域,增长方向是从高地址向低地址增长(由硬件决定)。栈的大小和位置是在.ICF文件中定义,如图所示,栈大小为1k字节,栈的位置是由栈顶__BOOT_STACK_ADDRESS决定的。
这里可能和其他环境不同:如果栈的位置由栈底决定,这样在堆区域之后指定栈底地址,这样栈区域就紧接着堆区域,RAM的空白区就集中在RAM的高地址区域;而对于KL25+IAR环境,栈的位置是由栈顶决定,这个栈顶在默认的.ICF文件中被定义在RAM最高地址-8的位置上,也就是说栈没有紧接着堆区域来安排,而是贴着RAM最高地址的区域安排,这样在堆和栈之间就会有未使用的RAM空白区域。如果栈顶未定义在RAM最高地址-8的位置,比如RAM最高地址-0x80,则会在RAM最高地址区域留下0x80个字节的空白区域。至于为什么默认的.ICF将栈顶定义在RAM最高地址-8的位置,还没有找到明确的说明。我试过将栈顶直接定义在RAM的最高地址后运行暂无异常。(M0内核压栈是,SP先-4,然后再将数据压到SP所指的地址上)
通过测试程序,可以看到局部变量在栈中的位置。
void TestFn()
{
unsigned char Local_Var1 = 2; //在栈区域
unsigned char Local_Var2 = 0; //在栈区域
static unsigned char Static_Local_Var1 = 3; //在已初始化的全局区域
static unsigned char Static_Local_Var2 = 0; //在未初始化的全局区域
unsigned char *pHeap_Var = NULL; //pHeap_Var这个指针变量在栈区域
……
}
上图可见初始化的局部变量Local_Var1和未初始化的局部变量Local_Var2都定义在栈区域上,地址分别为0x20002FD9和0x20002FD8
所以,RAM区域第六块内容为栈区域,区域为0x20002BF8~0x20002FF8,大小为0x400字节。剩下的区域就是未使用的空白区,大小约12k字节。至此KL25在IAR环境下的RAM结构模型如下表所示。
RAM |
中断向量表 |
RAM运行代码段 |
|
已初始化的全局/静态数据区 |
|
未初始化的全局/静态数据区 |
|
堆区域 |
|
空白区(约12k字节) |
|
栈区域 |
|
空白区(8个字节) |
上述分析仅为KL25在IAR环境的一个实例,不同的芯片在不同的环境下或许有不同的组织方式,但大体的模型应该是类似的。对RAM的结构有了大概的了解后,能更好的使用RAM资源,同时有助于提高代码质量。RAM中每块区域都有值得深入研究的细节,以后在做探讨。