目录

1. 任务概述

1.1 任务表示

1.2 任务状态

1.2.1 运行态

1.2.2 就绪态

1.2.3 阻塞态

1.2.4 挂起态

1.3 任务优先级

1.3.1 FreeRTOS优先级配置

1.3.2 任务优先级级数限制

1.3.3 相同优先级多任务配置

1.4 任务实现

2. 任务组织

2.1 就绪列表

2.2 延时列表

2.3 挂起列表

2.4 挂起解除就绪列表

2.5 删除任务列表

2.6 当前任务指针

3. 任务创建与删除

3.1 动态创建任务

3.1.1 xTaskCreate函数

3.1.2 prvInitialiseNewTask函数

3.1.3 prvAddNewTaskToReadyList函数

3.2 静态创建任务

3.3 删除任务

4. 启动调度器

4.1 vTaskStartScheduler函数

4.2 xPortStartScheduler函数

4.3 prvStartFirtstTask函数

5. 启动任务的方法

5.1 从reset到main函数

5.1.1 reset异常处理函数

5.1.2 SystemInit函数

5.1.3 __main函数

5.2 在main函数中启动任务的2种方式

5.2.1 有专门的启动task

5.2.2 无专门的启动task

6. 任务的挂起与恢复

6.1 任务的挂起

6.2 任务的恢复

6.2.1 在任务中恢复

6.2.2 在中断中恢复

7. 调度器的挂起与恢复

7.1 调度器的挂起

7.2 调度器的恢复

8. 任务切换

8.1 如何触发任务切换

8.2 任务切换的实现

9. 空闲任务

9.1 运行时机

9.2 具体工作

9.3 任务函数

10. 任务设计要点

10.1 中断服务函数

10.2 普通任务

10.3 空闲任务


1. 任务概述

1.1 任务表示

FreeRTOS中使用TCB_t描述任务控制块,也就是在OS层面表示一个任务,下面说明其中的重要字段

