当前位置: 首页 > news >正文

(学习日记)2024.04.16:UCOSIII第四十四节:内存管理

写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。


标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。


点击此处进入学习日记的总目录

2024.04.16:UCOSIII第四十四节:内存管理

  • 五十八、UCOSIII:内存管理
    • 1、内存管理的基本概念
    • 2、内存管理的运作机制
    • 3、内存管理的应用场景
    • 4、内存管理函数接口讲解
      • 1. 内存池创建函数
      • 2. 内存申请函数OSMemGet()
      • 3. 内存释放函数
    • 5、内存管理实验
    • 6、内存管理实验现象

五十八、UCOSIII:内存管理

1、内存管理的基本概念

在计算系统中,变量、中间数据一般存放在系统存储空间中,只有在实际使用时才将它们从存储空间调入到中央处理器内部进行运算。
通常存储空间可以分为两种:内部存储空间和外部存储空间。

  • 内部存储空间访问速度比较快,能够按照变量地址随机地访问, 也就是我们通常所说的RAM(随机存储器),或计算机的内存;
  • 而外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失, 可以把它理解为计算机的硬盘。

在这一章中我们主要讨论内部存储空间(RAM)的管理——内存管理。

在嵌入式系统设计中,内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法, 一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。
静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。

μC/OS的内存管理是采用内存池的方式进行管理,也就是创建一个内存池,静态划分一大块连续空间作为内存管理的空间, 里面划分为很多个内存块,我们在使用的时候就从这个内存池中获取一个内存块,使用完毕的时候用户可以将其放回内存池中, 这样子就不会导致内存碎片的产生。

μC/OS内存管理模块管理用于系统中内存资源,它是操作系统的核心模块之一,主要包括内存池的创建、分配以及释放。

很多人会有疑问,为什么不直接使用C标准库中的内存管理函数呢?
在计算机中我们可以用 malloc()和 free()这两个函数动态的分配内存和释放内存。
但是,在嵌入式实时操作系统中,调用 malloc()和 free()却是危险的,原因有以下几点:

  • 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的RAM不足。
  • 它们的实现可能非常的大,占据了相当大的一块代码空间。
  • 他们几乎都不是安全的。
  • 它们并不是确定的,每次调用这些函数执行的时间可能都不一样。
  • 它们有可能产生碎片。
  • 这两个函数会使得链接器配置得复杂。
  • 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为debug的灾难。

在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。
所有的内存都需要用户参与分配,直接操作物理内存, 所分配的内存不能超过系统的物理内存,所有的系统栈的管理,都由用户自己管理。

同时,在嵌入式实时操作系统中,对内存的分配时间要求更为苛刻,分配内存的时间必须是确定的。
一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面, 而寻找这样一个空闲内存块所耗费的时间是不确定的,因此对于实时系统来说,这就是不可接受的。
实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。

在嵌入式系统中,内存是十分有限而且是十分珍贵的,用一块内存就少了一块内存,而在分配中随着内存不断被分配和释放, 整个系统内存区域会产生越来越多的碎片,因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块, 它们地址不连续,不能够作为一整块的大内存分配出去,所以一定会在某个时间,系统已经无法分配到合适的内存了,导致系统瘫痪。
其实系统中实际是还有内存的,但是因为小块的内存的地址不连续,导致无法分配成功,所以我们需要一个优良的内存分配算法来避免这种情况的出现。
所以μC/OS提供的内存分配算法是只允许用户分配固定大小的内存块,当使用完成就将其放回内存池中,这样子分配效率极高, 时间复杂度是O(1),也就是一个固定的时间常数,并不会因为系统内存的多少而增加遍历内存块列表的时间,并且还不会导致内存碎片的出现。
但是这样的内存分配机制会导致内存利用率的下降以及申请内存大小的限制。

2、内存管理的运作机制

内存池(Memory Pool)是一种用于分配大量大小相同的内存对象的技术,它可以极大加快内存分配/释放的速度。

在系统编译的时候,编译器就静态划分了一个大数组作为系统的内存池,然后在初始化的时候将其分成大小相等的多个内存块, 内存块直接通过链表连接起来(此链表也称为空闲内存块列表)。
每次分配的时候,从空闲内存块列表中取出表头上第一个内存块, 提供给申请者。
物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个大小相同的空闲内存块组成。
我们必须先创建内存池才能去使用内存池里面的内存块,在创建的时候,我们必须定义一个内存池控制块,然后进行相关初始化, 内存控制块的参数包括内存池名称,内存池起始地址,内存块大小,内存块数量等信息,在以后需要从内存池取出内存块或者释放内存块的时候, 我们只需根据内存控制块的信息就能很轻易做到,内存控制块的数据结构具体如下。

struct os_mem
{OS_OBJ_TYPE          Type;              (1)void                *AddrPtr;           (2)CPU_CHAR            *NamePtr;           (3)void                *FreeListPtr;       (4)OS_MEM_SIZE          BlkSize;           (5)OS_MEM_QTY           NbrMax;            (6)OS_MEM_QTY           NbrFree;           (7)
#if OS_CFG_DBG_EN > 0uOS_MEM              *DbgPrevPtr;OS_MEM              *DbgNextPtr;
#endif
};
  • (1):内核对象类型。
  • (2):内存池的起始地址。
  • (3):内存池名称。
  • (4):空闲内存块列表。
  • (5):内存块大小。
  • (6):内存池中内存块的总数量。
  • (7):空闲内存块数量。

内存池一旦创建完成, 其内部的内存块大小将不能再做调整,具体见图
在这里插入图片描述

