本帖最后由 没有你 于 2020-1-16 00:25 编辑
之前发的帖子介绍了OSAL在STC8A8K64S4A12单片机的移植方式和简单使用,今天就简单介绍一下OSAL的内存管理方式。在介绍之前,首先要了解单片机的栈和堆的区别。 一提到“栈”,很容易想到单片机的压栈(PUSH)和出栈(POP)中用到的栈,这个栈是系统自动分配和释放的,具体分配多少栈空间,在程序编译后就已经确定不变了、而且无法更改。子函数内部的局部变量定义和使用,也是系统自动分配栈空间来保存变量,等函数执行完,系统就会将栈空间收回。另一个“堆”,就没有“栈”那么熟悉了,因为初学者用单片机的时候,不会想到用堆的,也比较少接触。“堆”就是用户自己管理的内存空间,一般都是定义一个大数组全局变量,再基于这个数组的内存空间做管理。自己编写类似malloc和free函来实现内存空间的申请和释放,即需要用到空间,要自己申请,等空间使用周期结束了,还要自己释放空间。
既然 “栈”那么方便,由系统自动申请,自动释放,肯定比自己去管理方便多了,那为什么还要“堆”呢?前面讲过,“栈”空间的使用在程序编译后就定死了,无法自己制定使用多大的栈,这样就很不方便,尤其是在传输处理动态数据流的时候就很麻烦。那肯定有人想过,在函数里面定义一个很大的数组局部变量,那不就可以应对各种各样的数据流了。如果在函数里面定义很大的数组,系统在跑的使用,其他函数也会用到栈空间哦,一旦栈空间使用过大导致内存溢出,那系统肯定奔溃了。“堆”就不一样了,要用多少空间,完全可以动态申请,需要多少,就申请多少。由于是在自己定义全局数组变量的地址上管理内存操作,不怕内存溢出。
栈和堆的共同点和区别:
1、共同点:a:都是占用ram空间;
b:可使用最大ram空间在程序编译后就确定不变了;
2、区别:栈空间由系统自动申请和释放,堆空间由用户自己申请和释放。
接下来介绍一下OSAL的内存管理,OSAL的内存管理是堆管理,OSAL_Memory.c里面定义了一个很大的数组theHeap,内存管理把堆空间分成两个部分,第一个部分是针对小块内存的管理,第二部分是针对大块内存的管理。这样做的好处是容易申请到连续的大空间,因为小块内存处理会使整个内存空间碎片化,那么会导致内存空间不连续,不连续的空间是对申请大空间是非常不利的。在申请堆空间时,会自动合并之前释放的free块,等到找到合适连续块时,会自动裁剪多余的块空间,以免造成空间的浪费。
下面列举出已去除其他无关代码和修改部分名称后的内存管理代码,代码处理有详细注释,还有测试过程。另外,代码的测试是基于STC8A8K64S4A12单片机,ram空间为8K。
首先是相关宏和变量定义
//堆总空间
#define HEAP_SIZE 2048
#define HEAPMEM_IN_USE 0x8000
//堆空间结束位置
#define HEAP_LASTBLK_IDX ((HEAP_SIZE / HEAPMEM_HDRSZ) - 1)
//区分块位置
#define HEAPMEM_SMALLBLK_HDRCNT (HEAPMEM_SMALLBLK_BUCKET / HEAPMEM_HDRSZ)
//大块起始位置
#define HEAPMEM_BIGBLK_IDX (HEAPMEM_SMALLBLK_HDRCNT + 1)
//首部大小
#define HEAPMEM_HDRSZ sizeof(heapMemHdr_t)
//最小块大小
#define HEAPMEM_MIN_BLKSZ (HEAPMEM_ROUND((HEAPMEM_HDRSZ * 2)))
//小块大小
#define HEAPMEM_SMALL_BLKSZ (HEAPMEM_ROUND(16))
//默认长块大小
#define HEAPMEM_LL_BLKSZ (HEAPMEM_ROUND(417) + (19 * HEAPMEM_HDRSZ))
//小块总空间
#define HEAPMEM_SMALLBLK_BUCKET ((HEAPMEM_SMALL_BLKSZ * HEAPMEM_SMALL_BLKCNT) + HEAPMEM_LL_BLKSZ)
//大块总空间
#define HEAPMEM_BIGBLK_SZ (HEAP_SIZE - HEAPMEM_SMALLBLK_BUCKET - HEAPMEM_HDRSZ*2)
//默认小块数量
#define HEAPMEM_SMALL_BLKCNT 8
// 调整申请内存大小宏操作(如申请17字节空间,则调整为18字节)
#define HEAPMEM_ROUND(X) ((((X) + HEAPMEM_HDRSZ - 1) / HEAPMEM_HDRSZ) * HEAPMEM_HDRSZ)
typedef struct {
unsigned len : 15;//本快的长度最大为2^16-1个字节,且申请空间的最小粒度为2个字节
unsigned inUse : 1;//标志位表示本快是否已经被使用
} heapMemHdrHdr_t;
typedef union {
//因此,当halDataAlign\u t小于UINT16时,编译器强制结构对齐最大的元素,而不会在目标上浪费空间。
uint8 alignDummy;
uint16 val;//存储上一块长度,in use信息
heapMemHdrHdr_t hdr;//快头指针
} heapMemHdr_t;
static __no_init heapMemHdr_t all_heap[HEAP_SIZE];//定义堆空间数组
static __no_init heapMemHdr_t *ff1; //第一个空块
static uint8 heapMemStat = 0x01; // 离散状态标志 0x01:踢出
说明,每个all_heap元素占用2个字节,即16个bit,最高位bit代表使用状态,1表示非free,0表示free。剩下15个bit可以表示32768个byte堆空间,高达32K了,应付51单片机是完全没有问题的。定义了一个全局数组变量all_heap作为堆总空间,大小为2048个byte。可以根据单片机资源自行修改堆总空间大小,宏HEAPMEM_SMALLBLK_HDRCNT 是小块内存和大块内存的分界数值。
下面是堆空间初始化函数
void heap_init(void)
{
all_heap[HEAP_LASTBLK_IDX].val = 0;//在堆的末尾设置一个空块,以便与零进行快速比较
ff1 = all_heap;//设置管理小块空间的首部
ff1->val = HEAPMEM_SMALLBLK_BUCKET;
//设置划分小块空间与大块空间的首部
all_heap[HEAPMEM_SMALLBLK_HDRCNT].val = (HEAPMEM_HDRSZ | HEAPMEM_IN_USE);
// 设置管理大块空间首部
all_heap[HEAPMEM_BIGBLK_IDX].val = HEAPMEM_BIGBLK_SZ; // Set 'len'& clear 'inUse' field.
}
下面是执行heap_init()之后的内存地址空间示例(初始地址是某次上电随机出现的)
说明,第一个all_heap[0]存放这个小块空间首部,这个首部有小块空间剩余量,584表示可以使用584个byte的小块空间,每次有小块空间申请成功,这个剩余值就会减少。蓝色标注的是小块内存和大块内存的格挡板,位置为all_heap[292],状态标为1表示已使用,这样在小块内存在分配内存的时候就不会合并到大块内存上面。all_heap[293]存放大块空间首部,这个首部有大块空间剩余量,1460表示可以使用1460个byte的大块空间,每次有大块空间申请成功,这个剩余值就会减少。堆末尾all_heap[1023]值被初始化为0,以便与零进行快速比较。从0x036d-0x0b6c这2048个byte空间就是本次堆的全部空间大小。
下面是堆空间申请函数
void *heap_alloc(uint16 size)
{
heapMemHdr_t *prev = NULL;
heapMemHdr_t *hdr;
uint8 intState;
uint8 coal = 0;
size += HEAPMEM_HDRSZ; //给需要申请的空间分配一个管理首部
//进入临界区
//调整size大小,是空间对齐(与处理器和编译器相关)
if ( sizeof( uint8 ) == 2 )//假设uint8占用2个字节
{
size += (size& 0x01);//假设为196个,则size为196;假设为197个,则size要198才满足
}
else if ( sizeof( uint8 ) != 1 )
{
const uint8 mod = size % sizeof( uint8 );
if ( mod != 0 )
{
size += (sizeof( uint8 ) - mod);
}
}
//判断小块内存空间是否足够分配,否则向大块内存空间申请
if ((heapMemStat == 0) || (size< = HEAPMEM_SMALL_BLKSZ))
{
hdr = ff1;//小块内存,从ff1开始查找
}
else
{
hdr = (all_heap + HEAPMEM_BIGBLK_IDX);//从大块开始查找
}
//开始迭代的寻找适合的内存空间
do
{
if ( hdr->hdr.inUse )//遇到非free块
{
coal = 0;//告诉下一块,本块非free
}
else //遇到free块
{
if ( coal != 0 )//上一块是free块
{
prev->hdr.len += hdr->hdr.len;//两个free块合并相邻内存空间
if ( prev->hdr.len >= size ) //合并后的大小满足size
{
hdr = prev; //得到块的地址
break;
}
}
else //上一块是非free块
{
if ( hdr->hdr.len >= size )//一个快的大小就可以满足情况,分配,跳出循环返回
{
break;
}
coal = 1;//否则,标记coal为1,告诉下一块,本快是free的
prev = hdr; //保存当前内存地址
}
}
//(uint8 *)hdr这个操作使本来2个字节,强制转换成1个字节
hdr = (heapMemHdr_t *)((uint8 *)hdr + hdr->hdr.len);//经典malloc实现方式,迭代下一块
if ( hdr->val == 0 )//已经到达堆底部(初始化时,已经让堆底为零,方便识别)
{
hdr = NULL;//空指针,表示找不到合适size块
break;
}
}while(1);
if ( hdr != NULL )//已经找到合适size块
{
uint16 tmp = hdr->hdr.len - size;//表示块的大小大于请求的大小时,为了不浪费空间,还要把块切开
//确定是否满足拆分阈值
if ( tmp >= HEAPMEM_MIN_BLKSZ )//剩下的大小可以单独成为一个free块
{
heapMemHdr_t *next = (heapMemHdr_t *)((uint8 *)hdr + size);
next->val = tmp; // 告诉后一个块自己的信息
hdr->val = (size | HEAPMEM_IN_USE); // value代表前一块的大小和使用情况,这样相当于双向链表
}
else
{
hdr->hdr.inUse = TRUE; //标记本块已经被使用
}
if ((heapMemStat != 0)&& (ff1 == hdr))
{
ff1 = (heapMemHdr_t *)((uint8 *)hdr + hdr->hdr.len);
}
hdr++;
}
//退出临界区
return (void *)hdr;
}
其中,size是申请空间的多少,每次申请空间为1个byte。比如要申请10个元素uint16类型的数组,代码可以写:
uint16 *test;
test = (uint16*)heap_alloc(20);
下面写一段代码来测试heap_alloc函数
heap_init(); //初始化堆
uint8 *test1;
uint16 *test2;
uint32 *test3;
uint8 *test4;
test1 = (uint8 *)heap_alloc(1);
test2 = (uint16 *)heap_alloc(2);
test3 = (uint32 *)heap_alloc(4);
test4 = (uint8 *)heap_alloc(1);
下面是测试代码运行后申请空间情况示例(地址:0x036d-0x037e)
说明,灰色标识前后块信息(非free态),里面的数值是剩余byte空间,本次测试执行后,总共用去16个byte(地址0x036d-0x037e)空间,其中8个byte用于剩余头部信息存储,8个bytes才是有效空间。每个申请内存块都带有前后信息块,借鉴了双向链表的数据结构。568没有颜色的表示free态。
下面是堆空间释放函数
void heap_free(void *ptr)
{
//如果heapMemHdr_t为2个字节,则下面指针减去1,物理地址会改变2
heapMemHdr_t *hdr = (heapMemHdr_t *)ptr - 1;//获取该内存空间首部
uint8 intState;
//进入中断临界
hdr->hdr.inUse = FALSE; //标记使用状态为:未使用
if (ff1 > hdr)
{
ff1 = hdr;//调整ff1位置
}
//退出中断临界
}
比如执行heap_free(test1),传入地址为0x036f,执行过程中会将0x036d的状态标为free态,也就是释放掉堆占用了,584那块会由灰色变成无色。那么,下次申请空间的时候,可以申请使用这块内存(地址:0x036d-0x037e)
上面的示例是小内存的管理过程,大内存的管理也是一样的过程,这里就不列举了。
在实际项目中,搭载TI OSAL的ble芯片或者zibee芯片运行都非常稳定,这也要归功于OSAL高效可靠的内存管理。非常值得研究和借鉴!
之前发的帖子介绍了OSAL在STC8A8K64S4A12单片机的移植方式和简单使用,今天就简单介绍一下OSAL的内存管理方式。在介绍之前,首先要了解单片机的栈和堆的区别。 一提到“栈”,很容易想到单片机的压栈(PUSH)和出栈(POP)中用到的栈,这个栈是系统自动分配和释放的,具体分配多少栈空间,在程序编译后就已经确定不变了、而且无法更改。子函数内部的局部变量定义和使用,也是系统自动分配栈空间来保存变量,等函数执行完,系统就会将栈空间收回。另一个“堆”,就没有“栈”那么熟悉了,因为初学者用单片机的时候,不会想到用堆的,也比较少接触。“堆”就是用户自己管理的内存空间,一般都是定义一个大数组全局变量,再基于这个数组的内存空间做管理。自己编写类似malloc和free函来实现内存空间的申请和释放,即需要用到空间,要自己申请,等空间使用周期结束了,还要自己释放空间。
既然 “栈”那么方便,由系统自动申请,自动释放,肯定比自己去管理方便多了,那为什么还要“堆”呢?前面讲过,“栈”空间的使用在程序编译后就定死了,无法自己制定使用多大的栈,这样就很不方便,尤其是在传输处理动态数据流的时候就很麻烦。那肯定有人想过,在函数里面定义一个很大的数组局部变量,那不就可以应对各种各样的数据流了。如果在函数里面定义很大的数组,系统在跑的使用,其他函数也会用到栈空间哦,一旦栈空间使用过大导致内存溢出,那系统肯定奔溃了。“堆”就不一样了,要用多少空间,完全可以动态申请,需要多少,就申请多少。由于是在自己定义全局数组变量的地址上管理内存操作,不怕内存溢出。
栈和堆的共同点和区别:
1、共同点:a:都是占用ram空间;
b:可使用最大ram空间在程序编译后就确定不变了;
2、区别:栈空间由系统自动申请和释放,堆空间由用户自己申请和释放。
接下来介绍一下OSAL的内存管理,OSAL的内存管理是堆管理,OSAL_Memory.c里面定义了一个很大的数组theHeap,内存管理把堆空间分成两个部分,第一个部分是针对小块内存的管理,第二部分是针对大块内存的管理。这样做的好处是容易申请到连续的大空间,因为小块内存处理会使整个内存空间碎片化,那么会导致内存空间不连续,不连续的空间是对申请大空间是非常不利的。在申请堆空间时,会自动合并之前释放的free块,等到找到合适连续块时,会自动裁剪多余的块空间,以免造成空间的浪费。
下面列举出已去除其他无关代码和修改部分名称后的内存管理代码,代码处理有详细注释,还有测试过程。另外,代码的测试是基于STC8A8K64S4A12单片机,ram空间为8K。
首先是相关宏和变量定义
//堆总空间
#define HEAP_SIZE 2048
#define HEAPMEM_IN_USE 0x8000
//堆空间结束位置
#define HEAP_LASTBLK_IDX ((HEAP_SIZE / HEAPMEM_HDRSZ) - 1)
//区分块位置
#define HEAPMEM_SMALLBLK_HDRCNT (HEAPMEM_SMALLBLK_BUCKET / HEAPMEM_HDRSZ)
//大块起始位置
#define HEAPMEM_BIGBLK_IDX (HEAPMEM_SMALLBLK_HDRCNT + 1)
//首部大小
#define HEAPMEM_HDRSZ sizeof(heapMemHdr_t)
//最小块大小
#define HEAPMEM_MIN_BLKSZ (HEAPMEM_ROUND((HEAPMEM_HDRSZ * 2)))
//小块大小
#define HEAPMEM_SMALL_BLKSZ (HEAPMEM_ROUND(16))
//默认长块大小
#define HEAPMEM_LL_BLKSZ (HEAPMEM_ROUND(417) + (19 * HEAPMEM_HDRSZ))
//小块总空间
#define HEAPMEM_SMALLBLK_BUCKET ((HEAPMEM_SMALL_BLKSZ * HEAPMEM_SMALL_BLKCNT) + HEAPMEM_LL_BLKSZ)
//大块总空间
#define HEAPMEM_BIGBLK_SZ (HEAP_SIZE - HEAPMEM_SMALLBLK_BUCKET - HEAPMEM_HDRSZ*2)
//默认小块数量
#define HEAPMEM_SMALL_BLKCNT 8
// 调整申请内存大小宏操作(如申请17字节空间,则调整为18字节)
#define HEAPMEM_ROUND(X) ((((X) + HEAPMEM_HDRSZ - 1) / HEAPMEM_HDRSZ) * HEAPMEM_HDRSZ)
typedef struct {
unsigned len : 15;//本快的长度最大为2^16-1个字节,且申请空间的最小粒度为2个字节
unsigned inUse : 1;//标志位表示本快是否已经被使用
} heapMemHdrHdr_t;
typedef union {
//因此,当halDataAlign\u t小于UINT16时,编译器强制结构对齐最大的元素,而不会在目标上浪费空间。
uint8 alignDummy;
uint16 val;//存储上一块长度,in use信息
heapMemHdrHdr_t hdr;//快头指针
} heapMemHdr_t;
static __no_init heapMemHdr_t all_heap[HEAP_SIZE];//定义堆空间数组
static __no_init heapMemHdr_t *ff1; //第一个空块
static uint8 heapMemStat = 0x01; // 离散状态标志 0x01:踢出
说明,每个all_heap元素占用2个字节,即16个bit,最高位bit代表使用状态,1表示非free,0表示free。剩下15个bit可以表示32768个byte堆空间,高达32K了,应付51单片机是完全没有问题的。定义了一个全局数组变量all_heap作为堆总空间,大小为2048个byte。可以根据单片机资源自行修改堆总空间大小,宏HEAPMEM_SMALLBLK_HDRCNT 是小块内存和大块内存的分界数值。
下面是堆空间初始化函数
void heap_init(void)
{
all_heap[HEAP_LASTBLK_IDX].val = 0;//在堆的末尾设置一个空块,以便与零进行快速比较
ff1 = all_heap;//设置管理小块空间的首部
ff1->val = HEAPMEM_SMALLBLK_BUCKET;
//设置划分小块空间与大块空间的首部
all_heap[HEAPMEM_SMALLBLK_HDRCNT].val = (HEAPMEM_HDRSZ | HEAPMEM_IN_USE);
// 设置管理大块空间首部
all_heap[HEAPMEM_BIGBLK_IDX].val = HEAPMEM_BIGBLK_SZ; // Set 'len'& clear 'inUse' field.
}
下面是执行heap_init()之后的内存地址空间示例(初始地址是某次上电随机出现的)
下面是堆空间申请函数
void *heap_alloc(uint16 size)
{
heapMemHdr_t *prev = NULL;
heapMemHdr_t *hdr;
uint8 intState;
uint8 coal = 0;
size += HEAPMEM_HDRSZ; //给需要申请的空间分配一个管理首部
//进入临界区
//调整size大小,是空间对齐(与处理器和编译器相关)
if ( sizeof( uint8 ) == 2 )//假设uint8占用2个字节
{
size += (size& 0x01);//假设为196个,则size为196;假设为197个,则size要198才满足
}
else if ( sizeof( uint8 ) != 1 )
{
const uint8 mod = size % sizeof( uint8 );
if ( mod != 0 )
{
size += (sizeof( uint8 ) - mod);
}
}
//判断小块内存空间是否足够分配,否则向大块内存空间申请
if ((heapMemStat == 0) || (size< = HEAPMEM_SMALL_BLKSZ))
{
hdr = ff1;//小块内存,从ff1开始查找
}
else
{
hdr = (all_heap + HEAPMEM_BIGBLK_IDX);//从大块开始查找
}
//开始迭代的寻找适合的内存空间
do
{
if ( hdr->hdr.inUse )//遇到非free块
{
coal = 0;//告诉下一块,本块非free
}
else //遇到free块
{
if ( coal != 0 )//上一块是free块
{
prev->hdr.len += hdr->hdr.len;//两个free块合并相邻内存空间
if ( prev->hdr.len >= size ) //合并后的大小满足size
{
hdr = prev; //得到块的地址
break;
}
}
else //上一块是非free块
{
if ( hdr->hdr.len >= size )//一个快的大小就可以满足情况,分配,跳出循环返回
{
break;
}
coal = 1;//否则,标记coal为1,告诉下一块,本快是free的
prev = hdr; //保存当前内存地址
}
}
//(uint8 *)hdr这个操作使本来2个字节,强制转换成1个字节
hdr = (heapMemHdr_t *)((uint8 *)hdr + hdr->hdr.len);//经典malloc实现方式,迭代下一块
if ( hdr->val == 0 )//已经到达堆底部(初始化时,已经让堆底为零,方便识别)
{
hdr = NULL;//空指针,表示找不到合适size块
break;
}
}while(1);
if ( hdr != NULL )//已经找到合适size块
{
uint16 tmp = hdr->hdr.len - size;//表示块的大小大于请求的大小时,为了不浪费空间,还要把块切开
//确定是否满足拆分阈值
if ( tmp >= HEAPMEM_MIN_BLKSZ )//剩下的大小可以单独成为一个free块
{
heapMemHdr_t *next = (heapMemHdr_t *)((uint8 *)hdr + size);
next->val = tmp; // 告诉后一个块自己的信息
hdr->val = (size | HEAPMEM_IN_USE); // value代表前一块的大小和使用情况,这样相当于双向链表
}
else
{
hdr->hdr.inUse = TRUE; //标记本块已经被使用
}
if ((heapMemStat != 0)&& (ff1 == hdr))
{
ff1 = (heapMemHdr_t *)((uint8 *)hdr + hdr->hdr.len);
}
hdr++;
}
//退出临界区
return (void *)hdr;
}
其中,size是申请空间的多少,每次申请空间为1个byte。比如要申请10个元素uint16类型的数组,代码可以写:
uint16 *test;
test = (uint16*)heap_alloc(20);
下面写一段代码来测试heap_alloc函数
heap_init(); //初始化堆
uint8 *test1;
uint16 *test2;
uint32 *test3;
uint8 *test4;
test1 = (uint8 *)heap_alloc(1);
test2 = (uint16 *)heap_alloc(2);
test3 = (uint32 *)heap_alloc(4);
test4 = (uint8 *)heap_alloc(1);
下面是测试代码运行后申请空间情况示例(地址:0x036d-0x037e)
说明,灰色标识前后块信息(非free态),里面的数值是剩余byte空间,本次测试执行后,总共用去16个byte(地址0x036d-0x037e)空间,其中8个byte用于剩余头部信息存储,8个bytes才是有效空间。每个申请内存块都带有前后信息块,借鉴了双向链表的数据结构。568没有颜色的表示free态。
下面是堆空间释放函数
void heap_free(void *ptr)
{
//如果heapMemHdr_t为2个字节,则下面指针减去1,物理地址会改变2
heapMemHdr_t *hdr = (heapMemHdr_t *)ptr - 1;//获取该内存空间首部
uint8 intState;
//进入中断临界
hdr->hdr.inUse = FALSE; //标记使用状态为:未使用
if (ff1 > hdr)
{
ff1 = hdr;//调整ff1位置
}
//退出中断临界
}
比如执行heap_free(test1),传入地址为0x036f,执行过程中会将0x036d的状态标为free态,也就是释放掉堆占用了,584那块会由灰色变成无色。那么,下次申请空间的时候,可以申请使用这块内存(地址:0x036d-0x037e)
在实际项目中,搭载TI OSAL的ble芯片或者zibee芯片运行都非常稳定,这也要归功于OSAL高效可靠的内存管理。非常值得研究和借鉴!