typedef struct tskTaskControlBlock
{// 任务栈栈顶指针,必须是TCB的第一个成员volatile StackType_t *pxTopOfStack;// 状态列表项,根据任务当前所处状态被加入不同列表ListItem_t xStateListItem;// 事件列表项,用于将任务加入事件对象(e.g. 消息队列、信号量等)ListItem_t xEventListItem;// 任务优先级UBaseType_t uxPriority;// 指向任务栈起始地址,在递减栈中,为任务栈低地址处StackType_t *pxStack;// 任务名称char pcTaskName[ configMAX_TASK_NAME_LEN ];// 由于互斥信号量涉及优先级继承// 此处记录任务的原始优先级与获取的互斥信号量个数#if ( configUSE_MUTEXES == 1 )UBaseType_t uxBasePriority;UBaseType_t uxMutexesHeld;#endif// 用于统计任务运行时间#if( configGENERATE_RUN_TIME_STATS == 1 )uint32_t ulRunTimeCounter;#endif// 用于任务通知机制#if( configUSE_TASK_NOTIFICATIONS == 1 )volatile uint32_t ulNotifiedValue;volatile uint8_t ucNotifyState;#endif// 表示TCB & 任务栈内存是静态还是动态获得,避免错误释放#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )uint8_t   ucStaticallyAllocated;#endif} tskTCB;typedef tskTCB TCB_t;

1.2 任务状态

1.2.1 运行态

① 处于运行态的任务就是当前正在使用处理器的任务

② FreeRTOS的任务调度器被设计为可预测的,总是运行处于就绪态的优先级最高的任务(RTOS类的调度器均是如此)

③ 如果使用单核处理器,任何时刻只有一个任务处于运行态

1.2.2 就绪态

处于就绪态的任务就是已经准备就绪(没有被阻塞或挂起),可以运行的任务

1.2.3 阻塞态

① 如果一个任务正在等待某个外部事件就处于阻塞态

② 任务调用延时函数以及等待队列、信号量、事件组、通知或互斥信号量时均会进入阻塞态

1.2.4 挂起态

① 与阻塞态一样,任务进入挂起态之后也不能被调度器调度到运行态

② 挂起态没有超时时间

③ 任务进入和退出挂起态通过调用函数vTaskSuspen & vTaskResume实现

1.3 任务优先级

1.3.1 FreeRTOS优先级配置

① configMAX_PRIORITIES宏指定了系统中使用的优先级级数

② 在FreeRTOS中,数值越大优先级越高,系统中可设置的优先级为0 ~ (configMAX_PRIORITIES - 1)

③ 空闲任务优先级为0(即最低优先级),软件定时器任务优先级为configMAX_PRIORITIES - 1(即最高优先级)

1.3.2 任务优先级级数限制

① FreeRTOS可使用的任务优先级级数与task_select的方法有关,

a. 如果设置了configUSE_PORT_OPTIMISED_TASK_SELECT宏,就会使用位图 + 前导零计算命令(clz)确定要执行的任务,此时优先级级数不能超过32,因为标识任务优先级的位图为32位

b. 如果使用通用方法实现task_select,则任务优先级可认为没有限制(理论上限就是42亿多)

② configMAX_PRIORITIES一般设置为一个满足应用的最小值,例如在STM32F103 demo中设置的任务优先级级数为5

1.3.3 相同优先级多任务配置

① 当设置了configUSE_TIME_SLICING宏,则允许多个任务共用一个优先级,数量不限

② 此时处于就绪态的优先级相同的任务以时间片轮转方式运行,时间片的粒度为一个SysTick(FreeRTOS中实际上没有指定任务运行时间片的概念)

1.4 任务实现

void vTaskFuntion(void *pvParameters)
{for (;;){// 任务实现// 可以导致任务切换的函数}// 退出循环时,删除此任务vTaskDelete(NULL);
}

① 任务函数一般实现为一个不会返回的循环,如果退出循环则一定要调用vTaskDelete函数删除此任务

② 任务中一般需要调用会导致任务阻塞的函数(e.g. 延时、等待IPC对象),否则该任务将持续占用CPU,直到有更高优先级的任务或者被中断打断

注意:空闲任务不允许阻塞

说明:如果任务退出,不调用vTaskDelete会怎样 ?

在使用pxPortInitialiseStack函数初始化任务栈时,会在LR寄存器对应的位置部署一个prvTaskExitError函数,如果任务返回了,就会进入该函数(已上机调试验证)

2. 任务组织

说明:根据任务当前所处状态的不同,TCB中的状态列表项会被加入不同的列表,而这些列表实现了对系统中所有任务的组织

2.1 就绪列表

// tasks.c
static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

就绪列表是一个列表数组,根据任务优先级组织,不同优先级的任务会加入不同的列表

2.2 延时列表

// tasks.c
static List_t xDelayedTaskList1;
static List_t xDelayedTaskList2;
static List_t * volatile pxDelayedTaskList;
static List_t * volatile pxOverflowDelayedTaskList;

延时列表有2个,分别使用pxDelayedTaskList和pxOverflowDelayedTaskList指针指向,用于实现对延时溢出的处理

当SysTick计数溢出时,交换两个列表指针的指向

2.3 挂起列表

// tasks.c
#if ( INCLUDE_vTaskSuspend == 1 )static List_t xSuspendedTaskList;
#endif

所有被vTaskSuspend函数挂起的任务加入挂起列表

2.4 挂起解除就绪列表

// tasks.c
static List_t xPendingReadyList;

当调用vTaskResume函数恢复任务时,如果调度器被挂起(vTaskSuspendAll函数实现),则被恢复的任务不能直接加入就绪列表,而是加入挂起解除就绪列表。当调度器挂起被解除时(vTaskResumeAll函数实现),将处理该列表,并将任务加入就绪列表

2.5 删除任务列表

#if( INCLUDE_vTaskDelete == 1 )static List_t xTasksWaitingTermination;static volatile UBaseType_t uxDeletedTasksWaitingCleanUp = ( UBaseType_t ) 0U;
#endif

如果调用vTaskDelete函数删除的是当前任务(即任务调用vTaskDelete函数删除自己),由于需要一次上下文切换调度其他任务运行,所以当前任务的内存不能释放

此时就将当前任务加入删除任务列表,并更新uxDeletedTaskWaitingCleanUp计数,后续由空闲任务释放被删除任务的内存

2.6 当前任务指针

TCB_t * volatile pxCurrentTCB = NULL;

当前任务指针指向当前处于运行态的任务

3. 任务创建与删除

3.1 动态创建任务

3.1.1 xTaskCreate函数

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )/** pxTaskCode:任务函数* pcName:任务名称,长度不超过configMAX_TASK_NAME_LEN* usStackDepth:任务栈深度,以StackType_t(32位CPU中为4B)为单位* pvParameters:任务函数参数* uxPriority:任务优先级* pxCreatedTask:出参,用于返回任务句柄* 返回值:成功返回pdPASS,否则返回错误码*/BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,const char * const pcName,const uint16_t usStackDepth,void * const pvParameters,UBaseType_t uxPriority,TaskHandle_t * const pxCreatedTask ){TCB_t *pxNewTCB;BaseType_t xReturn;/** 如果使用递增栈,则先分配TCB后分配任务栈,* 如果使用递减栈,则先分配任务栈后分配TCB* 目的是避免任务栈溢出时破坏TCB*/#if( portSTACK_GROWTH > 0 ){pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );if( pxNewTCB != NULL ){pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );if( pxNewTCB->pxStack == NULL ){vPortFree( pxNewTCB );pxNewTCB = NULL;}}}#else /* portSTACK_GROWTH */{StackType_t *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 */// 任务栈和TCB内存均分配成功if( pxNewTCB != NULL ){#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ){// 标记任务栈与TCB是动态分配的pxNewTCB->ucStaticallyAllocated =tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;}#endif /* configSUPPORT_STATIC_ALLOCATION */// 初始化新建任务prvInitialiseNewTask( pxTaskCode, pcName,( uint32_t ) usStackDepth, pvParameters, uxPriority,pxCreatedTask, pxNewTCB, NULL );// 将新建任务加入就绪列表prvAddNewTaskToReadyList( pxNewTCB );xReturn = pdPASS;}else{// 返回错误码xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;}return xReturn;}#endif /* configSUPPORT_DYNAMIC_ALLOCATION */

说明1:任务句柄

任务句柄类型如下,

typedef void * TaskHandle_t;

实际返回的任务句柄就是TCB地址,但是出于信息隐藏的目的,TCB_t类型的细节并不向应用程序暴露,所以返回void *类型的句柄

说明2:任务栈溢出

xTaskCreate函数中为了防止任务栈溢出破坏TCB,根据不同的栈类型,使用不同的分配顺序,效果如下图所示

但任务栈的使用不应溢出,否则仍然会破坏其他内核组件,造成系统崩溃

3.1.2 prvInitialiseNewTask函数

/*
* 新增参数xRegions:描述被MPU保护的内存区间
*/
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,const char * 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;// 如果使能了栈溢出检查功能,则将任务栈内存均初始化为特殊值0xa5#if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) ||\( configUSE_TRACE_FACILITY == 1 ) ||\( INCLUDE_uxTaskGetStackHighWaterMark == 1 ) ){( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE,( size_t ) ulStackDepth * sizeof( StackType_t ) );}#endif#if( portSTACK_GROWTH < 0 ){/** 计算任务栈栈顶地址,计算过程中,* 1. 先指向栈顶最后一个可用单元(防止体系结构使用空减栈)* 2. 栈顶地址做8B向下对齐(由portBYTE_ALIGNMENT_MASK宏指定 )*/pxTopOfStack = pxNewTCB->pxStack +( ulStackDepth - ( uint32_t ) 1 );pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) &( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );}#else// 省略递增栈计算#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;}}pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';// 修正指定的任务优先级不超过系统支持的最高优先级if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES ){uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;}pxNewTCB->uxPriority = uxPriority;// 初始化任务基础优先级#if ( configUSE_MUTEXES == 1 ){pxNewTCB->uxBasePriority = uxPriority;pxNewTCB->uxMutexesHeld = 0;}#endif /* configUSE_MUTEXES */// 初始化状态列表项 & 事件列表项vListInitialiseItem( &( pxNewTCB->xStateListItem ) );vListInitialiseItem( &( pxNewTCB->xEventListItem ) );// 设置状态列表项拥有者为任务TCBlistSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );/** 设置事件列表项值为系统最大任务优先级 - 任务优先级* 这样做的目的是使得优先级高的任务表项值小,在按升序插入事件列表时就可以* 在列表前列,在唤醒任务时就可以优先唤醒*/listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ),( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );// 设置事件列表项的拥有者为任务TCBlistSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );// 如果使能任务运行时间计数,此处初始化计数值#if ( configGENERATE_RUN_TIME_STATS == 1 ){pxNewTCB->ulRunTimeCounter = 0UL;}#endif /* configGENERATE_RUN_TIME_STATS */// 如果使能MPU,则设置任务内存属性#if ( portUSING_MPU_WRAPPERS == 1 ){vPortStoreTaskMPUSettings( &( pxNewTCB->xMPUSettings ),xRegions, pxNewTCB->pxStack, ulStackDepth );}#else{/* Avoid compiler warning about unreferenced parameter. */( void ) xRegions;}#endif// 初始化任务通知状态#if ( configUSE_TASK_NOTIFICATIONS == 1 ){pxNewTCB->ulNotifiedValue = 0;pxNewTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;}#endif// 初始化任务栈#if( portUSING_MPU_WRAPPERS == 1 ){pxNewTCB->pxTopOfStack =pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters,xRunPrivileged );}#else /* portUSING_MPU_WRAPPERS */{pxNewTCB->pxTopOfStack =pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );}#endif /* portUSING_MPU_WRAPPERS */// 返回的任务句柄就是TCB地址if( ( void * ) pxCreatedTask != NULL ){*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;}
}

说明1:STM32栈顶做8B向下对齐

STM32虽然是32位体系结构,但是由于浮点数计算为8B,所以在计算栈顶地址时进行了8B向下对齐

说明2:初始化任务栈

对任务栈的初始化是和体系结构相关的,任务栈初始化的就是任务第一次运行时的环境,此处以STM32的实现为例加以说明

/*
* pxTopOfStack:任务栈栈顶指针
* pxCode:任务函数
* pvParameters:任务函数参数
*/
StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack,
TaskFunction_t pxCode, void *pvParameters )
{pxTopOfStack--; // STM32使用满减栈*pxTopOfStack = portINITIAL_XPSR; /* xPSR */pxTopOfStack--;// PC*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;pxTopOfStack--;*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */pxTopOfStack -= 5; /* R12, R3, R2 and R1. */*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */return pxTopOfStack;
}

初始化后的任务栈如下图所示,

其中硬件维护部分是进出异常时硬件负责保存 / 恢复的寄存器;而r4 ~ r11则是需要软件保存 / 恢复的任务上下文

3.1.3 prvAddNewTaskToReadyList函数

