《DMG474开发指南_V1.1》第三十二章 内存管理实验

第三十二章 内存管理实验


       如果我们所用的内存都是直接定义一个数组来使用,灵活性会比较差,很多时候不能满足实际使用需求。为了解决这些问题,我们来学习内存管理,实现对内存的动态管理。

       本章分为如下几个小节:

       32.1 内存管理简介

       32.2 硬件设计

       32.3 程序设计

       32.4 下载验证


        32.1 内存管理简介

       内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效、快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种,其实最终都是要实现两个函数:malloc和free。malloc函数用来内存申请,free函数用于内存释放。

       本章,我们介绍一种比较简单的办法来实现:分块式内存管理。下面我们介绍一下该方法的实现原理,如图32.1.1所示:


图32.1.1 分块式内存管理原理


       从上图可以看出,分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为了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个内存管理表项目的值都清零,标记释放,完成一次内存释放。


        32.2 硬件设计


       1. 例程功能

       开机后,显示提示信息,等待外部输入。KEY0用于申请内存,每次申请2K字节内存,KEY1用于释放内存,KEY2用于切换操作内存区(SRAMIN/SRAMCCM,总共管理2个内存块)。还可以通过USMART调试,测试内存管理函数。

       LED0闪烁用于提示程序正在运行。


       2. 硬件资源


       1)LED灯

              LED0 – PE0

       2)独立按键  

              KEY0 – PE12

              KEY1 – PE13

              KEY2 – PE14

       3)串口1 (PB6/PB7连接在板载USB转串口芯片CH340上面)

       4)正点原子1.3寸TFTLCD模块(SPI接口)


        32.3 程序设计


       32.3.1 程序流程图


图32.3.1.1 内存管理实验程序流程图


       32.3.2 程序解析


       1. MALLOC代码

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MALLOC驱动源码包括两个文件:malloc.c和malloc.h。

       下面我们介绍malloc.h中比较重要的一些结构体和内存参数宏定义,其定义如下:

/* 定义两个内存池 */
#define     SRAMIN   0 /* 内部内存池 */
#define     SRAMCCM   1 /* CCM内存池(此部分SRAM仅仅CPU可以访问!!!) */
 
#define     SRAMBANK   2 /* 定义支持的SRAM块数 */
 
/* 定义内存管理表类型,当外扩SDRAM的时候,必须使用uint32_t类型,
否则定义成uint16_t,以节省内存占用 */
#define MT_TYPE     uint16_t
 
/* mem1内存参数设定.mem1是G4内部的SRAM1+SRAM2内存 */
#define     MEM1_BLOCK_SIZE         32   /* 内存块大小为32字节 */
#define     MEM1_MAX_SIZE           70 * 1024  /* 最大可管理内存70K */
 
/* 内存表大小 */
#define     MEM1_ALLOC_TABLE_SIZE   MEM1_MAX_SIZE/MEM1_BLOCK_SIZE   
 
/* mem2内存参数设定.mem2处于CCM,用于管理CCM(特别注意,这部分SRAM,仅CPU可以访问!!)  */
#define     MEM2_BLOCK_SIZE         32   /* 内存块大小为32字节 */
 
/* 最大可管理内存 30K */
#define     MEM2_MAX_SIZE           30 * 1024    
  
/* 内存表大小 */
#define     MEM2_ALLOC_TABLE_SIZE   MEM2_MAX_SIZE/MEM2_BLOCK_SIZE   
 
/* 内存管理控制器 */
struct _m_mallco_dev
{
    void (*init)(uint8_t);    /* 初始化 */
    uint16_t (*perused)(uint8_t);  /* 内存使用率 */
    uint8_t *membase[SRAMBANK];   /* 内存池 管理SRAMBANK个区域的内存 */
    MT_TYPE *memmap[SRAMBANK];   /* 内存管理状态表 */
    uint8_t  memrdy[SRAMBANK];   /* 内存管理是否就绪 */
};

       我们可以定义几个不同的内存管理表,再分配相应的指针给到管理控制器即可。程序中我们用宏定义MEM1_BLOCK_SIZE来定义malloc可以管理的内部内存池总大小,实际上我们定义为一个大小为MEM1_BLOCK_SIZE的数组,这样编译后就能获得一块实际的连续内存区域,这里是70K,MEM1_ALLOC_TABLE_SIZE代表内存池的内存管理表大小。我们可以定义多个内存管理表,这样就可以同时管理多块内存。

       从这里可以看出,如果内存分块越小,那么内存管理表就越大,当分块为4字节1个块的时候,内存管理表就和内存池一样大了(管理表的每项都是uint16_t类型)。显然是不合适的,我们这里取32字节,比例为1:32,内存管理表相对就比较小了。

       通过这个内存管理控制器_m_malloc_dev结构体,我们把分块式内存管理的相关信息,其初始化函数、获取使用率、内存池、内存管理表以及内存管理的状态保存下来,实现对内存池的管理控制。

       下面介绍malloc.c文件,内存池、内存管理表、内存管理参数和内存管理控制器的定义如下:

#if !(__ARMCC_VERSION >= 6010050)  /* 不是AC6编译器,即使用AC5编译器时 */
 
/* 内存池(64字节对齐) */
static __align(64) uint8_t mem1base[MEM1_MAX_SIZE]; /* 内部SRAM内存池 */
static __align(64) uint8_t mem2base[MEM2_MAX_SIZE] 
__attribute__((at(0x10000000)));  /* 内部CCM内存池 */
 
/* 内存管理表 */
static MT_TYPE mem1mapbase[MEM1_ALLOC_TABLE_SIZE];  /* 内部SRAM内存池MAP */
 
static MT_TYPE mem2mapbase[MEM2_ALLOC_TABLE_SIZE] 
__attribute__((at(0x10000000 + MEM2_MAX_SIZE)));/* 内部CCM内存池MAP */
 
#else      /* 使用AC6编译器时 */
 
/* 内存池(64字节对齐) */
static __ALIGNED(64) uint8_t mem1base[MEM1_MAX_SIZE]; /* 内部SRAM1内存池 */
static __ALIGNED(64) uint8_t mem2base[MEM2_MAX_SIZE] __attribute__((section(".bss.ARM.__at_0x10000000")));  /* 内部CCM内存池  */
 
/* 内存管理表 */
static MT_TYPE mem1mapbase[MEM1_ALLOC_TABLE_SIZE];  /* 内部SRAM内存池MAP */
static MT_TYPE mem2mapbase[MEM2_ALLOC_TABLE_SIZE] __attribute__((section(".bss.ARM.__at_0x10007800"))); /* 内部CCM内存池MAP */
 
#endif
 
/* 内存管理参数 */
const uint32_t  memtblsize[SRAMBANK]= { MEM1_ALLOC_TABLE_SIZE, 
MEM2_ALLOC_TABLE_SIZE }; /* 内存表大小 */
/* 内存分块大小 */
const uint32_t  memblksize[SRAMBANK]= { MEM1_BLOCK_SIZE, MEM2_BLOCK_SIZE };  
 
/* 内存总大小 */
const uint32_t  memsize[SRAMBANK]= { MEM1_MAX_SIZE, MEM2_MAX_SIZE };
 
 
/* 内存管理控制器 */
struct _m_mallco_dev mallco_dev =
{
    my_mem_init,     /* 内存初始化 */
    my_mem_perused,    /* 内存使用率 */
    mem1base, mem2base,   /* 内存池 */
    mem1mapbase, mem2mapbase, /* 内存管理状态表 */
    0, 0,      /* 内存管理未就绪 */
};

       MDK支持用__attirbute__((at(地址)))的方法把变量定义到指定的区域,而且这个变量支持是算式,大家可以去MDK的帮助文件中查找__attribute__这个关键字查找相关信息,有比较详细的介绍。

       我们通过判断编译器的版本,来执行不同方式的定义,对于AC5来说,使用的是__attirbute__((at(地址))),但是如果你想换成AC6编译器,指定变量位置的函数变成__attribute__((section(“.bss.ARM.__at_地址”)))的方式,其中的.bss表示初始化值为0,而且这个方式不支持算式,所以还用上面的方法直接用宏计算出SRAM的地址的方法不可行了,所以我们需要直接手动算出SRAM对应的内存地址,同样地__align(64)在AC6下的写法也变成了__ALIGNED(64),还有其它差异的部分,大家参考MDK官方提供的AC5到AC6的迁移方法的文档,我们主要介绍AC5。

       我们通过内存管理控制器mallco_dev结构体,实现对两个内存池的管理控制。

       第一个是内部SRAM内存池,定义为:

static __align(64) uint8_t  mem1base[MEM1_MAX_SIZE];        /* 内部SRAM内存池 */

       第二个是内部CCM内存池,定义为:

static __align(64) uint8_t mem2base[MEM2_MAX_SIZE] 
                              __attribute__((at(0x10000000)));  /* 内部CCM内存池 */

       这里之所以要定义成2个,是因为这两个内存区域的地址不一样,STM32G4内部内存分为两大块:1,普通内存,这部分内存任何外设都可以访问;2,CCM内存,这部分内存仅CPU可以访问,DMA之类的不可以直接访问,使用时得特别注意!!

       下面介绍其他的malloc代码,具体如下:

/**
 * @brief       复制内存
 * @param       *des : 目的地址
 * @param       *src : 源地址
 * @param       n    : 需要复制的内存长度(字节为单位)
 * @retval      无
 */
void my_mem_copy(void *des, void *src, uint32_t n)
{
    uint8_t *xdes = des;
    uint8_t *xsrc = src;
 
    while (n--)*xdes++ = *xsrc++;
}
 
/**
 * @brief       设置内存值
 * @param       *s    : 内存首地址
 * @param       c     : 要设置的值
 * @param       count : 需要设置的内存大小(字节为单位)
 * @retval      无
 */
void my_mem_set(void *s, uint8_t c, uint32_t count)
{
    uint8_t *xs = s;
 
    while (count--)*xs++ = c;
}
 
/**
 * @brief       内存管理初始化
 * @param       memx : 所属内存块
 * @retval      无
 */
void my_mem_init(uint8_t memx)
{
/* 获取memmap数组的类型长度(uint16_t /uint32_t)*/
uint8_t mttsize = sizeof(MT_TYPE);  
 
/* 内存状态表数据清零 */
    my_mem_set(mallco_dev.memmap[memx], 0, memtblsize[memx]*mttsize); 
    mallco_dev.memrdy[memx] = 1;  /* 内存管理初始化OK */
}
 
/**
 * @brief       获取内存使用率
 * @param       memx : 所属内存块
 * @retval      使用率(扩大了10倍,0~1000,代表0.0%~100.0%)
 */
uint16_t my_mem_perused(uint8_t memx)
{
    uint32_t used = 0;
    uint32_t i;
 
    for (i = 0; i < memtblsize[memx]; i++)
    {
        if (mallco_dev.memmap[memx][i])used++;
    }
 
    return (used * 1000) / (memtblsize[memx]);
}
 
/**
 * @brief       内存分配(内部调用)
 * @param       memx : 所属内存块
 * @param       size : 要分配的内存大小(字节)
 * @retval      内存偏移地址
 *   @arg       0 ~ 0xFFFFFFFE : 有效的内存偏移地址
 *   @arg       0xFFFFFFFF     : 无效的内存偏移地址
 */
uint32_t my_mem_malloc(uint8_t memx, uint32_t size)
{
    signed long offset = 0;
    uint32_t  nmemb;       /* 需要的内存块数 */
    uint32_t  cmemb = 0;      /* 连续空内存块数 */
    uint32_t  i;
/* 未初始化,先执行初始化 */
    if (!mallco_dev.memrdy[memx]) mallco_dev.init(memx);        
    if (size == 0) return 0xFFFFFFFF;  /* 不需要分配 */
 
    nmemb = size/memblksize[memx];   /* 获取需要分配的连续内存块数 */
if (size%memblksize[memx]) nmemb++;
/* 搜索整个内存控制区 */
    for (offset = memtblsize[memx] - 1; offset >= 0; offset--)  
    {
        if (!mallco_dev.memmap[memx][offset]) cmemb++; /* 连续空内存块数增加 */
        else cmemb=0;      /* 连续内存块清零 */
        if (cmemb==nmemb)     /* 找到了连续nmemb个空内存块 */
        {
            for (i = 0; i < nmemb; i++)  /* 标注内存块非空 */
            {
                mallco_dev.memmap[memx][offset + i] = nmemb;
            }
            return (offset * memblksize[memx]);   /* 返回偏移地址 */
        }
    }
    return 0xFFFFFFFF;      /* 未找到符合分配条件的内存块 */
}
 
/**
 * @brief       释放内存(内部调用)
 * @param       memx   : 所属内存块
 * @param       offset : 内存地址偏移
 * @retval      释放结果
 *   @arg       0, 释放成功;
 *   @arg       1, 释放失败;
 *   @arg       2, 超区域了(失败);
 */
uint8_t my_mem_free(uint8_t memx, uint32_t offset)
{
    int i;
 
    if (!mallco_dev.memrdy[memx])  /* 未初始化,先执行初始化 */
    {
        mallco_dev.init(memx);
        return 1;      /* 未初始化 */
    }
 
    if (offset < memsize[memx])   /* 偏移在内存池内 */
    {
        int index = offset/memblksize[memx];    /* 偏移所在内存块号码 */
        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;      /* 偏移超区了 */
    }
}
 
