目录
前言
1. 内存管理介绍
1.1 分块式内存管理
2. 实验程序
2.1 main.c
2.2 Malloc.c
2.3 Malloc.h
前言
相信大家在学习C语言的过程中,都会学习到 malloc 动态开辟函数和 free 释放内存函数;这两个函数带给我们的优越性是:
我们在使用一块内存的时候,通常都不会恰到好处的使用完定义的内存,可能我们定义的内存不够我们使用,也可能我们定义的内存会多,这样会造成内存浪费;所以在此基础之上引入malloc动态开辟函数,可以在定义一块内存的基础之上,随着我们的需要进行动态内存开辟;简单来说就是,定义的内存不够使用,就随着我们的需求一次一次的动态开辟内存;如果动态开辟的内存多余,就是要 free 函数释放掉动态开辟的空间;当然,当程序运行完以后,动态开辟的内存会自动的释放掉!
STM32扩展的内存也存在这种情况;如果我们每次使用扩展内存的时候,都定义一个峰值的存储空间,那么难免造成内存空间的浪费,所以本节我们学习基于STM32的 malloc 动态内存开辟!
如果对malloc动态开辟函数和 free 空间释放函数不够了解,可参照以下笔记进行学习!!!
C语言_动态内存管理_light_2025的博客-CSDN博客
1. 内存管理介绍
内存管理是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现有很多种,其中最终都是要实现两个函数:malloc动态内存申请函数、free动态内存释放函数。
1.1 分块式内存管理
分块式内存管理主要由内存池和内存管理表两部分组成。内存池被等分为n块,对应的内存管理表大小也为n,内存管理表的每一项对应内存池的一块内存。
内存管理表的项数表示的意义是:
如果项数为0,那么相对应的内存块数量也为0,也就表示此时没有内存块被占用;如果项数非零,那么对应的内存块数也非零,此时项数为几对应的内存块数也为几,代表对应的内存块已经被占用,其数值代表被连续占用的内存块数。比方说:如果项数为10,那么算上这个项数,就表示分配了10个内存块给外部的指针。
内存分配方向:
内存分配是从顶到底的分配方向。也就是首先从最末端开始找空内存。当内存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。
分配原理:
当指针p调用malloc函数动态申请内存的时候,先去判断指针p要分配的内存块数m,然后从末端开始查找,直到找到m块连续的空内存块,然后把这m块空的内存块的内存管理项设置为m,表示对应块的内存被占用,最后将这m块对应的内存块地址返回给指针p,完成一次分配。
注:如果从末端开始查找,查找结束还是没有找到对应的连续空内存块,就表示内存不足,此时将NULL返回给指针p,表示分配失败。
释放原理:
当指针p申请的动态内存用完,需要释放的时候,调用free函数实现。free函数先找到内存地址所对应的内存块,然后找到与内存块所对应的内存管理表项目,得到指针p所占用的内存块数目m。然后将这m个内存管理表的项目都清零,标记完成一次内存释放。
2. 实验程序
实验功能:
开机后,显示提示信息、KEY0用于申请内存,每次申请2K字节内存。KEY1用于写数据到申请的内存里面。KEY2用于释放内存。KEY_UP用于切换操作内存区(内部SRAM内存/外部SRAM内存/内部CCM内存)
2.1 main.c
#include "stm32f4xx.h"
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "SRAM.h"
#include "Malloc.h"
//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
led_set(sta);
}
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
delay_init(168); //初始化延时函数
uart_init(115200); //初始化串口波特率为115200
LED_Init(); //初始化LED
LCD_Init(); //LCD初始化
Key_Init(); //按键初始化
FSMC_SRAM_Init(); //初始化外部SRAM
my_mem_init(SRAMIN); //初始化内部内存池
my_mem_init(SRAMEX); //初始化外部内存池
my_mem_init(SRAMCCM); //初始化CCM内存池
POINT_COLOR=RED;//设置字体为红色
LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
LCD_ShowString(30,70,200,16,16,"MALLOC TEST");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,"2023/08/02");
LCD_ShowString(30,130,200,16,16,"KEY0:Malloc KEY2:Free");
LCD_ShowString(30,150,200,16,16,"KEY_UP:SRAMx KEY1:Read");
POINT_COLOR=BLUE;//设置字体为蓝色
LCD_ShowString(30,170,200,16,16,"SRAMIN");
LCD_ShowString(30,190,200,16,16,"SRAMIN USED: %");
LCD_ShowString(30,210,200,16,16,"SRAMEX USED: %");
LCD_ShowString(30,230,200,16,16,"SRAMCCM USED: %");
while(1)
{
key=KEY_Scan(0);//不支持连按
switch(key)
{
case 0://没有按键按下
break;
case KEY0_PRESS: //KEY0按下
p=mymalloc(sramx,2048);//申请2K字节
if(p!=NULL)
sprintf((char*)p,"Memory Malloc Test%03d",i);//向p写入一些内容
break;
case KEY1_PRESS: //KEY1按下
if(p!=NULL)
{
sprintf((char*)p,"Memory Malloc Test%03d",i);//更新显示内容
LCD_ShowString(30,270,200,16,16,p); //显示P的内容
}
break;
case KEY2_PRESS: //KEY2按下
myfree(sramx,p);//释放内存
p=0; //指向空地址
break;
case KEY_UP_PRESS: //KEY UP按下
sramx++;
if(sramx>2) //因为只有三个存储区
sramx=0;
if(sramx==0)
LCD_ShowString(30,170,200,16,16,"SRAMIN "); //内部SRAM存储区
else if(sramx==1)
LCD_ShowString(30,170,200,16,16,"SRAMEX "); //外部扩展存储区
else
LCD_ShowString(30,170,200,16,16,"SRAMCCM"); //内部CCM存储区
break;
}
if(tp!=p)
{
tp=p;
sprintf((char*)paddr,"P Addr:0X%08X",(u32)tp);
LCD_ShowString(30,250,200,16,16,paddr); //显示p的地址
if(p)
LCD_ShowString(30,270,200,16,16,p);//显示P的内容
else
LCD_Fill(30,270,239,266,WHITE); //p=0,清除显示
}
delay_ms(10);
i++;
if((i%20)==0)//DS0闪烁.
{
LCD_ShowNum(30+104,190,my_mem_perused(SRAMIN),3,16);//显示内部内存使用率
LCD_ShowNum(30+104,210,my_mem_perused(SRAMEX),3,16);//显示外部内存使用率
LCD_ShowNum(30+104,230,my_mem_perused(SRAMCCM),3,16);//显示CCM内存使用率
LED0=!LED0;
}
}
}
2.2 Malloc.c
#include "stm32f4xx.h"
#include "Malloc.h"
//内存池(32字节对齐)
__align(32) u8 mem1base[MEM1_MAX_SIZE]; //内部SRAM内存池
__align(32) u8 mem2base[MEM2_MAX_SIZE] __attribute__((at(0x68000000))); //外部SRAM内存池
__align(32) u8 mem3base[MEM3_MAX_SIZE] __attribute__((at(0x10000000))); //内部CCM内存池
//注:
// __align(32)是负责内存对齐的宏,它会补充一些数据以便下面的数据对齐;简单来说就是让接下来的数据或者指令32位对齐
// __attribute__((at(0x68000000)))表示定义的数据的起始地址是从0x68000000开始的
// u8 mem1base[MEM1_MAX_SIZE];表示拿出内部内存池的32K的空间来做实验。之所以定义u8类型,是因为计算机内存都是以字节为单位的存储空间
// 内存中的每个字节都有唯一的编号,这个编号叫做地址。
// 不管存储什么数据类型的地址编号都是32位的,1个字节是8位,那么每个地址编号可以容纳四个字节,这也是为什么定义变量之前一定要先声明数据类型的原因,一个字符char占1个字节,一个整型int占4个字节
// __align(32)让接下来的数据和指令32位对齐,也就是让对应的地址编号和指针32位对齐
//内存管理表
u16 mem1mapbase[MEM1_ALLOC_TABLE_SIZE]; //内部SRAM内存管理表 实际上就是u16 mem1mapbase[3200],意思是定义一个有3200个内存块的数组
// 数组名mem1mapbase就是mem1mapbase[0] 该数组的第一个元素代表3200个内存块中的第一个内存块
u16 mem2mapbase[MEM2_ALLOC_TABLE_SIZE] __attribute__((at(0X68000000+MEM2_MAX_SIZE))); //外部SRAM内存池
u16 mem3mapbase[MEM3_ALLOC_TABLE_SIZE] __attribute__((at(0X10000000+MEM3_MAX_SIZE))); //内部CCM内存池
//注:之所以是__attribute__((at(0X68000000+MEM2_MAX_SIZE)));是因为分块式内存管理下,外部SRAM区动态开辟时,是从末端依次向里的,所以起始地址应该是0x68000000加上最大的外部管理内存MEM2_MAX_SIZE
//内存管理参数
const u32 memtblsize[SRAMBANK]={MEM1_ALLOC_TABLE_SIZE,MEM2_ALLOC_TABLE_SIZE,MEM3_ALLOC_TABLE_SIZE}; //内存表大小
const u32 memblksize[SRAMBANK]={MEM1_BLOCK_SIZE,MEM2_BLOCK_SIZE,MEM3_BLOCK_SIZE}; //内存分块大小
const u32 memsize[SRAMBANK]={MEM1_MAX_SIZE,MEM2_MAX_SIZE,MEM3_MAX_SIZE}; //内存总大小
//内存管理控制器 初始化结构体成员
struct _m_malloc_dev malloc_dev=
{
my_mem_init, //内存初始化
my_mem_perused, //内存使用率
mem1base,mem2base,mem3base, //内存池 mem1base有100K元素,mem2base有960K元素,mem3base有60K元素
mem1mapbase,mem2mapbase,mem3mapbase, //内存管理状态表
0,0,0, //内存管理未就绪
};
//复制内存
//*destinct:目的地址
//*source:源地址
//n:需要复制的内存长度(单位字节)
void mymemcpy(void *destinct,void *source,u32 n) //该函数表示从source处复制n个字节长度的数据到destinct
{
u8 *xdestinct=destinct; //定义一个指针xdestinct指向目标地址的首元素
u8 *xsource=source; //定义一个指针xsource指向源地址的首元素
while(n--) //通过while循环一个字节一个字节的复制
{
*xdestinct++=*xsource++; //字符串拷贝函数的逻辑,
}
}
//设置内存
//*S:内存首地址
//C:要设置的值
//Count:需要设置的内存大小(字节为单位)
void mymemset(void *S,u8 C,u8 Count)
{
u8 *xs=S; //定义一个新指针*xs指向内存首地址对应的元素
while(Count--) //每设置一个,需要设置的内存大小就减少一点
*xs++=C; //把u8类型数据C填充到以指针变量xs为首地址的内存空间中,填充个数由Count值决定
}
//内存管理初始化
//memoryx:所属的内存块
void my_mem_init(u8 memoryx)
{
mymemset(malloc_dev.memmap[memoryx],0,memtblsize[memoryx]*2); //内存状态表数据清零
//第一个参数是内存首地址,malloc_dev.memmap[memoryx]调用结构体表示拿到了内存池中第一个内存块的地址,以此地址作为存储内存的首地址
//初始化设置状态表数据为0
//清零自然是整个内存表的数据全部清零,所以设置的大小就是内存块对应的整个内存表的大小
//之所以乘以2是因为:调用的mymemset函数是将u8类型的数据C填充到对应内存块的首地址,而设置的*memmap[SRAMBANK]是u16位的
//所以需要乘以2保证将其全部清零
mymemset(malloc_dev.membase[memoryx],0,memsize[memoryx]); //内存池数据清零
//对应内存池设置的结构体u8 *membase[SRAMBANK];所以不需要乘以2就可以实现内存池数据清零
malloc_dev.memrdy[memoryx]=1; //内存管理初始化OK,0表示内存管理初始化未就绪
}
//获取内存使用率
//memoryx:所属的内存块 也可以说是我们想要获得哪个内存块的使用率
//返回值:使用率(0~100)
u8 my_mem_perused(u8 memoryx)
{
u32 used=0;
u32 i;
for(i=0;i<memtblsize[memoryx];i++) //循环范围是在整个内存块个数之间循环
{
if(malloc_dev.memmap[memoryx][i]) //内存管理表和内存块是一一对应的,内存块分配内存时,如果对应内存块的内存没有被占用,那么标记为0
//只要申请开辟内存,那么首先就要获取需要开辟的内存块数m,然后从末端依次开始开辟m个连续的内存块,只要内存足够,就将这m个连续的内存块标记为m,表示已经被占用了
//所以只要内存被占用,那么对应的内存块的数据就不为0,至于对应数据是几,那要根据所需的内存块数进行判断
//换句话说就是,如果对应内存块被占用,那么if判断语句一定为真
{
used++; //定义一个变量表示使用率,单位%,只要对应内存块被占用,used++
}
}
return (used*100)/(memtblsize[memoryx]); //该公式很简单,被占用的内存块数除以有多少个内存块,然后乘以100%转换成百分比
}
//内存分配(内部调用)
//memory:所属内存块
//size:要分配的内存大小(字节)
//返回值:0xFFFFFFFF,代表错误;其他,内存偏移地址
u32 my_mem_malloc(u8 memory,u32 size)
{
signed long offset=0;
u32 need_memory; //需要的内存块数
u32 continue_memory=0; //连续的空内存块数
u32 i;
if(!malloc_dev.memrdy[memory]) //!malloc_dev.memrdy[memory]为真,则表示malloc_dev.memrdy[memory]为假
//malloc_dev.memrdy[memory]=0表示内存管理还未就绪
{
malloc_dev.init(memory); //内存管理还没有就绪,那么先执行初始化
}
if(size==0) //表示要分配的内存大小为0
{
return 0xFFFFFFFF;
}
need_memory=size/memblksize[memory]; //得到需要分配的连续内存块数
//size是总的要分配的内存大小;memblksize[memory]表示所属内存块的大小,相除表示需要多少个内存块才可以储存size大小的内存
if(size%memblksize[memory]) //该if语句表示分配过后,所需内存块的个数不是整数,结合实际,不能使用半个或者几分之几个内存块
//所以在原本需要的内存块数上++
{
need_memory++;
}
for(offset=memtblsize[memory]-1;offset>=0;offset--) //搜索整个内存控制区,寻找有无连续个need_memory个内存块
//offset-- 表示从内存管理表的末端开始搜索,依次向前搜索need_memory个连续的内存块
{
if(!malloc_dev.memmap[memory][offset]) //整体!malloc_dev.memmap[memory][offset]取反为真,那么malloc_dev.memmap[memory][offset]为假
//malloc_dev.memmap[memory][offset]=0;表示对应的内存块的数值为0,对应内存块为空,也就表示该内存块可以被使用
{
continue_memory++; //连续空内存块数增加
}
else
continue_memory=0; //只要找到一个内存块不为空,那么就表示所需的内存块不连续了,continue_memory=0;
if(continue_memory==need_memory) //搜索找到的连续的内存块数恰好等于所需的内存块数
{
for(i=0;i<need_memory;i++) //标记这些连续的所需的内存块非空
{
malloc_dev.memmap[memory][offset+i]=need_memory; //将这些所需的连续的内存块的数据改为need_memory
}
return (offset*memblksize[memory]); //返回偏移地址
}
}
return 0xFFFFFFFF; //未找到符合分配条件的内存块
}
//释放内存
//memory:所属的内存块
//offset:内存地址偏移
//返回值:0,释放成功;1,释放失败
u8 my_mem_free(u8 memory,u32 offset)
{
int i;
if(!malloc_dev.memrdy[memory])//整体为真,则表示malloc_dev.memrdy[memory]为假,表示内存管理未就绪
{
malloc_dev.init(memory); //调用结构体指针函数指向初始化函数
return 1;//释放失败,未初始化
}
if(offset<memsize[memory]) //偏移在内存池内
{
int index=offset/memblksize[memory]; //偏移所在内存块号,也就是得到从末端开始偏移,偏移到第几块内存块
int nmemb=malloc_dev.memmap[memory][index]; //定义变量记录内存块的数量
for(i=0;i<nmemb;i++) //内存块清零
{
malloc_dev.memmap[memory][index+i]=0;
}
return 0; //内存释放成功
}
else
return 2; //偏移超区了
}
//释放内存(外部调用)
//memory:所属内存块
//ptr:内存首地址
void myfree(u8 memory,void *ptr)
{
u32 offset;
if(ptr==NULL)
return; //首元素地址指向空指针,地址为空
offset=(u32)ptr-(u32)malloc_dev.membase[memory]; //记录偏移量
my_mem_free(memory,offset); //释放内存
}
//分配内存(外部调用)
//memory:s所属内存块
//size:内存大小(字节)
//返回值:分配到的内存首地址
void *mymalloc(u8 memory,u32 size)
{
u32 offset;
offset=my_mem_malloc(memory,size); //offset接收到动态开辟内存以后,返回的地址偏移量,得到偏移量就表示得到了动态开辟内存的大小
if(offset==0xFFFFFFFF)
return NULL;
else
return (void*)((u32)malloc_dev.membase[memory]+offset); //该语句是C语言malloc动态开辟空间内存的表示
}
//重新分配内存(外部调用)
//memory:所属内存块
//*ptr:旧内存首地址
//size:要分配的内存大小(字节)
//返回值:新分配的内存首地址
void *myrealloc(u8 memory,void *ptr,u32 size)
{
u32 offset;
offset=my_mem_malloc(memory,size);
if(offset==0XFFFFFFFF)return NULL;
else
{
mymemcpy((void*)((u32)malloc_dev.membase[memory]+offset),ptr,size); //拷贝旧内存内容到新内存
myfree(memory,ptr); //释放旧内存
return (void*)((u32)malloc_dev.membase[memory]+offset); //返回新内存首地址
}
}
2.3 Malloc.h
#ifndef _Malloc__H_
#define _Malloc__H_
#include "stm32f4xx.h"
/
#ifndef NULL
#define NULL 0 //宏定义空指针返回0,C语言标准规定当内存不足时,或者动态开辟内存失败时,返回空指针
#endif
/
//定义三个内存池
#define SRAMIN 0 //STM32内部的SRAM内存池 192K
#define SRAMEX 1 //STM32外部的SRAM扩展内存池 1M
#define SRAMCCM 2 //CCM内存池(此部分SRAM仅仅CPU可以访问!!!)
#define SRAMBANK 3 //定义支持的SRAM块数 这里定义支持的块数是3,包括内部SRAM、外部SRAM、CCM内存池
//Memory1内存参数设定,Memory1完全处于内部SRAM里面 192K
#define MEM1_BLOCK_SIZE 32 //内存块大小为32个字节,也就是说我动态开辟了1个内存块,就相当于开辟了一个32字节大小的空间
#define MEM1_MAX_SIZE 100*1024 //最大管理内存 100K 1K=1024个字节 相当于我设定的内存峰值是100K
#define MEM1_ALLOC_TABLE_SIZE MEM1_MAX_SIZE/MEM1_BLOCK_SIZE //内存管理表大小 相当于设置了3200个内存块 也就是说将内部SRAM内存池划分为3200个内存块
//Memory2内存参数设定,Memory2的内存池处于外部SRAM里面 1M
#define MEM2_BLOCK_SIZE 32 //内存块大小32个字节
#define MEM2_MAX_SIZE 960*1024 //外部SRAM最大管理内存960K
#define MEM2_ALLOC_TABLE_SIZE MEM2_MAX_SIZE/MEM2_BLOCK_SIZE //内存管理表大小 相当于设置了30720个内存块
//Memory3内存参数设定,Memory3处于CCM,用于管理CCM(特别注意:这部分的SRAM,仅CPU可以访问!!!)
#define MEM3_BLOCK_SIZE 32 //内存块大小为32个字节
#define MEM3_MAX_SIZE 60*1024 //内部CCM最大管理内存60K
#define MEM3_ALLOC_TABLE_SIZE MEM3_MAX_SIZE/MEM3_BLOCK_SIZE //内存管理表大小 相当于设置了1920个内存块
//内存管理控制器
struct _m_malloc_dev
{
void (*init)(u8); //定义一个init指针,该指针指向初始化函数,初始化函数有一个unsigned char型参数
//void (*init)(u8); 定义了一个指向函数的指针变量,该指针变量名是init;void表示该函数没有返回值,u8是函数的形参
//数据类型+(*变量名)(形参);
u8 (*perused)(u8); //定义一个perused指针,该指针指向一个内存使用率的函数,该函数有一个参数
u8 *membase[SRAMBANK]; //定义一个membase指针指向内存池,管理三个(SRAMBANK)区域的内存
u16 *memmap[SRAMBANK]; //定义一个memmap指针指向内存管理状态表,该数组成员表示三个SRAM块
// u16 *memmap[3]={mem1mapbase,mem2mapbase,mem3mapbase};其中mem2mapbase就表示外部扩展的SRAM的第一个内存块的地址
// 也就是说该数组中对应的三个成员分别代表内部SRAM、外部扩展的SRAM、CCM的第一个内存块的地址
u8 memrdy[SRAMBANK]; //内存管理是否就绪
};
extern struct _m_malloc_dev malloc_dev; //定义一个结构体变量,该变量在malloc.c里面定义
void mymemcpy(void *destinct,void *source,u32 n);
void mymemset(void *S,u8 C,u8 Count);
void my_mem_init(u8 memoryx);
u8 my_mem_perused(u8 memoryx);
u32 my_mem_malloc(u8 memory,u32 size);
u8 my_mem_free(u8 memory,u32 offset);
void myfree(u8 memory,void *ptr);
void *mymalloc(u8 memory,u32 size);
void *myrealloc(u8 memory,void *ptr,u32 size);
#endif