FreeRTOS内存管理和任务管理

一 .FreeRTOS 整体架构

1、FreeRTOS简介

​ FreeRTOS是指小型实时操作系统内核。作为一个轻量级的操作系统,其功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。在嵌入式领域中,嵌入式实时操作系统正得到越来越广泛的应用。采用嵌入式实时操作系统(RTOS)可以更合理、更有效地利用CPU的资源,简化应用软件的设计,缩短系统开发时间,更好地保证系统的实时性和可靠性。

2、FreeRTOS架构框图

在这里插入图片描述

FreeRTOS支持市面上的多种芯片架构,如Cortex-M 、Cortex-R 、Cortex-A、RISC-V等,可以通过官方提供的Demo快速完成移植。除基础内核外,FreeRTOS包含了丰富的组件和第三方库,还有专门应用于物联网的AWS版本等。

二. FreeRTOS 编程规范

1、编码标准

FreeRTOS的核心代码遵从MISRA-C(汽车产业软件可靠性协会)编码标准。如果出现有不满足标准的会在代码中显示注释出来。可以在代码中全局搜索"MISRA exception"找到不满足MISRA-C标准的编码。例:

if( pxQueue->pcWriteTo >= pxQueue->u.xQueue.pcTail )    /*lint !e946 MISRA exception justified as comparison of pointers is the cleanest solution. */

2、命名规则

2.1 宏

2.1.1 规则

前缀:小写,作为宏的起始部分(所在文件的简写)
其余部分为大写,两个单词之间用下划线隔开(第一个大写和前缀之间没有下划线间隔)

2.1.2 实例

portmacro.h

    #define portCHAR          char
    #define portFLOAT         float
    #define portDOUBLE        double
    #define portLONG          long
    #define portSHORT         short
    #define portSTACK_TYPE    uint32_t
    #define portBASE_TYPE     long

FreeRTOSConfig.h

#define configUSE_PREEMPTION			1
#define configCPU_CLOCK_HZ				( 100000000 )

task.c task.h

#define tskDEFAULT_INDEX_TO_NOTIFY     ( 0 )
#define tskRUNNING_CHAR      ( 'X' )

queue.c queue.h

#define queueSEND_TO_BACK                     ( ( BaseType_t ) 0 )
#define queueSEND_TO_FRONT                    ( ( BaseType_t ) 1 )
#define queueOVERWRITE                        ( ( BaseType_t ) 2 )

2.2、变量

命名规则:位宽前缀Name1Name2…Namex

2.2.1 stdint 命名的变量

8bit(char)无符号:uint8_t 前缀 ‘uc’
8bit(char)有符号:int8_t 前缀 ‘c’

uint8_t ucQueueType;
int8_t  cRxLock;
char cStatus;

16bit(short)无符号:uint16_t 前缀 ‘us’
16bit(short)有符号:int16_t 前缀 ‘s’

FreeRTOS中没有直接用 uint16_t 和 int16_t 命名的变量

32bit(long)无符号:uint32_t 前缀 ‘ul’
32bit(long)有符号:int32_t 前缀 ‘l’

uint32_t ulStackDepth
int32_t  lArrayIndex;
2.2.2 FreeRTOS 定义的数据类型

FreeRTOS 的自定义的数据类型如下:

/* integer data type */
typedef portSTACK_TYPE   StackType_t;
typedef long             BaseType_t;
typedef unsigned long    UBaseType_t;
typedef uint32_t     TickType_t;  /* 32bit hardware*/

/* defined by freertos */
typedef struct tskTaskControlBlock * TaskHandle_t;
typedef struct QueueDefinition   * QueueHandle_t;
typedef xTIMER Timer_t;
typedef struct EventGroupDef_t   * EventGroupHandle_t

FreeRTOS自定义的数据类型只有两种前缀,如下:

整型无符号前缀 ‘ux’
整型有符号和FreeRTOS自定义的数据类型前缀 ‘x’

/* integer data type */
UBaseType_t uxTimerNumber;
BaseType_t xListWasEmpty;
StackType_t xStack[ STACK_SIZE ];
TickType_t xNextExpireTime;

/* defined by freertos */
TaskHandle_t xTimerTaskHandle = NULL;
QueueHandle_t xTimerQueue = NULL;
Timer_t xNewTimer;
EventGroupHandle_t xEventGroup = NULL;
2.2.3 指针类型

在2.2.1和2.2.2的数据类型的命名前面增加 ‘p’