/**
 * @brief       释放内存(外部调用)
 * @param       memx : 所属内存块
 * @param       ptr  : 内存首地址
 * @retval      无
 */
void myfree(uint8_t memx, void *ptr)
{
    uint32_t offset;
    
    if (ptr == NULL) return; /* 地址为0 */
    
    offset = (uint32_t)ptr - (uint32_t)mallco_dev.membase[memx];
    my_mem_free(memx, offset); /* 释放内存 */
}
 
/**
 * @brief       分配内存(外部调用)
 * @param       memx : 所属内存块
 * @param       size : 要分配的内存大小(字节)
 * @retval      分配到的内存首地址.
 */
void *mymalloc(uint8_t memx, uint32_t size)
{
    uint32_t  offset;
    offset = my_mem_malloc(memx, size);
 
    if (offset == 0xFFFFFFFF)
    {
        return NULL;
    }
    else
    {
        return (void*)((uint32_t )mallco_dev.membase[memx] + offset);
    }
}
 
/**
 * @brief       重新分配内存(外部调用)
 * @param       memx : 所属内存块
 * @param       *ptr : 旧内存首地址
 * @param       size : 要分配的内存大小(字节)
 * @retval      新分配到的内存首地址.
 */
void *myrealloc(uint8_t memx, void *ptr, uint32_t size)
{
    uint32_t  offset;
    offset = my_mem_malloc(memx, size);
    
    if (offset == 0xFFFFFFFF)
    {
        return NULL;
    }
    else
    {
        my_mem_copy((void*)((uint32_t)mallco_dev.membase[memx] + offset),
 ptr, size); /* 拷贝旧内存内容到新内存 */
        myfree(memx, ptr);  /* 释放旧内存 */
 
/* 返回新内存首地址 */
        return (void*)((uint32_t)mallco_dev.membase[memx] + offset);    
    }
}

       整个malloc代码的核心函数:my_mem_malloc和my_mem_free,分别用于内存申请和内存释放。思路就是前面32.1所介绍的分配内存和释放内存,不过在这里,这两个函数知识内部调用,外部调用我们另外定义了mymalloc和myfree两个函数,其他函数我们就不多介绍了。


       2. main.c代码

       main.c代码如下:

const char *SRAM_NAME_BUF[SRAMBANK] = {" SRAMIN ", " SRAMCCM "};
 
