一、内存分配方式
FreeRTOS 提供了 5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。这 5 个文件再 FreeRTOS 源码中,路径:FreeRTOS->Source->portable->MemMang。
1.1、heap_1
使用 heap_1,一旦申请内存成功就不允许释放!
1、适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用,实际上大多数的FreeRTOS 应用都是这样的。
2、具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。
3、代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合于那些不需要动态内存分配的应用。
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;
//确保字节对齐
#if( portBYTE_ALIGNMENT != 1 ) (1)
{
if( xWantedSize & portBYTE_ALIGNMENT_MASK ) (2)
{
//需要进行字节对齐
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &\ (3)
portBYTE_ALIGNMENT_MASK ) );
}
}
#endif
vTaskSuspendAll(); (4)
{
if( pucAlignedHeap == NULL )
{
//确保内存堆的开始地址是字节对齐的
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )\ (5)
&ucHeap[ portBYTE_ALIGNMENT ] ) &\
( ~( ( portPOINTER_SIZE_TYPE )\
portBYTE_ALIGNMENT_MASK ) ) );
}
//检查是否有足够的内存供分配,有的话就分配内存
if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) && (6)
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
{
pvReturn = pucAlignedHeap + xNextFreeByte; (7)
xNextFreeByte += xWantedSize; (8)
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll(); (9)
#if( configUSE_MALLOC_FAILED_HOOK == 1 ) (10)
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn; (11)
}
1.2、heap_2
heap_2提供了一个更好的分配算法,不像heap_1那样,heap_2提供了内存释放函数。heap_2不会把释放的内存块合并成一个大块,导致内存碎片。
1、可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生!
2、如果分配和释放的内存 n 大小是随机的,那么就要慎重使用了,比如下面的示例:
● 如果一个应用动态的创建和删除任务,而且任务需要分配的堆栈大小都是一样的,那么 heap_2 就非常合适。如果任务所需的堆栈大小每次都是不同,那么 heap_2 就不适合了,因为这样会导致内存碎片产生,最终导致任务分配不到合适的堆栈!不
过 heap_4 就很适合这种场景了。
● 如果一个应用中所使用的队列存储区域每次都不同,那么 heap_2 就不适合了,和上面一样,此时可以使用 heap_4。
● 应用需要调用 pvPortMalloc()和 vPortFree()来申请和释放内存,而不是通过其他FreeRTOS 的其他 API 函数来间接的调用。
3、如果应用中的任务、队列、信号量和互斥信号量具有不可预料性(如所需的内存大小不能确定,每次所需的内存都不相同,或者说大多数情况下所需的内存都是不同的)的话可能会导致内存碎片。虽然这是小概率事件,但是还是要引起我们的注意!
4、具有不可确定性,但是也远比标准 C 中的 mallo()和 free()效率高!heap_2 基本上可以适用于大多数的需要动态分配内存的工程中,而 heap_4 更是具有将内存碎片合并成一个大的空闲内存块(就是内存碎片回收)的功能。
1.3、heap_3
对标准 C 中的函数 malloc()和 free()的简单封装,FreeRTOS 对这两个函数做了线程保护。
1、需要编译器提供一个内存堆,编译器库要提供 malloc()和 free()函数。比如使用 STM32的话可以通过修改启动文件中的 Heap_Size 来修改内存堆的大小。
2、具有不确定性
3、可能会增加代码量。
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;
vTaskSuspendAll(); (1)
{
pvReturn = malloc( xWantedSize ); (2)
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll(); (3)
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
void vPortFree( void *pv )
{
if( pv )
{
vTaskSuspendAll(); (4)
{
free( pv ); (5)
traceFREE( pv, 0 );
}
( void ) xTaskResumeAll(); (6)
}
}
1.4、heap_4
heap_4 提供了一个最优的匹配算法,不像 heap_2,heap_4 会将内存碎片合并成一个大的可用内存块,它提供了内存块合并算法。内存堆为ucHeap[],大小同样为configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小
1、可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。
2、不会像 heap_2 那样产生严重的内存碎片,即使分配的内存大小是随机的。
3、具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。heap_4 非常适合于那些需要直接调用函数 pvPortMalloc()和 vPortFree()来申请和释放内存的应用。
4、申请的地址刚好和内存块 Block2 的末地址一样,所以这两个内存块可以合并在一起。合并以后 Block2的大小 xBlockSize 要更新为最新的内存块大小,即 64+80=144。再接着检查合并的新内存块是否可以和下一个内存块合并,也就是 Block3,如果可以的话就再次合并,
1.5、heap_5
heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是 heap_5 允许内存堆跨越多个不连续的内存段
HeapRegion_t xHeapRegions[] =
{
{ ( uint8_t * ) 0X10000000UL, 0x10000 }, //CCM 内存,起始地址 0X10000000,大小 64KB
{ ( uint8_t * ) 0X20000000UL, 0x30000 },//内部 SRAM 内存,起始地址 0X20000000,大小为192KB
{ ( uint8_t * ) 0XC0000000UL, 0x2000000},//外部 SDRAM 内存,起始地址 0XC0000000,大小为 32MB
{ NULL, 0 } //数组结尾
};
数组中成员顺序按照地址从低到高的顺序排列,而且最后一个成员必须使用 NULL。heap_5 允许内存堆不连续,说白了就是允许有多个内存堆。在 heap_2 和 heap_4 中只有一个内存堆,初始化的时候只也只需要处理一个内存堆。 heap_5 有多个内存堆,这些内存堆会被连接在一起,和空闲内存块链表类似,这个处理过程由函数 vPortDefineHeapRegions()完成。
使用 heap_5 的时候在一开始就应该先调用函数 vPortDefineHeapRegions()完成内存堆的初始化!
1.6、总结
- heap_1 最简单,但是只能申请内存,不能释放。
- heap_2 提供了内存释放函数,用户代码也可以直接调用函数 pvPortMalloc()和vPortFree()来申请和释放内存,但是 heap_2 会导致内存碎片的产生!
- heap_3 是对标准 C 库中的函数 malloc()和 free()的简单封装,并且提供了线程保护。
- heap_4 相对与 heap_2 提供了内存合并功能,可以降低内存碎片的产生,我们移植 FreeRTOS 的时候就选择了 heap_4。
- heap_5 基本上和 heap_4 一样,只是 heap_5 支持内存堆使用不连续的内存块。
二、任务API函数
uxTaskPriorityGet() 查询某个任务的优先级。
vTaskPrioritySet() 改变某个任务的任务优先级。
uxTaskGetSystemState() 获取系统中任务状态。
vTaskGetInfo() 获取某个任务信息。
xTaskGetApplicationTaskTag() 获取某个任务的标签(Tag)值。
xTaskGetCurrentTaskHandle() 获取当前正在运行的任务的任务句柄。
xTaskGetHandle() 根据任务名字查找某个任务的句柄
xTaskGetIdleTaskHandle() 获取空闲任务的任务句柄。
uxTaskGetStackHighWaterMark()获取任务的堆栈的历史剩余最小值,FreeRTOS中叫做“高水位线”
eTaskGetState() 获取某个任务的壮态,这个壮态是 eTaskState 类型。
pcTaskGetName() 获取某个任务的任务名字。
xTaskGetTickCount() 获取系统时间计数器值。
xTaskGetTickCountFromISR() 在中断服务函数中获取时间计数器值
xTaskGetSchedulerState() 获取任务调度器的壮态,开启或未开启。
uxTaskGetNumberOfTasks() 获取当前系统中存在的任务数量。
vTaskList() 以一种表格的形式输出当前系统中所有任务的详细信息。
vTaskGetRunTimeStats() 获取每个任务的运行时间。
vTaskSetApplicationTaskTag() 设置任务标签(Tag)值。
SetThreadLocalStoragePointer() 设置线程本地存储指针
GetThreadLocalStoragePointer() 获取线程本地存储指针
三、任务通知函数
1、发送任务通知
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction )
xTaskToNotify : 任务句柄,指定任务通知是发送给哪个任务的。
ulValue : 任务通知值。
eAction : 任务通知更新的方法,eNotifyAction 是个枚举类型,在文件 task.h下定义。
2、接收任务通知
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t * pulNotificationValue,
TickType_t xTicksToWait );
ulBitsToClearOnEntry: :当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务通知值清零。
ulBitsToClearOnExit: :如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者ULONG_MAX 的时候就会将任务通知值清零。
pulNotificationValue :此参数用来保存任务通知值。
xTickToWait: 阻塞时间。
四、任务调度
前面多次提到 FreeRTOS 支持多个任务同时拥有一个优先级,这些任务的调度是一个值得考虑的问题,不过这不是我们要考虑的。在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行,,FreeRTOS 中的这种调度方法就是时间片调度。图9.6.1 展示了运行在同一优先级下的执行时间图,在优先级 N 下有 3 个就绪的任务。