static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
{// 进入临界段taskENTER_CRITICAL();{// 增加系统中任务计数uxCurrentNumberOfTasks++;if( pxCurrentTCB == NULL ){// 如果当前任务指针尚无指向,则指向当前新建立的任务pxCurrentTCB = pxNewTCB;// 如果是创建首个任务,则初始化相关组织列表if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 ){prvInitialiseTaskLists();}}else{/** 如果任务调度器尚未开启,且新创建任务的优先级高于当前任务* 则更新当前任务指针指向*/if( xSchedulerRunning == pdFALSE ){if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ){pxCurrentTCB = pxNewTCB;}}}// 将新建任务加入就绪列表prvAddTaskToReadyList( pxNewTCB );}// 退出临界段taskEXIT_CRITICAL();if( xSchedulerRunning != pdFALSE ){/** 如果任务调度器已经开启,且新创建任务优先级高于当前任务* 则触发一次任务切换*/if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority ){taskYIELD_IF_USING_PREEMPTION();}}
}

说明1:组织列表初始化

此处初始化的列表就是上文介绍的用于组织任务的不同列表

说明2:临界段保护的内容

此处临界段保护的就是这些全局的任务组织列表

说明3:触发任务切换

此处触发任务切换的目的是确保FreeRTOS时刻运行处于就绪态的优先级最高的任务,而触发任务切换的方式就是触发PendSV中断

#define taskYIELD_IF_USING_PREEMPTION() portYIELD_WITHIN_API()// FreeRTOS.h
#ifndef portYIELD_WITHIN_API#define portYIELD_WITHIN_API portYIELD
#endif#define portYIELD()   \
{ \/* Set a PendSV to request a context switch. */  \portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \__dsb( portSY_FULL_READ_WRITE ); \__isb( portSY_FULL_READ_WRITE ); \
}

3.2 静态创建任务

#if( configSUPPORT_STATIC_ALLOCATION == 1 )/** puxStackBuffer:静态分配的任务栈起始地址* pxTaskBuffer:静态分配的TCB地址* 返回值:成功返回任务句柄,否则返回NULL*/TaskHandle_t xTaskCreateStatic(    TaskFunction_t pxTaskCode,const char * const pcName,const uint32_t ulStackDepth,void * const pvParameters,UBaseType_t uxPriority,StackType_t * const puxStackBuffer,StaticTask_t * const pxTaskBuffer ){TCB_t *pxNewTCB;TaskHandle_t xReturn;// 静态分配的任务栈 & TCB地址必须有效configASSERT( puxStackBuffer != NULL );configASSERT( pxTaskBuffer != NULL );if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) ){pxNewTCB = ( TCB_t * ) pxTaskBuffer;pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;// 标记任务栈 & TCB地址为静态分配#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ){pxNewTCB->ucStaticallyAllocated =tskSTATICALLY_ALLOCATED_STACK_AND_TCB;}#endif /* configSUPPORT_DYNAMIC_ALLOCATION */// 初始化任务,并将任务加入就绪列表prvInitialiseNewTask( pxTaskCode, pcName, ulStackDepth,pvParameters, uxPriority, &xReturn, pxNewTCB, NULL );prvAddNewTaskToReadyList( pxNewTCB );}else{xReturn = NULL;}return xReturn;}#endif /* SUPPORT_STATIC_ALLOCATION */

说明1:静态创建任务的核心

静态创建任务的核心就是任务栈 & TCB所使用的内存不是动态分配的,而是静态数据

说明2:StaickTask_t类型信息隐藏

静态创建任务时传递的TCB数据类型为StaickTask_t,并不是TCB_t类型,从类型定义可知,StaticTask_t类型与TCB_t类型的成员的一一对应的,但是隐藏了所有字段信息,实现了对应用程序的信息隐藏

3.3 删除任务

#if ( INCLUDE_vTaskDelete == 1 )// xTaskToDelete:要删除的任务句柄,如果传递NULL,则删除当前任务void vTaskDelete( TaskHandle_t xTaskToDelete ){TCB_t *pxTCB;taskENTER_CRITICAL();{// 通过任务句柄获得TCB,本质是进行一次类型转换pxTCB = prvGetTCBFromHandle( xTaskToDelete );/** 将任务从当前列表删除(不一定是就绪列表,* 也可能是等待 / 挂起列表)* 如果是从就绪列表删除,且删除后该优先级已无任务,* 则注销该优先级(这里其实有问题)*/if( uxListRemove( &( pxTCB->xStateListItem ) ) ==( UBaseType_t ) 0 ){taskRESET_READY_PRIORITY( pxTCB->uxPriority );}// 如果要删除的任务还在等待事件,则将其从等待事件的列表中删除if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ){( void ) uxListRemove( &( pxTCB->xEventListItem ) );}if( pxTCB == pxCurrentTCB ){/** 如果是任务调用vTaskDelete函数删除自己,由于还需要一次任务* 切换,所以不能在此处释放任务栈和TCB* 此时将该任务加入删除任务列表,该任务将由空闲任务释放资源*/vListInsertEnd( &xTasksWaitingTermination,&( pxTCB->xStateListItem ) );++uxDeletedTasksWaitingCleanUp;}else{/** 如果不是任务删除自己,则此处直接释放任务资源* 同时更新下一任务解锁时间,因为要删除的任务可能在等待列表中* 更新下一任务解锁时间的操作将在时间管理笔记中说明*/--uxCurrentNumberOfTasks;prvDeleteTCB( pxTCB );prvResetNextTaskUnblockTime();}}taskEXIT_CRITICAL();// 如果删除的是任务自己,则触发一次任务切换if( xSchedulerRunning != pdFALSE ){if( pxTCB == pxCurrentTCB ){/** 如果是任务删除自己,在调用vTaskDelete这类会导致任务切换* 的函数时,就不能挂起调度器*/configASSERT( uxSchedulerSuspended == 0 );portYIELD_WITHIN_API();}}}#endif /* INCLUDE_vTaskDelete */

说明1:默认从就绪列表删除的问题

在vTaskDelete删除任务时,首先通过TCB的状态列表项(xStateListItem)将任务从当前列表删除。但是需要注意的是,被删除的任务当前不一定在就绪列表

但是代码中默认任务处于就绪列表,所以当删除任务后如果列表为空,会注销任务对应的优先级

这种处理是会导致问题的,如果同优先级有多个任务,而其中一个任务被vTaskSuspend函数挂起,且假设挂起列表只有这一个任务。如果此时删除该任务,实际上是从挂起列表删除,并且会判断出删除任务后列表为空,就会错误地注销任务优先级

更正:此处删除任务,就是从任务当前所属列表删除,但是在注销任务优先级时,会先判断就绪列表是否为空,只有在就绪列表为空时,才会注销任务优先级,所以不会导致问题

这里其实是代码注释的一个锅,

说明2:删除任务时,只会自动释放内核本身分配给任务的内存(TCB & 任务栈),应用程序分配给任务的内存或其他资源必须是删除任务时由应用程序显式释放

所以在删除任务之前,应用程序需要先释放分配给该任务的资源,但是FreeRTOS并未设置相应的机制(比如删除任务时的回调函数)

4. 启动调度器

4.1 vTaskStartScheduler函数