uint8_t * pucStackByte;
int8_t * pcOriginalReadPosition;
uint32_t * pulNotificationValue;

StackType_t * pxTopOfStack;
BaseType_t * pxHigherPriorityTaskWoken
UBaseType_t *puxVariableToIncrement;
TickType_t * const pxTicksToWait;

TaskHandle_t *pxCreatedTask;
Queue_t * pxNewQueue ;
Timer_t * const pxTimer;
EventGroup_t * pxEventBits;

2.3、函数

2.3.1 static 函数

只在当前文件有效的, 被 static 修饰: 增加前缀 “prv” ,private的意思 。不区分有无返回值、返回值是否为指针等,统一固定前缀 “prv”。
规则: prvName1Name2…Namex
实例如下:

/*  task.c */
static void prvDeleteTCB( TCB_t * pxTCB )
/*  queue.c */
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue, const void * pvItemToQueue, const BaseType_t xPosition )
2.3.2 全局函数

有返回值前缀为 ‘x’ 或’ux’,无返回值前缀为 ‘v’
返回值为指针,增加前缀 ‘p’
前缀后面紧跟着所在文件名的缩写
实例如下:

/* task.c */
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask )
/* queue.c */
BaseType_t xQueueIsQueueEmptyFromISR( const QueueHandle_t xQueue )
/* timer.c */
void vTimerSetTimerID( TimerHandle_t xTimer, void * pvNewID )
/* porttable.h */
void * pvPortMalloc( size_t xSize )

3、数据类型

见 2.2节,变量的命令。主要就是三种数据类型

stdint.h 命名的整型
FreeRTOS 命名的整型
FreeRTOS自定义的 链表、句柄等服务于各个模块的结构体

4、注释和风格

注释:只用 /* */ 进行注释
头文件中会给出函数的说明和例程,方便查阅和使用
源文件中关键处也会有对应的注释,辅助理解
缩进:4个空格
关键字 for、if、while等后面没有空格
实例如下:

/**************** timer.h **************/
/**

 * BaseType_t xTimerIsTimerActive( TimerHandle_t xTimer );
   *
 * Queries a timer to see if it is active or dormant.
   *
   BaseType_t xTimerIsTimerActive( TimerHandle_t xTimer ) PRIVILEGED_FUNCTION;

/**************** timer.c**************/
    BaseType_t xTimerIsTimerActive( TimerHandle_t xTimer )
    {
        BaseType_t xReturn;
        Timer_t * pxTimer = xTimer;

​    configASSERT( xTimer );

​    /* Is the timer in the list of active timers? */
​    taskENTER_CRITICAL();
​    {
​        if( ( pxTimer->ucStatus & tmrSTATUS_IS_ACTIVE ) == 0 )
​        {
​            xReturn = pdFALSE;
​        }
​        else
​        {
​            xReturn = pdTRUE;
​        }
​    }
​    taskEXIT_CRITICAL();

​    return xReturn;
} /*lint !e818 Can't be pointer to const due to the typedef. */

三. FreeRTOS 内存管理

1、内存管理的作用

嵌入式软件开发中,有一定代码规模的软件产品,会自己进行内存管理。针对操作系统而言,内存管理也是其核心功能。基本思路是将一段已知的内存块给到对应的软件产品,作为它的内存池。内存管理将软件产品中需要操作内存的,都限制在这个内存池中。这样能在复杂的系统中,便于内存的规划和问题定位。

2、FreeRTOS的内存策略

FreeRTOS支持静态内存和动态内存,其选择取决于应用场景。

2.1 静态内存

静态内存一般用在系统对可靠性要求非常高的场景。静态内存是编译器自动分配在栈区;分配时间可以认为固定为0,且大小固定。但是内存使用效率低。
在FreeRTOS的内核中,静态内存接口在功能模块后面增加后缀“static”单独实现(configSUPPORT_STATIC_ALLOCATION设置为 1),需要显示的将内存传到对应的功能模块中,如下:

TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
                                const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
                                const uint32_t ulStackDepth,
                                void * const pvParameters,
                                UBaseType_t uxPriority,
                                StackType_t * const puxStackBuffer,
                                StaticTask_t * const pxTaskBuffer ) PRIVILEGED_FUNCTION;                                

puxStackBuffer 即为创建任务时,给到当前任务的静态内存。

2.2 动态内存

