1, 内存管理简介
内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种,他们其实最终都是要实现2个函数:malloc和free;malloc函数用于内存申请,free函数用于内存释放。
先回顾一下c语言知识:计算机内存一般分为静态存储区用以存储全局变量或常量和动态存储区用以存储函数内部变量或形参或函数运算结果。malloc()函数的作用是请求系统在内存的动态存储区分配若干个字节的存储空间,函数的返回值是首字节地址,可见malloc()函数是指针类型。free(P)的作用是释放指针变量P所指向的动态空间。
本章,我们介绍一种比较简单的办法来实现:分块式内存管理。下面我们介绍一下该方法的实现原理,如图所示(示意图):
内存块1 内存块2 内存块3 ……内存块n 内存池 | | | | 第1项 第2项 第3项 ……第n项 内存管理表 <<-----分配方向 | malloc,free等函数
图解:从上图可以看出,分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为n块,对应的内存管理表,大小也为n,内存管理表的每一个项对应内存池的一块内存。
内存管理表的项值代表的意义为:当该项值为0的时候,代表对应的内存块未被占用,当该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。比如某项值为10,那么说明包括本项对应的内存块在内,总共分配了10个内存块给外部的某个指针。
内寸分配方向如图所示,是从顶à底的分配方向。即首先从最末端开始找空内存。当内存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。
分配原理:
当指针p调用malloc申请内存的时候,先判断p要分配的内存块数(m),然后从第n项开始,向下查找,直到找到m块连续的空内存块(即对应内存管理表项为0),然后将这m个内存管理表项的值都设置为m(标记被用),最后,把最后的这个空内存块的地址返回指针p,完成一次分配。注意,如果当内存不够的时候(找到最后也没找到连续的m块空闲内存),则返回NULL(空指针)给p,表示分配失败。
释放原理:
当p申请的内存用完,需要释放的时候,调用free函数实现。free函数先判断p指向的内存地址所对应的内存块,然后找到对应的内存管理表项目,得到p所占用的内存块数目m(内存管理表项目的值就是所分配内存块的数目),将这m个内存管理表项目的值都清零,标记释放,完成一次内存释放。
关于分块式内存管理的原理,我们就介绍到这里。
2, 硬件设计:
本章实验功能简介:开机后,显示提示信息,等待外部输入。KEY0用于申请内存,每次申请2K字节内存。KEY1用于写数据到申请到的内存里面。KEY2用于释放内存。WK_UP用于切换操作内存区(内部内存/外部内存)。DS0用于指示程序运行状态。本章我们还可以通过USMART调试,测试内存管理函数。
本实验用到的硬件资源有:
1) 指示灯DS0
2) 四个按键
3) 串口 //USMART
4) TFTLCD模块
5) IS62WV51216
3, 软件设计:
本章,我们将内存管理部分单独做一个分组,在工程目录下新建一个MALLOC的文件夹,然后新建malloc.c和malloc.h两个文件,将他们保存在MALLOC文件夹下。
在MDK新建一个MALLOC的组,然后将malloc.c文件加入到该组,并将MALLOC文件夹添加到头文件包含路径。
打开malloc.c文件,输入如下代码:由于本实验涉及到的c语言知识,尤其是指针知识较多,所以就边用边学
#include "malloc.h"
//内存池(4字节对齐)
__align(4) u8 mem1base[MEM1_MAX_SIZE];//内部SRAM内存池
/*
" u8 mem1base[MEM1_MAX_SIZE];"该数组是定义拿出内部内存池的40K的空间来做实验,为什么该数组是u8类型?计算机内存是以字节为单位的存储空间,内存中的每个字节都有唯一的编号,这个编号就叫地址。在这里就是定义40K个元素,每个元素代表一个字节。整个数组就代表整个内部SRAM内存池的总容量即40K个元字节的总空间容量。因为不管是存储什么数据类型内存中的地址编号都是32位的,即每个地址编号可以容纳4个字节,而不同的数据类型存储在不同的内存存储区,这就是为什么定义变量时一定要先声明其数据类型的原因。存储一个字符需要一个字节的存储空间,存储一个short类型需要2个字节的存储空间,存储一个int或float需要4个字节空间,就如同PLC内存中的字节,字,双字的定义规则一样(如字节MB0,MB1,MB0和MB1构成MW0;MW0和MW2构成32位的双字DW0,DW4,DW8)。“__align(4)”就是规定4个字节对齐,即每个32的地址编号存储一个数据类型?比如,字符存储区中地址编号MB0可以存储一个字节即8个位的数据,而存储MB0这个地址编号是以32位的空间来存储,也就是说不管是什么类型数据,存储它的地址编号都是32的,所以指针值一定是32位的。
//“#define MEM1_MAX_SIZE40*1024 //最大管理内存 40K”,意思是mem1base[MEM1_MAX_SIZE]有40k个元素
*/
__align(4) u8 mem2base[MEM2_MAX_SIZE] __attribute__((at(0X68000000)));//外部SRAM内存池
//#define MEM2_MAX_SIZE 200*1024 //最大管理内存200K,意思是mem2base[MEM2_MAX_SIZE]数组有200K个u8类型元素,第一个元素的地址存储在 //外部存储器SRAM的0X68000000地址,
//内存管理表
u16 mem1mapbase[MEM1_ALLOC_TABLE_SIZE];//内部SRAM内存池MAP
/*
//#define MEM1_ALLOC_TABLE_SIZEMEM1_MAX_SIZE/MEM1_BLOCK_SIZE //内存表大小,MEM1_MAX_SIZE/MEM1_BLOCK_SIZE==1250
//#define MEM1_BLOCK_SIZE32 //内存块大小为32字节;“MEM1_MAX_SIZE/MEM1_BLOCK_SIZE ”的含义是内部SRAM内存池总共40K字节的容量除以32个字节,得到一共40K/32==1250个内存块;也就是说将内部SRAM内存池划为1250个内存块。
“u16 mem1mapbase[MEM1_ALLOC_TABLE_SIZE];”实际上就等于“u16 mem1mapbase[1250];”意思是定义一个有1250个内存块(元素)的数组,每个元素是u16类型数据;数组名“mem1mapbase”就是mem1mapbase[0](该数组的第一个元素它代表1250个内存块中的第一个内存块)的地址,也可以说是指针常量;结合与之关联的结构体成员“u16 *memmap[2]={ mem1mapbase,mem2mapbase}”指针类型数组;在这里“mem2mapbase”是外部内存的第一个内存块的地址,是个指针常量用以存放u16类型数据的地址值;结合
“mymemset(mallco_dev.memmap[0], 0,memtblsize[0]*2);”函数分析:结合本程序和结构体有关定义“u16 *memmap[2]; ”,首元素memmap[0]=mem1mapbase;也就是说“mallco_dev.memmap[0]”在这里表示1250个内部内存块中第一个内存块的地址,根据“u16 *memmap[2]={ mem1mapbase,mem2mapbase}”推断出“mallco_dev.memmap[0]”是u16类型指针;
“memtblsize[0]”是什么意思呢?根据“const u32 memtblsize[2]={1250,6250};”可以得知memtblsize[0]==1250即内部内存一共有1250个管理项,
void mymemset(void *s,u8 c,u32 count)
{
u8 *xs = s;
while(count--)*xs++=c;
}//把u8类型数据c填充到以指针变量s为首地址的内存空间中,填充多少个数由count值决定
该函数的意思是把u8类型的数据“c”填充到u16类型指针元素memmap[0]中(根据结构体定义“u16 *memmap[2]; ”,而memmap[0]=mem1mapbase),说白了就是把u8类型的数据“c”填充到1250个内存块中的count个内存块中。
而mallco_dev.memmap[memx]是16位的,为了将其全部清零,所以乘以2.
本例中,用到了指针类型数组“u16 *memmap[2]={ mem1mapbase,mem2mapbase}”,为什么要定义指针类型数组呢?mem1mapbase是数组
“u16 mem1mapbase[1250];”的首个元素地址(即*mem1mapbase等价于mem1mapbase[0]),而mem1mapbase[0]就代表内部存储器1250个存储块中的第一个存储块;根据结构体赋值定义可知:memmap[0]=mem1mapbase。所以mem1mapbase就是“mallco_dev.memmap[0]”,即mem1mapbase是函数mymemset(mallco_dev.memmap[memx], 0,memtblsize[memx]*2)的第一个形参,因为*mem1mapbase等价于mem1mapbase[0]),而mem1mapbase[0]就代表内部存储器1250个存储块中的第一个存储块。结合
void mymemset(void *s,u8 c,u32 count)函数分析, mymemset(mallco_dev.memmap[memx], 0,memtblsize[memx]*2)函数的意思是:把0写入到1250个存储块中的第一个存储块中;这样就将一个存储块的值赋值为0了。
推断出“mallco_dev.memmap[0]”是u16类型指针;
;
*/
u16 mem2mapbase[MEM2_ALLOC_TABLE_SIZE] __attribute__((at(0X68000000+MEM2_MAX_SIZE)));
/*
“#define MEM2_ALLOC_TABLE_SIZEMEM2_MAX_SIZE/MEM2_BLOCK_SIZE”
“#define MEM2_BLOCK_SIZE32”
外部SRAM内存池MAP,同理,“MEM2_MAX_SIZE/MEM2_BLOCK_SIZE”的含义是外部SRAM内存池总共200K字节的容量除以32个字节,得到一共200K/32==6250个内存块;也就是说将外部SRAM内存池划为6250个内存块。
*/
//内存管理参数
/*
内存管理表“MEM1_ALLOC_TABLE_SIZE,MEM2_ALLOC_TABLE_SIZE”分别是1250和6250个“项”.
每个内存分块大小即内部和外部SRAM每个内存块占有32个字节空间“MEM1_BLOCK_SIZE,MEM2_BLOCK_SIZE”分别是32个字节;
内存总大小“MEM1_MAX_SIZE,MEM2_MAX_SIZE”,分别是40K和200K个字节的总容量空间
mymemset(mallco_dev.memmap[memx], 0,memtblsize[memx]*2);
*/
const u32 memtblsize[2]={MEM1_ALLOC_TABLE_SIZE,MEM2_ALLOC_TABLE_SIZE};//内存管理表大小
const u32 memblksize[2]={MEM1_BLOCK_SIZE,MEM2_BLOCK_SIZE};//内存分块大小
const u32 memsize[2]={MEM1_MAX_SIZE,MEM2_MAX_SIZE};//内存总大小
/*
struct _m_mallco_dev //内存管理控制器,定义一个结构体类型数据,或结构体变量,
{
void (*init)(u8);//初始化
u8 (*perused)(u8); //内存使用率
u8 *membase[2];//内存池 管理2个区域的内存 mem1base,mem2base内存池
u16 *memmap[2]; //内存管理状态表 mem1mapbase(==1250块),mem2mapbase(6250), //内存管理状态表
u8 memrdy[2]; //内存管理是否就绪
};
1,结构体成员“void (*init)(u8);”是定义了一个指向函数的指针变量,该指针变量名是init;void表示该函数没有返回值(函数的数据类型由返回值决定);u8是函数的形参。指向函数的指针变量格式:数据类型 + (*变量名)(形参)
本例中:
void mem_init(u8 memx)
{
mymemset(mallco_dev.memmap[memx], 0,memtblsize[memx]*2);//内存状态表数据清零 memx:所属内存块,即几号内存块
mymemset(mallco_dev.membase[memx], 0,memsize[memx]);//内存池所有数据清零
mallco_dev.memrdy[memx]=1;//内存管理初始化OK
}
也就是说,本例中用指向函数的指针变量来表示函数。c语言规定函数名就是函数的入口地址,也就是说函数名也是一个指针,指向函数的入口,根据这个原理,可以将指向函数的指针作为函数的参数调用,可以在不同的情况调用不同的函数;如果一个指向函数的指针变量等于函数名就可以说该指向函数的指针变量指向了该函数,那么指针变量与函数就是一样的了。比如:“mem_init(memx);”就等同于“mallco_dev.init(memx);”
2,指针类型数组“u8 *membase[2];”,意思是该指针类型数组有两个“char *”类型的指针元素或者说有两个“u8 *”类型指针元素;为什么要定义“u8 *”类型呢?因为内存存储区是根据数据类型来划分的,如果不明确声明类型就乱套了。
在C语言和C++语言中,数组元素全为指针的数组称为指针数组。一维指针数组的定义形式为:“类型名 *数组标识符[数组长度]”。
例如,一个一维指针数组的定义:int *ptr_array[10]。该指针数组有10个元素,每个元素都是int类型的指针即“int *”类型;
指针类型数组“u8 *membase[2];”的赋值是mem1base,mem2base,“mem1base,mem2base”分别是内部内存池和外部内存池的数组名,是指针常量即首元素的地址;因为事先已经定义“u8 mem1base[MEM1_MAX_SIZE]”即“u8 mem1base[40K];”。
*/
//内存管理控制器,结构体变量赋值,即初始化
struct _m_mallco_dev mallco_dev=
{
mem_init,//内存初始化,将函数名“mem_init”赋给结构体成员“void (*init)(u8);”即指向函数的指针变量,
mem_perused,//内存使用率
mem1base,mem2base,//内存池
mem1mapbase,mem2mapbase, //内存管理状态表,mem1mapbase(1250项),mem2mapbase(6250项)
0,0, //内存管理未就绪
};
/*
1,“void *des”无类型指针,不能指向具体的数据,“void *des”无类型指针指向内存中的数据类型由用户自己确定,如malloc()函数的返回值就是“void *des”无类型指针,因为malloc()函数的返回值是不确定的是根据形参的数据类型确定的
2,“void mymemcpy(void *des,void *src,u32 n) ”函数的理解:
des是指针,但是不确定指向什么类型的数据,换句话说des指针存储的什么类型数据不确定,“u8 *xdes=des;”将des指针存储的数据
存储到一个新的“u8 *”类型指针xdes中;“u8 *xsrc=src;”同理。
“*xdes++=*xsrc++; ”,当*xsrc++(自增)时,即指针“src”指针自增,也就是说把“src”指针逐位复制到des目标指针去了。复制个数就是n。
3,“*P”的意义:a),“*P”就是以指针变量P的内容(P的内容就是指针变量P里存储的某一类型数据的指针值)为地址的变量;b),指针运算符“*”如果是在定义变量时候加在前面,意思是这个变量是指针变量,如 char *a;如果是在访问指针变量的时候加在前面(如*a),意思是取指针变量指向的值,如 char b=*a; 上面定义了a是一个字符指针,这里就是把指针变量a指向的值取出来并赋给b。
*/
//复制内存,作用是将源地址的内容复制到目标地址
//*des:目的地址
//*src:源地址
//n:需要复制的内存长度(字节为单位)
void mymemcpy(void *des,void *src,u32 n)
{ //“void *des”无类型指针,不能指向具体的数据,“void *des”无类型指针指向内存中的数据类型由用户自己确定
u8 *xdes=des;//目标地址,“*xdes”转换成u8类型,也可以理解为把目的地地址des存储到xdes指针中
u8 *xsrc=src;
while(n--)*xdes++=*xsrc++;
}
//设置内存
//*s:内存首地址
//c :要设置的值
//count:需要设置的内存大小(字节为单位)
void mymemset(void *s,u8 c,u32 count)
{
u8 *xs = s;
while(count--)*xs++=c;
}//以*s为内存首地址的count个字节中,填充c,即把c写入到*s为首地址的内存中,个数多少由count值决定
//内存管理初始化
//memx:所属内存块,要么SRAMEX==1(外部内存);要么SRAMIN(内部内存)==0
/*
const u32 memtblsize[2]={MEM1_ALLOC_TABLE_SIZE,MEM2_ALLOC_TABLE_SIZE};//内存管理表大小
const u32 memblksize[2]={MEM1_BLOCK_SIZE,MEM2_BLOCK_SIZE};//内存分块大小
const u32 memsize[2]={MEM1_MAX_SIZE,MEM2_MAX_SIZE};//内存总大小
*/
void mem_init(u8 memx) //如“mem_init(SRAMIN);”表示内部内存块
{ //memmap,是16位的,mymemset,设置是针对8位的,那么1个16位的数据是不是2个8位组成的啊?!
mymemset(mallco_dev.memmap[memx], 0,memtblsize[memx]*2);//内存状态表数据清零
//把u8类型的数据“0”填充到u16类型指针元素memmap[0]中(根据结构体定义“u16 *memmap[2]; ”),memmap[0]=mem1mapbase==1250,
//也就是说“mallco_dev.memmap[memx]”在这里表示1250个内部内存块用以存储u16类型指针,
//“memtblsize[memx]”是什么呢?memtblsize[memx]即memtblsize[0]==1250个内部内存管理表,
//而mallco_dev.memmap[memx]是16位的,为了将其全部清零,所以乘以2.
mymemset(mallco_dev.membase[memx], 0,memsize[memx]);//内存池所有数据清零
//memsize[0]==40K字节空间,mallco_dev.membase[memx]==40K字节空间,
mallco_dev.memrdy[memx]=1;//内存管理初始化OK
}
/*
*/
//获取内存使用率
//memx:所属内存块,要么SRAMEX==1(外部内存);要么SRAMIN(内部内存)==0
//返回值:使用率(0~100)
u8 mem_perused(u8 memx)
{
u32 used=0;
u32 i;
for(i=0;i<memtblsize[memx];i++)
{
if(mallco_dev.memmap[memx][i])used++;
} //mallco_dev.memmap[memx][i]是二维数组。当内存块初始化后该值为0,
return (used*100)/(memtblsize[memx]); //used*100,乘以100是将小数变成整数
}
//内存分配(内部调用)
//memx:所属内存块
//size:要分配的内存大小(字节数)
//返回值:0XFFFFFFFF,代表错误;其他,内存偏移地址
//向memx存储器申请size个字节的连续存储空间,并将size个字节中首个字节的地址偏移值标注出来,注意是地址偏移值而不是地址。
u32 mem_malloc(u8 memx,u32 size)
{
signed long offset=0;
u16 nmemb;//需要的内存块数
u16 cmemb=0;//连续空内存块数
u32 i;
if(!mallco_dev.memrdy[memx])mallco_dev.init(memx);//未初始化,先执行初始化
/*
“mallco_dev.init(memx);”是什么意思?mallco_dev.init(memx)是结构体变量mallco_dev的一个成员,本句中就是对结构体成员的引用,即执行
mem_init(u8 memx)函数的意思;如何引用结构体中指向函数的指针变量成员?既然是指向函数的指针变量且有赋值,在引用时按照格式:
结构体变量名.指向函数的指针变量名(形参);
*/
if(size==0)return 0XFFFFFFFF;//不需要分配 memblksize[memx]==32
nmemb=size/memblksize[memx]; //获取需要分配的连续内存块数
/*
c语言规定:除法的运算结果与运算对象的数据类型有关,两个数都是int则商(即结果)是int,若商(即结果)有小数则省略掉小数点部分。本例中
size和memblksize[memx]都是int,所以结果只能是int。假设size<32,则nmemb==0;
c语言规定取余运算的运算对象必须是int。当小数对大数取余时余(即结果)是小数本身;例如,在“if(size%memblksize[memx])nmemb++;”中 ,
假设size<32,则size%memblksize[memx]的结果是size值本身,所以执行“nmemb++;”运算,这时运算结果是nmemb==1;如果size是32的整数倍则不执行
“nmemb++;”运算;
memtblsize[0]==1250,memtblsize[1]==6250,
mallco_dev.memmap[memx][offset]是什么意思?
*/
if(size%memblksize[memx])nmemb++;
for(offset=memtblsize[memx]-1;offset>=0;offset--)//搜索整个内存控制区
{
if(!mallco_dev.memmap[memx][offset])cmemb++;//连续空内存块数增加,offset从1249->0变化
/*
如,{ memmap[0][149],memmap[0][148],...memmap[0][1],memmap[0][0]};实际上可以把“mallco_dev.memmap[memx][offset]”视为具有1250个变量的
一维数组,每个元素对应的实际意义是对应的一个内存块,顺序是offset从1249(高)->0(低)变化;如果哪个变量等于0(即空闲)就执行
“cmemb++;”操作,这样就可以计算出连续空闲内存块数cmemb;切记!目的是要获取连续的空闲的内存块数!这样就必须结合下一句
“else cmemb=0;”来分析;如果没有出现连续的空闲内存块(即数组顺序相连的变量值没有出现类似“0,0,0,0,0”这样的情况),程序会执行下一语
句“else cmemb=0;”即把上面的“cmemb”统计值清零,这样程序就会在for循环里面继续寻找符合“if(cmemb==nmemb)”条件的状态出现,
如果for循环执行完了还没有出现符合“if(cmemb==nmemb)”条件的状态,则返回0XFFFFFFFF结束本函数表示没有找到符合条件的内存块。假
设:size=65,那么nmemb就是3即需要获取连续3个内存块来存放65个字节,再假设数组顺序相连的变量值出现了类似“0,0,0,0,0”这样的情况(即有
连续4个空闲的内存块),这时就出现了符合“if(cmemb==nmemb)”条件的状态,即当cmemb计数计到3的时候(即出现了连续相连的3个内存块)就
符合“cmemb==nmemb”了,程序就自然进入“if(cmemb==nmemb)”语句。
offset*memblksize[memx]代表什么呢?offset的取值范围是0-1249,memblksize[memx]代表每个内存块的字节数即32,offset*memblksize[memx]就
是返回偏移地址值;也就是把连续空闲的内存块对应的地址的首地址值标注出来。
*/
else cmemb=0;//连续内存块清零
if(cmemb==nmemb)//找到了连续nmemb个空内存块
{
for(i=0;i<nmemb;i++) //标注内存块非空,以免下一个for循环时再次将该空间计入
{
mallco_dev.memmap[memx][offset+i]=nmemb;
}
return (offset*memblksize[memx]);//返回偏移地址
}
}
return 0XFFFFFFFF;//未找到符合分配条件的内存块
}
//释放内存(内部调用)
//memx:所属内存块
//offset:内存地址偏移
//返回值:0,释放成功;1,释放失败;
u8 mem_free(u8 memx,u32 offset)
{
int i;
if(!mallco_dev.memrdy[memx])//未初始化,先执行初始化
{
mallco_dev.init(memx); //本句等价于“mem_init(memx);”
return 1;//未初始化
}
if(offset<memsize[memx])//以免偏移在内存池内. memsize[memx]==40K字节
{
int index=offset/memblksize[memx];//偏移所在内存块号码 memblksize[memx]==32,
int nmemb=mallco_dev.memmap[memx][index];//内存块数量
for(i=0;i<nmemb;i++) //内存块清零
{
mallco_dev.memmap[memx][index+i]=0;
}
return 0;
}else return 2;//偏移超区了.
}
//释放内存(外部调用)
//memx:所属内存块
//ptr:内存首地址
void myfree(u8 memx,void *ptr)
{
u32 offset;
if(ptr==NULL)return;//地址为0.
offset=(u32)ptr-(u32)mallco_dev.membase[memx];
mem_free(memx,offset);//释放内存
}
//分配内存(外部调用)
//memx:所属内存块
//size:内存大小(字节)
//返回值:分配到的内存首地址.
//在memx存储器中,找出size个字节的连续空闲的内存空间,并将连续空闲的内存空间指针值标注出来;返回值就是这个指针值
/*
mallco_dev.membase[memx]即mallco_dev.membase[0]代表MCU内部存储器的40K字节中的第一个字节变量的地址,是u8类型指针变量,也就是说一个字节占用一个地址;换句话说,把内部存储器的40K字节的地址定义为一个“u8 mem1base[MEM1_MAX_SIZE]”数组,指针类型数组“u8 *membase[2];”的赋值是{mem1base,mem2base},而“mem1base,mem2base”分别是内部内存池和外部内存池的数组名,各自首元素的地址亦是个指针常量;因为事先已经定义
“u8 mem1base[MEM1_MAX_SIZE]”即“u8 mem1base[40K];”。如何理解“(void*)((u32)mallco_dev.membase[memx]+offset); ”呢?
1),已经说过mallco_dev.membase[memx]是首个变量的地址即40k字节中首个字节的地址值;
2),“offset”是:向memx存储器申请size个字节的连续空闲存储空间,这个找到的连续空闲空间当中首个字节的地址偏移值就是offset,offset==32(将32个字节空间组成一个内存块)*内存块号(如,假设向内部存储器申请64个字节的连续空闲存储空间,通过“mem_malloc(memx,size); ”函数得到在第五个存储块开始有连续2个存储快空闲可供使用(假设是5号和4号存储快),因为每个存储快有32个字节即有32个地址编号,4*32==128(这里的4是指第四块),5*32==160(这里的5是指第五块),那么这个160就是40K个字节编号当中的地址偏移值offset,即128-192号就是第四块和第五块内存块所对应的指针编号);注意offset是地址偏移值而不是地址;为什么要引入地址偏移值这个概念呢?假设第一个字节的地址值是0x0000 6800,那么就知道(0x0000 6800+160)的值就是第五块内存的指针。
3),“(u32)mallco_dev.membase[memx]”代表指针类型数组,意义是内部存储器40K字节中的第一个字节变量的地址,原来存放的是u8类型数据的地址,现在强制类型转换扩展为u32类型;
4),(void*)((u32)mallco_dev.membase[memx]+offset); 转换为无类型指针,指针值是32位,由此可知,“void *mymalloc(u8 memx,u32 size)”函数的返回值就是一个指针,即形参size所指向的由高向低的首个指针值;“void *mymalloc(u8 memx,u32 size)”是个指针类型函数,只能赋给指针。
*/
void *mymalloc(u8 memx,u32 size) //p=mymalloc(sramx,2048)
{
u32 offset;
offset=mem_malloc(memx,size);
if(offset==0XFFFFFFFF)return NULL;
else return (void*)((u32)mallco_dev.membase[memx]+offset);
}
//重新分配内存(外部调用)
//memx:所属内存块
//*ptr:旧内存首地址
//size:要分配的内存大小(字节)
//返回值:新分配到的内存首地址.
void *myrealloc(u8 memx,void *ptr,u32 size)
{
u32 offset;
offset=mem_malloc(memx,size);
if(offset==0XFFFFFFFF)return NULL;
else
{
mymemcpy((void*)((u32)mallco_dev.membase[memx]+offset),ptr,size);//拷贝旧内存内容到新内存
// 把size个字节指针ptr复制到“((u32)mallco_dev.membase[memx]+offset)”,
myfree(memx,ptr); //释放旧内存,因为在mem_malloc(memx,size)中已经将连续空闲内存块标注为1(已被占用),清除掉原来的标记
return (void*)((u32)mallco_dev.membase[memx]+offset); //返回新内存首地址,无类型指针
}
}
头文件:
#ifndef __MALLOC_H
#define __MALLOC_H
typedef unsigned long u32;
typedef unsigned short u16;
typedef unsigned char u8;
#ifndef NULL
#define NULL 0
#endif
#define SRAMIN0//内部内存池
#define SRAMEX 1//外部内存池
//mem1内存参数设定.mem1完全处于内部SRAM里面
#define MEM1_BLOCK_SIZE32 //内存块大小为32字节
#define MEM1_MAX_SIZE40*1024 //最大管理内存 40K
#define MEM1_ALLOC_TABLE_SIZEMEM1_MAX_SIZE/MEM1_BLOCK_SIZE //内存表大小
//mem2内存参数设定.mem2的内存池处于外部SRAM里面,其他的处于内部SRAM里面
#define MEM2_BLOCK_SIZE32 //内存块大小为32字节
#define MEM2_MAX_SIZE 200*1024 //最大管理内存200K
#define MEM2_ALLOC_TABLE_SIZEMEM2_MAX_SIZE/MEM2_BLOCK_SIZE //内存表大小
struct _m_mallco_dev //内存管理控制器
{
void (*init)(u8);//初始化
u8 (*perused)(u8); //内存使用率
u8 *membase[2];//内存池 管理2个区域的内存
u16 *memmap[2]; //内存管理状态表
u8 memrdy[2]; //内存管理是否就绪
};
extern struct _m_mallco_dev mallco_dev;//在mallco.c里面定义,定义全局变量,结构体变量mallco_dev
void mymemset(void *s,u8 c,u32 count);//设置内存
void mymemcpy(void *des,void *src,u32 n);//复制内存
void mem_init(u8 memx);//内存管理初始化函数(外/内部调用)
u32 mem_malloc(u8 memx,u32 size);//内存分配(内部调用)
u8 mem_free(u8 memx,u32 offset);//内存释放(内部调用)
u8 mem_perused(u8 memx);//获得内存使用率(外/内部调用)
////////////////////////////////////////////////////////////////////////////////
//用户调用函数
void myfree(u8 memx,void *ptr); //内存释放(外部调用)
void *mymalloc(u8 memx,u32 size);//内存分配(外部调用)
void *myrealloc(u8 memx,void *ptr,u32 size);//重新分配内存(外部调用)
#endif
这部分代码,定义了很多关键数据,比如内存块大小的定义:MEM1_BLOCK_SIZE和MEM2_BLOCK_SIZE,都是32字节。内存池总大小,内部为40K,外部为200K(最大支持到近1M字节,不过为了方便演示,这里只管理200K内存)。MEM1_ALLOC_TABLE_SIZE和MEM2_ALLOC_TABLE_SIZE,则分别代表内存池1和2的内存管理表大小。
从这里可以看出,如果内存分块越小,那么内存管理表就越大,当分块为2字节1个块的时候,内存管理表就和内存池一样大了(管理表的每项都是u16类型)。显然是不合适的,我们这里取32字节,比例为1:16,内存管理表相对就比较小了。
主函数部分:
int main(void)
{
u8 key;
u8 i=0;
u8 *p=0;
u8 *tp=0;
u8 paddr[18];//存放的内容“P Addr:+p地址的ASCII值”
u8 sramx=0;//默认为内部sram
Stm32_Clock_Init(9);//系统时钟设置
uart_init(72,9600);//串口初始化为9600
delay_init(72); //延时初始化
led_init(); //初始化与LED连接的硬件接口
LCD_Init(); //初始化LCD
usmart_dev.init(72);//初始化USMART
Key_Init();//按键初始化
FSMC_SRAM_Init();//初始化外部SRAM,因为用到了外部sram
mem_init(SRAMIN);//初始化内部内存池,SRAMIN==0
mem_init(SRAMEX);//初始化外部内存池,SRAMEX==1
POINT_COLOR=RED;//设置字体为红色
LCD_ShowString(60,50,200,16,16,"WarShip STM32");
LCD_ShowString(60,70,200,16,16,"MALLOC TEST");
LCD_ShowString(60,90,200,16,16,"WANG YAN");
LCD_ShowString(60,110,200,16,16,"2013/12/16");
LCD_ShowString(60,130,200,16,16,"key_right:Malloc key_left:Free");
LCD_ShowString(60,150,200,16,16,"wake_up:SRAMx key_down:Read");
POINT_COLOR=BLUE;//设置字体为蓝色
LCD_ShowString(60,170,200,16,16,"SRAMIN");
LCD_ShowString(60,190,200,16,16,"SRAMIN USED: %");
LCD_ShowString(60,210,200,16,16,"SRAMEX USED: %");
while(1)
{
key=Key_Scan(0);//不支持连按
switch(key)
{
case 0://没有按键按下
break;
case key_right://KEY0按下
p=mymalloc(sramx,2048);//申请2K字节,即64个内存块的空间
if(p!=NULL)sprintf((char*)p,"Memory Malloc Test%03d",i);//向p写入一些内容
break;
case key_down://KEY1按下
if(p!=NULL) //NULL==0;
{
sprintf((char*)p,"Memory Malloc Test%03d",i);//更新显示内容
//LCD_ShowString(60,270,200,16,16,p);
LCD_ShowString(60,250,200,16,16,p);//显示P的内容
printf("Memory Malloc Test%03d\n",i);//将“Memory Malloc Test”用串口输出,利用串口助手可以看到输出的结果
//"03"表示参数“i”的值只显示3位,%-输出控制符;d-将“i”以十进制的形式输出;i的范围0--255;输出参数可以是多个,可以参考郝斌老师的相关视频;
//输出控制符包含:%Ld--L代表long类型;%c--代表字符类型;:%X--代表16进制并大写;
}
break;
case key_left://KEY2按下
myfree(sramx,p);//释放内存
p=0;//指向空地址
break;
case wake_up://KEY UP按下
sramx=!sramx;//切换当前malloc/free操作对象
if(sramx)LCD_ShowString(60,170,200,16,16,"SRAMEX");
else LCD_ShowString(60,170,200,16,16,"SRAMIN");
break;
}
if(tp!=p)
{//在内存paddr值处显示:“P Addr:0X%08X”,“0X%08X”以大写16进制显示参数tp(32位),“08”表示8位数。0X AAAA AAAA
//刚进入程序时,因为执行了“mem_init(SRAMIN);”初始化函数,所以p==0;所以LCD不会有显示
//因为程序一开始就有“u8 *tp=0;”,所以若不按下任何按键LCD就不会显示下面的内容(即“if(tp!=p)”控制的显示内容);
tp=p;//PAddr显示的是指针p本身的地址值;指针值是u32类型
sprintf((char*)paddr,"P Addr:0X%08X",(u32)tp);//将指针p本身的地址值在LCD上打印出来即显示
LCD_ShowString(60,230,200,16,16,paddr);//显示p的地址
if(p)
LCD_ShowString(60,250,200,16,16,p);//显示P的内容,即指针p内存储的数据“Memory Malloc Test%03d”
else LCD_Fill(60,250,239,266,WHITE);//p=0,清除显示
}
delay_ms(10);
i++;
if((i%20)==0)//DS0闪烁.
{
LCD_ShowNum(60+96,190,mem_perused(SRAMIN),3,16);//显示内部内存使用率
LCD_ShowNum(60+96,210,mem_perused(SRAMEX),3,16);//显示外部内存使用率
led0=!led0;
}
}
}
总结:通过内存管理的学习,更加深刻的领会到指针是c语言的灵魂,对c语言的知识是一个巩固和提高;同时也学习到了sprintf()函数的运用技巧。
本章希望利用USMART调试内存管理,所以在USMART里面添加了mymalloc和myfree两个函数,用于测试内存分配和内存释放。大家可以通过USMART自行测试。
4,下载验证:
在代码编译成功之后,我们通过下载代码到ALIENTEK战舰STM32开发板上,得到如图所示界面:
可以看到,内外内存的使用率均为0%,说明还没有任何内存被使用,此时我们按下KEY0,就可以看到内部内存被使用5%(每按下一次申请2K的空间,lcd上显示的使用率递增5%;20*2K==40K)了,同时看到下面提示了指针p所指向的地址(其实就是被分配到的内存地址)和内容。多按几次KEY0,可以看到内存使用率持续上升(注意对比p的值,可以发现是递减的,说明是从顶部开始分配内存!),此时如果按下KEY2,可以发现内存使用率降低了5%,但是再按KEY2将不再降低,说明“内存泄露”了。这就是前面提到的对一个指针多次申请内存,而之前申请的内存又没释放,导致的“内存泄露”。
按KEY_UP按键,可以切换当前操作内存(内部内存/外部内存),KEY1键用于更新p的内容,更新后的内容将重新显示在LCD模块上面。
本章,我们还可以借助USMART,测试内存的分配和释放,有兴趣的朋友可以动手试试。如右图USMART测试内存管理函数所示。
/////////////////////////插补:printf和sprintf函数的用法////////////////////////////
printf和sprintf函数的用法非常重要,用于程序参数调试。这两个函数都包含在系统启动代码“stdio.h”头文件中;
1,例:printf("Memory Malloc Test%03d\n",i);//将“Memory Malloc Test”用串口输出,利用串口助手可以看到输出的结果;
"03"表示参数“i”的值只显示3位,%d-输出控制符;d-将“i”以十进制的形式输出;i的范围0--255(因为是u8类型);输出参数可以是多个,可以参考郝斌老师的相关视频;输出控制符包含:%Ld--L代表long类型;%c--代表字符类型;:%X--代表16进制并大写;%s-字符串类型
2,如何理解字符串打印函数int sprintf(char * __restrict /*s*/, const char * __restrict /*format*/, ...) __attribute__((__nonnull__(1,2)));?
在内存管理实验中例如,sprintf((char*)p,"Memory Malloc Test%03d",i)函数的使用问题:
1),第一个形参(char*)p的意思是(第一个形参必须是指针类型),第二个形参即字符串“Memory Malloc Test%03d”存储在内存中的具体指针值,因为字符串是u8类型即char*类型,所以“(char*)p”与之呼应;因为第二个形参“Memory Malloc Test%03d”中有输出控制符“%03d”,所以第一个形参(char*)p的值是变化的(因为参数“i”的值在变);这里输出控制符“%03d”的意思可以参考printf()函数;
也就是说,sprintf函数的第一个形参必须是指针类型,它是第二个形参(输出内容)在存储器中存储的首地址,是一个指针变量,第三个形参就是要输出的参数;所以说sprintf函数包含的内容很多,作用很大。
2),sprintf函数的作用是在显示屏中显示相关参数,即向p写入一些内容即Memory Malloc Test%03d”;
结合LCD_ShowString(60,270,200,16,16,p)的显示结果更好理解,即显示P的存储内容即在相应的坐标处“Memory Malloc Test%03d”;”
3),例子:
u8 s[8];
char* who = "I"; //将字符“I”赋给char* 类型变量who;
char* whom = "STM32"; //将字符串“STM32”赋给char* 类型变量whom;
sprintf(s, "%s love %s.", who, whom); //产生:"I love STM32. " 这字符串写到s中
LCD_ShowString(60,250,200,16,16,s);
//sprintf(s, "%10.3f", 3.1415626); //产生:" 3.142",浮点型显示
4),sprintf函数一般情况下是用在需要字符显示的场合,比如你要显示一个数字,通常的做法是取出某一位然后加上0x30这个数,这样一位一位来比较麻烦,用sprintf这个函数呢,一次性就给你搞定了
比如你想打印3.1415926这个数值到液晶上显示,通常的做法代码就很多而且乱,有了这个函数呢,直接这样
float PI=3.1415926;
u16 strbuffer[10];
sprintf(strbuffer,"PI=:%09d",PI);
然后直接将strbuffer这个数组送去显示即可,或者打印到串口,这样就可以直接字符显示了
注意:sprintf函数必须结合LCD显示函数使用才能有效!并且形参必须定义好合适的数据类型;sprintf()函数的最大作用就是非常方便的在LCD显示屏上显示自己想要的数据类型!参考关于sprintf函数的实验。
3,疑问?
a,在51单片机中,如何将sprintf函数包含进51的启动代码中?如果不将sprintf函数包含进51的头文件,显示屏肯定不能用sprintf函数显示数据。
b,在stdio.h中,找到的是int sprintf(char * __restrict /*s*/, const char * __restrict /*format*/, ...) __attribute__((__nonnull__(1,2)));怎么看不到函数内容?
sprintf是C语言标准库提供的函数, 包含在stdio.h中, 只要在文件头#include <stdio.h>即可.
原型为int sprintf ( char * str, const char * format, ... );
/////////////////////////插补:printf和sprintf函数的用法////////////////////////////