void vTaskStartScheduler( void )
{
BaseType_t xReturn;#if( configSUPPORT_STATIC_ALLOCATION == 1 ){StaticTask_t *pxIdleTaskTCBBuffer = NULL;StackType_t *pxIdleTaskStackBuffer = NULL;uint32_t ulIdleTaskStackSize;/** 如果使用静态分配内存的方式,就需要实现* vApplicationGetIdleTaskMemory函数,该函数用于提供* 为空闲任务静态分配的任务栈、TCB以及栈的大小*/vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,&pxIdleTaskStackBuffer, &ulIdleTaskStackSize );// 空闲任务优先级为0,即最低优先级xIdleTaskHandle = xTaskCreateStatic(prvIdleTask,"IDLE",ulIdleTaskStackSize,( void * ) NULL,( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer );if( xIdleTaskHandle != NULL ){xReturn = pdPASS;}else{xReturn = pdFAIL;}}#else{   xReturn = xTaskCreate( prvIdleTask,"IDLE", configMINIMAL_STACK_SIZE,( void * ) NULL,( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),&xIdleTaskHandle );}#endif /* configSUPPORT_STATIC_ALLOCATION */// 如果使用软件定时器,还需要创建定时器任务#if ( configUSE_TIMERS == 1 ){if( xReturn == pdPASS ){xReturn = xTimerCreateTimerTask();}}#endif /* configUSE_TIMERS */if( xReturn == pdPASS ){// 在启动调度器的阶段,先关闭中断portDISABLE_INTERRUPTS();// 初始化下一任务解锁时间为最大计数值xNextTaskUnblockTime = portMAX_DELAY;// 标识调度器已经开启xSchedulerRunning = pdTRUE;// 初始化SysTick计数器xTickCount = ( TickType_t ) 0U;/** 如果使能任务耗时统计功能,需要配置一个时基定时器* 该定时器的精度需要是SysTick定时器的10 ~ 20倍*/portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();// port层面启动调度器if( xPortStartScheduler() != pdFALSE ){/* Should not reach here as if the scheduler is running the* function will not return.*/}else{/* Should only reach here if a task calls * xTaskEndScheduler(). */}}else{// 启动调度器失败configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );}// 抑制编译警告( void ) xIdleTaskHandle;
}

说明:创建定时器任务

BaseType_t xTimerCreateTimerTask( void )
{
BaseType_t xReturn = pdFAIL;// 初始化定时器任务所需组件prvCheckForValidListAndQueue();if( xTimerQueue != NULL ){// 定时器任务的优先级一般设置为系统中的最高优先级#if( configSUPPORT_STATIC_ALLOCATION == 1 ){StaticTask_t *pxTimerTaskTCBBuffer = NULL;StackType_t *pxTimerTaskStackBuffer = NULL;uint32_t ulTimerTaskStackSize;// 静态创建定时器任务,需要实现vApplicationGetTimerMemory函数vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize );xTimerTaskHandle = xTaskCreateStatic(prvTimerTask,"Tmr Svc",ulTimerTaskStackSize,NULL,( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) |portPRIVILEGE_BIT,xTimerTaskStackBuffer,pxTimerTaskTCBBuffer );if( xTimerTaskHandle != NULL ){xReturn = pdPASS;}}#else{xReturn = xTaskCreate(prvTimerTask,"Tmr Svc",configTIMER_TASK_STACK_DEPTH,NULL,( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) |portPRIVILEGE_BIT,&xTimerTaskHandle );}#endif /* configSUPPORT_STATIC_ALLOCATION */}configASSERT( xReturn );return xReturn;
}

4.2 xPortStartScheduler函数

BaseType_t xPortStartScheduler( void )
{// 将PendSV & SysTick的中断优先级设为最低portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;// 设置SysTick,产生SysTick中断vPortSetupTimerInterrupt();// 初始化临界段嵌套计数uxCriticalNesting = 0;// 启动第一个任务prvStartFirstTask();/* Should not get here! */return 0;
}

4.3 prvStartFirtstTask函数

__asm void prvStartFirstTask( void )
{PRESERVE8// 获取MSP指针ldr r0, =0xE000ED08ldr r0, [r0]ldr r0, [r0]// 设置MSP指针msr msp, r0/** 打开中断,注意,此时BASEPRI寄存器尚未解除屏蔽* 需要在第一个任务启动后才解除屏蔽*/cpsie icpsie fdsbisb// 触发SVC中断svc 0nopnop
}

说明1:获取MSP指针

根据之前的说明,中断向量表的第一个位置存放的就是MSP指针地址,由于Cortex-M体系结构支持中断向量表重定向,所以此处从SCB的VTOR寄存器(0xE000ED08)中读取中断向量表地址,然后取出MSP指针地址并赋值给MSP寄存器

在中断处理函数中使用的就是MSP

补充:异常向量表重定位与MSP初始值

① STM32F407在SystemInit函数中将异常向量表进行了重定位

根据上机调试,F407将异常向量表重定位到FLASH中,地址为0x08000000

#define FLASH_BASE       ((uint32_t)0x08000000)
#define VECT_TAB_OFFSET  0x00

② 为何重定位到0x08000000

根据F407的Memory布局,0x08000000是IROM的起始地址,也就是说在IROM起始地址烧写的就是异常向量表

③ 0x08000000地址处的值是多少

由于异常向量表的第1项是MSP地址,所以reset之后MSP的值为0x20005F10,这与上机调试的结果是一致的

④ 凭什么说0x08000000处部署的就是异常向量表

a. 链接器脚本中的RESET段

b. RESET段内容

可见RESET段部署的就是异常向量表,而且在启动时,异常向量表会被映射到0地址处,所以启动时使用的也是这张异常向量表

说明2:在SVC中断中启动第一个任务

以何种方式启动第一个任务依据不同的体系结构而定,在Cortex-M体系结构中,在SVC中断处理函数中启动第一个任务,而启动第一个任务的过程就是用初始化的任务栈恢复一次下文

__asm void vPortSVCHandler( void )
{PRESERVE8// 获取第一个任务的任务栈地址ldr r3, =pxCurrentTCBldr r1, [r3]ldr r0, [r1]// 恢复下文ldmia r0!, {r4-r11}msr psp, r0isb// 取消屏蔽,至此,FreeRTOS管理的中断开始被响应mov r0, #0msr basepri, r0// SVC异常返回后进入线程模式且使用PSPorr r14, #0xdbx r14
}

说明3:对SVC中断的响应

如上文所述,在prvStartFirstTask函数中只是使能了PRIMASK & FAULTMASK寄存器,BASEPRI寄存器仍处于屏蔽状态

但是此时SVC中断却可以被响应,那么说明SVC中断的优先级高于BASEPRI寄存器中的设置,我们对此进行验证

① 程序运行到prvStartFirstTask时,各中断寄存器的状态

a. 使能全局中断之前

b. 使能全局中断之后

可见在FreeRTOS中,关中断的方式主要是依靠BASEPRI寄存器,而不是全局中断屏蔽

② SVC中断的优先级

根据系统处理优先级寄存器与SVC的异常编号,可知SVC中断优先级寄存器地址为0xE000ED1F,可见SVC中断优先级默认为0,高于BASEPRI寄存器的设置。同时也可以看到,PendSV & SYSTICK中断的优先级均被设置为最低

5. 启动任务的方法

5.1 从reset到main函数

说明:以下分析以Cortex-M3的CMSIS为例

5.1.1 reset异常处理函数

系统上电后,首先进入reset异常处理函数,在reset异常处理函数中,会先后调用SystemInit & __main函数

5.1.2 SystemInit函数

在CMSIS提供的SystemInit函数中,主要设置了2点,

① 配置是否支持内存非对齐访问

② 设置系统时钟频率

再补充一个原子F407挑战者开发板的SystemInit函数,注意其中对中断向量表的重定位操作

5.1.3 __main函数

根据MDK文档,__main函数解释如下,

It is automatically created by the linker when it sees a definition of main()

也就是如果用户定义了main函数,链接器会自动生成__main函数,该函数完成如下2个操作,

① 调用__scatterload函数,将RW / RO输出段从装载地址复制到运行地址,并完成ZI区域的初始化工作

② 调用__rt_entry函数,初始化栈,完成库函数的初始化,最后自动跳转到main函数

至此,就开始运行用户定义的main函数

5.2 在main函数中启动任务的2种方式

5.2.1 有专门的启动task