动态内存一般用在系统对可靠性要求不高的场景。动态内存在运行过程中被分配,一般分配的耗时不固定。但是内存的使用效率很高。
针对动态内存,FreeRTOS规定了统一的内存接口,在内核或者应用开发中都调用定好的内存接口。基于接口,用户可以使用官方的动态内存策略,也可以自己实现。这样将内存管理和FreeRTOS的内核实现通过接口完全隔离,使得系统更加的灵活。 在不同的应用场景中我们根据需要去选择不同的接口实现即可。configSUPPORT_DYNAMIC_ALLOCATION设置为 1
接口如下:

/*
 - Map to the memory management routines required for the port.
   */
   void * pvPortMalloc( size_t xSize ) ;
   void vPortFree( void * pv ) ;
   void vPortInitialiseBlocks( void ) ;
   size_t xPortGetFreeHeapSize( void ) ;

2.3 heap文件

如果FreeRTOS是动态的创建对象,FreeRTOS认为直接使用 C标准库的 malloc 和 free 存在如下问题:

它们在嵌入式系统中并不是总是可用的
它们可能会消耗大量的代码空间
它们不是线程安全的
它们是不确定性的
FreeRTOS提供了5个 heap文件,简要说明如下:

heap_1.c: 只实现了 pvPortMalloc ,不允许内存的释放,申请时间确定。适用于不需要释放内存的场景,相当于是“静态内存”
heap_2.c: 允许内存的释放,申请内存时用了最佳匹配算法
heap_3.c: 只是对 malloc 和 free进行简单封装,以使的在FreeRTOS中使用时安全的
heap_4.c: 在heap_2的基础上,在释放内存的时候增加了相邻内存块的合并算法,减少碎片的产生
heap_5.c: 在heap_4的基础上,能够跨多个非相邻的内存区域。
除了 heap_1.c ,申请内存都是不确定的,但是比标准C库要好很多

3、heap_4.c 详解

这是 FreeRTOS 的一个内存管理模块,具体实现了动态内存分配(pvPortMalloc)和释放(vPortFree),并带有合并相邻空闲块的功能,以减少内存碎片。


3.1 宏定义**

#define heapMINIMUM_BLOCK_SIZE ( ( size_t ) ( xHeapStructSize << 1 ) )
  • 定义最小内存块大小,至少为 xHeapStructSize 的两倍。
#define heapBITS_PER_BYTE ( ( size_t ) 8 )
  • 假设一个字节为 8 位。
#define heapSIZE_MAX ( ~( ( size_t ) 0 ) )
  • 定义 size_t 能表示的最大值,用于检查内存大小溢出。
#define heapBLOCK_ALLOCATED_BITMASK ( ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 ) )
  • 使用最高位标记内存块是否已分配。

3.2 数据结构 BlockLink_t

typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK * pxNextFreeBlock; /**< 指向下一个空闲块 */
    size_t xBlockSize;                     /**< 当前块大小(包含 BlockLink_t) */
} BlockLink_t;
  • 这是内存块的链表结构,用于管理内存的分配和空闲状态。
  • 每个块包含两部分:
    1. 指向下一个空闲块的指针 pxNextFreeBlock
    2. 当前块的大小 xBlockSize,包括链表头部的大小。

3.3 全局变量

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
static BlockLink_t xStart;   // 链表起始节点(虚拟头节点,不分配内存)
static BlockLink_t * pxEnd;  // 指向链表的最后一个空闲块
static size_t xFreeBytesRemaining = 0; // 当前剩余堆内存大小
  • ucHeap 是实际的堆空间。
  • xStart 是虚拟头节点,用于指向第一个空闲块。
  • pxEnd 是链表末尾的标志节点。
  • xFreeBytesRemaining 跟踪当前剩余可用的内存。

3.4 主要函数分析

1. prvHeapInit
c复制代码static void prvHeapInit( void )
{
    BlockLink_t * pxFirstFreeBlock;
    portPOINTER_SIZE_TYPE uxStartAddress, uxEndAddress;
    size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

    /* 确保堆起始地址对齐 */
    uxStartAddress = ( portPOINTER_SIZE_TYPE ) ucHeap;
    if( ( uxStartAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
    {
        uxStartAddress += ( portBYTE_ALIGNMENT - 1 );
        uxStartAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
        xTotalHeapSize -= ( size_t ) ( uxStartAddress - ( portPOINTER_SIZE_TYPE ) ucHeap );
    }

    /* 设置链表起点和终点 */
    xStart.pxNextFreeBlock = ( void * ) heapPROTECT_BLOCK_POINTER( uxStartAddress );
    xStart.xBlockSize = 0;

    uxEndAddress = uxStartAddress + ( portPOINTER_SIZE_TYPE ) xTotalHeapSize;
    uxEndAddress -= ( portPOINTER_SIZE_TYPE ) xHeapStructSize;
    uxEndAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );

    pxEnd = ( BlockLink_t * ) uxEndAddress;
    pxEnd->xBlockSize = 0;
    pxEnd->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( NULL );

    /* 初始化第一个空闲块 */
    pxFirstFreeBlock = ( BlockLink_t * ) uxStartAddress;
    pxFirstFreeBlock->xBlockSize = ( size_t ) ( uxEndAddress - ( portPOINTER_SIZE_TYPE ) pxFirstFreeBlock );
    pxFirstFreeBlock->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxEnd );

    /* 更新全局变量 */
    xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
    xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
}

