FreeRTOS进阶之任务创建完全解析

目录

在FreeRTOS基础系列《FreeRTOS系列第10篇---FreeRTOS任务创建和删除》中介绍了任务创建API函数xTaskCreate(),我们这里先回顾一下这个函数的声明:

        BaseType_t xTaskCreate(                            TaskFunction_tp vTaskCode,                            const char * constpcName,                            unsigned short usStackDepth,                            void *pvParameters,                            UBaseType_t uxPriority,                            TaskHandle_t *pvCreatedTask                          );

这个API函数的作用是创建新的任务并将它加入到任务就绪列表,函数参数含义为:

pvTaskCode:函数指针,指向任务函数的入口。任务永远不会返回(位于死循环内)。该参数类型TaskFunction_t定义在文件projdefs.h中,定义为:typedef void(*TaskFunction_t)( void * ),即参数为空指针类型并返回空类型。pcName:任务描述。主要用于调试。字符串的最大长度(包括字符串结束字符)由宏configMAX_TASK_NAME_LEN指定,该宏位于FreeRTOSConfig.h文件中。usStackDepth:指定任务堆栈大小,能够支持的堆栈变量数量(堆栈深度),而不是字节数。比如,在16位宽度的堆栈下,usStackDepth定义为100,则实际使用200字节堆栈存储空间。堆栈的宽度乘以深度必须不超过size_t类型所能表示的最大值。比如,size_t为16位,则可以表示堆栈的最大值是65535字节。这是因为堆栈在申请时是以字节为单位的,申请的字节数就是堆栈宽度乘以深度,如果这个乘积超出size_t所表示的范围,就会溢出,分配的堆栈空间也不是我们想要的。pvParameters:指针,当任务创建时,作为一个参数传递给任务。uxPriority:任务的优先级。具有MPU支持的系统,可以通过置位优先级参数的portPRIVILEGE_BIT位,随意的在特权(系统)模式下创建任务。比如,创建一个优先级为2的特权任务,参数uxPriority可以设置为 ( 2 | portPRIVILEGE_BIT )。pvCreatedTask:用于回传一个句柄(ID),创建任务后可以使用这个句柄引用任务。

虽然xTaskCreate()看上去很像函数,但其实是一个宏,真正被调用的函数是xTaskGenericCreate(),xTaskCreate()宏定义如下所示:

#define xTaskCreate( pvTaskCode, pcName, usStackDepth,pvParameters, uxPriority, pxCreatedTask )    \      xTaskGenericCreate( ( pvTaskCode ),( pcName ), ( usStackDepth ), ( pvParameters ), ( uxPriority ), ( pxCreatedTask), ( NULL ), ( NULL ), ( NULL ) )

可以看到,xTaskCreate比xTaskGenericCreate少了三个参数,在宏定义中,这三个参数被设置为NULL。这三个参数用于使用静态变量的方法分配堆栈、任务TCB空间以及设置MPU相关的参数。一般情况下,这三个参数是不使用的,所以任务创建宏xTaskCreate定义的时候,将这三个参数对用户隐藏了。接下来的章节中,为了方便,我们还是称xTaskCreate()为函数,虽然它是一个宏定义。

上面我们提到了任务TCB(任务控制块),这是一个需要重点介绍的关键点。它用于存储任务的状态信息,包括任务运行时的环境。每个任务都有自己的任务TCB。任务TCB是一个相对比较大的数据结构,这也是情理之中的,因为与任务相关的代码占到整个FreeRTOS代码量的一半左右,这些代码大都与任务TCB相关,我们先来介绍一下任务TCB数据结构的定义:

typedef struct tskTaskControlBlock{    volatile StackType_t    *pxTopOfStack; /*当前堆栈的栈顶,必须位于结构体的第一项*/     #if ( portUSING_MPU_WRAPPERS == 1 )        xMPU_SETTINGS   xMPUSettings;      /*MPU设置,必须位于结构体的第二项*/    #endif     ListItem_t          xStateListItem; /*任务的状态列表项,以引用的方式表示任务的状态*/    ListItem_t          xEventListItem;    /*事件列表项,用于将任务以引用的方式挂接到事件列表*/    UBaseType_t         uxPriority;        /*保存任务优先级,0表示最低优先级*/    StackType_t         *pxStack;           /*指向堆栈的起始位置*/    char               pcTaskName[ configMAX_TASK_NAME_LEN ];/*任务名字*/     #if ( portSTACK_GROWTH > 0 )        StackType_t     *pxEndOfStack;     /*指向堆栈的尾部*/    #endif     #if ( portCRITICAL_NESTING_IN_TCB == 1 )        UBaseType_t     uxCriticalNesting; /*保存临界区嵌套深度*/    #endif     #if ( configUSE_TRACE_FACILITY == 1 )        UBaseType_t     uxTCBNumber;       /*保存一个数值,每个任务都有唯一的值*/        UBaseType_t     uxTaskNumber;      /*存储一个特定数值*/    #endif     #if ( configUSE_MUTEXES == 1 )        UBaseType_t     uxBasePriority;    /*保存任务的基础优先级*/        UBaseType_t     uxMutexesHeld;    #endif     #if ( configUSE_APPLICATION_TASK_TAG == 1 )        TaskHookFunction_t pxTaskTag;    #endif     #if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )        void *pvThreadLocalStoragePointers[configNUM_THREAD_LOCAL_STORAGE_POINTERS ];    #endif     #if( configGENERATE_RUN_TIME_STATS == 1 )        uint32_t        ulRunTimeCounter;  /*记录任务在运行状态下执行的总时间*/    #endif     #if ( configUSE_NEWLIB_REENTRANT == 1 )        /* 为任务分配一个Newlibreent结构体变量。Newlib是一个C库函数,并非FreeRTOS维护,FreeRTOS也不对使用结果负责。如果用户使用Newlib,必须熟知Newlib的细节*/        struct _reent xNewLib_reent;    #endif     #if( configUSE_TASK_NOTIFICATIONS == 1 )        volatile uint32_t ulNotifiedValue; /*与任务通知相关*/        volatile uint8_t ucNotifyState;    #endif     #if( configSUPPORT_STATIC_ALLOCATION == 1 )        uint8_t ucStaticAllocationFlags; /* 如果堆栈由静态数组分配,则设置为pdTRUE,如果堆栈是动态分配的,则设置为pdFALSE*/    #endif     #if( INCLUDE_xTaskAbortDelay == 1 )        uint8_t ucDelayAborted;    #endif } tskTCB; typedef tskTCB TCB_t;

下面我们详细的介绍这个数据结构的主要成员:

指针pxTopOfStack必须位于结构体的第一项,指向当前堆栈的栈顶,对于向下增长的堆栈,pxTopOfStack总是指向最后一个入栈的项目。

如果使用MPU,xMPUSettings必须位于结构体的第二项,用于MPU设置。

接下来是状态列表项xStateListItem和事件列表项xEventListItem,我们在上一章介绍列表和列表项的文章中提到过:列表被FreeRTOS调度器使用,用于跟踪任务,处于就绪、挂起、延时的任务,都会被挂接到各自的列表中。调度器就是通过把任务TCB中的状态列表项xStateListItem和事件列表项xEventListItem挂接到不同的列表中来实现上述过程的。在task.c中,定义了一些静态列表变量,其中有就绪、阻塞、挂起列表,例如当某个任务处于就绪态时,调度器就将这个任务TCB的xStateListItem列表项挂接到就绪列表。事件列表项也与之类似,当队列满的情况下,任务因入队操作而阻塞时,就会将事件列表项挂接到队列的等待入队列表上。

uxPriority用于保存任务的优先级,0为最低优先级。任务创建时,指定的任务优先级就被保存到该变量中。

指针pxStack指向堆栈的起始位置,任务创建时会分配指定数目的任务堆栈,申请堆栈内存函数返回的指针就被赋给该变量。很多刚接触FreeRTOS的人会分不清指针pxTopOfStack和pxStack的区别,这里简单说一下:pxTopOfStack指向当前堆栈栈顶,随着进栈出栈,pxTopOfStack指向的位置是会变化的;pxStack指向当前堆栈的起始位置,一经分配后,堆栈起始位置就固定了,不会被改变了。那么为什么需要pxStack变量呢,这是因为随着任务的运行,堆栈可能会溢出,在堆栈向下增长的系统中,这个变量可用于检查堆栈是否溢出;如果在堆栈向上增长的系统中,要想确定堆栈是否溢出,还需要另外一个变量pxEndOfStack来辅助诊断是否堆栈溢出,后面会讲到这个变量。

字符数组pcTaskName用于保存任务的描述或名字,在任务创建时,由参数指定。名字的长度由宏configMAX_TASK_NAME_LEN(位于FreeRTOSConfig.h中)指定,包含字符串结束标志。

如果堆栈向上生长(portSTACK_GROWTH > 0),指针pxEndOfStack指向堆栈尾部,用于检验堆栈是否溢出。

变量uxCriticalNesting用于保存临界区嵌套深度,初始值为0。

接下来两个变量用于可视化追踪,仅当宏configUSE_TRACE_FACILITY(位于FreeRTOSConfig.h中)为1时有效。变量uxTCBNumber存储一个数值,在创建任务时由内核自动分配数值(通常每创建一个任务,值增加1),每个任务的uxTCBNumber值都不同,主要用于调试。变量uxTaskNumber用于存储一个特定值,与变量uxTCBNumber不同,uxTaskNumber的数值不是由内核分配的,而是通过API函数vTaskSetTaskNumber()来设置的,数值由函数参数指定。

如果使用互斥量(configUSE_MUTEXES == 1),任务优先级被临时提高时,变量uxBasePriority用来保存任务原来的优先级。

变量ucStaticAllocationFlags也需要说明一下,我们前面说过任务创建API函数xTaskCreate()只能使用动态内存分配的方式创建任务堆栈和任务TCB,如果要使用静态变量实现任务堆栈和任务TCB就需要使用函数xTaskGenericCreate()来实现。如果任务堆栈或任务TCB由静态数组和静态变量实现,则将该变量设置为pdTRUE(任务堆栈空间由静态数组变量实现时为0x01,任务TCB由静态变量实现时为0x02,任务堆栈和任务TCB都由静态变量实现时为0x03),如果堆栈是动态分配的,则将该变量设置为pdFALSE。

到这里任务TCB的数据结构就讲完了,下面我们用一个例子来讲述任务创建的过程,为方便起见,假设被创建的任务叫“任务A”,任务函数为vTask_A():

    TaskHandle_t xHandle;    xTaskCreate(vTask_A,”Task A”,120,NULL,1,&xHandle);

这里创建了一个任务,任务优先级为1,由于硬件平台是32为架构,所以指定了120*4=480字节的任务堆栈,向任务函数vTask_A()传递的参数为空(NULL),任务句柄由变量xHandle保存。当这个语句执行后,任务A被创建并加入就绪任务列表,我们这章的主要目的,就是看看这个语句在执行过程中,发生了什么事情。

1.创建任务堆栈和任务TCB

调用函数prvAllocateTCBAndStack()创建任务堆栈和任务TCB。有两种方式创建任务堆栈和任务TCB,一种是使用动态内存分配方法,这样当任务删除时,任务堆栈和任务控制块空间会被释放,可用于其它任务;另一种是使用静态变量来实现,在创建任务前定义好全局或者静态堆栈数组和任务控制块变量,在调用创建任务API函数时,将这两个变量以参数的形式传递给任务创建函数xTaskGenericCreate()。如果使用默认的xTaskCreate()创建任务函数,则使用动态内存分配,因为与静态内存分配有关的参数不可见(在本文一开始我们说过xTaskCreate()其实是一个带参数的宏定义,真正被执行的函数是xTaskGenericCreate(),参考宏xTaskCreate()的定义可以知道,xTaskCreate()对外隐藏了使用静态内存分配的参数,在调用xTaskGenericCreate()时,这些参数被设置为NULL)。

任务堆栈成功分配后,经过对齐的堆栈起始地址被保存到任务TCB的pxStack字段。如果使能堆栈溢出检查或者使用可视化追踪功能,则使用固定值tskSTACK_FILL_BYTE(0xa5)填充堆栈。

函数prvAllocateTCBAndStack()的源码去除断言和不常用的条件编译后如下所示:

static TCB_t *prvAllocateTCBAndStack( const uint16_t usStackDepth, StackType_t * const puxStackBuffer, TCB_t * const pxTaskBuffer ){TCB_t *pxNewTCB;StackType_t *pxStack;     /* 分配堆栈空间*/    pxStack = ( StackType_t * ) pvPortMallocAligned( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ), puxStackBuffer );    if( pxStack != NULL )    {        /* 分配TCB空间 */        pxNewTCB = ( TCB_t * ) pvPortMallocAligned( sizeof( TCB_t ), pxTaskBuffer );         if( pxNewTCB != NULL )        {            /* 将堆栈起始位置存入TCB*/            pxNewTCB->pxStack = pxStack;        }        else        {            /* 如果TCB分配失败,释放之前申请的堆栈空间 */            if( puxStackBuffer == NULL )            {                vPortFree( pxStack );            }        }    }    else    {        pxNewTCB = NULL;    }     if( pxNewTCB != NULL )    {        /* 如果需要,使用固定值填充堆栈 */        #if( ( configCHECK_FOR_STACK_OVERFLOW> 1 ) || ( configUSE_TRACE_FACILITY == 1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark== 1 ) )        {            /* 仅用于调试 */            ( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) usStackDepth * sizeof( StackType_t ) );        }        #endif    }     return pxNewTCB;}

2.初始化任务TCB必要的字段

调用函数prvInitialiseTCBVariables()初始化任务TCB必要的字段。在调用创建任务API函数xTaskCreate()时,参数pcName(任务描述)、uxPriority(任务优先级)都会被写入任务TCB相应的字段,TCB字段中的xStateListItem和xEventListItem列表项也会被初始化,初始化后的列表项如图2-1所示。在图2-1中,列表项xEventListItem的成员列表项值xItemValue被初始为4,这是因为我在应用中设置的最大优先级数目(configMAX_PRIORITIES)为5,而xEventListItem. xItemValue等于configMAX_PRIORITIES减去任务A的优先级(为1),即5-1=4。这一点很重要,在这里xItemValue不是直接保存任务优先级,而是保存优先级的补数,这意味着xItemValue的值越大,对应的任务优先级越小。FreeRTOS内核使用vListInsert函数(详细见高级篇第一章)将事件列表项插入到一个列表,这个函数根据xItemValue的值的大小顺序来进行插入操作。使用宏listGET_OWNER_OF_HEAD_ENTRY获得列表中的第一个列表项的xItemValue值总是最小,也就是优先级最高的任务!

图2-1:初始化状态和事件列表项

此外,TCB其它的一些字段也被初始化,比如临界区嵌套次数、运行时间计数器、任务通知值、任务通知状态等,函数prvInitialiseTCBVariables()的源码如下所示:

static void prvInitialiseTCBVariables( TCB_t * const pxTCB, const char * const pcName, UBaseType_t uxPriority,   \                              const MemoryRegion_t * const xRegions, const uint16_t usStackDepth ){UBaseType_t x;     /* 将任务描述存入TCB */    for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )    {        pxTCB->pcTaskName[ x ] = pcName[ x ];        if( pcName[ x ] == 0x00 )        {            break;        }    }    /* 确保字符串有结束 */    pxTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';     /* 调整优先级,宏configMAX_PRIORITIES的值在FreeRTOSConfig.h中设置 */    if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )    {        uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;    }     pxTCB->uxPriority = uxPriority;    #if ( configUSE_MUTEXES == 1 )              /*使用互斥量*/    {          pxTCB->uxBasePriority = uxPriority;        pxTCB->uxMutexesHeld = 0;    }    #endif /* configUSE_MUTEXES */       /*初始化列表项*/    vListInitialiseItem( &( pxTCB->xStateListItem ) );    vListInitialiseItem( &( pxTCB->xEventListItem ) );     /* 设置列表项xStateListItem的成员pvOwner指向当前任务控制块 */    listSET_LIST_ITEM_OWNER( &( pxTCB->xStateListItem ), pxTCB );     /* 设置列表项xEventListItem的成员xItemValue*/    listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );    /* 设置列表项xEventListItem的成员pvOwner指向当前任务控制块 */    listSET_LIST_ITEM_OWNER( &( pxTCB->xEventListItem ), pxTCB );     #if ( portCRITICAL_NESTING_IN_TCB ==1 )    /*使能临界区嵌套功能*/    {          pxTCB->uxCriticalNesting = ( UBaseType_t ) 0U;    }    #endif /* portCRITICAL_NESTING_IN_TCB */     #if ( configUSE_APPLICATION_TASK_TAG == 1 ) /*使能任务标签功能*/    {          pxTCB->pxTaskTag = NULL;    }    #endif /* configUSE_APPLICATION_TASK_TAG */     #if ( configGENERATE_RUN_TIME_STATS == 1 )  /*使能事件统计功能*/    {        pxTCB->ulRunTimeCounter = 0UL;    }    #endif /* configGENERATE_RUN_TIME_STATS */     #if ( portUSING_MPU_WRAPPERS == 1 )         /*使用MPU功能*/    {        vPortStoreTaskMPUSettings( &( pxTCB->xMPUSettings ), xRegions, pxTCB->pxStack, usStackDepth );    }    #else /* portUSING_MPU_WRAPPERS */    {        ( void ) xRegions;        ( void ) usStackDepth;    }    #endif /* portUSING_MPU_WRAPPERS */     #if( configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0 )/*使能线程本地存储指针*/    {        for( x = 0; x < ( UBaseType_t )configNUM_THREAD_LOCAL_STORAGE_POINTERS; x++ )        {            pxTCB->pvThreadLocalStoragePointers[ x ] = NULL;        }    }    #endif     #if ( configUSE_TASK_NOTIFICATIONS == 1 )   /*使能任务通知功能*/    {        pxTCB->ulNotifiedValue = 0;        pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;    }    #endif     #if ( configUSE_NEWLIB_REENTRANT == 1 )     /*使用Newlib*/    {        _REENT_INIT_PTR( ( &( pxTCB->xNewLib_reent ) ) );    }    #endif     #if( INCLUDE_xTaskAbortDelay == 1 )    {        pxTCB->ucDelayAborted = pdFALSE;    }    #endif}

3.初始化任务堆栈

调用函数pxPortInitialiseStack()初始化任务堆栈,并将最新的栈顶指针赋值给任务TCB的pxTopOfStack字段。

调用函数pxPortInitialiseStack()后,相当于执行了一次系统节拍时钟中断:将一些重要寄存器入栈。虽然任务还没开始执行,也并没有中断发生,但看上去就像寄存器已经被入栈了,并且部分堆栈值被修改成了我们需要的已知值。对于不同的硬件架构,入栈的寄存器也不相同,所以我们看到这个函数是由移植层提供的。对于Cortex-M3架构,需要依次入栈xPSR、PC、LR、R12、R3~R0、R11~R4,假设堆栈是向下生长的,初始化后的堆栈如图3-1所示。

在图3-1中我们看到寄存器xPSR被初始为0x01000000,其中bit24被置1,表示使用Thumb指令;寄存器PC被初始化为任务函数指针vTask_A,这样当某次任务切换后,任务A获得CPU控制权,任务函数vTask_A被出栈到PC寄存器,之后会执行任务A的代码;LR寄存器初始化为函数指针prvTaskExitError,这是由移植层提供的一个出错处理函数。当中断发生时,LR被设置成中断要返回的地址,但是每个任务都是一个死循环,正常情况下不应该退出任务函数,所以一旦从任务函数退出,说明那里出错了,这个时候会调用寄存器LR指向的函数来处理这个错误,即prvTaskExitError;根据ATPCS(ARM-Thumb过程调用标准),我们知道子函数调用通过寄存器R0~R3传递参数,在文章的最开始讲xTaskCreate()函数时,提到这个函数有一个空指针类型的参数pvParameters,当任务创建时,它作为一个参数传递给任务,所以这个参数被保存到R0中,用来向任务传递参数。

任务TCB结构体成员pxTopOfStack表示当前堆栈的栈顶,它指向最后一个入栈的项目,所以在图中它指向R4,TCB结构体另外一个成员pxStack表示堆栈的起始位置,所以在图中它指向堆栈的最开始处。

图3-1:初始化任务堆栈

4.进入临界区

调用taskENTER_CRITICAL()进入临界区,这是一个宏定义,最终进入临界区的代码由移植层提供。

5.当前任务数量增加1

在tasks.c中 ,定义了一些静态私有变量,用来跟踪任务的数量或者状态等等,其中变量uxCurrentNumberOfTasks表示当前任务的总数量,每创建一个任务,这个变量都会增加1。

6.为第一次运行做必要的初始化

如果这是第一个任务(uxCurrentNumberOfTasks等于1),则调用函数prvInitialiseTaskLists()初始化任务列表。FreeRTOS使用列表来跟踪任务,在tasks.c中,定义了静态类型的列表变量:

PRIVILEGED_DATAstatic List_t pxReadyTasksLists[ configMAX_PRIORITIES ];/*按照优先级排序的就绪态任务*/PRIVILEGED_DATAstatic List_t xDelayedTaskList1;                        /*延时的任务 */PRIVILEGED_DATAstatic List_t xDelayedTaskList2;                        /*延时的任务 */PRIVILEGED_DATAstatic List_t xPendingReadyList;                        /*任务已就绪,但调度器被挂起 */ #if (INCLUDE_vTaskDelete == 1 )    PRIVILEGED_DATA static List_t xTasksWaitingTermination;             /*任务已经被删除,但内存尚未释放*/#endif #if (INCLUDE_vTaskSuspend == 1 )    PRIVILEGED_DATA static List_t xSuspendedTaskList;                   /*当前挂起的任务*/#endif

现在这些列表都要进行初始化,会调用API函数vListInitialise()初始化列表,这个函数在《FreeRTOS高级篇1---FreeRTOS列表和列表项》中讲过,每个列表的初始化方式都是相同的,以就绪态列表pxReadyTasksLists[0]为例,初始化后如图6-1所示:

图6-1:初始化后的列表

函数prvInitialiseTaskLists()的源代码如下所示:

static void prvInitialiseTaskLists( void ){UBaseType_tuxPriority;     for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )    {        vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );    }     vListInitialise( &xDelayedTaskList1 );    vListInitialise( &xDelayedTaskList2 );    vListInitialise( &xPendingReadyList );     #if ( INCLUDE_vTaskDelete == 1 )    {        vListInitialise( &xTasksWaitingTermination );    }    #endif /* INCLUDE_vTaskDelete */     #if ( INCLUDE_vTaskSuspend == 1 )    {        vListInitialise( &xSuspendedTaskList );    }    #endif /* INCLUDE_vTaskSuspend */     /* Start with pxDelayedTaskList using list1 and the pxOverflowDelayedTaskListusing list2. */    pxDelayedTaskList = &xDelayedTaskList1;    pxOverflowDelayedTaskList = &xDelayedTaskList2;}

7.更新当前正在运行的任务TCB指针

tasks.c中定义了一个任务TCB指针型变量:

PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB= NULL;

这是一个全局变量,在tasks.c中只定义了这一个全局变量。这个变量用来指向当前正在运行的任务TCB,我们需要多了解一下这个变量。FreeRTOS的核心是确保处于优先级最高的就绪任务获得CPU运行权。在下一章讲述任务切换时会知道,任务切换就是找到优先级最高的就绪任务,而找出的这个最高优先级任务的TCB,就被赋给变量pxCurrentTCB。

如果调度器还没有准备好(程序刚开始运行时,可能会先创建几个任务,之后才会启动调度器),并且新创建的任务优先级大于变量pxCurrentTCB指向的任务优先级,则设置pxCurrentTCB指向当前新创建的任务TCB(确保pxCurrentTCB指向优先级最高的就绪任务)。

if( xSchedulerRunning == pdFALSE ){    if( pxCurrentTCB->uxPriority <= uxPriority )    {        pxCurrentTCB = pxNewTCB;    }    else    {        mtCOVERAGE_TEST_MARKER();    }}

8.将新创建的任务加入就绪列表数组

调用prvAddTaskToReadyList(pxNewTCB)将创建的任务TCB加入到就绪列表数组中,任务的优先级确定了加入到就绪列表数组的哪个下标。比如我们新创建的任务优先级为1,则这个任务被加入到列表pxReadyTasksLists[1]中。

prvAddTaskToReadyList()其实是一个宏,由一系列语句组成,去除其中的跟踪宏外,这个宏定义如下所示:

#defineprvAddTaskToReadyList( pxTCB )                        \    taskRECORD_READY_PRIORITY( ( pxTCB)->uxPriority );       \    vListInsertEnd( &( pxReadyTasksLists[ (pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) );

宏taskRECORD_READY_PRIORITY()用来更新变量uxTopReadyPriority,这个变量在tasks.c中定义为静态变量,记录处于就绪态的最高任务优先级。这个变量参与了FreeRTOS的最核心代码:确保处于优先级最高的就绪任务获得CPU运行权。它在这里参与如何最快的找到优先级最高的就绪任务。为了最快,不同的架构会各显神通,一些架构还有特殊指令可用,所以这个宏由移植层提供。我们会在下一章介绍任务切换时,以Cortex-M3架构为例,详细介绍如何最快的找到优先级最高的就绪任务。

函数vListInsertEnd()将列表项插入到列表末端,在《FreeRTOS高级篇1---FreeRTOS列表和列表项》中已经提到过,这里会结合着例子再看一下这个函数。从前面我们直到,在调用函数vListInsertEnd()之前,就绪列表pxReadyTasksLists[1]和任务TCB的状态列表项xStateListItem都已经初始化好了,见图6-1和图2-1,为了方便查看,我们将这两幅图合成一副,见图8-1。

图8-1:初始化后的列表和列表项

调用vListInsertEnd(a,b)会将列表项b,插入到列表a的后面,函数执行完毕后,列表和列表项的关系如图8-2所示。

图8-2:插入一个列表项后的列表

在此基础上,假设又创建了任务B,任务A和任务B优先级相同,都为1。和任务A一样,任务B也有它自己的任务TCB,其中的状态列表项字段xStateListItem也要插入到列表pxReadyTasksLists[1]中,新的列表和列表项如图8-3所示。

图8-3:相同优先级就绪列表挂接两个列表项

9.退出临界区

调用taskEXIT_CRITICAL()退出临界区,这是一个宏定义,最终退出临界区的代码由移植层提供。

10.执行上下文切换

如果上面的步骤都正确执行,并且调度器也开始工作,则判断当前任务的优先级是否大于新创建的任务优先级。如果新创建的任务优先级更高,则调用taskYIELD_IF_USING_PREEMPTION()强制进行一次上下文切换,切换后,新创建的任务将获得CPU控制权,精简后的代码如下所示。

 if( xReturn == pdPASS )    {        if( xSchedulerRunning != pdFALSE )        {            /* 如果新创建的任务优先级大于当前任务优先级,则新创建的任务应该被立即执行。*/            if(pxCurrentTCB->uxPriority < uxPriority )            {                taskYIELD_IF_USING_PREEMPTION();            }        }    }

以上就是FreeRTOS进阶之任务创建完全解析的详细内容,更多关于FreeRTOS进阶任务创建分析的资料请关注我们其它相关文章!

(0)

相关推荐

  • FreeRTOS实时操作系统的任务创建和删除

    目录 前言 1.任务创建 1.1函数描述 1.2参数描述 1.3返回值 1.4用法举例 2.任务删除 2.1任务描述 2.2参数描述 前言 在FreeRTOS移植到Cortex-M3硬件平台的文章中,我们已经见过任务创建API,但那篇文章的重点在于如何移植FreeRTOS,本文将重点放在任务的创建和删除API函数上面. 任务创建和删除API函数位于文件task.c中,需要包含task.h头文件. 1.任务创建 1.1函数描述 BaseType_t xTaskCreate( TaskFunctio

  • FreeRTOS进阶任务通知示例分析

    目录 在FreeRTOS版本V8.2.0中推出了全新的功能:任务通知.在大多数情况下,任务通知可以替代二进制信号量.计数信号量.事件组,可以替代长度为1的队列(可以保存一个32位整数或指针值),并且任务通知速度更快.使用的RAM更少!我在< FreeRTOS系列第14篇---FreeRTOS任务通知>一文中介绍了任务通知如何使用以及局限性,今天我们将分析任务通知的实现源码,看一下任务通知是如何做到效率与RAM消耗双赢的.        在<FreeRTOS高级篇6---FreeRTOS信

  • FreeRTOS实时操作系统的任务创建与任务切换

    目录 任务控制块数据结构 任务创建函数 定义就绪表 就绪表初始化 启动调度器 任务切换 任务控制块数据结构 任务控制块数据结构在task.c声明 typedef struct tskTaskControlBlock { volatile StackType_t * pxTopOfStack; //栈顶指针 ListItem_t xStateListItem; //任务节点 StackType_t * pxStack; //任务栈起始地址 char pcTaskName[configMAX_TAS

  • FreeRTOS实时操作系统的任务通知方法

    目录 前言 1.发送通知-方法1 1.1函数描述 1.2参数描述 1.3返回值 2.发送通知-方法2 2.1函数描述 2.2参数描述 2.3用法举例 3.获取通知 3.1函数描述 3.2参数描述 3.3返回值 4.等待通知 4.1函数描述 4.2参数描述 4.3返回值 4.4用法举例 5.任务通知并查询 5.1函数描述 5.2参数描述 5.3返回值 前言 注:本文介绍任务通知的基础知识,详细源码分析见FreeRTOS进阶<FreeRTOS高级篇8---FreeRTOS任务通知分析> 每个RTO

  • FreeRTOS使用任务通知实现命令行解释器

    目录 前言 1.编码风格 2.一些准备工作 2.1串口硬件驱动 2.2一个类printf函数 3.使用任务通知 4.数据结构 4.1与命令有关的数据结构 4.2与分析命令有关数据结构 5.串口接收中断处理函数 6.命令行分析任务 6.1去除无效字符和控制字符 6.2参数分析 6.3定义命令回调函数 6.3.1不带参数的命令回调函数举例 6.3.2带参数的命令行回调函数举例 6.5命令行分析任务实现 7.使用的串口工具 7.1设置串口参数 7.2设置新行模式 7.3设置本地回显 8.测试 8.1无

  • FreeRTOS任务控制API函数的功能分析

    目录 1.相对延时 1.1函数描述 1.2参数描述 1.3用法举例 2.绝对延时 2.1函数描述 2.2参数描述 2.3用法举例 3.获取任务优先级 3.1函数描述 3.2参数描述 3.3返回值 3.4用法举例 4.设置任务优先级 4.1函数描述 4.2参数描述 4.3用法举例 5.任务挂起 5.1函数描述 5.2参数描述 5.3用法举例 6.恢复挂起的任务 6.1函数描述 6.2参数描述 7.恢复挂起的任务(在中断服务函数中使用) 7.1函数描述 7.2参数描述 7.3返回值 7.4用法举例

  • FreeRTOS实时操作系统空闲任务的阻塞延时实现

    目录 什么是阻塞延时.为什么需要空闲任务 空闲任务的实现 阻塞延时的实现 xTicksToDelay 递减 SysTick初始化 仿真 什么是阻塞延时.为什么需要空闲任务 RTOS中的延时叫阻塞延时,即任务需要延时时,任务会放弃cpu使用权,cpu转而去做其他的事,当任务延时时间到后,任务重新请求获得cpu使用权.但当所有的任务都处于阻塞后,为了不让cpu空闲没事干就需要一个空闲任务让cpu干活. 空闲任务的实现 空闲任务实现和创建普通任务没区别,空闲任务在调用vTaskStartSchedul

  • FreeRTOS实时操作系统的任务应用函数详解

    目录 1.获取任务系统状态 1.1函数描述 1.2参数描述 1.3返回值 1.4用法举例 2.获取当前任务句柄 2.1函数描述 2.2返回值 3.获取空闲任务句柄 3.1函数描述 3.2返回值 4.获取任务堆栈最大使用深度 4.1函数描述 4.2参数描述 4.3返回值 4.4用法举例 5.获取任务状态 5.1函数描述 5.2参数描述 5.3返回值 6.获取任务描述内容 6.1函数描述 6.2参数描述 6.3返回值 7.获取系统节拍次数 7.1函数描述 7.2返回值 8.获取调度器状态 8.1函数

  • FreeRTOS进阶之任务创建完全解析

    目录 在FreeRTOS基础系列<FreeRTOS系列第10篇---FreeRTOS任务创建和删除>中介绍了任务创建API函数xTaskCreate(),我们这里先回顾一下这个函数的声明: BaseType_t xTaskCreate( TaskFunction_tp vTaskCode, const char * constpcName, unsigned short usStackDepth, void *pvParameters, UBaseType_t uxPriority, Task

  • FreeRTOS进阶内存管理示例完全解析

    内存管理对应用程序和操作系统来说都非常重要.现在很多的程序漏洞和运行崩溃都和内存分配使用错误有关. FreeRTOS操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的.这样做大有好处,可以增加系统的灵活性:不同的应用场合可以使用不同的内存分配实现,选择对自己更有利的内存管理策略.比如对于安全型的嵌入式系统,通常不允许动态内存分配,那么可以采用非常简单的内存管理策略,一经申请的内存,甚至不允许被释放.在满足设计要求的前提下,系统越简单

  • FreeRTOS进阶信号量示例的完全解析

    目录 FreeRTOS的信号量包括二进制信号量.计数信号量.互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量).关于它们的区别可以参考< FreeRTOS系列第19篇---FreeRTOS信号量>一文. 信号量API函数实际上都是宏,它使用现有的队列机制.这些宏定义在semphr.h文件中.如果使用信号量或者互斥量,需要包含semphr.h头文件. 二进制信号量.计数信号量和互斥量信号量的创建API函数是独立的,但是获取和释放API函数都是相同的:递归互斥信号量的创建.获取和释

  • FreeRTOS进阶之系统延时完全解析

    目录 1. 相对延时函数vTaskDelay() 2. 绝对延时函数vTaskDelayUntil() 3.小结 FreeRTOS提供了两个系统延时函数:相对延时函数vTaskDelay()和绝对延时函数 vTaskDelayUntil().相对延时是指每次延时都是从任务执行函数vTaskDelay()开始,延时指定的时间结束: 绝对延时是指每隔指定的时间,执行一次调用vTaskDelayUntil()函数的任务.换句话说:任务以固定的频率执行. 在<FreeRTOS任务控制>一文中,已经介绍

  • FreeRTOS进阶之队列示例完全解析

    目录 前言 1.队列创建函数 2.入队 2.1 xQueueGenericSend() 2.2 xQueueGenericSendFromISR () 3.出队 前言 FreeRTOS提供了多种任务间通讯方式,包括: 任务通知(版本V8.2以及以上版本) 队列 二进制信号量 计数信号量 互斥量 递归互斥量 其中,二进制信号量.计数信号量.互斥量和递归互斥量都是使用队列来实现的,因此掌握队列的运行机制,是很有必要的. 队列是FreeRTOS主要的任务间通讯方式.可以在任务与任务间.中断和任务间传送

  • FreeRTOS进阶之调度器启动过程分析

    目录 使用FreeRTOS,一个最基本的程序架构如下所示: int main(void) { 必要的初始化工作; 创建任务1; 创建任务2; ... vTaskStartScheduler(); /*启动调度器*/ while(1); } 任务创建完成后,静态变量指针pxCurrentTCB(见<FreeRTOS进阶之任务创建完全解析>第7节内容)指向优先级最高的就绪任务.但此时任务并不能运行,因为接下来还有关键的一步:启动FreeRTOS调度器. 调度器是FreeRTOS操作系统的核心,主要

  • FreeRTOS进阶之任务通知示例完全解析

    目录 前言 1.发送通知 1.1 xTaskGenericNotify() 1.2 vTaskNotifyGiveFromISR() 1.3 xTaskGenericNotifyFromISR() 2.等待通知 2.1 ulTaskNotifyTake() 2.2 xTaskNotifyWait() 前言 在FreeRTOS版本V8.2.0中推出了全新的功能:任务通知.在大多数情况下,任务通知可以替代二进制信号量.计数信号量.事件组,可以替代长度为1的队列(可以保存一个32位整数或指针值),并且

  • FreeRTOS进阶之任务切换完全分析

    目录 FreeRTOS任务切换过程 代码分析 运行FreeRTOS过程 FreeRTOS任务切换过程 FreeRTOS任务相关的代码大约占总代码的一半左右,这些代码都在为一件事情而努力,即找到优先级最高的就绪任务,并使之获得CPU运行权.任务切换是这一过程的直接实施者,为了更快的找到优先级最高的就绪任务,任务切换的代码通常都是精心设计的,甚至会用到汇编指令或者与硬件相关的特性,比如Cortex-M3的CLZ指令.因此任务切换的大部分代码是由硬件移植层提供的,不同的平台,实现发方法也可能不同,这篇

  • FreeRTOS进阶之空闲任务示例完全解析

    目录 当RTOS调度器开始工作后,为了保证至少有一个任务在运行,空闲任务被自动创建,占用最低优先级(0优先级). xReturn = xTaskCreate( prvIdleTask, "IDLE",configMINIMAL_STACK_SIZE, (void * ) NULL, (tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle); 空闲任务是FreeRTOS不可缺少的任务,因为FreeRTOS设计要求必须至少

  • FreeRTOS进阶系统节拍时钟示例的完全解析

    目录 操作系统的运行是由系统节拍时钟驱动的. 在FreeRTOS中,我们知道系统延时和阻塞时间都是以系统节拍时钟周期为单位.在配置文件FreeRTOSConfig.h,改变宏configTICK_RATE_HZ的值,可以改变系统节拍时钟的中断频率,也间接的改变了系统节拍时钟周期(T=1/f).比如设置宏configTICK_RATE_HZ为100,则系统节拍时钟周期为10ms,设置宏configTICK_RATE_HZ为1000,则系统节拍时钟周期为1ms. 系统节拍中断服务程序会调用函数xTa

随机推荐