FreeRTOS内存管理 基于STM32

📅 2026/8/20 22:07:30
FreeRTOS内存管理 基于STM32
目录一、内存管理的基本概念二、内存管理的应用场景三、heap_4.c1.内存申请函数 pvPortMalloc()2.内存释放函数 vPortFree()四、内存管理的实验五、内存管理的实验现象一、内存管理的基本概念在计算系统中变量、中间数据一般存放在系统存储空间中只有在实际使用时才将 它们从存储空间调入到中央处理器内部进行运算。通常存储空间可以分为两种内部存储 空间和外部存储空间。内部存储空间访问速度比较快能够按照变量地址随机地访问也 就是我们通常所说的 RAM随机存储器或电脑的内存而外部存储空间内所保存的内 容相对来说比较固定即使掉电后数据也不会丢失可以把它理解为电脑的硬盘。在这一 章中我们主要讨论内部存储空间RAM的管理——内存管理。FreeRTOS 操作系统将内核与内存管理分开实现操作系统内核仅规定了必要的内存管 理函数原型而不关心这些内存管理函数是如何实现的所以在 FreeRTOS 中提供了多种 内存分配算法分配策略但是上层接口API却是统一的。这样做可以增加系统的 灵活性用户可以选择对自己更有利的内存管理策略在不同的应用场合使用不同的内存 分配策略。在嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存 分配还是静态内存分配算法一些可靠性要求非常高的系统应选择使用静态的而普通的 业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内 存上限内存使用效率低而动态则是相反。FreeRTOS 内存管理模块管理用于系统中内存资源它是操作系统的核心模块之一。主 要包括内存的初始化、分配以及释放。很多人会有疑问什么不直接使用 C 标准库中的内存管理函数呢在电脑中我们可以 用 malloc()和 free()这两个函数动态的分配内存和释放内存。但是在嵌入式实时操作系统 中调用 malloc()和 free()却是危险的原因有以下几点1.这些函数在小型嵌入式系统中并不总是可用的小型嵌入式设备中的 RAM 不足。2.它们的实现可能非常的大占据了相当大的一块代码空间。3.他们几乎都不是安全的。4.它们并不是确定的每次调用这些函数执行的时间可能都不一样。5. 它们有可能产生碎片。6. 这两个函数会使得链接器配置得复杂。7.如果允许堆空间的生长方向覆盖其他变量占据的内存它们会成为 debug 的灾难。在一般的实时嵌入式系统中由于实时性的要求很少使用虚拟内存机制。所有的内 存都需要用户参与分配直接操作物理内存所分配的内存不能超过系统的物理内存所 有的系统堆栈的管理都由用户自己管理。同时在嵌入式实时操作系统中对内存的分配时间要求更为苛刻分配内存的时间 必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块然后将数据存储在里面。而寻找这样一个空闲内存块所耗费 的时间是不确定的因此对于实时系统来说这就是不可接受的实时系统必须要保证内 存块的分配过程在可预测的确定时间内完成否则实时任务对外部事件的响应也将变得不可确定。而在嵌入式系统中内存是十分有限而且是十分珍贵的用一块内存就少了一块内存 而在分配中随着内存不断被分配和释放整个系统内存区域会产生越来越多的碎片因为 在使用过程中申请了一些内存其中一些释放了导致内存空间中存在一些小的内存块 它们地址不连续不能够作为一整块的大内存分配出去所以一定会在某个时间系统已 经无法分配到合适的内存了导致系统瘫痪。其实系统中实际是还有内存的但是因为小 块的内存的地址不连续导致无法分配成功所以我们需要一个优良的内存分配算法来避 免这种情况的出现。不同的嵌入式系统具有不同的内存配置和时间要求。所以单一的内存分配算法只可能 适合部分应用程序。因此FreeRTOS 将内存分配作为可移植层面相对于基本的内核代码 部分而言FreeRTOS 有针对性的提供了不同的内存分配管理算法这使得应用于不同场 景的设备可以选择适合自身内存算法。FreeRTOS 对内存管理做了很多事情FreeRTOS 的 V9.0.0 版本为我们提供了 5 种内存 管理算法分别是 heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c源文件存放于 FreeRTOS\Source\portable\MemMang 路径下在使用的时候选择其中一个添加到我们的工 程中去即可。FreeRTOS 的内存管理模块通过对内存的申请、释放操作来管理用户和系统对内存的 使用使内存的利用率和使用效率达到最优同时最大限度地解决系统可能产生的内存碎片问题。二、内存管理的应用场景首先在使用内存分配前必须明白自己在做什么这样做与其他的方法有什么不同 特别是会产生哪些负面影响在自己的产品面前应当选择哪种分配策略。内存管理的主要工作是动态划分并管理用户分配好的内存区间主要是在用户需要使 用大小不等的内存块的场景中使用当用户需要分配内存时可以通过操作系统的内存申 请函数索取指定大小内存块一旦使用完毕通过动态内存释放函数归还所占用内存使 之可以重复使用heap_1.c 的内存管理除外。例如我们需要定义一个 float 型数组floatArr[];但是在使用数组的时候总有一个问题困扰着我们数组应该有多大在很多的情 况下你并不能确定要使用多大的数组可能为了避免发生错误你就需要把数组定义得足 够大。即使你知道想利用的空间大小但是如果因为某种特殊原因空间利用的大小有增加 或者减少你又必须重新去修改程序扩大数组的存储范围。这种分配固定大小的内存分 配方法称之为静态内存分配。这种内存分配的方法存在比较严重的缺陷在大多数情况下会浪费大量的内存空间在少数情况下当你定义的数组不够大时可能引起下标越界错 误甚至导致严重后果。我们用动态内存分配就可以解决上面的问题。所谓动态内存分配就是指在程序执行的 过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内 存分配方法那样需要预先分配存储空间而是由系统根据程序的需要即时分配且分配的 大小就是程序要求的大小。三、heap_4.c我们这里只讲heap_4.c 因为这个最常用 。 内 存 分 配 时 需 要 的 总 的 堆 空 间 由 文 件 FreeRTOSConfig.h 中 的 宏 configTOTAL_HEAP_SIZE 配置单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可 以知道还剩下多少内存没有使用但是并不包括内存碎片。这样一来我们可以实时的调整 和优化 configTOTAL_HEAP_SIZE 的大小。heap_4.c 方案的空闲内存块也是以单链表的形式连接起来的BlockLink_t 类型的局部 静态变量 xStart 表示链表头但 heap_4.c 内存管理方案的链表尾部则保存在内存堆空间最 后位置并使用 BlockLink_t 指针类型局部静态变量 pxEnd 指向这个区域而 heap_2.c 内 存管理方案则使用 BlockLink_t 类型的静态变量 xEnd 表示链表尾heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的而是以内存块起 始地址大小排序内存地址小的在前地址大的在后因为 heap_4.c 方案还有一个内存合 并算法在释放内存的时候假如相邻的两个空闲内存块在地址上是连续的那么就可以 合并为一个内存块这也是为了适应合并算法而作的改变。heap_4.c 方案具有以下特点1、可用于重复删除任务、队列、信号量、互斥量等的应用程序2、可用于分配和释放随机字节内存的应用程序但并不像 heap2.c 那样产生严重的内 存碎片。3、具有不确定性但是效率比标准 C 库中的 malloc 函数高得多。1.内存申请函数 pvPortMalloc()heap_4.c 方案的内存申请函数与 heap_2.c 方案的内存申请函数大同小异同样是从链 表头 xStart 开始遍历查找合适的内存块如果某个空闲内存块的大小能容得下用户要申请 的内存则将这块内存取出用户需要内存空间大小的部分返回给用户剩下的内存块组成 一个新的空闲块按照空闲内存块起始地址大小顺序插入到空闲块链表中内存地址小的 在前内存地址大的在后。在插入到空闲内存块链表的过程中系统还会执行合并算法将 地址相邻的内存块进行合并判断这个空闲内存块是相邻的空闲内存块合并成一个大内存 块如果可以则合并合并算法是 heap_4.c 内存管理方案和 heap_2.c 内存管理方案最大的 不同之处这样一来会导致的内存碎片就会大大减少内存管理方案适用性就很强能 一样随机申请和释放内存的应用中灵活性得到大大的提高heap_4.c 内存初始化完成示意图具体见图1。heap_4.c 的内存 申请源码void *pvPortMalloc( size_t xWantedSize ) { BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink; void *pvReturn NULL; vTaskSuspendAll(); { /* If this is the first call to malloc then the heap will require initialisation to setup the list of free blocks. */ if( pxEnd NULL ) { prvHeapInit(); } else { mtCOVERAGE_TEST_MARKER(); } /* Check the requested block size is not so large that the top bit is set. The top bit of the block size member of the BlockLink_t structure is used to determine who owns the block - the application or the kernel, so it must be free. */ if( ( xWantedSize xBlockAllocatedBit ) 0 ) { /* The wanted size is increased so it can contain a BlockLink_t structure in addition to the requested amount of bytes. */ if( xWantedSize 0 ) { xWantedSize xHeapStructSize; /* Ensure that blocks are always aligned to the required number of bytes. */ if( ( xWantedSize portBYTE_ALIGNMENT_MASK ) ! 0x00 ) { /* Byte alignment required. */ xWantedSize ( portBYTE_ALIGNMENT - ( xWantedSize portBYTE_ALIGNMENT_MASK ) ); configASSERT( ( xWantedSize portBYTE_ALIGNMENT_MASK ) 0 ); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } if( ( xWantedSize 0 ) ( xWantedSize xFreeBytesRemaining ) ) { /* Traverse the list from the start (lowest address) block until one of adequate size is found. */ pxPreviousBlock xStart; pxBlock xStart.pxNextFreeBlock; while( ( pxBlock-xBlockSize xWantedSize ) ( pxBlock-pxNextFreeBlock ! NULL ) ) { pxPreviousBlock pxBlock; pxBlock pxBlock-pxNextFreeBlock; } /* If the end marker was reached then a block of adequate size was not found. */ if( pxBlock ! pxEnd ) { /* Return the memory space pointed to - jumping over the BlockLink_t structure at its start. */ pvReturn ( void * ) ( ( ( uint8_t * ) pxPreviousBlock-pxNextFreeBlock ) xHeapStructSize ); /* This block is being returned for use so must be taken out of the list of free blocks. */ pxPreviousBlock-pxNextFreeBlock pxBlock-pxNextFreeBlock; /* If the block is larger than required it can be split into two. */ if( ( pxBlock-xBlockSize - xWantedSize ) heapMINIMUM_BLOCK_SIZE ) { /* This block is to be split into two. Create a new block following the number of bytes requested. The void cast is used to prevent byte alignment warnings from the compiler. */ pxNewBlockLink ( void * ) ( ( ( uint8_t * ) pxBlock ) xWantedSize ); configASSERT( ( ( ( size_t ) pxNewBlockLink ) portBYTE_ALIGNMENT_MASK ) 0 ); /* Calculate the sizes of two blocks split from the single block. */ pxNewBlockLink-xBlockSize pxBlock-xBlockSize - xWantedSize; pxBlock-xBlockSize xWantedSize; /* Insert the new block into the list of free blocks. */ prvInsertBlockIntoFreeList( pxNewBlockLink ); } else { mtCOVERAGE_TEST_MARKER(); } xFreeBytesRemaining - pxBlock-xBlockSize; if( xFreeBytesRemaining xMinimumEverFreeBytesRemaining ) { xMinimumEverFreeBytesRemaining xFreeBytesRemaining; } else { mtCOVERAGE_TEST_MARKER(); } /* The block is being returned - it is allocated and owned by the application and has no next block. */ pxBlock-xBlockSize | xBlockAllocatedBit; pxBlock-pxNextFreeBlock NULL; } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } traceMALLOC( pvReturn, xWantedSize ); } ( void ) xTaskResumeAll(); #if( configUSE_MALLOC_FAILED_HOOK 1 ) { if( pvReturn NULL ) { extern void vApplicationMallocFailedHook( void ); vApplicationMallocFailedHook(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif configASSERT( ( ( ( size_t ) pvReturn ) ( size_t ) portBYTE_ALIGNMENT_MASK ) 0 ); return pvReturn; }图1 内存初始化完成示意图2.内存释放函数 vPortFree()heap_4.c 内存管理方案的内存释放函数 vPortFree()也比较简单根据传入要释放的内 存块地址偏移之后找到链表节点然后将这个内存块插入到空闲内存块链表中在内存 块插入过程中会执行合并算法这个我们已经在内存申请中讲过了而且合并算法多用于 释放内存中。最后是将这个内存块标志为“空闲”内存块节点的 xBlockSize 成员变量 最高位清 0、再更新未分配的内存堆大小即可下面来看看 vPortFree()的源码实现过程。void vPortFree( void *pv ) { uint8_t *puc ( uint8_t * ) pv; BlockLink_t *pxLink; if( pv ! NULL ) { /* The memory being freed will have an BlockLink_t structure immediately before it. */ puc - xHeapStructSize; /* This casting is to keep the compiler from issuing warnings. */ pxLink ( void * ) puc; /* Check the block is actually allocated. */ configASSERT( ( pxLink-xBlockSize xBlockAllocatedBit ) ! 0 ); configASSERT( pxLink-pxNextFreeBlock NULL ); if( ( pxLink-xBlockSize xBlockAllocatedBit ) ! 0 ) { if( pxLink-pxNextFreeBlock NULL ) { /* The block is being returned to the heap - it is no longer allocated. */ pxLink-xBlockSize ~xBlockAllocatedBit; vTaskSuspendAll(); { /* Add this block to the list of free blocks. */ xFreeBytesRemaining pxLink-xBlockSize; traceFREE( pv, pxLink-xBlockSize ); prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); } ( void ) xTaskResumeAll(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } }调用 prvInsertBlockIntoFreeList()函数将释放的内存块添加到空闲 内存块链表中在这过程中如果内存块可以合并就会进行内存块合并否则就单纯插入 空闲内存块链表按内存地址排序。 按照内存释放的过程当我们释放一个内存时如果与它相邻的内存块都不是空闲的 那么该内存块并不会合并只会被添加到空闲内存块链表中其过程示意图具体见图 2。而如果某个时间段释放了另一个内存块发现该内存块前面有一个空闲内存块与它 在地址上是连续的那么这两个内存块会合并成一个大的内存块并插入空闲内存块链表 中其过程示意图具体见图 3图二释放一个内存块无法合并图3 释放一个内存块可以合并四、内存管理的实验内存管理实验使用 heap_4.c 方案进行内存管理测试创建了两个任务分别是 LED 任 务与内存管理测试任务内存管理测试任务通过检测按键是否按下来申请内存或释放内存 当申请内存成功就像该内存写入一些数据如当前系统的时间等信息并且通过串口输出 相关信息LED 任务是将 LED 翻转表示系统处于运行状态。在不需要再使用内存时注 意要及时释放该段内存避免内存泄露。/* FreeRTOS头文件 */ #include FreeRTOS.h #include task.h /* 开发板硬件bsp头文件 */ #include bsp_led.h #include bsp_usart.h #include bsp_key.h /**************************** 任务句柄 ********************************/ /* * 任务句柄是一个指针用于指向一个任务当任务创建好之后它就具有了一个任务句柄 * 以后我们要想操作这个任务都需要通过这个任务句柄如果是自身的任务操作自己那么 * 这个句柄可以为NULL。 */ static TaskHandle_t AppTaskCreate_Handle NULL;/* 创建任务句柄 */ static TaskHandle_t LED_Task_Handle NULL;/* LED_Task任务句柄 */ static TaskHandle_t Test_Task_Handle NULL;/* Test_Task任务句柄 */ /******************************* 全局变量声明 ************************************/ /* * 当我们在写应用程序的时候可能需要用到一些全局变量。 */ uint8_t *Test_Ptr NULL; /* ************************************************************************* * 函数声明 ************************************************************************* */ static void AppTaskCreate(void);/* 用于创建任务 */ static void LED_Task(void* pvParameters);/* LED_Task任务实现 */ static void Test_Task(void* pvParameters);/* Test_Task任务实现 */ static void BSP_Init(void);/* 用于初始化板载相关资源 */ /***************************************************************** * brief 主函数 * param 无 * retval 无 * note 第一步开发板硬件初始化 第二步创建APP应用任务 第三步启动FreeRTOS开始多任务调度 ****************************************************************/ int main(void) { BaseType_t xReturn pdPASS;/* 定义一个创建信息返回值默认为pdPASS */ /* 开发板硬件初始化 */ BSP_Init(); printf(这是一个FreeRTOS内存管理实验\n); printf(按下KEY1申请内存按下KEY2释放内存\n); /* 创建AppTaskCreate任务 */ xReturn xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */ (const char* )AppTaskCreate,/* 任务名字 */ (uint16_t )512, /* 任务栈大小 */ (void* )NULL,/* 任务入口函数参数 */ (UBaseType_t )1, /* 任务的优先级 */ (TaskHandle_t* )AppTaskCreate_Handle);/* 任务控制块指针 */ /* 启动任务调度 */ if(pdPASS xReturn) vTaskStartScheduler(); /* 启动任务开启调度 */ else return -1; while(1); /* 正常不会执行到这里 */ } /*********************************************************************** * 函数名 AppTaskCreate * 功能说明 为了方便管理所有的任务创建函数都放在这个函数里面 * 参数 无 * 返回值 无 **********************************************************************/ static void AppTaskCreate(void) { BaseType_t xReturn pdPASS;/* 定义一个创建信息返回值默认为pdPASS */ taskENTER_CRITICAL(); //进入临界区 /* 创建LED_Task任务 */ xReturn xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */ (const char* )LED_Task,/* 任务名字 */ (uint16_t )512, /* 任务栈大小 */ (void* )NULL, /* 任务入口函数参数 */ (UBaseType_t )2, /* 任务的优先级 */ (TaskHandle_t* )LED_Task_Handle);/* 任务控制块指针 */ if(pdPASS xReturn) printf(创建LED_Task任务成功\n); /* 创建Test_Task任务 */ xReturn xTaskCreate((TaskFunction_t )Test_Task, /* 任务入口函数 */ (const char* )Test_Task,/* 任务名字 */ (uint16_t )512, /* 任务栈大小 */ (void* )NULL,/* 任务入口函数参数 */ (UBaseType_t )3, /* 任务的优先级 */ (TaskHandle_t* )Test_Task_Handle);/* 任务控制块指针 */ if(pdPASS xReturn) printf(创建Test_Task任务成功\n\n); vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务 taskEXIT_CRITICAL(); //退出临界区 } /********************************************************************** * 函数名 LED_Task * 功能说明 LED_Task任务主体 * 参数 * 返回值 无 ********************************************************************/ static void LED_Task(void* parameter) { while (1) { LED1_TOGGLE; vTaskDelay(1000);/* 延时1000个tick */ } } /********************************************************************** * 函数名 Test_Task * 功能说明 Test_Task任务主体 * 参数 * 返回值 无 ********************************************************************/ static void Test_Task(void* parameter) { uint32_t g_memsize; while (1) { if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) KEY_ON ) { /* KEY1 被按下 */ if(NULL Test_Ptr) { /* 获取当前内存大小 */ g_memsize xPortGetFreeHeapSize(); printf(系统当前内存大小为 %d 字节开始申请内存\n,g_memsize); Test_Ptr pvPortMalloc(1024); if(NULL ! Test_Ptr) { printf(内存申请成功\n); printf(申请到的内存地址为%#x\n,(int)Test_Ptr); /* 获取当前内剩余存大小 */ g_memsize xPortGetFreeHeapSize(); printf(系统当前内存剩余存大小为 %d 字节\n,g_memsize); //向Test_Ptr中写入当数据:当前系统时间 sprintf((char*)Test_Ptr,当前系统TickCount %d \n,xTaskGetTickCount()); printf(写入的数据是 %s \n,(char*)Test_Ptr); } } else { printf(请先按下KEY2释放内存再申请\n); } } if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) KEY_ON ) { /* KEY2 被按下 */ if(NULL ! Test_Ptr) { printf(释放内存\n); vPortFree(Test_Ptr); //释放内存 Test_PtrNULL; /* 获取当前内剩余存大小 */ g_memsize xPortGetFreeHeapSize(); printf(系统当前内存大小为 %d 字节内存释放完成\n,g_memsize); } else { printf(请先按下KEY1申请内存再释放\n); } } vTaskDelay(20);/* 延时20个tick */ } } /*********************************************************************** * 函数名 BSP_Init * 功能说明 板级外设初始化所有板子上的初始化均可放在这个函数里面 * 参数 * 返回值 无 *********************************************************************/ static void BSP_Init(void) { /* * STM32中断优先级分组为4即4bit都用来表示抢占优先级范围为0~15 * 优先级分组只需要分组一次即可以后如果有其他的任务需要用到中断 * 都统一用这个优先级分组千万不要再分组切忌。 */ NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 ); /* LED 初始化 */ LED_GPIO_Config(); /* 串口初始化 */ USART_Config(); /* 按键初始化 */ Key_GPIO_Config(); } /********************************END OF FILE****************************/五、内存管理的实验现象程序编译好用 USB 线连接电脑和开发板的 USB 接口对应丝印为 USB 转串口 用 DAP 仿真器把配套程序下载到 STM32 开发板在电脑上打开串口调试助手然后复位开发板我 们按下 KEY1 申请内存然后按下 KEY2 释放内存可以在调试助手中看到串口打印信息 与运行结果开发板的 LED 也在闪烁。