作用

  • 初始化堆的链表结构。
  • 确保堆空间从一个对齐的地址开始。
  • 第一个空闲块覆盖整个堆空间(除了 pxEnd)。

2. pvPortMalloc
void * pvPortMalloc( size_t xWantedSize )
{
    // 如果需要分配的大小为非零,则调整大小以满足内存对齐要求
    if( xWantedSize > 0 )
    {
        // 为内存块的头部大小(管理数据)增加额外的空间
        xWantedSize += xHeapStructSize;

        // 确保分配的大小满足端口定义的字节对齐要求
        if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
        {
            size_t xAdditionalRequiredSize = portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK );
            xWantedSize += xAdditionalRequiredSize;
        }
    }

    // 暂停所有任务,以防止任务切换干扰内存分配操作
    vTaskSuspendAll();
    {
        // 如果堆尚未初始化,则初始化它
        if( pxEnd == NULL )
        {
            prvHeapInit();
        }

        // 检查是否有足够的内存可以分配
        if( xWantedSize <= xFreeBytesRemaining )
        {
            BlockLink_t * pxPreviousBlock = &xStart; // 链表的头部(虚拟块)
            BlockLink_t * pxBlock = heapPROTECT_BLOCK_POINTER( xStart.pxNextFreeBlock ); // 获取第一个空闲块

            // 遍历链表,找到一个足够大的空闲块
            while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock != pxEnd ) )
            {
                pxPreviousBlock = pxBlock;
                pxBlock = heapPROTECT_BLOCK_POINTER( pxBlock->pxNextFreeBlock );
            }

            // 如果找到足够大的块,则分配内存
            if( pxBlock != pxEnd )
            {
                // 计算返回给用户的内存地址(跳过块头)
                void * pvReturn = ( void * ) ( ( uint8_t * ) pxBlock + xHeapStructSize );

                // 从空闲链表中移除当前块
                pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

                // 如果当前块的剩余空间大于最小块大小,则拆分块
                if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
                {
                    // 创建一个新的块,表示当前块的剩余部分
                    BlockLink_t * pxNewBlockLink = ( void * ) ( ( uint8_t * ) pxBlock + xWantedSize );
                    pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize; // 更新剩余块大小
                    pxBlock->xBlockSize = xWantedSize; // 更新分配块的大小

                    // 将新块插入空闲链表
                    pxNewBlockLink->pxNextFreeBlock = pxPreviousBlock->pxNextFreeBlock;
                    pxPreviousBlock->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxNewBlockLink );
                }

                // 更新剩余内存大小
                xFreeBytesRemaining -= pxBlock->xBlockSize;

                // 标记当前块已被分配(通常会清除块头标志位)
                heapALLOCATE_BLOCK( pxBlock );

                // 返回分配的内存地址
                return pvReturn;
            }
        }
    }
    // 恢复任务切换
    ( void ) xTaskResumeAll();

    // 如果内存分配失败,返回 NULL
    return NULL;
}

作用

  • 分配指定大小的内存块。
  • 找到合适的空闲块,分割并返回。
  • 更新全局变量以反映分配后的内存状态。

3. vPortFree
void vPortFree( void * pv )
{
    uint8_t * puc = ( uint8_t * ) pv;
    if( pv != NULL )
    {
        puc -= xHeapStructSize;
        BlockLink_t * pxLink = ( void * ) puc;

        vTaskSuspendAll();
        {
            heapFREE_BLOCK( pxLink );
            prvInsertBlockIntoFreeList( pxLink );
        }
        ( void ) xTaskResumeAll();
    }
}