int main(void)
{
    uint8_t paddr[20];      /* 存放P Addr:+p地址的ASCII值 */
    uint16_t memused = 0;
    uint8_t key;
    uint8_t i = 0;
    uint8_t *p = 0;
    uint8_t *tp = 0;
    uint8_t sramx = 0;      /* 默认为内部sram */
 
    
    HAL_Init();        /* 初始化HAL库 */
    sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
    delay_init(170);                            /* 延时初始化 */
    usart_init(115200);      /* 串口初始化为115200 */
    usmart_dev.init(170);     /* 初始化USMART */
    led_init();        /* 初始化LED */
    lcd_init();        /* 初始化LCD */
    key_init();        /* 初始化按键 */
    
    my_mem_init(SRAMIN);     /* 初始化内部SRAM内存池 */
    my_mem_init(SRAMCCM);     /* 初始化内部SRAMCCM内存池 */
    /* 显示提示信息 */
    lcd_show_string(10, 10, 140, 32, 32, "STM32", RED);
    lcd_show_string(10, 42, 140, 24, 24, "MALLOC TEST", RED);
    lcd_show_string(10, 66, 200, 24, 24, "ATOM@ALIENTEK", RED);
    lcd_show_string(10, 90, 200, 16, 16, "KEY0:Malloc & WR & Show", RED);    
    lcd_show_string(10, 110, 200, 16, 16, "KEY2:SRAMx KEY1:Free", RED);
    lcd_show_string(50, 130, 200, 16, 16, " SRAMIN ", BLUE);
    lcd_show_string(10, 150, 200, 16, 16, "SRAMIN   USED:", BLUE);
    lcd_show_string(10, 170, 200, 16, 16, "SRAMCCM  USED:", BLUE);
    
    while (1)
    {
        key = key_scan(0);     /* 不支持连按 */
 
        switch (key)
        {
            case KEY0_PRES:     /* KEY0按下 */
/* 申请2K字节,并写入内容,显示在lcd屏幕上面 */
                p = mymalloc(sramx, 2048);
                if (p != NULL)
                {
/* 向p写入一些内容 */
                    sprintf((char *)p, "Memory Malloc Test%03d", i);       
/* 显示P的内容 */
                    lcd_show_string(10, 210, 209, 16, 16, (char *)p, BLUE);     
                }
                break;
 
            case KEY1_PRES:     /* KEY1按下 */
                myfree(sramx, p);    /* 释放内存 */
                p = 0;       /* 指向空地址 */
                break;
 
            case KEY2_PRES:     /* KEY2按下 */
                sramx++;
                if (sramx > 1)sramx = 0;
                lcd_show_string(50, 130, 200, 16, 16, 
(char *)SRAM_NAME_BUF[sramx], BLUE);
                break;
        }
 
        if (tp != p)
        {
            tp = p;
            sprintf((char *)paddr, "P Addr:0x%08X", (uint32_t)tp);
/* 显示p的地址 */
            lcd_show_string(10, 190, 209, 16, 16, (char *)paddr, BLUE); 
 
            if (p)
            {
/* 显示p的内容 */
                lcd_show_string(10, 210, 280, 16, 16, (char *)p, BLUE); 
            }
            else 
            {
                lcd_fill(10, 210, 239, 239, WHITE); /* p=0,清除显示 */
            }
        }
 
        delay_ms(10);
        i++;
 
        if ((i % 20) == 0)  /* DS0闪烁. */
        {
            memused = my_mem_perused(SRAMIN);
            sprintf((char *)paddr, "%d.%01d%%", memused / 10, memused % 10);
/* 显示内部内存使用率 */
            lcd_show_string(10 + 112, 150, 200, 16, 16, (char *)paddr, BLUE);   
 
            memused = my_mem_perused(SRAMCCM);
            sprintf((char *)paddr, "%d.%01d%%", memused / 10, memused % 10);
/* 显示CCM内存使用率 */
            lcd_show_string(10 + 112, 170, 200, 16, 16, (char *)paddr, BLUE);   
         
            LED0_TOGGLE();  /* LED0闪烁 */
        }
    }
}

       该部分代码比较简单,主要是对mymalloc和myfree的应用。不过这里提醒大家,如果对一个指针进行多次内存申请,而之前的申请又没释放,那么将造成“内存泄露”,这是内存管理所不希望发生的,久而久之,可能导致无内存可用的情况!所以,在使用的时候,请大家一定记得,申请的内存在用完以后,一定要释放。

       另外,本章希望利用USMART调试内存管理,所以在USMART里面添加了mymalloc和myfree两个函数,用于测试内存分配和内存释放。大家可以通过USMART自行测试。


        32.4 下载验证

       将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD内容如下图所示:


图32.4.1 内存管理实验测试图


       可以看到,内存的使用率均为0%,说明还没有任何内存被使用。我们可以通过KEY2选择申请内存的位置:SRAIN为内部,SRAMCCM为内部。此时我们选择从内部申请内存,按下KEY0,就可以看到申请了2%的一个内存块,同时看到下面提示了指针p所指向的地址(其实就是被分配到的内存地址)和内容。效果如图35.4.2所示。

       KEY0键用来更新p的内容,更新后的内容将重新显示在LCD模块上。多按几次KEY0,可以看到内存使用率持续上升(注意比对p的值,可以发现是递减的,说明是从顶部开始分配内存!)。每次申请一个内存块后,可以通过按下KEY0释放本次申请的内存,如果我们每次申请完内存不再使用却不及时释放掉,再按KEY1将无法释放之前的内存了,当这样的情况重复了多次,就会造成“内存泄漏”。我们程序就是模拟这样一个情况,请大家在实际使用的时候去注意到这种做法的危险性,必须在编程时严格避免内存泄漏的情况发生。


图32.4.2 按下KEY0申请了部分内存


       本章,我们还可以借助USMART,测试内存的分配和释放,有兴趣的朋友可以动手试试。如图32.4.3所示:


图32.4.3 USMART测试内存管理图


       图中,我们先申请了4660字节的内存,然后得到申请到的内存首地址为:0x20012480,说明我们申请内存成功(如果不成功,则会收到0),然后释放内存的时候,参数是指针的地址,即执行:myfree(0x20012480),就可以释放我们申请到的内存。其他情况,大家可以自行测试并分析。


请使用浏览器的分享功能分享到微信等