int main(void)
{// 各种硬件初始化xTaskCreate(start_task, "start_task", START_STACK_SIZE, NULL,START_TASK_PRIO, &StartTask_Handler);// 启动任务调度器vTaskStartScheduler();return 0;
}void start_task(void *pvParameters)
{taskENTER_CRITICAL();// 创建task1xTaskCreate(task1_func, "task1", TASK1_STACK_SIZE, NULL,TASK1_PRIO, &Task1_Handler);// 创建task2xTaskCreate(task2_func, "task2", TASK2_STACK_SIZE, NULL,TASK2_PRIO, &Task1_Handler);// 删除启动任务vTaskDelete(StartTask_Handler);taskEXIT_CRITICAL();
}

说明1:系统首先启动start_task任务,之后在start_task中启动其他任务,待其他任务启动完成后,删除start_task

说明2:一般在start_task中启动其他任务,需要临界段保护,目的是确保在任务启动过程中不发生任务切换

5.2.2 无专门的启动task

int main(void)
{// 各种硬件初始化// 创建task1xTaskCreate(task1_func, "task1", TASK1_STACK_SIZE, NULL,TASK1_PRIO, &Task1_Handler);// 创建task2xTaskCreate(task2_func, "task2", TASK2_STACK_SIZE, NULL,TASK2_PRIO, &Task1_Handler);// 启动任务调度器vTaskStartScheduler();return 0;
}

说明1:在这种情况下,先创建所有任务,之后启动调度器

说明2:这2种方法均可使用,并不优劣之分,依习惯而定

6. 任务的挂起与恢复

6.1 任务的挂起

#if ( INCLUDE_vTaskSuspend == 1 )void vTaskSuspend( TaskHandle_t xTaskToSuspend ){TCB_t *pxTCB;// 进入临界段,保护全局资源taskENTER_CRITICAL();{/** 将任务句柄类型转换为TCB* 如果传入的参数为NULL,则是挂起当前任务(即函数调用者)*/pxTCB = prvGetTCBFromHandle( xTaskToSuspend );/** 将任务从当前列表删除,此时任务可能在就绪列表 / 延时列表,* 甚至本身就在挂起列表(比如已经被挂起了一次)* 如果删除任务后就绪列表为空,则注销相应的优先级*/if( uxListRemove( &( pxTCB->xStateListItem ) ) ==( UBaseType_t ) 0 ){taskRESET_READY_PRIORITY( pxTCB->uxPriority );}// 如果任务在等待事件,从相应的事件列表删除if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) )!= NULL ){( void ) uxListRemove( &( pxTCB->xEventListItem ) );}// 将任务加入挂起列表vListInsertEnd( &xSuspendedTaskList,&( pxTCB->xStateListItem ) );}taskEXIT_CRITICAL();/** 如果调度器已经启动,则重置下一任务解锁时间* 因为被挂起的任务可能是延时列表的队首任务* 由于延时列表为全局资源,需要临界段保护*/if( xSchedulerRunning != pdFALSE ){taskENTER_CRITICAL();{prvResetNextTaskUnblockTime();}taskEXIT_CRITICAL();}if( pxTCB == pxCurrentTCB ){/** 如果被挂起的是当前任务,且调度器已经运行,* 则触发一次任务切换* 需要注意的是,在调用可能导致任务切换的函数时,调度器不能挂起* 所以此处使用assert判断调度器是否被挂起*/if( xSchedulerRunning != pdFALSE ){configASSERT( uxSchedulerSuspended == 0 );portYIELD_WITHIN_API();}/** 如果被挂起的是当前任务,但是调度器尚未运行,* 则修改当前任务指针指向*/else{if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) ==uxCurrentNumberOfTasks ){pxCurrentTCB = NULL;}else{vTaskSwitchContext();}}}}#endif /* INCLUDE_vTaskSuspend */

说明1:在调度器运行之后,不会发生所有任务均被挂起的情况,因为至少还有空闲任务还在运行。这里就提出了一个要求,就是空闲任务不能被挂起

说明2:vTaskSwitchContex函数

void vTaskSwitchContext( void )
{// 如果切换任务时调度器被挂起,则标记xYieldPendingif( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){xYieldPending = pdTRUE;}/** 如果切换任务时调度器没有被挂起,则选择就绪列表中优先级最高的任务* taskSELECT_HIGHEST_PRIORITY_TASK宏会修改当前任务指针*/else{xYieldPending = pdFALSE;taskSELECT_HIGHEST_PRIORITY_TASK();}
}

可见当调度器被挂起时,不会发生任务切换

6.2 任务的恢复

6.2.1 在任务中恢复

#if ( INCLUDE_vTaskSuspend == 1 )void vTaskResume( TaskHandle_t xTaskToResume ){TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;/** 传入NULL,恢复当前任务是没有意义的* 因为当前调用vTaskResume的任务肯定没有被挂起*/configASSERT( xTaskToResume );if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) ){// 进入临界段,保护全局资源taskENTER_CRITICAL();{// 判断任务是否处于挂起态if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ){/** 将任务从挂起列表删除,并加入就绪列表* 由于当前处于临界段,即使调度器被挂起,也可以访问就绪列表*/( void ) uxListRemove(  &( pxTCB->xStateListItem ) );prvAddTaskToReadyList( pxTCB );/** 如果被恢复的任务优先级高于当前任务,* 则触发一次任务切换*/if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){taskYIELD_IF_USING_PREEMPTION();}}}taskEXIT_CRITICAL();}}#endif /* INCLUDE_vTaskSuspend */

说明1:从vTaskSuspend & vTaskResume的实现分析,一个任务即使被挂起多次,也只需要调用一次vTaskResume进行恢复

说明2:判断任务是否处于挂起态

#if ( INCLUDE_vTaskSuspend == 1 )static BaseType_t prvTaskIsTaskSuspended( const TaskHandle_t xTask ){BaseType_t xReturn = pdFALSE;const TCB_t * const pxTCB = ( TCB_t * ) xTask;/** 由于需要访问挂起解除就绪列表,所以该函数需要在临界段中调用*/if( listIS_CONTAINED_WITHIN( &xSuspendedTaskList,&( pxTCB->xStateListItem ) ) != pdFALSE ){if( listIS_CONTAINED_WITHIN( &xPendingReadyList,&( pxTCB->xEventListItem ) ) == pdFALSE ){if( listIS_CONTAINED_WITHIN( NULL,&( pxTCB->xEventListItem ) ) != pdFALSE ){xReturn = pdTRUE;}}}return xReturn;}#endif /* INCLUDE_vTaskSuspend */

从代码中可知,判断任务是否处于挂起态,需要同时满足3个条件,

① 任务在挂起挂起列表中(状态列表项)

② 任务不在挂起解除列表中,即没有被xTaskResumeFromISR恢复过(事件列表项)

③ 任务不在等待事件(如果使能vTaskSuspend,当任务不设超时地等待事件时,也会被加入挂起列表,详见后续笔记对vTaskDelay函数的分析)

说明3:如果被恢复的任务优先级高于当前任务,会触发一次任务切换,由于此时处于临界段,切换不会立即发生,而是要等到退出临界段时才会进行

6.2.2 在中断中恢复

