(学习日记)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、内存管理实验现象

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/307807.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Elastic 线下 Meetup 将于 2024 年 4 月 27 号在重庆举办

2024 Elastic Meetup 重庆站活动&#xff0c;由 Elastic、新智锦绣联合举办&#xff0c;现诚邀广大技术爱好者及开发者参加。 活动时间 2024年4月27日 13:30-18:00 活动地点 中国重庆 沙坪坝区学城大道62-1号研发楼一期b3栋1楼(瑞幸咖啡旁&#xff09; 活动流程 14:00-14:50…

【电控笔记4】拉普拉斯-传递函数-pid

数据标幺化 拉普拉斯变换 欧拉公式 常见s变换 s变换性质

《QT实用小工具·二十一》鼠标十字线

1、概述 源码放在文章末尾 该项目实现了界面绘制十字线并跟随鼠标移动的过程&#xff0c;下面是demo演示&#xff1a; 项目部分代码如下&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget>namespace Ui { class Widget; }class Widget : public QWidg…

PE文件的分析和构造超详细过程

本文详细讲述如何从0构造一个PE文件&#xff0c;运行该文件会弹出一个HelloPE的窗口 目录 预备知识 1. 构造DOS头IMAGE_DOS_HEADER 1.1 构造DOS_MZ头 1.2 构造DOS_STUB 2、构造PE头IMAGE_NT_HEADERS 248字节 2.1 signature 2.2 IMAGE_FILE_HEADER 2.3 IMAGE_OPTI…

Jenkins UI 自动化持续化集成测试

一&#xff1a;安装jenkins 环境 在官网下载msi 直接安装即可 二&#xff1a;设置全局变量 设置allure 路径 三&#xff1a;创建项目 1、创建自由风格项目 2、如果项目在本地&#xff0c;且本地服务器是windows &#xff0c;找到Jenkins安装根目录&#xff0c;寻找config…

Excel从零基础到高手【办公】

第1课 - 快速制作目录【上篇】第1课 - 快速制作目录【下篇】第2课 - 快速定位到工作表的天涯海角第3课 - 如何最大化显示工作表的界面第4课 - 给你的表格做个瘦身第5课 - 快速定位目标区域所在位置第6课 - 快速批量填充序号第7课 - 按自定义的序列排序第8课 - 快速删除空白行第…

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 *…

使用 Python 标记具有相同名称的条目

如果大家想在 Python 中标记具有相同名称的条目&#xff0c;可以使用字典&#xff08;Dictionary&#xff09;或集合&#xff08;Set&#xff09;来实现。这取决于你们希望如何存储和使用这些条目。下面我将提供两种常见的方法来实现这个目标。 1、问题背景 在处理数据时&…

Linux上的可执行文件在Windows上是不能运行的

一、概要 1、可执行文件的格式 Linux上的可执行文件是elf格式的 Windows上的可执行文件是exe格式的 Linux上的可执行文件在Windows上是不能运行的 2、程序的普通构建与静态构建 普通构建&#xff1a; 一个.c文件&#xff0c;用gcc命令编译成可执行文件(程序)&#xff0c…

登陆qq,经常收到qq游戏中心的推送信息,关闭推送信息

手动关闭推送信息的步骤&#xff1a; 1.点开左侧游戏中心 2、在打开界面&#xff0c;点击左下角自己的头像 3、打开设置中心&#xff0c;关闭所有的推送 4、完成关闭&#xff0c;不会推送了

Elasticsearch8.x 设置密码

文章目录 一、环境说明二、使用elasticsearch-reset-password工具修改1、elasticsearch-reset-password工具位置2、设置密码 一、环境说明 elasticsearch版本&#xff1a;8.13.0 系统版本&#xff1a;Ubuntu 18.04.6 二、使用elasticsearch-reset-password工具修改 1、elast…

通讯录实现

实现通讯录可以采用顺序表&#xff0c;单链表&#xff0c;双链表等数据结构实现&#xff0c;所以我们需要先写出顺序表&#xff0c;单链表&#xff0c;双链表的代码实现。 顺序表&#xff1a; 定义&#xff1a;顺序表&#xff08;Sequence List&#xff09;是一种线性表的存储…

anaconda创建了虚拟python环境,且安装了pytorch,但是pycharm中import torch运行时报错

报错如下&#xff1a; C:\Users\tashi\.conda\envs\test1\python.exe D:\project\python\transformer\main.py C:\Users\tashi\.conda\envs\test1\lib\site-packages\numpy\__init__.py:127: UserWarning: mkl-service package failed to import, therefore Intel(R) MKL init…

【计算机网络】ip子网划分--超详细例题解析

Hello!这一篇主要是计算机网络中的ip地址子网划分的例题&#xff0c;这里例举了四个题型。保证即便从0也可以掌握&#xff01;(前面是一些预备知识&#xff0c;不熟悉的小伙伴一定要看下学习下哦&#xff5e;) 这也是博主的学习过程&#xff0c;做题中仅仅我的理解哦。若文章中…

langchain 文本向量化存储,并检索相似 topK

目录 chroma 检索 faiss 检索 检索器 相似性 最大相关性mmr 相似数阈值 txt 有多行&#xff0c;我的这份数据有 67 行&#xff0c;样例如下&#xff1a; 字段1\t值1\n 字段2\t值2\n ... chroma 检索 pip install langchain-chroma 在本地下载了 embedding 模型&…

FastAPI - uvicorn设置 logger 日志格式

怎么将日志打印到文件 在main.py加入log_config“./uvicorn_config.json” import uvicornif __name__ "__main__":uvicorn.run("app:app", host"0.0.0.0", port8000, log_config"./uvicorn_config.json")uvicorn_config.json {&qu…

吴恩达深度学习 (week3,4)

文章目录 一、神经网络概述二、神经网络的表示三、神经网络的输出四、多个例子的向量化五、向量化实现的解释六、深度学习激活函数七、激活函数导数八、神经网络的梯度下降法九、深度学习随机初始化十、上述学习总结1、第一题2、第二题3、第三题4、第四题5、第五题6、第六题7、…

langchain LCEL,prompt模块,outputparse输出模块

目录 基本代码 prompt模块 prompt模版控制长度 outputparse格式化输出 并行使用调用链 LangChain表达式语言&#xff0c;或者LCEL&#xff0c;是一种声明式的方式&#xff0c;可以轻松地将链条组合在一起 langchian 可以使用 通义千问&#xff0c;我们用通义千问&#x…

STM32H7的MPU学习和应用示例

STM32H7的MPU学习记录 什么是MPU&#xff1f;MPU的三种内存类型内存映射MPU保护区域以及优先级 MPU的寄存器XN位AP位TEX、C、B、S位SRD 位SIZE 位CTRL 寄存器的各个位 示例总结 什么是MPU&#xff1f; MPU&#xff08;Memory Protection Unit&#xff0c;内存保护单元&#xf…

Pytorch Windows EOFError: Ran out of input when num_workers>0

关于深度学习的一些学习框架,我使用过pytorch,caffe,caffe2,openchatkit,oneflow等,最近我将长达几十万字的报错手册重新进行了整理,制作出一个新的专栏,主要记录这几种常见的开发框架在安装和使用过程中常见的报错,以及我是如何解决掉的,以此来帮助更多的深度学习开…