开局一张图。一步一步分析就好。
(一)什么是任务?
在多任务系统中,我们按照功能不同,把整个系统分割成一个个独立的,且无法返回的函数,这个函数我们称为任务;任务包含几个属性:任务堆栈,任务函数、任务控制块、任务优先级;下面主要介绍一下任务控制块,其他都比较容易理解。
(二)什么是任务控制块?
任务控制块内包含了该任务的全部信息,任务的执行需要通过任务调度器来控制,那么任务调度器怎么“控制”任务实体的呢?就要抓住任务的小辫子---“任务控制块”,系统对任务的全部操作都可以通过任务控制块来实现!它是一种特别的数据结构。
在任务创建函数xTaskCreat()创建任务的时候就会自动给每个任务分配一个任务控制块。
typedefstruct tskTaskControlBlock {volatile StackType_t *pxTopOfStack;/*任务堆栈栈顶指针*/#if ( portUSING_MPU_WRAPPERS == 1 ) xMPU_SETTINGS xMPUSettings;/*MPU相关设置*/#endif ListItem_t xStateListItem;/*状态列表项,这是一个内置在TCB控制块中的一个链表节点,通过这个节点,将任务挂到其他链表中
比如就绪列表,阻塞列表,挂起列表等*/
ListItem_t xEventListItem;/*事件列表项,用于引用事件列表中的任务*/ UBaseType_t uxPriority;/*任务优先级*/ 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;/*debug的时候用到*/ UBaseType_t uxTaskNumber;/*trace的时候用到*/#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 )struct _reent xNewLib_reent;/*定义一个newlib结构体变量*/#endif#if( configUSE_TASK_NOTIFICATIONS == 1 ) /*任务通知相关变量*/volatile uint32_t ulNotifiedValue;/*任务通知值*/volatile uint8_t ucNotifyState;/*任务通知状态*/#endif/* 用来标记任务是动态创建还是静态创建*/#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) uint8_t ucStaticallyAllocated;/*静态创建此变量为pdTURE;动态创建此变量为pdFALSE*/#endif#if( INCLUDE_xTaskAbortDelay == 1 ) uint8_t ucDelayAborted;#endif } tskTCB;
注:#if 开头的都是条件编译,咱们可以先不用理解。基本结构如下:
指针pxStack指向堆栈的起始位置,任务创建时会分配指定数目的任务堆栈,申请堆栈内存函数返回的指针就被赋给该变量。
很多刚接触FreeRTOS的人会分不清指针pxTopOfStack和pxStack的区别,这里简单说一下:pxTopOfStack指向当前堆栈栈顶,随着进栈出栈,pxTopOfStack指向的位置是会变化的;pxStack指向当前堆栈的起始位置,一经分配后,堆栈起始位置就固定了,不会被改变了。那么为什么需要pxStack变量呢,这是因为随着任务的运行,堆栈可能会溢出,在堆栈向下增长的系统中,这个变量可用于检查堆栈是否溢出;如果在堆栈向上增长的系统中,要想确定堆栈是否溢出,还需要另外一个变量pxEndOfStack来辅助诊断是否堆栈溢出。
(三)任务是怎么创建出来的?
任务有两种创建方式,动态创建和静态创建,两者的区别就是: 静态创建时候任务控制块和任务堆栈的内存是由用户自己定义的,任务删除的时候,内存不能自动释放。动态创建,任务堆栈和任务控制块的内存是由系统自动创建的,自动释放的。
动态创建任务的函数为 xTaskCreate();
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, //任务函数的名称constchar *const pcName, //任务的名称const uint16_t usStackDepth, //任务堆栈大小void *const pvParameters, //任务的形参 UBaseType_t uxPriority, //任务优先级 TaskHandle_t*const pxCreatedTask ) // 用于传回一个任务句柄,创建任务后使用这个句柄引用(控制)任务。本质上是一个空指针。 { TCB_t*pxNewTCB; BaseType_t xReturn;#define portSTACK_GROWTH//-1表示满减栈#if( portSTACK_GROWTH > 0 ){ }#else{ /* portSTACK_GROWTH<0 代表堆栈向下增长 */ StackType_t*pxStack;/* 任务栈内存分配,stm32是向下增长的堆栈,获取到的pxStack 是一个栈底的指针*/ pxStack= ( StackType_t *) pvPortMalloc(((( size_t) usStackDepth ) *sizeof( StackType_t)));if( pxStack != NULL ){/* 任务控制块内存分配*/ pxNewTCB= ( TCB_t * ) pvPortMalloc(sizeof( TCB_t ) );if( pxNewTCB != NULL ){/* 赋值栈地址*/ pxNewTCB->pxStack = pxStack; }else{/* 释放栈空间*/ vPortFree( pxStack ); } }else{/* 没有分配成功*/ pxNewTCB= NULL; } }#endif /* portSTACK_GROWTH */if( pxNewTCB != NULL ) {/* 新建任务初始化*/ prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );/* 把任务添加到就绪列表中*/ prvAddNewTaskToReadyList( pxNewTCB ); xReturn= pdPASS; }else{ xReturn= errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY; }return xReturn; }
之后,又调用了函数prvInitialiseNewTask()来新建任务初始化。我们看看下面是如何定义的。
staticvoid prvInitialiseNewTask(TaskFunction_t pxTaskCode,constchar *const pcName,const uint32_t ulStackDepth,void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t*const pxCreatedTask, TCB_t * pxNewTCB, //任务控制块const MemoryRegion_t *const xRegions ){ StackType_t*pxTopOfStack; UBaseType_t x;/* 计算栈顶的地址*/#if( portSTACK_GROWTH < 0 ){/* 把栈空间的高地址分配给栈顶*/ pxTopOfStack= pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t )1 );/* 栈对齐----栈要8字节对齐*/ pxTopOfStack= (StackType_t *)(((portPOINTER_SIZE_TYPE) pxTopOfStack) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK)));/* 检查是否有错误*/ configASSERT((((portPOINTER_SIZE_TYPE) pxTopOfStack& (portPOINTER_SIZE_TYPE) portBYTE_ALIGNMENT_MASK) ==0UL)); }#else /* portSTACK_GROWTH */ { }#endif /* portSTACK_GROWTH *//* 存储任务名称*/for( x = ( UBaseType_t )0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ){ pxNewTCB->pcTaskName[ x ] = pcName[ x ];if( pcName[ x ] ==0x00 ){break; }else{ mtCOVERAGE_TEST_MARKER(); } }/* \0补齐字符串*/ pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN -1 ] ='\0';/* 判断任务分配的优先级,是否大于最大值 如果超过最大值,赋值最大值*/if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES ){ uxPriority= ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t )1U; }else{ mtCOVERAGE_TEST_MARKER(); }/* 赋值任务优先级到任务控制块*/ pxNewTCB->uxPriority = uxPriority;/* 任务状态表 事件表初始化*/ vListInitialiseItem(&( pxNewTCB->xStateListItem ) ); vListInitialiseItem(&( pxNewTCB->xEventListItem ) );/* 设置任务控制块中的状态列表项的成员变量ower ,是属于PxNewTCB(拥有该结点的内核对象)*/ listSET_LIST_ITEM_OWNER(&( pxNewTCB->xStateListItem ), pxNewTCB ); /*更改事件列表项中的成员变量xItemValue的值,目的是列表在排列的时候,是按照优先级由大到小排列 */ listSET_LIST_ITEM_VALUE(&( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
/*设置任务控制块中事件列表项的成员变量ower,同上*/ listSET_LIST_ITEM_OWNER(&( pxNewTCB->xEventListItem ), pxNewTCB );#if( portUSING_MPU_WRAPPERS == 1 ){ }#else{ /* portUSING_MPU_WRAPPERS *//* 初始化任务堆栈,之后返回任务栈顶*/ pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); }#endif /* portUSING_MPU_WRAPPERS */if( (void * ) pxCreatedTask != NULL ){/* 任务句柄指向任务控制块*/ *pxCreatedTask = ( TaskHandle_t ) pxNewTCB; }else{ mtCOVERAGE_TEST_MARKER(); } }
prvInitialiseNewTask()函数的形参,出来xTaskCreat()的形参之外,又多出来pxNewTCB和xRegions两个形参;
后面又调用了pxPortInitialiseStack(StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters)
来初始化任务堆栈。
StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack, TaskFunction_t pxCode,void *pvParameters){ pxTopOfStack--;/* 入栈程序状态寄存器*/ *pxTopOfStack = portINITIAL_XPSR;/* xPSR*/ pxTopOfStack--;/* 入栈PC指针*/ *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;/* PC*/ pxTopOfStack--;/* 入栈LR链接寄存器*/ *pxTopOfStack = ( StackType_t ) prvTaskExitError;/* LR*/ pxTopOfStack-=5;/* 跳过R12, R3, R2 and R1这四个寄存器,不初始化*/ *pxTopOfStack = ( StackType_t ) pvParameters;/* R0作为传参入栈*/ pxTopOfStack--;/* 保存EXC_RETURN的值,用于退出SVC或PendSV中断时候,处理器处于什么状态*/ *pxTopOfStack = portINITIAL_EXEC_RETURN; pxTopOfStack-=8;/* 跳过R11, R10, R9, R8, R7, R6, R5 and R4这8个寄存器,不初始化*/return pxTopOfStack;/*最终返回栈顶*/
初始化堆栈完成之后堆栈如下图:
层层深入完毕,现在我们返回到xTaskCreat()函数后面,看看 prvAddNewTaskToReadyList( pxNewTCB ); 函数是怎么把任务添加到就绪列表中!
staticvoid prvAddNewTaskToReadyList( TCB_t *pxNewTCB ) { taskENTER_CRITICAL(); { uxCurrentNumberOfTasks++;if( pxCurrentTCB == NULL )//正在运行的任务块为NULL,没有任务运行; { pxCurrentTCB= pxNewTCB;//将新任务控制块赋值给pxCurrentTCBif( uxCurrentNumberOfTasks == ( UBaseType_t )1 ) //为1说明正在创建的任务是第一个任务。 { prvInitialiseTaskLists();//初始化列表,就绪列表、阻塞列表等等 }else { mtCOVERAGE_TEST_MARKER(); } }else {if( xSchedulerRunning == pdFALSE )//判断任务调度器是否运行,pdfalse代表没有运行 {if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ) { pxCurrentTCB= pxNewTCB;//将新创建的任务控制块赋值给当前任务控制块 }else { mtCOVERAGE_TEST_MARKER(); } }else { mtCOVERAGE_TEST_MARKER(); } } uxTaskNumber++;// 用于任务控制块编号#if ( configUSE_TRACE_FACILITY == 1 ) { pxNewTCB->uxTCBNumber = uxTaskNumber; }#endif /* configUSE_TRACE_FACILITY */ traceTASK_CREATE( pxNewTCB ); prvAddTaskToReadyList( pxNewTCB );//将任务添加到就绪列表 portSETUP_TCB( pxNewTCB ); } taskEXIT_CRITICAL();if( xSchedulerRunning != pdFALSE )//如果任务调调度器在运行,新任务优先级比正在运行的优先级高 {if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority ) { taskYIELD_IF_USING_PREEMPTION();//调用此函数完成一次任务切换 }else { mtCOVERAGE_TEST_MARKER(); } }else { mtCOVERAGE_TEST_MARKER(); } }
一定要耐心分析,别无他法,加油!不难。