#if ( ( INCLUDE_xTaskResumeFromISR == 1 ) &&\
( INCLUDE_vTaskSuspend == 1 ) )BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ){BaseType_t xYieldRequired = pdFALSE;TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume;UBaseType_t uxSavedInterruptStatus;configASSERT( xTaskToResume );/** 判断当前的中断是否在FreeRTOS管理之内* 只有在FreeRTOS管理之内的中断才能调用带FromISR后缀的函数*/portASSERT_IF_INTERRUPT_PRIORITY_INVALID();// 关中断uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();{if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ){/** 判断当前是否可以访问就绪列表* 在FreeRTOS中,如果调度器被挂起,中断则不能访问* 状态列表项可加入的列表(比如就绪列表、延时列表)* 如果调度器没有被挂起,则将任务从挂起列表删除,* 并加入就绪列表*/if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){// 如果恢复的任务优先级更高,返回需要任务切换if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){xYieldRequired = pdTRUE;}( void ) uxListRemove( &( pxTCB->xStateListItem ) );prvAddTaskToReadyList( pxTCB );}/** 如果调度器被挂起,则将任务的事件列表项加入挂起解除就绪列表* 特别注意!!!这里使用的是事件列表项*/else{vListInsertEnd( &( xPendingReadyList ),&( pxTCB->xEventListItem ) );}}}// 开中断portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );return xYieldRequired;}#endif

说明1:xTaskResumeFromISR使用示例

void vAnExampleISR(void)
{BaseType_t xYieldRequired = pdFALSE;xYieldRequired = xTaskResultFromISR(xHandle);if (xYieldRequired == pdTRUE){portYIELD_FROM_ISR(xYieldRequired);}
}

说明2:xTaskResumeFromISR使用注意事项

根据FreeRTOS源码注释,该函数不能用于任务与中断间的同步,因为如果中断到来时任务尚未挂起(即中断可能在任务被挂起之前到来),则此次恢复不会进行,则相当于丢失了一次中断处理

任务和中断间的同步应该使用信号量等合适的事件机制

我们来详细说明一下不能使用xTaskResumeFromISR进行任务与中断之间同步的场景,假设一个中断处理任务的逻辑就是挂起自己,然后等待中断恢复,被恢复后则进行中断处理,处理完成后再次挂起自己,等待下一次唤醒

这个逻辑看上去非常美好,但是如果在任务被恢复并进行中断处理的过程中,中断再次发生,则此时不会有任务被唤醒(因为要被唤醒的任务目前还在工作),这就相当于丢失了一次中断处理

这种场景类似Linux中基于事件的通知机制,比如在调用wake_up函数唤醒进程时,可能任务尚未进入等待队列进行等待,此时就适合使用基于资源的通知机制

严格意义上说,如果不存在中断可能在任务被挂起之前就到来,xTaskResumeFromISR还是可以用于实现同步的

7. 调度器的挂起与恢复

7.1 调度器的挂起

void vTaskSuspendAll( void )
{++uxSchedulerSuspended;
}

说明1:调度器挂起的影响

挂起调度器的操作就是增加调度器挂起计数,在FreeRTOS中,如果调度器被挂起会造成产生如下效果,

① 任务不会被切换

这点可以参考上文中说明的vTaskSwitchContext函数,此时会标记xYieldPending标记

② 中断上下文不能操作任务的状态列表项(xStateListItem),进而不能访问状态列表项加入的列表(就绪列表 & 延时列表)

③ 如果在中断上下文中调用vTaskResumeFromISR恢复任务,只能将任务的事件列表项加入挂起解除就绪列表

需要注意的是,挂起解除就绪列表也只能在临界段中被访问

个人:在FreeRTOS中,如果调度器被挂起,vTaskResume在任务上下文中可以访问就绪列表;vTaskResumeFromISR在中断上下文中不可以访问就绪列表,个人觉得这更是一种人为的规定,而不是理论限制

更正:上面的理解是错误的,这就是一个理论限制,错误的根源在于忘记了对资源的互斥保护必须所有操作者都施加才能生效,下面予以分析

① 假设A & B两个操作者(可以是任务,也可以是ISR)要操作同一个资源,如果只有一方使用互斥手段而另一方不使用,则对资源的互斥保护是无效的

② vTaskSuspendAll函数的目的就是在不关闭中断的状态下,让任务可以不被切换地完成可能比较耗时的操作,这里不关闭中断是为了确保操作系统的实时性

这里要特别注意的是,在vTaskSuspendAll中,只是关闭了调度器,并没有进入临界区,而且使用vTaskSuspendAll的目的就是为了避免长时间关闭中断

当然,任务在调用vTaskSuspendAll之后,可以再进入临界区保护各种列表资源,但是vTaskResumeFromISR函数不能对此做出假设。因此在vTaskResumeFromISR函数中如果判断出调度器被关闭,就不会再去操作列表资源

③ 这里就进入了一种场景,vTaskResumeFromISR进入了临界区,但是vTaskSuspendAll没有进入临界区,即只有一方施加了互斥保护,所以在vTaskResumeFromISR函数中如果判断出调度器被关闭,就不能访问列表资源

如果vTaskResumeFromISR函数中判断调度器没有被关闭,则可以访问列表资源,其他操作者有责任完成自己的互斥操作

④ vTaskResume函数可以在调度器被关闭的情况下访问列表资源,是因为如果调度器被关闭,那么在一个CPU核上调用vTaskResume函数的任务一定就是调用vTaskSuspendAll的任务,此时不会产生竞态(即不存在另一个未施加保护就访问列表资源的任务)

如果在调用vTaskResume时,调度器没有被关闭,由于vTaskResume函数进行了互斥处理,所以也可以访问。同样地,其他操作者要完成自己的互斥操作

说明2:由于调度器被关闭后,调用vTaskSuspendAll的任务可以继续运行而不必担心被切换,所以该任务不能调用可能导致任务切换的函数(e.g. vTaskDelay、xQueueSend等)

实验与讨论:如果在调度器挂起的情况下触发调度会怎样

① 调用vTaskSuspenAll之后触发调度

经过上机调试,由于调度器被挂起,任务不会进入等待,而是会继续运行。但是此时的运行已经不符合语义了,因为该任务已经被移出就绪列表并加入延时列表

因此在vTaskDelay等会导致任务被挂起的接口中,均会判断调度器是否被挂起

② 进入临界区之后触发调度

如上文所述,调用vTaskSuspendAll之后只是关闭了调度器,中断仍然在运行。而关闭中断会导致PendSV中断不被响应,也会导致任务无法调度,所以也是不能调用会导致任务切换的函数的

经过上机调试,任务也不会进入等待,而是继续运行,与上文的分析是一致的

③ 互斥手段的强度

关中断(进入临界段) > 关调度器 > 其他互斥手段(e.g. 信号量等)

关中断 & 关调度器又是其他互斥手段的基础

a. 关中断的强度最大,其他任务不会被调度,中断也不会被响应

临界区中执行的操作不会被打断(除非是不在RTOS管理之下的高优先级中断)

b. 关调度器强度次之,其他任务不会被调度,但是中断仍可被响应

临界区中执行的操作可能被中断打断,不会被其他任务打断

c. 其他互斥手段强度最小,同时代价也最小

临界区中执行的操作可能被中断打断,也可能被其他任务打断,但是也不会有互斥问题

注意:这里的"打断"是指执行流上的打断,并不是临界区被破坏。即临界段的不可打断是逻辑上的,而不是执行流上的

④ 与Linux自旋锁的对照

a. spin_lock会关闭抢占,类似于FreeRTOS中的关闭调度器,其他任务无法抢占当前的任务,但是中断仍可以响应

b. spin_lock_irq会关闭抢占同时关闭中断,所以其他任务和中断都不会打断当前的执行流

c. 在Linux中,关中断本身也会导致抢占被关闭,因为在抢占之前会调用preemptible宏进行判断,而该宏中,只要中断被关闭,也会返回不可抢占

当然,Linux中的自旋锁更大的用处是处理SMP间的互斥,但是可见技术都是相通的

7.2 调度器的恢复

BaseType_t xTaskResumeAll( void )
{
TCB_t *pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;// xTaskResumeAll的调用必须与vTaskSuspendAll匹配configASSERT( uxSchedulerSuspended );// 进入临界段,保护全局资源taskENTER_CRITICAL();{// 减少调度器挂起次数--uxSchedulerSuspended;// 如果调度器挂起次数减少到0,则进行一系列恢复动作if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U ){/** 将挂起解除就绪列表中的任务加入就绪列表* 这些任务都是在xTaskResumeFromISR中被解除挂起的,* 这些任务的状态列表项在挂起列表,* 事件列表项在挂起解除就绪列表*/while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE ){pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY(( &xPendingReadyList ) );( void ) uxListRemove( &( pxTCB->xEventListItem ) );( void ) uxListRemove( &( pxTCB->xStateListItem ) );prvAddTaskToReadyList( pxTCB );// 如果恢复的任务优先级高于当前任务,则标识需要任务切换if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ){xYieldPending = pdTRUE;}}if( pxTCB != NULL ){/** 如果有任务在调度器被挂起期间被恢复,* 则重新计算下一任务解时间,详细场景见下文分析*/prvResetNextTaskUnblockTime();}{UBaseType_t uxPendedCounts = uxPendedTicks; /* Non-volatile copy. *//** 如果在调度器挂起的过程中发生SysTick,此处处理积累的* 时基递增操作,如果有任务切换的需求,也标记* xYieldPending变量*/if( uxPendedCounts > ( UBaseType_t ) 0U ){do{if( xTaskIncrementTick() != pdFALSE ){xYieldPending = pdTRUE;}--uxPendedCounts;} while( uxPendedCounts > ( UBaseType_t ) 0U );uxPendedTicks = 0;}}/** 如果有需要进行任务切换,且使能了抢占式调度,* 则触发一次任务切换*/if( xYieldPending != pdFALSE ){#if( configUSE_PREEMPTION != 0 ){xAlreadyYielded = pdTRUE;}#endif// taskYIELD_IF_USING_PREEMPTION调用在条件编译之外,// 是因为这个宏本身就被configUSE_PREEMPTION宏控制,// 如果configUSE_PREEMPTION宏值为0,则该宏为空taskYIELD_IF_USING_PREEMPTION();}}}taskEXIT_CRITICAL();return xAlreadyYielded;
}

说明1:xTaskResumeAll函数的返回值,表示在该函数中是否触发过任务切换

说明2:如果在调度器被挂起的过程中有任务被恢复,为何需要更新下一任务解锁时间 ?

首先分析一下,在什么情况下需要更新下一任务解锁时间。任务被加入延时队列后(调用vTaskDelay或者带延时地等待事件),如果延时尚未到期,任务就被从延时队列删除(例如任务被挂起或者被删除),就需要更新下一任务解锁时间

下面再分析一下,什么情况下任务会被加入挂起解除就绪列表(xPendingReadyList),FreeRTOS内核中共有如下3种场景,

① xTaskResumeFromISR函数在恢复任务时,调度器被挂起

② xTaskRemoveFromEventList函数在内核对象上实现唤醒时,调度器被挂起

③ xTaskGenericNotifyFromISR函数进行任务通知时,调度器被挂起

根据FreeRTOS论坛上Richard Barry的回答,此处更新解锁时间,主要是事件通知 & tickless模式的原因,详情要到学习完tickless模式才能理解

https://forums.freertos.org/t/specific-scenario-of-xtaskresumeall-function/10225

其实简单分析下xTaskRemoveFromEventList函数就能看出端倪(该函数可能会被FromISR的函数调用,也就是在中断处理函数中被调用),

下面我们来梳理下整个逻辑链条,

① 如果任务带延时地等待事件,任务的状态列表项会加入延时列表,事件列表项会加入相应的事件列表(e.g. 在消息队列结构的xTasksWaitingToSend或xTasksWaitingToReceive列表)

② 当调用xTaskRemoveFromEventList函数唤醒事件时,会先将任务的事件列表项从相应的事件列表删除

③ 如果此时调度器被挂起,就只是将任务的事件列表项加入挂起解除就绪列表

④ 如果使能了tickless模式,就更新下一任务解锁时间。但是这里就遇到了一个问题,如果调度器被挂起,此时任务的状态列表项仍然在延时列表中,更新下一任务解锁时间可能是不生效的

⑤ 所以这里就引出了为何在xTaskResumeAll中,如果有任务从挂起解除就绪列表中被加入就绪列表,就要更新下一任务解锁时间。因为之前的更新被阻止了,这点和内核注释就是匹配的了

8. 任务切换

8.1 如何触发任务切换

在Cortex-M体系结构中,触发任务切换的根本是触发PendSV中断,也就是调用portYIELD函数

说明1:在SysTick中断处理函数中,在更新时基后,也会根据情况触发任务切换

说明2:某些FreeRTOS API也会触发任务切换,比如vTaskDelay等函数,也就是所有可能导致任务进入阻塞态的API

8.2 任务切换的实现

在Cortex-M体系结构中,任务切换在PendSV中断处理函数中完成,一般使用汇编语言完成,具体如下,

__asm void xPortPendSVHandler( void )
{extern uxCriticalNesting;extern pxCurrentTCB;extern vTaskSwitchContext;PRESERVE8// 保存上文mrs r0, pspisbldr   r3, =pxCurrentTCBldr   r2, [r3]stmdb r0!, {r4-r11}str r0, [r2]// 此处保存r3 & r14是因为后续要使用,避免被vTaskSwitchContext函数修改stmdb sp!, {r3, r14}// 关闭中断mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITYmsr basepri, r0dsbisb// 选择下一个要执行的任务bl vTaskSwitchContext// 开中断mov r0, #0msr basepri, r0ldmia sp!, {r3, r14}// 恢复下文ldr r1, [r3]ldr r0, [r1]ldmia r0!, {r4-r11}msr psp, r0isbbx r14nop
}

说明:保存上文与恢复下文的操作都是在开中断的情况下完成的,中断仅仅保护了vTaskSwitchContext函数,也就是该函数使用的全局资源

由于在Cortex-M + FreeRTOS的环境中,PendSV中断被设置为最低优先级,所以这个过程是可以被更高优先级的中断打断的。那么更高优先级的中断处理函数就需要保存 & 恢复可能使用到的寄存器(硬件保存的寄存器除外),而这个工作很大程度上是编译器完成的

9. 空闲任务

9.1 运行时机

① 空闲任务在启动调度器时由系统自动创建,从而确保系统中至少有一个可以运行的任务

② 当系统中没有其他就绪任务时,空闲任务开始运行

9.2 具体工作

① 删除调用vTaskDelete函数删除自身的任务,即调用vTaskDelete(NULL)的任务

② 让CPU进入低功耗模式(非必需)

9.3 任务函数

static portTASK_FUNCTION( prvIdleTask, pvParameters )
{/* 消除编译警告 */( void ) pvParameters;for( ;; ){// 删除调用vTaskDelete删除自身的任务prvCheckTasksWaitingTermination();#if ( configUSE_PREEMPTION == 0 ){/** 如果未使能内核抢占,此处主动切换一次,查看是否有已经就绪的任务* 如果已使能内核抢占,则无需该操作,其他任务会自行抢占空闲任务*/taskYIELD();}#endif /* configUSE_PREEMPTION *//** 如果使能了内核抢占与configIDLE_SHOULD_YIELD宏,* 就可以定义与空闲任务同优先级的任务,并按同优先级共享时间片运行*/#if ( ( configUSE_PREEMPTION == 1 ) &&( configIDLE_SHOULD_YIELD == 1 ) ){/** 此处读取就绪列表长度没有进入临界段* 根据源码注释,此处只是读取变量值,即使读取时没有被及时更新* 也不会有问题,所以没有进行互斥*/if( listCURRENT_LIST_LENGTH( &(pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 ){taskYIELD();}}#endif/** 调用空闲任务hook函数* 一般在该函数中进入低功耗模式* 在该函数中不能调用可能导致阻塞的接口,因为空闲任务不能阻塞*/#if ( configUSE_IDLE_HOOK == 1 ){extern void vApplicationIdleHook( void );vApplicationIdleHook();}#endif /* configUSE_IDLE_HOOK */#if ( configUSE_TICKLESS_IDLE != 0 ){// 进行tickless低功耗操作,详见后续笔记}#endif /* configUSE_TICKLESS_IDLE */}
}

说明1:空闲任务函数名定义

空闲任务函数名以宏定义方式生成

因此实际定义的就是,

void prvIdleTask(void *pvParameters)
{// 函数体
}

说明2:prvCheckTasksWaitingTermination函数

static void prvCheckTasksWaitingTermination( void )
{#if ( INCLUDE_vTaskDelete == 1 ){BaseType_t xListIsEmpty;while( uxDeletedTasksWaitingCleanUp > ( UBaseType_t ) 0U ){/** 判断xTasksWaitingTermination列表是否为空时,* 使用关闭调度器的方式进行互斥是足够的* 因为只有当任务调用vTaskDelete函数删除自己时,才会访问该列表* 现在关闭了调度器,自然不会有其他任务删除自己*/vTaskSuspendAll();{xListIsEmpty = listLIST_IS_EMPTY(&xTasksWaitingTermination );}( void ) xTaskResumeAll();if( xListIsEmpty == pdFALSE ){TCB_t *pxTCB;/** 由于此处要更新全局任务计数, 所以需要进入临界段*/taskENTER_CRITICAL();{pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY(( &xTasksWaitingTermination ) );( void ) uxListRemove( &( pxTCB->xStateListItem ) );--uxCurrentNumberOfTasks;--uxDeletedTasksWaitingCleanUp;}taskEXIT_CRITICAL();prvDeleteTCB( pxTCB );}}}#endif /* INCLUDE_vTaskDelete */
}

10. 任务设计要点

10.1 中断服务函数

① 中断服务函数运行在非任务的上下文环境中,因此不能使用挂起当前任务的操作,不允许调用任何会阻塞运行的API接口

否则会将pxCurrentTCB指向的当前任务错误地挂起

② 中断服务函数必须精简短小,快进快出。一般在中断服务函数中只标记事件的发生,然后通知任务,让对应任务去执行相关处理

③ 在设计时必须考虑中断的频率、中断的处理时间等重要因素

10.2 普通任务

① 在一个优先级明确的实时操作系统中,如果一个任务不放弃CPU,那么比这个任务优先级低的任务都将无法运行

因此在设计任务时,应该确保任务在不活跃时可以进入阻塞态,以交出CPU使用权

② 一般来说,处理时间更短的任务,优先级应该设置得更高一些

10.3 空闲任务

空闲任务是唯一不允许出现阻塞的任务,对于空闲任务hook函数,需要满足如下2个条件,

① 永远不会挂起任务

② 不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收

FreeRTOS源码分析与应用开发02:任务管理相关推荐

  1. FreeRTOS源码分析与应用开发01:中断配置与临界段

    目录 1. 异常与中断的基本概念 1.1 异常分类 1.2 中断概述 1.2.1 中断处理宜短暂 1.2.2 临界段影响中断实时性 1.3 中断硬件基础 1.3.1 外设 1.3.2 中断控制器 1. ...

  2. FreeRTOS源码分析与应用开发07:事件标志组

    目录 1. 概述 2. 事件标志组类型 3. 创建事件标志组 4. 删除事件标志组 5. 设置事件标志位 5.1 任务级设置 5.2 中断级设置 6. 清除事件标志位 6.1 任务级清除 6.2 中断 ...

  3. FreeRTOS源码分析与应用开发04:消息队列

    目录 1. 队列结构 2. 创建队列 2.1 动态创建队列 2.1.1 xQueueCreate函数 2.1.2 xQueueGenericCreate函数 2.1.3 xQueueGenericRe ...

  4. FreeRTOS源码分析与应用开发09:低功耗Tickless模式

    目录 1. STM32F4低功耗模式简介 2. Tickless模式详解 2.1 如何降低功耗 2.2 关闭SysTick的问题与解决方案 2.2.1 关闭SysTick导致系统节拍计数器停止 2.2 ...

  5. FreeRTOS源码分析与应用开发11(完):编译、链接与部署

    目录 1. 存储设备布局 2. 链接器脚本 2.1 链接器脚本生成 2.2 链接器脚本分析 2.2.1 分散加载文件 2.2.2 加载区 & 运行区 2.2.3 ER_IROM1运行区分析 2 ...

  6. FreeRTOS源码分析与应用开发10:内存管理

    目录 1. 概述 1.1 RTOS中内存分配特点 1.2 内存堆(heap space)来源 1.2.1 ucHeap数组 1.2.2 链接器设置的堆 1.2.3 多个非连续内存堆 1.3 关于字节对 ...

  7. FreeRTOS源码分析与应用开发08:任务通知

    目录 1. 概述 1.1 任务通知概念 1.2 任务通知控制结构 2. 发送任务通知 2.1 任务级发送 2.2 中断级发送 2.2.1 xTaskNotifyFromISR函数 2.2.2 vTas ...

  8. FreeRTOS源码分析与应用开发06:软件定时器

    目录 1. 概述 1.1 软件定时器 & 硬件定时器 1.2 软件定时器精度 1.3 单次模式 & 周期模式 2. 软件定时器组件 2.1 定时器任务 2.2 定时器列表 2.3 定时 ...

  9. FreeRTOS源码分析与应用开发05:信号量

    目录 1. 信号量概述 1.1 信号量概念 1.2 4种信号量 1.2.1 二值信号量 1.2.2 计数信号量 1.2.3 互斥信号量 1.2.4 递归互斥信号量 1.3 信号量相关控制结构 1.3. ...

最新文章

  1. 第十、十一周项目二-存储班长信息的学生类
  2. 一个栗子上手CSS3动画
  3. 通过JS如何获取IP地址
  4. 美赛开赛在即,你准备好了吗?
  5. laravel大型项目系列教程(六)之优化、单元测试以及部署
  6. 再见,2014;你好2015
  7. [Leetcode][第79题][JAVA][单词搜索][DFS][回溯]
  8. Java-static关键字
  9. 每天CookBook之Python-062
  10. apache环境下web站点禁止用服务器ip访问
  11. MSSQL表别名使用注意事项
  12. 《动手学深度学习》Mxnet环境搭建
  13. 【历史上的今天】11 月 22 日:PHP 创始人诞生;2020 年图灵奖得主出生;IE 2.0 发布
  14. PS各个工具的字母快捷键和英文全名
  15. 剑指Offer——迅雷笔试题+知识点总结
  16. 中大计算机保研复试,过来人分享:平凡的我如何成功保研中山大学?
  17. mysql backtrace_是什么导致Linux 64位上的backtrace()崩溃(SIGSEGV)
  18. DC基础学习(四)综合优化的三个阶段
  19. Scala中List的步长by
  20. 熔断器熔断时间标准_正确认识熔断器的熔断时间

热门文章

  1. android view设置按钮颜色_Android 酷炫自定义 View:高仿 QQ 窗帘菜单
  2. html演示 用鼠标画记号,html怎么用鼠标画出一条直线,鼠标移动时候要能看到线条...
  3. python技术简介_Python多线程技术简介,简单,阐述,python
  4. python3连接mysql获取ansible动态inventory
  5. 第四章 ---- 面向对象(一)
  6. IDEA----将本地svn项目导入idea后没有拉取提交按钮
  7. 动力环境监控系统论文_浅析建设智能化动力环境监控系统维护水平论文
  8. apriori算法python_清华学霸亲测有效,每日自学两小时Python,学完就能做项目
  9. 一支python教学_第一只python爬虫
  10. JVM设置最大最小内存参数