找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 3062|回复: 7
收起左侧

TI OSAL内存管理实现方式

[复制链接]
ID:351097 发表于 2020-1-15 00:53 | 显示全部楼层 |阅读模式
本帖最后由 没有你 于 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()之后的内存地址空间示例(初始地址是某次上电随机出现的)
use
value
地址
对应数组
0
584
0x036d - 0x036e
all_heap[0]
0
-
-
-
0
-
-
-
1
2
0x05b5 - 0x05b6
all_heap[292]
0
1460
0x05b7 - 0x05b8
all_heap[293]
0
-
-
-
0
-
-
-
0
0
0x0b6b - 0x0b6c
all_heap[1023]
    说明,第一个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)
0x03-前缀
6d
6e
6f
70
71
72
73
74
75
76
77
78
79
7a
7b
7c
7d
7e
      申请块
test1
test2
test3
Test4
      标识块
584
581
577
571
568

   说明,灰色标识前后块信息(非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)
   0x03-前缀
6d
6e
6f
70
71
72
73
74
75
76
77
78
79
7a
7b
7c
7d
7e
      申请块
test2
test3
Test4
      标识块
584
581
577
571
568
    上面的示例是小内存的管理过程,大内存的管理也是一样的过程,这里就不列举了。
  在实际项目中,搭载TI OSAL的ble芯片或者zibee芯片运行都非常稳定,这也要归功于OSAL高效可靠的内存管理。非常值得研究和借鉴!




评分

参与人数 1黑币 +50 收起 理由
admin + 50 共享资料的黑币奖励!

查看全部评分

回复

使用道具 举报

ID:117433 发表于 2020-1-15 16:46 | 显示全部楼层
你写这么多,为什么就不能让大家都拿来就用体验一下
回复

使用道具 举报

ID:351097 发表于 2020-1-15 20:43 | 显示全部楼层
xizhe2005 发表于 2020-1-15 16:46
你写这么多,为什么就不能让大家都拿来就用体验一下

兄弟,代码都亮出了,不就可以体验了吗
回复

使用道具 举报

ID:117433 发表于 2020-1-17 09:31 | 显示全部楼层
给个现成的KEIL工程,让我下载到我的单片机里,谢了,我不太懂原理,只是个用户
回复

使用道具 举报

ID:351097 发表于 2020-1-17 13:14 来自手机 | 显示全部楼层
xizhe2005 发表于 2020-1-17 09:31
给个现成的KEIL工程,让我下载到我的单片机里,谢了,我不太懂原理,只是个用户

我电脑安装keil打开没响应,所以没有搞keil工程。你直接把现成的代码添加到你的keil工程里面,编译不成问题的。
回复

使用道具 举报

ID:117433 发表于 2020-1-17 16:46 | 显示全部楼层
你都没试过呀,那你怎么知道没问题
回复

使用道具 举报

ID:351097 发表于 2020-1-17 19:09 来自手机 | 显示全部楼层
xizhe2005 发表于 2020-1-17 16:46
你都没试过呀,那你怎么知道没问题

我在IAR平台调试的
回复

使用道具 举报

ID:351097 发表于 2020-1-17 19:24 | 显示全部楼层
xizhe2005 发表于 2020-1-17 16:46
你都没试过呀,那你怎么知道没问题

就算有点问题,应该也是数据类型定义那边修改一下就可以了。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表