作用

  • 释放指定的内存块。
  • 调用 prvInsertBlockIntoFreeList 将其重新插入链表,并尝试合并相邻空闲块。

4. prvInsertBlockIntoFreeList
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert )
{
    BlockLink_t * pxIterator;

    // 找到适合插入的位置,遍历链表直到找到比要插入块大的位置
    for( pxIterator = &xStart; heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock ) < pxBlockToInsert; 
         pxIterator = heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock ) )
    {
        // 空循环体,因为主要操作在条件和更新语句中完成
    }

    // pxIterator 指向当前块的前一个块,用于检查是否可以与前面的块合并

    uint8_t * puc = ( uint8_t * ) pxIterator;

    // 检查要插入的块是否与当前块连续,如果是,则合并它们
    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
    {
        // 合并当前块和要插入的块
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        pxBlockToInsert = pxIterator;  // 更新要插入的块指针,指向合并后的块
    }

    // 更新指针,指向插入块的内存地址
    puc = ( uint8_t * ) pxBlockToInsert;

    // 检查插入的块是否与下一个空闲块连续,如果是,则合并它们
    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock ) )
    {
        // 合并插入块和下一个块
        pxBlockToInsert->xBlockSize += heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock )->xBlockSize;

        // 将插入块的下一个指针指向下一个块的下一个指针
        pxBlockToInsert->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxIterator->pxNextFreeBlock )->pxNextFreeBlock;
    }
    else
    {
        // 如果不能合并,将插入块的下一个指针指向当前块的下一个指针
        pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
    }

    // 将当前块的下一个指针指向插入的块
    pxIterator->pxNextFreeBlock = heapPROTECT_BLOCK_POINTER( pxBlockToInsert );
}

作用

  • 将释放的内存块插入空闲链表。
  • 尝试合并前后的相邻块以减少碎片化。

4、总结

pvPortMalloc时分割的内存块,不会发生合并,直接走插入流程 (理论就是这样:分割下来的是一块大内存的后面,只能和后面的 合并,如果能合并就不会分开)
只有在 pvPortFree的时候才会发生内存合并。xPortGetFreeHeapSize 和 vPortGetHeapStats 对debug内存问题有一定的帮助。 比如可以调用他们确认系统中有无内存泄露(有泄露的话空闲内存量会一直减小)

四. FreeRTOS 任务管理

FreeRTOS 通过 基于优先级的抢占式调度器 实现任务管理,负责管理任务的创建、切换、调度、挂起和删除。以下是 FreeRTOS 任务管理的核心原理和实现机制。


1、FreeRTOS 任务管理的基本原理

1.1 任务的基本结构

每个任务在 FreeRTOS 中被抽象为一个 TCB(Task Control Block),其中存储了任务运行所需的信息:

  • 任务状态:运行态、就绪态、阻塞态、挂起态。
  • 任务优先级:影响调度器的选择。
  • 任务栈指针:保存任务的运行上下文。
  • 任务句柄:供用户和调度器引用任务。
  • 任务周期信息:记录任务延时或阻塞所需的时间。
typedef struct tskTaskControlBlock
{
    volatile StackType_t * pxTopOfStack;  // 任务栈指针
    ListItem_t xStateListItem;           // 用于链接任务的链表项
    UBaseType_t uxPriority;              // 任务优先级
    char pcTaskName[configMAX_TASK_NAME_LEN];  // 任务名称
    // ... 其他信息
} TCB_t;

1.2 任务状态

任务可以处于以下几种状态,调度器会根据任务的状态决定哪个任务运行:

  1. 运行态(Running):
    • 当前被 CPU 执行的任务。
    • 系统中只能有一个任务处于运行态。
  2. 就绪态(Ready):
    • 所有条件满足,可以立即执行的任务。
  3. 阻塞态(Blocked):
    • 等待某事件发生(如延时、信号量、消息队列)。
  4. 挂起态(Suspended):
    • 被显式挂起的任务,不参与调度。
  5. 删除态(Deleted):
    • 已删除但尚未释放内存的任务。

1.3 调度器的工作机制

FreeRTOS 的调度器是 基于优先级的抢占式调度器,负责决定哪个任务应该运行:

  1. 优先级调度:
    • FreeRTOS 中任务优先级由用户定义,调度器总是选择最高优先级的任务运行。
  2. 时间片轮转:
    • 如果多个任务具有相同优先级,则调度器按照时间片轮转执行这些任务。
  3. 抢占式调度:
    • 高优先级任务进入就绪态时,立即抢占当前运行的低优先级任务。
  4. 中断优先:
    • 中断处理优先于任务,系统会在中断完成后重新调度任务。