注意:
内存池中的内存块是通过单链表连接起来的,类似于消息池,内存池在创建的时候内存块地址是连续的, 但是经过多次申请以及释放后,空闲内存块列表的内存块在地址上不一定是连续的。

3、内存管理的应用场景

首先,在使用内存分配前,必须明白自己在做什么,这样做与其他的方法有什么不同,特别是会产生哪些负面影响,在自己的产品面前,应当选择哪种分配策略。

内存管理的主要工作是动态划分并管理用户分配好的内存区间,主要是在用户需要使用大小不等的内存块的场景中使用, 当用户需要分配内存时,可以通过操作系统的内存申请函数索取指定大小内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存, 使之可以重复使用(heap_1.c的内存管理除外)。

例如我们需要定义一个float型数组:floatArr[];

但是,在使用数组的时候,总有一个问题困扰着我们:数组应该有多大?
在很多的情况下,你并不能确定要使用多大的数组, 可能为了避免发生错误你就需要把数组定义得足够大。即使你知道想利用的空间大小,但是如果因为某种特殊原因空间利用的大小有增加或者减少, 你又必须重新去修改程序,扩大数组的存储范围。
这种分配固定大小的内存分配方法称之为静态内存分配
这种内存分配的方法存在比较严重的缺陷, 在大多数情况下会浪费大量的内存空间,在少数情况下,当你定义的数组不够大时,可能引起下标越界错误,甚至导致严重后果。

μC/OS将系统静态分配的大数组作为内存池,然后进行内存池的初始化,然后分配固定大小的内存块。

注意:
μC/OS也不能很好解决这种问题,因为内存块的大小是固定的,无法解决这种弹性很大的内存需求,只能按照最大的内存块进行分配。
但是μC/OS的内存分配能解决内存利用率的问题,在不需要使用内存的时候,将内存释放到内存池中,让其他任务能正常使用该内存块。

4、内存管理函数接口讲解

1. 内存池创建函数

在使用内存池的时候首先要创建一个内存池,需要用户静态分配一个数组空间作为系统的内存池,且用户还需定义一个内存控制块。
创建内存池后,任务才可以通过系统的内存申请、释放函数从内存池中申请或释放内存。
μC/OS提供内存池创建函数OSMemCreate(), 内存池创建函数源码具体如下:

void  OSMemCreate (OS_MEM       *p_mem,     (1)     //内存池控制块CPU_CHAR     *p_name,       (2)     //命名内存池void         *p_addr,       (3)     //内存池首地址OS_MEM_QTY    n_blks,       (4)     //内存块数目OS_MEM_SIZE   blk_size,     (5)     //内存块大小(单位:字节)OS_ERR       *p_err)        (6)     //返回错误类型
{
#if OS_CFG_ARG_CHK_EN > 0uCPU_DATA       align_msk;
#endifOS_MEM_QTY     i;OS_MEM_QTY     loops;CPU_INT08U    *p_blk;void         **p_link;               //二级指针,存放指针的指针CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏,该宏声明和//定义一个局部变量,用于保存关中断前的 CPU 状态寄存器// SR(临界段关中断只需保存SR),开中断时将该值还原。#ifdef OS_SAFETY_CRITICAL//如果启用了安全检测if (p_err == (OS_ERR *)0)            //如果错误类型实参为空{OS_SAFETY_CRITICAL_EXCEPTION();  //执行安全检测异常函数return;                          //返回,停止执行}
#endif#ifdef OS_SAFETY_CRITICAL_IEC61508//如果启用了安全关键if (OSSafetyCriticalStartFlag == DEF_TRUE){*p_err = OS_ERR_ILLEGAL_CREATE_RUN_TIME;//错误类型为“非法创建内核对象”return;                                  //返回,停止执行}
#endif#if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u//如果启用了中断中非法调用检测if (OSIntNestingCtr > (OS_NESTING_CTR)0)   //如果该函数是在中断中被调用{*p_err = OS_ERR_MEM_CREATE_ISR;         //错误类型为“在中断中创建对象”return;                                //返回,停止执行}
#endif#if OS_CFG_ARG_CHK_EN > 0u//如果启用了参数检测if (p_addr == (void *)0)                (7)//如果 p_addr 为空{*p_err   = OS_ERR_MEM_INVALID_P_ADDR;    //错误类型为“内存池地址非法”return;                                        //返回,停止执行}if (n_blks < (OS_MEM_QTY)2)             (8)//如果内存池的内存块数目少于2{*p_err = OS_ERR_MEM_INVALID_BLKS;         //错误类型为“内存块数目非法”return;                                        //返回,停止执行}if (blk_size <sizeof(void *))          (9)//如果内存块空间小于指针的{*p_err = OS_ERR_MEM_INVALID_SIZE;          //错误类型为“内存空间非法”return;                                        //返回,停止执行}align_msk = sizeof(void *) - 1u;        (10)//开始检查内存地址是否对齐if (align_msk > 0u){if (((CPU_ADDR)p_addr & align_msk) != 0u)  //如果首地址没对齐{*p_err = OS_ERR_MEM_INVALID_P_ADDR;   //错误类型为“内存池地址非法”return;                                    //返回,停止执行}if ((blk_size & align_msk) != 0u)   (11)//如果内存块地址没对齐{*p_err = OS_ERR_MEM_INVALID_SIZE;     //错误类型为“内存块大小非法”return;                                    //返回,停止执行}}
#endif/* 将空闲内存块串联成一个单向链表 */p_link = (void **)p_addr;              (12)//内存池首地址转为二级指针p_blk  = (CPU_INT08U *)p_addr;         (13)//首个内存块地址loops  = n_blks - 1u;for (i = 0u; i < loops; i++)           (14)//将内存块逐个串成单向链表{p_blk +=  blk_size;                            //下一内存块地址*p_link = (void  *)p_blk;//在当前内存块保存下一个内存块地址p_link = (void **)(void *)p_blk;//下一个内存块的地址转为二级指针}*p_link             = (void *)0;       (15)//最后一个内存块指向空OS_CRITICAL_ENTER();                             //进入临界段p_mem->Type        = OS_OBJ_TYPE_MEM;  (16)//设置对象的类型p_mem->NamePtr     = p_name;           (17)//保存内存池的命名p_mem->AddrPtr     = p_addr;           (18)//存储内存池的首地址p_mem->FreeListPtr = p_addr;           (19)//初始化空闲内存块池的首地址p_mem->NbrFree     = n_blks;          (20)//存储空闲内存块的数目p_mem->NbrMax      = n_blks;           (21)//存储内存块的总数目p_mem->BlkSize     = blk_size;         (22)//存储内存块的空间大小#if OS_CFG_DBG_EN > 0u//如果启用了调试代码和变量OS_MemDbgListAdd(p_mem);      //将内存管理对象插入内存管理双向调试列表
#endifOSMemQty++;             (23)//内存管理对象数目加1OS_CRITICAL_EXIT_NO_SCHED();  //退出临界段(无调度)*p_err = OS_ERR_NONE;          //错误类型为“无错误”
}
  • (1):内存池控制块指针。
  • (2):内存池名字。
  • (3):内存池首地址。
  • (4):内存块数目。
  • (5):内存块大小(单位:字节)。
  • (6):返回的错误类型。
  • (7):如果启用了参数检测,在编译的时候回包含参数检测相关代码, 如果 p_addr 为空,返回错误类型为“内存池地址非法”的错误代码。
  • (8):如果内存池的内存块数目少于2,返回错误类型为“内存块数目非法”错误代码。
  • (9):如果内存块空间小于一个指针的大小(在stm32上是4字节), 返回错误类型为“内存空间非法”的错误代码。sizeof(void *)是求出 CPU 指针的字节大小,STM32 是 32 位单片机, 求出的指针所占字节大小是 4,减去 1 后就是 3,3的二进制数是 11(B)。如果一个地址或者内存块字节大小是4 字节对齐的, 那么用二进制表示地址或内存块大小最低两位都是 0,比如 11100(B)、101010100(B)这些 4 字节对齐的都最低 2 位都是 0, 那么 11(B)与上一个低两位字节都是0 的数结果肯定为 0,不为 0 说明不是 4字节对齐。同理可以检测内存块的大小是否是 4的倍数。
  • (10):开始检查内存地址是否对齐,如果内存池首地址没对齐,返回错误类型为“内存池地址非法”的错误代码。
  • (11):如果内存块地址没对齐,返回错误类型为“内存块大小非法”的错误代码。
  • (12):程序执行到这里,就表示传递进来的参数都是正确的, 下面开始初始化内存池以及内存控制块的信息,将内存池首地址转为二级指针保存在p_link变量中。
  • (13):获取内存池中首个内存块地址。
  • (14):将空闲内存块逐个连接成一个单向链表, 根据内存块起始地址与内存块大小获取下一个内存块的地址,然后在当前内存块中保存下一个内存块的地址, 再将下一个内存块的地址转为二级指针,将这些内存块连接成一个单链表,也就是空闲内存块链表。

一个内存块的操作是先计算是下一个内存块的地址,因为此时数组元素的地址是连续的, 所以开始的时候只要在前一个内存块的首地址加上内存块字节大小即可得到下一个内存块的首地址, 然后把下一个内存块的首地址放在前一个内存块中,就将他们串起来了,如此循环反复即可串成空闲内存块列表。

  • (15):然后将最后一个内存块存储的地址为空, 表示到达空闲内存块列表尾部,连接完成的示意图具体见图
    在这里插入图片描述
  • (16):设置对象的类型。
  • (17):保存内存池的名称。
  • (18):保存内存池的首地址。
  • (19):初始化空闲内存块列表的首地址,指向下一个可用的内存块。
  • (20):保存空闲内存块的数目。
  • (21):保存内存块的总数目。
  • (22):保存内存块的空间大小。
  • (23):创建完成,内存管理对象数目加1。

整个内存池创建完成示意图具体见图
在这里插入图片描述
内存池创建函数的使用实例具体如下:

OS_MEM  mem;                    //声明内存管理对象
uint8_t ucArray [ 3 ] [ 20 ];   //声明内存池大小OS_ERR      err;
/* 创建内存管理对象 mem */
OSMemCreate ((OS_MEM      *)&mem,             //指向内存管理对象(CPU_CHAR    *)"Mem For Test",   //命名内存管理对象(void        *)ucArray,          //内存池的首地址(OS_MEM_QTY   )3,                //内存池中内存块数目(OS_MEM_SIZE  )20,               //内存块的字节数目(OS_ERR      *)&err);            //返回错误类型

2. 内存申请函数OSMemGet()

这个函数用于申请固定大小的内存块,从指定的内存池中分配一个内存块给用户使用,该内存块的大小在内存池初始化的时候就已经决定的。
如果内存池中有可用的内存块,则从内存池的空闲内存块列表上取下一个内存块并且返回对应的内存地址;
如果内存池中已经没有可用内存块, 则返回0与对应的错误代码OS_ERR_MEM_NO_FREE_BLKS,其源码具体如下:

void  *OSMemGet (OS_MEM  *p_mem,    (1)     //内存管理对象OS_ERR  *p_err)     (2)     //返回错误类型
{void    *p_blk;CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏,该宏声明和//定义一个局部变量,用于保存关中断前的 CPU 状态寄存器// SR(临界段关中断只需保存SR),开中断时将该值还原。#ifdef OS_SAFETY_CRITICAL//如果启用了安全检测if (p_err == (OS_ERR *)0)            //如果错误类型实参为空{OS_SAFETY_CRITICAL_EXCEPTION();  //执行安全检测异常函数return ((void *)0);              //返回0(有错误),停止执行}
#endif#if OS_CFG_ARG_CHK_EN > 0u//如果启用了参数检测if (p_mem == (OS_MEM *)0)              //如果 p_mem 为空{*p_err  = OS_ERR_MEM_INVALID_P_MEM; //错误类型为“内存池非法”return ((void *)0);                //返回0(有错误),停止执行}
#endifCPU_CRITICAL_ENTER();                    //关中断if (p_mem->NbrFree == (OS_MEM_QTY)0) (3)//如果没有空闲的内存块{CPU_CRITICAL_EXIT();                 //开中断*p_err = OS_ERR_MEM_NO_FREE_BLKS;     //错误类型为“没有空闲内存块”return ((void *)0);                  //返回0(有错误),停止执行}p_blk  = p_mem->FreeListPtr;    (4)     //如果还有空闲内存块,就获取它p_mem->FreeListPtr = *(void **)p_blk;(5)//调整空闲内存块指针p_mem->NbrFree--;                   (6)//空闲内存块数目减1CPU_CRITICAL_EXIT();                     //开中断*p_err = OS_ERR_NONE;                     //错误类型为“无错误”return (p_blk);                      (7)//返回获取到的内存块
}
  • (1):指定内存池对象。
  • (2):保存返回的错误类型。
  • (3):判断一下内存池控制块中NbrFree的值,如果没有空闲的内存块, 就没法申请内存,保存错误类型为“没有空闲内存块”的错误代码,返回0表示没申请到内存块。
  • (4):如果内存池中还有空闲内存块,就获取它, 获取的过程就是从空闲内存块中取出一个内存块,并且返回该内存块的地址。
  • (5):调整内存池控制块的空闲内存块指针,指向下一个可用的内存块。
  • (6):内存池中空闲内存块数目减1。
  • (7):返回获取到的内存块地址。

假设我们在内存池创建完成后就调用OSMemGet()函数申请一个内存块,那么申请完毕后的内存块示意图具体见图
在这里插入图片描述
被申请出去的内存块会脱离空闲内存块列表,并且内存控制块中的NbrFree变量会减一。

OSMemGet()函数的使用实例具体如下:

OS_MEM  mem;                    //声明内存管理对象
OS_ERR      err;
/* 向 mem 获取内存块 */
p_mem_blk = OSMemGet ((OS_MEM      *)&mem,              //指向内存管理对象(OS_ERR      *)&err);             //返回错误类型

3. 内存释放函数

嵌入式系统的内存对我们来说是十分珍贵的,任何内存块使用完后都必须被释放,否则会造成内存泄漏, 导致系统发生致命错误。
μC/OS提供了OSMemPut()函数进行内存的释放管理,使用该函数接口时,根据指定的内存控制块对象, 将内存块插入内存池的空闲内存块列表中,然后增加该内存池的可用内存块数目,其源码具体如下:

void  OSMemPut (OS_MEM  *p_mem,     (1)     //内存管理对象
void    *p_blk,     (2)     //要退回的内存块OS_ERR  *p_err)     (3)     //返回错误类型
{CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏,该宏声明和//定义一个局部变量,用于保存关中断前的 CPU 状态寄存器// SR(临界段关中断只需保存SR),开中断时将该值还原。#ifdef OS_SAFETY_CRITICAL//如果启用了安全检测if (p_err == (OS_ERR *)0)            //如果错误类型实参为空{OS_SAFETY_CRITICAL_EXCEPTION();  //执行安全检测异常函数return;                          //返回,停止执行}
#endif#if OS_CFG_ARG_CHK_EN > 0u//如果启用了参数检测if (p_mem == (OS_MEM *)0)               //如果 p_mem 为空{*p_err  = OS_ERR_MEM_INVALID_P_MEM;  //错误类型为“内存池非法”return;                             //返回,停止执行}if (p_blk == (void *)0)                 //如果内存块为空{*p_err  = OS_ERR_MEM_INVALID_P_BLK;  //错误类型为"内存块非法"return;                             //返回,停止执行}
#endifCPU_CRITICAL_ENTER();                   //关中断if (p_mem->NbrFree >= p_mem->NbrMax)  (4)//如果内存池已满{CPU_CRITICAL_EXIT();                 //开中断*p_err = OS_ERR_MEM_FULL;             //错误类型为“内存池已满”return;                              //返回,停止执行}*(void **)p_blk = p_mem->FreeListPtr; (5)//把内存块插入空闲内存块链表p_mem->FreeListPtr = p_blk;           (6)//内存块退回到链表的最前端p_mem->NbrFree++;                     (7)//空闲内存块数目加1CPU_CRITICAL_EXIT();                  //开中断*p_err              = OS_ERR_NONE;        //错误类型为“无错误”
}
  • (1):内存控制块指针,指向要操作的内存池。
  • (2):要释放的内存块。
  • (3):保存返回的错误类型。
  • (4):如果内存池已经满了,那是无法进行释放的,返回错误类型为“内存池已满”的错误代码。
  • (5):如果内存池没满,那么释放内存块到内存池中,把内存块插入空闲内存块列表。
  • (6):内存块退回到链表的最前端。
  • (7):空闲内存块数目加1。

我们在释放一个内存块的时候,我们会将内存插入内存池中空闲内存块列表的首部,然后增加内存池中空闲内存块的数量, 该函数的使用实例具体如下:

OS_MEM  mem;                    //声明内存管理对象OS_ERR      err;/* 释放内存块 */
OSMemPut ((OS_MEM  *)&mem,                        //指向内存管理对象(void    *)pMsg,                        //内存块的首地址(OS_ERR  *)&err);                       //返回错误类型

需要注意的是:
我们想要使用内存管理相关的函数时, 需要将os_cfg.h中的OS_CFG_MEM_EN宏定义配置为1;
OSMemCreate()只能在任务级被调用, 但是OSMemGet()和OSMemPut()可以在中断中被调用。

5、内存管理实验

本次的实验例程采用消息队列进行发送与接收消息,只不过存放消息的地方是在内存块中,在获取完消息的时候, 就进行释放内存块,反复使用内存块,具体如下:

#include <includes.h>
#include <string.h>OS_MEM  mem;                    //声明内存管理对象
uint8_t ucArray [ 3 ] [ 20 ];   //声明内存分区大小static  OS_TCB   AppTaskStartTCB;    //任务控制块
static  OS_TCB   AppTaskPostTCB;
static  OS_TCB   AppTaskPendTCB;static  CPU_STK  AppTaskStartStk[APP_TASK_START_STK_SIZE];       //任务栈
static  CPU_STK  AppTaskPostStk [ APP_TASK_POST_STK_SIZE ];
static  CPU_STK  AppTaskPendStk [ APP_TASK_PEND_STK_SIZE ];static  void  AppTaskStart  (void *p_arg);               //任务函数声明
static  void  AppTaskPost   ( void * p_arg );
static  void  AppTaskPend   ( void * p_arg );int  main (void)
{OS_ERR  err;OSInit(&err);//初始化 μC/OS-III/* 创建起始任务 */OSTaskCreate((OS_TCB     *)&AppTaskStartTCB,//任务控制块地址(CPU_CHAR   *)"App Task Start",//任务名称(OS_TASK_PTR ) AppTaskStart,//任务函数(void       *) 0,//传递给任务函数(形参p_arg)的实参(OS_PRIO     ) APP_TASK_START_PRIO,//任务的优先级(CPU_STK    *)&AppTaskStartStk[0],//任务栈的基地址(CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10,//任务栈空间剩下1/10时限制其增长(CPU_STK_SIZE) APP_TASK_START_STK_SIZE,//任务栈空间(单位:sizeof(CPU_STK))(OS_MSG_QTY  ) 5u,//任务可接收的最大消息数(OS_TICK     ) 0u,//任务的时间片节拍数(0表默认值OSCfg_TickRate_Hz/10)(void       *) 0,//任务扩展(0表不扩展)(OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),//任务选项(OS_ERR     *)&err);//返回错误类型OSStart(&err);//启动多任务管理(交由μC/OS-III控制)
}static  void  AppTaskStart (void *p_arg)
{CPU_INT32U  cpu_clk_freq;CPU_INT32U  cnts;OS_ERR      err;(void)p_arg;BSP_Init();   //板级初始化CPU_Init();   //初始化 CPU组件(时间戳、关中断时间测量和主机名)cpu_clk_freq = BSP_CPU_ClkFreq();cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz;OS_CPU_SysTickInit(cnts);Mem_Init();    //初始化内存管理组件(堆内存池和内存池表)#if OS_CFG_STAT_TASK_EN > 0u//如果启用(默认启用)了统计任务OSStatTaskCPUUsageInit(&err);
#endifCPU_IntDisMeasMaxCurReset();//复位(清零)当前最大关中断时间/* 创建内存管理对象 mem */OSMemCreate ((OS_MEM      *)&mem,             //指向内存管理对象(CPU_CHAR    *)"Mem For Test",   //命名内存管理对象(void        *)ucArray,          //内存分区的首地址(OS_MEM_QTY   )3,                //内存分区中内存块数目(OS_MEM_SIZE  )20,               //内存块的字节数目(OS_ERR      *)&err);            //返回错误类型/* 创建 AppTaskPost 任务 */OSTaskCreate((OS_TCB     *)&AppTaskPostTCB,//任务控制块地址(CPU_CHAR   *)"App Task Post",//任务名称(OS_TASK_PTR ) AppTaskPost,//任务函数(void       *) 0,//传递给任务函数(形参p_arg)的实参(OS_PRIO     ) APP_TASK_POST_PRIO,//任务的优先级(CPU_STK    *)&AppTaskPostStk[0],//任务栈的基地址(CPU_STK_SIZE) APP_TASK_POST_STK_SIZE / 10,//任务栈空间剩下1/10时限制其增长(CPU_STK_SIZE) APP_TASK_POST_STK_SIZE,//任务栈空间(单位:sizeof(CPU_STK))(OS_MSG_QTY  ) 5u,//任务可接收的最大消息数(OS_TICK     ) 0u,//任务的时间片节拍数(0表默认值OSCfg_TickRate_Hz/10)(void       *) 0,//任务扩展(0表不扩展)(OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),//任务选项(OS_ERR     *)&err);//返回错误类型/* 创建 AppTaskPend 任务 */OSTaskCreate((OS_TCB     *)&AppTaskPendTCB,//任务控制块地址(CPU_CHAR   *)"App Task Pend",//任务名称(OS_TASK_PTR ) AppTaskPend,//任务函数(void       *) 0,//传递给任务函数(形参p_arg)的实参(OS_PRIO     ) APP_TASK_PEND_PRIO,//任务的优先级(CPU_STK    *)&AppTaskPendStk[0],//任务栈的基地址(CPU_STK_SIZE) APP_TASK_PEND_STK_SIZE / 10,//任务栈空间剩下1/10时限制其增长(CPU_STK_SIZE) APP_TASK_PEND_STK_SIZE,//任务栈空间(单位:sizeof(CPU_STK))(OS_MSG_QTY  ) 50u,//任务可接收的最大消息数(OS_TICK     ) 0u,//任务的时间片节拍数(0表默认值OSCfg_TickRate_Hz/10)(void       *) 0,//任务扩展(0表不扩展)(OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),//任务选项(OS_ERR     *)&err);//返回错误类型OSTaskDel ( & AppTaskStartTCB, & err );//删除起始任务本身,该任务不再运行
}static  void  AppTaskPost ( void * p_arg )
{OS_ERR      err;char *   p_mem_blk;uint32_t ulCount = 0;(void)p_arg;while (DEF_TRUE)                              //任务体{/* 向 mem 获取内存块 */p_mem_blk = OSMemGet ((OS_MEM      *)&mem,//指向内存管理对象(OS_ERR      *)&err);    //返回错误类型sprintf ( p_mem_blk, "%d", ulCount ++ );//向内存块存取计数值/* 发布任务消息到任务 AppTaskPend */OSTaskQPost ((OS_TCB      *)&AppTaskPendTCB,//目标任务的控制块(void        *)p_mem_blk,//消息内容的首地址(OS_MSG_SIZE  )strlen ( p_mem_blk ),  //消息长度(OS_OPT       )OS_OPT_POST_FIFO,//发布到任务消息队列的入口端(OS_ERR      *)&err);              //返回错误类型OSTimeDlyHMSM ( 0, 0, 1, 0, OS_OPT_TIME_DLY, & err );}
}static  void  AppTaskPend ( void * p_arg )
{OS_ERR         err;OS_MSG_SIZE    msg_size;CPU_TS         ts;CPU_INT32U     cpu_clk_freq;CPU_SR_ALLOC();char * pMsg;(void)p_arg;cpu_clk_freq = BSP_CPU_ClkFreq();//获取CPU时钟,时间戳是以该时钟计数while (DEF_TRUE)                                             //任务体{/* 阻塞任务,等待任务消息 */pMsg = OSTaskQPend ((OS_TICK        )0,    //无期限等待(OS_OPT         )OS_OPT_PEND_BLOCKING,//没有消息就阻塞任务(OS_MSG_SIZE   *)&msg_size,  //返回消息长度(CPU_TS        *)&ts,//返回消息被发布的时间戳(OS_ERR        *)&err);  //返回错误类型ts = OS_TS_GET() - ts;//计算消息从发布到被接收的时间差macLED1_TOGGLE ();          //切换LED1的亮灭状态OS_CRITICAL_ENTER();//进入临界段,避免串口打印被打断printf ( "\r\n接收到的消息的内容为:%s,长度是:%d字节。",pMsg, msg_size );printf ( "\r\n任务消息从被发布到被接收的时间差是%dus\r\n",ts / ( cpu_clk_freq / 1000000 ) );OS_CRITICAL_EXIT();                               //退出临界段/* 退还内存块 */OSMemPut ((OS_MEM  *)&mem,                 //指向内存管理对象(void    *)pMsg,                        //内存块的首地址(OS_ERR  *)&err);                       //返回错误类型}
}

6、内存管理实验现象

打开串口调试助手,然后复位开发板就可以在调试助手中看到串口的打印信息与运行结果,具体见图
在这里插入图片描述

相关文章:

(学习日记)2024.04.16:UCOSIII第四十四节:内存管理

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…...

微信小程序Skyline模式下瀑布长列表优化成虚拟列表,解决内存问题

微信小程序长列表&#xff0c;渲染的越多就会导致内存吃的越多。特别是长列表的图片组件和广告组件。 为了解决内存问题&#xff0c;所以看了很多人的资料&#xff0c;都不太符合通用的解决方式&#xff0c;很多需要固定子组件高度&#xff0c;但是瀑布流是无法固定的&#xf…...

大语言模型LLM《提示词工程指南》学习笔记03

文章目录 大语言模型LLM《提示词工程指南》学习笔记03链式提示思维树检索增强生成自动推理并使用工具自动提示工程师Active-Prompt方向性刺激提示Program-Aided Language ModelsReAct框架Reflexion多模态思维链提示方法基于图的提示大语言模型LLM《提示词工程指南》学习笔记03 …...

239. 奇偶游戏(带权值并查集,邻域并查集,《算法竞赛进阶指南》)

239. 奇偶游戏 - AcWing题库 小 A 和小 B 在玩一个游戏。 首先&#xff0c;小 A 写了一个由 0 和 1 组成的序列 S&#xff0c;长度为 N。 然后&#xff0c;小 B 向小 A 提出了 M 个问题。 在每个问题中&#xff0c;小 B 指定两个数 l 和 r&#xff0c;小 A 回答 S[l∼r] 中…...

程序员做副业,AI头条,新赛道

大家好&#xff0c;我是老秦&#xff0c;6年编程&#xff0c;自由职业3年&#xff0c;今天继续更新副业内容。 在当今信息爆炸的时代&#xff0c;副业赚钱已成为许多人增加收入的重要途径。其中&#xff0c;AI头条模式以其独特的优势&#xff0c;吸引了越来越多的写作者加入。…...

Redis: 内存回收

文章目录 一、过期键删除策略1、惰性删除2、定时删除3、定期删除4、Redis的过期键删除策略 二、内存淘汰策略1、设置过期键的内存淘汰策略2、全库键的内存淘汰策略 一、过期键删除策略 1、惰性删除 顾名思义并不是在TTL到期后就立即删除&#xff0c;而是在访问一个key的时候&…...

【刷题篇】回溯算法(三)

文章目录 1、全排列2、子集3、找出所有子集的异或总和再求和4、全排列 II5、电话号码的字母组合6、括号生成 1、全排列 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 class Solution { public:vector<vector<i…...

pe格式从入门到图形化显示(八)-导入表

文章目录 前言一、什么是Windows PE格式中的导入表&#xff1f;二、解析导入表并显示1.导入表的结构2.解析导入表3.显示导入表 前言 通过分析和解析Windows PE格式&#xff0c;并使用qt进行图形化显示 一、什么是Windows PE格式中的导入表&#xff1f; 在Windows中&#xff0…...

如何将Paddle(Lite)模型转换为TensorFlow(Lite)模型

模型间的相互转换在深度学习应用中很常见&#xff0c;paddlelite和TensorFlowLite是移动端常用的推理框架&#xff0c;有时候需要将模型在两者之间做转换&#xff0c;本文将对转换方法做说明。 环境准备 建议使用TensorFlow2.14&#xff0c;PaddlePaddle 2.6 docker pull te…...

最新Zibll子比主题V7.1版本源码 全新推出开心版

源码下载地址&#xff1a;Zibll子比主题V7.1.zip...

响应式布局(其次)

响应式布局 一.响应式开发二.bootstrap前端开发框架1.原理2.优点3.版本问题4.使用&#xff08;1&#xff09;创建文件夹结构&#xff08;2&#xff09;创建html骨架结构&#xff08;3&#xff09;引入相关样式&#xff08;4&#xff09;书写内容 5.布局容器&#xff08;已经划分…...

arhtas idea plugin 使用手册

arthas idea plugin 使用文档 语雀...

数组算法——查询位置

需求 思路 使用二分查找找到第一个值&#xff0c;以第一个值作为界限&#xff0c;分为左右两个区间在左右两个区间分别使用二分查找找左边的7,&#xff1a;找到中间位置的7之后&#xff0c;将中间位置的7作为结束位置&#xff0c;依次循环查找&#xff0c;知道start>end,返回…...

【解决leecode打不开的问题】使用chrome浏览器和其他浏览器均打不开leecode

问题描述&#xff1a; 能进入leetcode力扣官网但是对某些栏目加载不出来&#xff0c;比如学习栏目能完成加载、题库栏目不能加载。 解决方法一&#xff1a;cookies缓存问题 首先尝试删除浏览器cookie缓存。 因为以下原因&#xff1a; Cookies损坏或过期&#xff1a;有些网站…...

尝试在手机上运行google 最新开源的gpt模型 gemma

Gemma介绍 Gemma简介 Gemma是谷歌于2024年2月21日发布的一系列轻量级、最先进的开放语言模型&#xff0c;使用了与创建Gemini模型相同的研究和技术。由Google DeepMind和Google其他团队共同开发。 Gemma提供两种尺寸的模型权重&#xff1a;2B和7B。每种尺寸都带有经过预训练&a…...

56、巴利亚多利德大学、马德里卡洛斯三世研究所:EEG-Inception-多时间尺度与空间卷积巧妙交叉堆叠,终达SOTA!

本次讲解一下于2020年发表在IEEE TRANSACTIONS ON NEURAL SYSTEMS AND REHABILITATION ENGINEERING上的专门处理EEG信号的EEG-Inception模型&#xff0c;该模型与EEGNet、EEG-ITNet、EEGNex、EEGFBCNet等模型均是专门处理EEG的SOTA。 我看到有很多同学刚入门&#xff0c;不太会…...

ORA-00600: internal error code, arguments: [krbcbp_9]

解决方案 1、清理过期 2、control_file_record_keep_time 修改 恢复时间窗口 RMAN (Recovery Manager) 是 Oracle 数据库的备份和恢复工具。在 RMAN 中&#xff0c;可以使用“恢复窗口”的概念来指定数据库可以恢复到的时间点。这个时间点是基于最近的完整备份或增量备份。 …...

uni-app实现分页--(2)分页加载,首页下拉触底加载更多

业务逻辑如下&#xff1a; api函数升级 定义分页参数类型 组件调用api传参...

前端工程化理解 (2024 面试题)

最好介绍远古世界最好随性一点&#xff0c;不要太刻板 &#xff0c;不然像背书 什么是前端工程化&#xff1f; - 知乎 前端工程化的历史 互联网初期&#xff0c;09 年以前&#xff0c;页面只需要展示一些列表、表格、文章内容以及简单图片即可&#xff0c;其目的是为了传送信…...

10 Php学习:循环

在 PHP 中&#xff0c;提供了下列循环语句&#xff1a; while - 只要指定的条件成立&#xff0c;则循环执行代码块do…while - 首先执行一次代码块&#xff0c;然后在指定的条件成立时重复这个循环for - 循环执行代码块指定的次数foreach - 根据数组中每个元素来循环代码块 当…...

FreeSWITCH 1.10.10 简单图形化界面17 - ubuntu22.04或者debian12 安装FreeSWITCH

FreeSWITCH 1.10.10 简单图形化界面17 - ubuntu22.04或者debian12 安装FreeSWITCH 界面预览00、先看使用手册0、安装操作系统1、下载脚本2、开始安装3、登录网页FreeSWITCH界面安装参考:https://blog.csdn.net/jia198810/article/details/132479324 界面预览 http://myfs.f3…...

ZStack Cloud 5.0.0正式发布——Vhost主存储、隔离PVLAN网络、云平台报警优化、灰度升级增强四大亮点简析

近日&#xff0c;ZStack Cloud 5.0.0正式发布&#xff0c;推出了包含Vhost主存储、隔离PVLAN网络、云平台报警优化、灰度升级增强在内的一系列重要功能。云主机管理、物理机运维、密评合规、灾备服务等诸多使用场景和功能模块均有更新&#xff0c;为您带来更完善的平台服务、更…...

商标没有去注册有哪些不好的影响!

有些商家咨询普推知产老杨&#xff0c;商标没有去注册有哪些不好的影响&#xff0c;其实对企业来说还有许多实际不利的影响&#xff0c;有时代价比注册一个商标要大很多。 想的商标名称没去注册商标&#xff0c;如果别人抢注拿下商标注册证&#xff0c;那就会涉及侵权&#xf…...

【小程序】常用方法、知识点汇总1

欢迎来到《小5讲堂》 这是《小程序》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 前言请求超时Markdown解析逐行显示效果文本变动事件转发…...

AugmentedReality之路-平面检测(5)

本文介绍通过AR检测水平平面和垂直平面&#xff0c;并将检测到的平面转化为Mesh 1、在首页添加功能入口 在首页添加一个按钮&#xff0c;命名为Start World Track 2、自定义ExecStartAREvent 创建ARSessionConfig并取名为ARSessionConfig_World 自定义ExecStartAREvent&…...

MQ:延迟队列

6.1场景&#xff1a; 1.定时发布文章 2.秒杀之后&#xff0c;给30分钟时间进行支付&#xff0c;如果30分钟后&#xff0c;没有支付&#xff0c;订单取消。 3.预约餐厅&#xff0c;提前半个小时发短信通知用户。 A -> 13:00 17:00 16:30 延迟时间&#xff1a; 7*30 * 60 *…...

Element ui 动态展示表格列,动态格式化表格列的值

需求 后台配置前端展示的表格列&#xff0c;遇到比如 文件大小这样的值&#xff0c;如果后台存的是纯数字&#xff0c;需要进行格式化展示&#xff0c;并且能控制显示的小数位数&#xff0c;再比如&#xff0c;部分列值需要加单位等信息&#xff0c;此外还有状态类&#xff0…...

xxl-job调度任务原理解析

xxljob可以对定时任务进行调度&#xff0c;现在看下定时任务调度的过程。XxlJobAdminConfig实现了InitializingBean接口&#xff0c;spring会调用afterPropertiesSet()进行初始化。大致有以下几个过程&#xff1a; admin服务端初始化 JobTriggerPoolHelper.java#toStart()方法…...

实验2 路由器基本配置

实验2 路由器基本配置 一、 原理描述二、 实验目的三、 实验内容四、 实验步骤1.建立实验拓扑2.基础配置3.配置路由器接口IP地址4.查看路由器配置信息5.连通性测试6.使用抓包工具 一、 原理描述 华为设备支持多种配置方式&#xff0c;操作人员要熟悉使用命令行的方式进行设备管…...

docker部署安装整理

centos下安装部署docker 在CentOS下部署Docker&#xff0c;你需要按照以下步骤进行操作&#xff1a; 更新系统&#xff1a; 首先&#xff0c;确保你的CentOS系统是最新的。打开终端&#xff0c;并运行以下命令来更新你的系统&#xff1a; sudo yum update -y安装所需的软件包…...

python能够做网站/关键词的优化和推广

mv&#xff1a;移动文件或目录1、命令格式mv [option] SRC(源目录或文件) DEC(目标目录)2、命令功能mv命令是move的缩写&#xff0c;可以用来移动文件或者将文件改名&#xff0c;是Linux系统下常用的命令&#xff0c;经常用来备份文件或者目录。mv 命令的第二个参数是…...

wordpress文本自动分页/上海百度推广

Cuda安装 安装cuda是比较麻烦的一步&#xff0c;以下安装说明来自cuda的安装说明文件。遇到问题找官方解决方案&#xff0c;最便捷有效。Perform the following steps to install CUDA and verify the installation.Disable the Nouveau drivers:首先需要屏蔽ubuntu默认的显卡驱…...

石家庄市建设局质监站网站/网络站点推广的方法有哪些

前言 在SpringBoot中使用自定义注解、aop切面打印web请求日志。主要是想把controller的每个request请求日志收集起来&#xff0c;调用接口、执行时间、返回值这几个重要的信息存储到数据库里&#xff0c;然后可以使用火焰图统计接口调用时长&#xff0c;平均响应时长&am…...

做时时彩网站微信平台/域名备案查询系统

oracle 设置日期的默认值 1.修改日期字段的默认值为但前系统的时间&#xff1a; alter table 表名 modify 日期字段 DATE default sysdate not null ; 2.修改日期字段的默认值为指定的时间&#xff1a;我们使用 to_date(2003-12-19,yyyy-mm-dd) to_date(2003-12-17 0…...

有没有专门帮人做图的网站/西安seo优化培训机构

一&#xff0e;OO(面向对象)的设计基础面向对象(OO)&#xff1a;就是基于对象概念&#xff0c;以对象为中心&#xff0c;以类和继承为构造机制&#xff0c;充分利用接口和多态提供灵活性&#xff0c;来认识、理解、刻划客观世界和设计、构建相应的软件系统。面向对象的特征&…...

阿里百秀wordpress/港港网app下载最新版

转载于:https://www.cnblogs.com/supper-Ho/p/6264023.html...