之所以要自己来实现内存动态分配函数,主要是基于以下三点考虑:(1) “stdlib.h”标准库中的malloc函数是针对x86 PC来实现的,因此在实际的使用过程中容易产生大量的碎片内存,如当你申请一个字节的空间时,有可能会返回给你32个字节,造成大量浪费。(2)当扩展外部内存时, “stdlib.h”标准库中的malloc函数无法对其进行管理,因此需要实现一个统一内存管理函数,可以方便地管理内部RAM和外部RAM。既然单片机的动态内存申请这么麻烦,为什么还要用它呢?绝大多数的单片机应用不用动态内存申请,只用数组定义一样可以非常好的实现。但是采用这种方法定义的数组在使用时不够灵活,申请到的内存只能以数组形式进行访问。然而使用动态方法申请到的内存就好比流水一样,装入方形的容器中,就变成了方形,装入圆形的容器中,就变成了圆形,具有非常强大的可塑性。如果将内存类比于流水,那么我们可以用它实现链表,线性表,队列,堆栈,图,树等等各种各样的数据结构。回顾 “stdlib.h”标准库中的malloc函数原型:/*__cdecl 是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈*/
void *__cdecl malloc(size_t _Size)
我们可以很清楚地看到,当我们需要申请一段内存时,需要传入申请的内存大小(以字节为单位),当顺利申请之后,这个函数就会返回一个有效内存的首地址。当我们申请成功之后,就可以使用强制类型转换,这个“void *”类型的指针,转换成任何我们想要的数据结构。这里需要补充一点,在C语言中,无论我们如何定义一个变量,C编译器关心的就只有两点,即定义的空间和定义的存储形式,而当我们使用malloc函数,并且使用如下形式申请一块内存,并将其转换成对应的数据结构时,也是基于以上两点来说的。type_t * allocMem = (type_t *)malloc(sizeof(allocMem));
为了更加直观,我们可以使用如下的示意图来表示这个内存申请步骤。
当我们使用C语言定义一个数组时,其实就是向编译器要了一块连续的指定大小的内存。当我们定义一个uint8_t,长度位1000的数组array[1000]时,我们可以得出两点结论:•可以使用C语言的取地址取出数组第0个元素的地址,此地址就是数组的起始地址,如果直接使用数组名也有同样的效果;•假设uint8_t类型数组的第0个元素的地址是addr,那么第1个元素的地址就是addr + 1,第2个元素的地址就是addr + 2,以此类推,通过此方法,就可以很好地将数组的索引引用和指针引用一一对应起来。如果系统外扩了外置的SDRAM,你使用的编译器又恰好是GCC,那么我们就可以使用GNU C 的__attribute__属性将这个数组指定的定义到外部的SDRAM中,如下:static uint8_t array[1000] __attribute__((section(".exsdram"))); //.exsdram section已经在ld script中定义
此时,假定我们已经在外部的SDRAM上定义了一个1M byte的数组,在程序使用的时候我们仅仅需要1000字节,那么我们是只需要使用array[0] ~ array[999]范围之内的内存即可。后续又想使用2000字节,那么只需要使用array[1000] ~ array[2999]范围之内的内存即可,以此类推。而当使用这些内存时,只需要知道这些内存块的起始地址和有效空间长度即可。基于上述结论,我们可以写一个算法来管理这一块数组,保证当有人需要使用时,我们返回一个起始地址给他,并且保证这一段内存不会被任意非法访问即可。当用完这段内存后,将其回收即可。这个算法就是我们需要实现的内存管理算法。 3 内存动态管理(Memory manage controller)常用的内存管理算法有很多,如First-Fit算法,best-fit/worst-fit算法,buddy-system算法等,值得一提的是Free RTOS的heap4使用的是First-Fit算法。First-fit算法:连续物理内存分配算法的一种,将空闲内存块按照地址从小到大的方式连起来,具体实现时使用了双向链表的方式。当分配内存时,从链表头开始向后找,这意味着从低地址向高地址查找,一旦找到可以满足要求的内存块,即将该内存块分配出去即可。在此处为了避免内部碎片问题,具体实现时我们将该可用内存块分为两部分:前一部分大小与所需内存相同,这样我们只需要将前一部分分配出去,然后将后一部分继续插入到链表中即可。通过这个方式我们实现了一定程度上的任意大小分配,但是这个算法不可避免的产生了较多的外部碎片。同时,我认为该算法有一个缺陷,那就是会在低地址部分产生很多小碎片,这些碎片不但浪费内存,同时会导致每次从低地址向高地址查找时都要遍历一遍,会使得算法的性能大大下降,查找的开销会逐渐上升。对于我们的应用,我改良了First-Fit算法的释放机制,使得其可以快速地释放,并且进行碎片整理。基于上述内容,单片机等物理内存设备上,动态内存管理无法做到带MMU器件那么智能,因此首先需要开辟出一段静态内存作为动态内存管理的内存池。如上述,可以直接使用静态数组定义方式去定义,如需要定义到外部地存储器件上,可以使用__attribute__关键词指定。static uint8_t exSdramHeap[MMC_EXSDRAM_MAX_AS_HEAP_SIZE] __attribute__((section(".exsdram"))); //.exsdram section已经在ld script中定义
前面讲述过了,动态内存算法的基本原理就是分割静态数组exSdramHeap,但是经过这种分割之后的内存还有个逻辑上的问题,那就是MMC程序无法识别到相应的内存块,因此在前述内容上,还需要定义一些统一的节点信息来标注这些内存块的特点,这些特点可以自己根据需求进行选定。我们的定义如下:typedef struct mmcBlock
{
size_t BlockSize; //当前块的大小
unsigned int BlockStatus; //当前块的状态
struct mmcBlock *NextBlock; //下一个块的起始地址
uint8_t *VaildSpaceStart; //当前块有效的存储单元起始地址
} mmc_Block_t;
上面mmc_Block_t节点中,有些元素其实是可以省略的,比如,VaildSpaceStart元素就是当前内存块的起始地址和前面三个元素大小的和。BlockSize就是NextBlock和当前块地址之差。但是为了标注和后续的运算方便,我还是为每个节点多准备了两个字节。内存申请函数的返回值就是VaildSpaceStart的值。BlockStatus标注了当前内存块的状态,这里暂定了三个标志。#define MMC_BLOCK_STATUS_EMPTY_FLAG 0x80 //空块标志
#define MMC_BLOCK_STATUS_START_BLOCK_FLAG 0x40 //起始块标志
#define MMC_BLOCK_STATUS_END_BLOCK_FLAG 0x20 //结束块标志
当MMC程序运行起来之后,exSdramHeap数组上的内容分布如下:现在,我们给自己的内存动态分配和回收函数定义函数原型:void *MMC_Alloc(size_t xRequestSize); //内存申请
void MMC_Free(void *pFreeAddress); //内存释放在第一次调用MMC_Alloc函数时,我们需要exSdramHeap数组初始化,初始化的目标主要有两点:字节对齐的目的是因为各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。