2、FreeRTOS 调度器的核心实现

2.1 调度器的启动

  1. 函数 vTaskStartScheduler():
    • 启动调度器后,任务切换由调度器接管。
    • 它初始化系统时间基准和任务列表,然后调用第一个任务。
void vTaskStartScheduler( void )
{
    // 初始化系统和调度器
    prvInitialiseTaskLists();
    xSchedulerRunning = pdTRUE;

    // 启动第一个任务
    prvStartFirstTask();
}
  1. 函数 prvStartFirstTask():
    • 加载第一个任务的上下文,并开始执行。

2.2 任务切换(上下文切换)

任务切换是 FreeRTOS 任务管理的核心机制,它通过保存和恢复任务上下文实现任务间的切换。

  1. 任务上下文:
    • 每个任务都有独立的栈,用于存储:
      • CPU 寄存器值。
      • 程序计数器(PC)。
      • 状态寄存器。
    • 当任务切换时,当前任务的上下文会保存在栈中,新任务的上下文会从其栈中恢复。
  2. 上下文切换的触发条件:
    • 任务调用 taskYIELD() 主动让出 CPU。
    • 中断触发任务切换。
    • 延时或阻塞超时后任务重新进入就绪态。
  3. 上下文切换过程:
    • 保存当前任务上下文:
      • 将 CPU 寄存器值压栈。
    • 更新当前任务的栈指针:
      • 保存到任务的 pxTopOfStack
    • 加载新任务上下文:
      • 恢复 pxTopOfStack 指向的栈内容到 CPU 寄存器。

2.3 就绪列表

FreeRTOS 维护多个任务列表,用于存储不同优先级的任务状态:

  • 就绪列表(Ready List):
    • 每个优先级一个列表,存储所有处于就绪态的任务。
  • 延时列表(Delay List):
    • 存储所有处于阻塞态且有延时的任务。
  • 挂起列表(Suspend List):
    • 存储所有被挂起的任务。

就绪列表结构:

static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
  • 每个任务通过链表项(ListItem_t)链接到就绪列表。

2.4 时间片轮转

时间片轮转是调度器在多个相同优先级任务之间分配 CPU 时间的一种机制。

  1. 实现机制:
    • 使用链表存储相同优先级的任务。
    • 每次切换时,从列表中取下一个任务执行。
    • 当前任务重新加入链表尾部。
  2. 触发条件:
    • 系统时钟节拍(SysTick)中断到达。

2.5 阻塞态的管理

  1. 任务延时(vTaskDelay:
    • 当前任务调用 vTaskDelay 后,被移动到延时列表中。
  2. 延时列表的更新:
    • 在每次系统时钟中断中,减小延时列表中每个任务的剩余时间。
    • 如果某任务的延时到达,将其移入就绪列表。

3、FreeRTOS 任务管理的关键函数

3.1 创建任务

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,
    const char * const pcName,
    uint16_t usStackDepth,
    void * pvParameters,
    UBaseType_t uxPriority,
    TaskHandle_t * pxCreatedTask
);
  • 创建一个新任务并将其添加到就绪列表。

3.2 延时任务

void vTaskDelay(const TickType_t xTicksToDelay);
  • 将当前任务移动到延时列表,并延迟指定的时钟周期。

3.3 挂起和恢复任务

  • 挂起任务:
void vTaskSuspend(TaskHandle_t xTaskToSuspend);
  • 恢复任务:
void vTaskResume(TaskHandle_t xTaskToResume);

3.4 删除任务

void vTaskDelete(TaskHandle_t xTaskToDelete);
  • 将任务从调度器中移除,并释放相关资源。

4、FreeRTOS 任务管理的特点

  1. 实时性:
    • 高优先级任务能够及时响应事件。
    • 支持中断嵌套和快速切换。
  2. 灵活性:
    • 用户可以动态创建、删除、挂起和恢复任务。
  3. 低开销:
    • 核心数据结构和算法设计紧凑,适合嵌入式系统。
  4. 扩展性:
    • 支持多种同步机制(信号量、队列、事件组)。

5、总结

FreeRTOS 的任务管理通过任务控制块(TCB)、任务列表和抢占式调度机制实现。调度器基于优先级选择任务运行,同时利用时间片轮转机制在相同优先级任务之间分配 CPU 时间。这种设计为嵌入式系统提供了高效、灵活的任务管理能力。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