文章目录
- 08. FreeRTOS任务调度与任务切换
- 1. FreeRTOS任务调度
- 2. SVC中断服务函数源码调试结果分析
- 3. FreeRTOS任务切换
- 3.1 PendSV异常
- 3.2 PendSV中断服务函数
- 3.3 PendSV中断服务函数源码调试分析
- 3.4 确定下一个要执行的任务
08. FreeRTOS任务调度与任务切换
1. FreeRTOS任务调度
任务调度的流程:
- 调用
vTaskStartScheduler()
函数,开启任务调度; - 在
vTaskStartScheduler()
函数中调用xPortStartScheduler()
函数,此函数主要用于启动任务调度器中与硬件架构相关的配置部分,以及开启第一个任务。配置PendSV和SysTick的中断优先级为最低优先级,配置SysTick中断并初始化临界区嵌套计数器; - 在
xPortStartScheduler()
函数调用prvStartFirstTask()
函数,从向量表中获取栈顶指针、配置主堆栈MSP、启动中断、触发SVC中断服务函数来调用操作系统服务; - 在
prvStartFirstTask()
函数中触发SVC中断服务函数vPortSVCHandler()
,用来读取当前任务控制块的栈顶指针,恢复保存的寄存器值,设置任务的栈指针为PSP,清除中断优先级掩码。
vTaskStartScheduler()
函数具体实现:
void vTaskStartScheduler( void )
{BaseType_t xReturn;/* Add the idle task at the lowest priority. *///1、创建空闲任务#if ( configSUPPORT_STATIC_ALLOCATION == 1 ){StaticTask_t * pxIdleTaskTCBBuffer = NULL;StackType_t * pxIdleTaskStackBuffer = NULL;uint32_t ulIdleTaskStackSize;/* The Idle task is created using user provided RAM - obtain the* address of the RAM then create the idle task. */vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,configIDLE_TASK_NAME,ulIdleTaskStackSize,( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */pxIdleTaskStackBuffer,pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */if( xIdleTaskHandle != NULL ){xReturn = pdPASS;}else{xReturn = pdFAIL;}}#else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */{/* The Idle task is being created using dynamically allocated RAM. */xReturn = xTaskCreate( prvIdleTask,configIDLE_TASK_NAME,configMINIMAL_STACK_SIZE,( void * ) NULL,portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */}#endif /* configSUPPORT_STATIC_ALLOCATION *///2.如果使能软件定时器,则创建定时器任务#if ( configUSE_TIMERS == 1 ){if( xReturn == pdPASS ){xReturn = xTimerCreateTimerTask();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_TIMERS */if( xReturn == pdPASS ){/* freertos_tasks_c_additions_init() should only be called if the user* definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is* the only macro called by the function. */#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT{freertos_tasks_c_additions_init();}#endif/* Interrupts are turned off here, to ensure a tick does not occur* before or during the call to xPortStartScheduler(). The stacks of* the created tasks contain a status word with interrupts switched on* so interrupts will automatically get re-enabled when the first task* starts to run. *///关中断portDISABLE_INTERRUPTS();#if ( configUSE_NEWLIB_REENTRANT == 1 ){/* Switch Newlib's _impure_ptr variable to point to the _reent* structure specific to the task that will run first.* See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html* for additional information. */_impure_ptr = &( pxCurrentTCB->xNewLib_reent );}#endif /* configUSE_NEWLIB_REENTRANT *///下一个距离取消任务阻塞的时间,初始化为最大值xNextTaskUnblockTime = portMAX_DELAY;//任务调度器运行标志,设为已运行xSchedulerRunning = pdTRUE;//系统使用节拍计数器,宏 configINITIAL_TICK_COUNT 默认为 0xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;/* If configGENERATE_RUN_TIME_STATS is defined then the following* macro must be defined to configure the timer/counter used to generate* the run time counter time base. NOTE: If configGENERATE_RUN_TIME_STATS* is set to 0 and the following line fails to build then ensure you do not* have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your* FreeRTOSConfig.h file. */portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();traceTASK_SWITCHED_IN();/* Setting up the timer tick is hardware specific and thus in the* portable interface. *///该函数用于完成启动任务调度器中与硬件架构相关的配置部分,//以及启动第一个任务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{/* This line will only be reached if the kernel could not be started,* because there was not enough FreeRTOS heap to create the idle task* or the timer task. */configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );}/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,* meaning xIdleTaskHandle is not used anywhere else. */( void ) xIdleTaskHandle;/* OpenOCD makes use of uxTopUsedPriority for thread debugging. Prevent uxTopUsedPriority* from getting optimized out as it is no longer used by the kernel. */( void ) uxTopUsedPriority;
}
xPortStartScheduler()
函数具体实现:
BaseType_t xPortStartScheduler( void )
{//1.检测用户在FreeRTOSConfig.h文件中对中断的相关配置是否有误#if ( configASSERT_DEFINED == 1 ){volatile uint32_t ulOriginalPriority;volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );volatile uint8_t ucMaxPriorityValue;/* Determine the maximum priority from which ISR safe FreeRTOS API* functions can be called. ISR safe functions are those that end in* "FromISR". FreeRTOS maintains separate thread and ISR API functions to* ensure interrupt entry is as fast and simple as possible.** Save the interrupt priority value that is about to be clobbered. */ulOriginalPriority = *pucFirstUserPriorityRegister;/* Determine the number of priority bits available. First write to all* possible bits. */*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;/* Read the value back to see how many bits stuck. */ucMaxPriorityValue = *pucFirstUserPriorityRegister;/* The kernel interrupt priority should be set to the lowest* priority. */configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );/* Use the same mask on the maximum system call priority. */ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;/* Calculate the maximum acceptable priority group value for the number* of bits read back. */ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE ){ulMaxPRIGROUPValue--;ucMaxPriorityValue <<= ( uint8_t ) 0x01;}#ifdef __NVIC_PRIO_BITS{/* Check the CMSIS configuration that defines the number of* priority bits matches the number of priority bits actually queried* from the hardware. */configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );}#endif#ifdef configPRIO_BITS{/* Check the FreeRTOS configuration that defines the number of* priority bits matches the number of priority bits actually queried* from the hardware. */configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );}#endif/* Shift the priority group value back to its position within the AIRCR* register. */ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;/* Restore the clobbered interrupt priority register to its original* value. */*pucFirstUserPriorityRegister = ulOriginalPriority;}#endif /* configASSERT_DEFINED *//* Make PendSV and SysTick the lowest priority interrupts. *///2.配置PendSV和SysTick的中断优先级为最低优先级portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;/* Start the timer that generates the tick ISR. Interrupts are disabled* here already. *///3.配置SysTickvPortSetupTimerInterrupt();/* Initialise the critical nesting count ready for the first task. *///4.初始化临界区嵌套计数器为0uxCriticalNesting = 0;/* Start the first task. *///5.启动第一个任务prvStartFirstTask();/* Should not get here! */return 0;
}
prvStartFirstTask()
函数具体实现:
__asm void prvStartFirstTask( void )
{//8字节对齐PRESERVE8//0xE000ED08 为 VTOR 地址ldr r0, =0xE000ED08//从 0xE000ED08 读取值到 r0。这将获取 Vector Table 的基地址ldr r0, [ r0 ]//再次从 Vector Table 的基地址中读取值,得到主栈指针(MSP)的初始值。这个值是任务栈的起始地址ldr r0, [ r0 ]//将 r0 中的值设置为主堆栈指针 (MSP)。这将栈指针重置到任务栈的起始位置。msr msp, r0//启用中断(控制寄存器 CPSR 中的 I 位),允许处理器响应 IRQ(中断请求)cpsie i//启用快速中断(控制寄存器 CPSR 中的 F 位),允许处理器响应 FIQ(快速中断请求)cpsie f//数据同步屏障,确保所有的数据访问操作完成dsb//指令同步屏障,确保所有的指令完成isb//执行一个 SVC (SuperVisor Call) 指令,这会触发一个 SVC 中断。这个中断通常用于调用操作系统的内核服务来启动第一个任务svc 0//空操作指令nopnop
}
vPortSVCHandler()
函数具体实现:
__asm void vPortSVCHandler( void )
{PRESERVE8//将 pxCurrentTCB(当前任务控制块的地址)加载到寄存器 r3。//pxCurrentTCB 是一个指向当前任务控制块的指针,该指针存储了任务的状态信息ldr r3, = pxCurrentTCB //从 pxCurrentTCB 地址中读取任务控制块的实际地址,将其加载到寄存器 r1。这实际上是获取当前任务控制块的地址ldr r1, [ r3 ] //从任务控制块中读取任务栈顶指针(栈指针),将其加载到寄存器 r0。任务控制块的第一个字段通常是任务栈的起始地址 ldr r0, [ r1 ] //从栈中弹出寄存器 r4 到 r11 的值。这些寄存器是在任务上下文中保存的寄存器,//并且在异常入口时不会自动保存,因此需要在恢复任务上下文时手动恢复ldmia r0 !, { r4 - r11 } //将任务的栈指针(从 r0 中获取的值)设置为进程栈指针(PSP)。这恢复了当前任务的栈指针msr psp, r0 isb//将 0 值加载到寄存器 r0mov r0, # 0//将 r0 的值(0)写入 BASEPRI 寄存器。BASEPRI 寄存器用于设置中断优先级掩码,0 表示不屏蔽任何中断msr basepri, r0//将 0xd(即条件码 0xd,表示 BX 指令的目标地址具有链接状态)或到寄存器 r14(也称为 lr,链接寄存器)中orr r14, # 0xd//使用 PSP 指针,并跳转到任务函数bx r14
}
2. SVC中断服务函数源码调试结果分析
- 软件定时器的任务创建
- 软件定时器任务创建完成,任务优先级为31,当前任务控制块的地址指向软件定时器任务的地址
r3
指向优先级最高的就绪态任务的任务控制块,r1
为任务控制块地址
r0
为任务控制块的第一个元素(栈顶)
- 栈顶元素出栈,栈顶指针变动
- 把任务的栈指针赋值给进程堆栈指针
PSP
,使能所有中断,使用PSP
指针,并跳转到任务函数
3. FreeRTOS任务切换
3.1 PendSV异常
任务切换的过程在PendSV中断服务函数里边完成 ,PendSV 通过延迟执行任务切换,必须将 PendSV 的中断优先级设置为最低的中断优先等级。如果操作系统决定切换任务,那么就将 PendSV 设置为挂起状态,并在 PendSV 的中断服务函数中执行任务切换。切换示意图:
- 任务一触发 SVC 中断以进行任务切换(例如,任务一正等待某个事件发生)。
- 系统内核接收到任务切换请求,开始准备任务切换,并挂起 PendSV 异常。
- 当退出 SVC 中断的时候,立刻进入 PendSV 异常处理,完成任务切换。
- 当 PendSV 异常处理完成,返回线程模式,开始执行任务二。
- 中断产生,并进入中断处理函数。
- 当运行中断处理函数的时候,SysTick 异常(用于内核时钟节拍)产生。
- 操作系统执行必要的操作,然后挂起 PendSV 异常,准备进行任务切换。
- 当 SysTick 中断处理完成,返回继续处理中断。
- 当中断处理完成,立马进入 PendSV 异常处理,完成任务切换。
- 当 PendSV 异常处理完成,返回线程模式,继续执行任务一。
PendSV在RTOS的任务切换中,起着至关重要的作用,FreeRTOS的任务切换就是在PendSV中完成的。
3.2 PendSV中断服务函数
__asm void xPortPendSVHandler( void )
{//导入全局变量及定义extern uxCriticalNesting;extern pxCurrentTCB;extern vTaskSwitchContext;//8字节对齐PRESERVE8//R0 为 PSP,即当前运行任务的任务栈指针mrs r0, pspisb//R3为 pxCurrentTCB 的地址值,即指向当前运行任务控制块的指针//R2为 pxCurrentTCB 的值,即当前运行任务控制块的首地址ldr r3, =pxCurrentTCB ldr r2, [ r3 ]//将 R4~R11 入栈到当前运行任务的任务栈中stmdb r0 !, { r4 - r11 } //R2 指向的地址为此时的任务栈指针str r0, [ r2 ] //将 R3、R14 入栈到 MSP 指向的栈中stmdb sp !, { r3, r14 }//屏蔽受 FreeRTOS 管理的所有中断mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITYmsr basepri, r0dsbisb//跳转到函数 vTaskSeitchContext//主要用于更新 pxCurrentTCB,//使其指向最高优先级的就绪态任务bl vTaskSwitchContext//使能所有中断mov r0, #0msr basepri, r0//将 R3、R14 重新从 MSP 指向的栈中出栈ldmia sp !, { r3, r14 }//注意:R3 为 pxCurrentTCB 的地址值,//pxCurrentTCB 已经在函数 vTaskSwitchContext 中更新为最高优先级的就绪态任务//因此 R1 为 pxCurrentTCB 的值,即当前最高优先级就绪态任务控制块的首地址ldr r1, [ r3 ]//R0 为最高优先级就绪态任务的任务栈指针ldr r0, [ r1 ] //从最高优先级就绪态任务的任务栈中出栈 R4~R11ldmia r0 !, { r4 - r11 } //更新 PSP 为任务切换后的任务栈指针msr psp, r0isb//跳转到切换后的任务运行//执行此指令,CPU 会自动从 PSP 指向的任务栈中,//出栈 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,//接着 CPU 就跳转到 PC 指向的代码位置运行,//也就是任务上次切换时运行到的位置bx r14nop
}
从上面的代码可以看出,FreeRTOS在进行任务切换的时候,会将CPU的运行状态,在当前任务进行任务切换前,进行保存,保存到任务的任务栈中,然后从切换后运行任务的任务栈中恢复切换后运行任务在上一次被切换时保存的CPU信息。
但是从PendSV的中断回调函数代码中,只看到程序保存和恢复的CPU信息中的部分寄存器信息(R4寄存器~R11寄存器),这是因为硬件会自动出栈和入栈其他CPU寄存器的信息。
在任务运行的时候,CPU使用PSP作为栈空间使用,也就是使用运行任务的任务栈。当 SysTick中断(SysTick的中断服务函数会判断是否需要进行任务切换,相关内容在后续章节会进行讲解)发生时,在跳转到SysTick中断服务函数运行前,硬件会自动将除R4~R11寄存器的其他CPU寄存器入栈,因此就将任务切换前CPU的部分信息保存到对应任务的任务栈中。当退出PendSV时,会自动从栈空间中恢复这部分CPU信息,以共任务正常运行。
因此在PεndSV中断服务函数中,主要要做的事情就是,保存硬件不会自动入栈的CPU信息,已经确定写一个要运行的任务,并将pxCurrentTCB指向该任务的任务控制块,然后更新 PSP指针为该任务的任务堆栈指针。
3.3 PendSV中断服务函数源码调试分析
-
PSP指向当前任务的任务栈顶指针
-
r0指向任务A的栈顶指针
-
r2指向当前任务控制块
-
将r4~r11压栈,r0改变
-
把r0写入栈顶指针
-
将 R3、R14 入栈到 MSP 指向的栈中
-
关中断,然后更新当前任务控制块,使其指向最高优先级的就绪任务,然后开中断
任务控制块的改变过程:
-
r0指向任务B的栈顶指针
-
任务B的寄存器r4~r11出栈,加载到CPU的寄存器中,栈顶指针变化
-
CPU从psp指向出自动出栈
3.4 确定下一个要执行的任务
vTaskSwitchContext()
函数
void vTaskSwitchContext( void )
{/* 判断任务调度器是否运行 */if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){/* 此全局变量用于在系统运行的任意时刻标记需要进行任务切换* 会在 SysTick 的中断服务函数中统一处理* 任务任务调度器没有运行,不允许任务切换,* 因此将 xYieldPending 设置为 pdTRUE* 那么系统会在 SysTick 的中断服务函数中持续发起任务切换* 直到任务调度器运行*/xYieldPending = pdTRUE;} else{/* 可以执行任务切换,因此将 xYieldPending 设置为 pdFALSE */xYieldPending = pdFALSE;/* 用于调试,不用理会 */traceTASK_SWITCHED_OUT();/* 此宏用于使能任务运行时间统计功能,不用理会 */#if ( configGENERATE_RUN_TIME_STATS == 1 ){#ifdef portALT_GET_RUN_TIME_COUNTER_VALUEportALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );#elseulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();#endifif( ulTotalRunTime > ulTaskSwitchedInTime ){pxCurrentTCB->ulRunTimeCounter +=( ulTotalRunTime - ulTaskSwitchedInTime );}else{mtCOVERAGE_TEST_MARKER();}ulTaskSwitchedInTime = ulTotalRunTime;}#endif/* 检查任务栈是否溢出,* 未定义,不用理会*/taskCHECK_FOR_STACK_OVERFLOW();/* 此宏为 POSIX 相关配置,不用理会 */#if ( configUSE_POSIX_ERRNO == 1 ){pxCurrentTCB->iTaskErrno = FreeRTOS_errno;}#endif/* 将 pxCurrentTCB 指向优先级最高的就绪态任务* 有两种方法,由 FreeRTOSConfig.h 文件配置决定*/taskSELECT_HIGHEST_PRIORITY_TASK();/* 用于调试,不用理会 */traceTASK_SWITCHED_IN();/* 此宏为 POSIX 相关配置,不用理会 */#if ( configUSE_POSIX_ERRNO == 1 ){FreeRTOS_errno = pxCurrentTCB->iTaskErrno;}#endif/* 此宏为 Newlib 相关配置,不用理会 */#if ( configUSE_NEWLIB_REENTRANT == 1 ){_impure_ptr = &( pxCurrentTCB->xNewLib_reent );}#endif}
}
- 软件方式实现
taskSELECT_HIGHEST_PRIORITY_TASK()
函数
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{ /* 全局变量 uxTopReadyPriority 以位图方式记录了系统中存在任务的优先级 */ /* 将遍历的起始优先级设置为这个全局变量, *//* 而无需从系统支持优先级的最大值开始遍历, */ /* 可以节约一定的遍历时间 */ UBaseType_t uxTopPriority = uxTopReadyPriority; /* Find the highest priority queue that contains ready tasks. */ /* 按照优先级从高到低,判断对应的就绪态任务列表中是否由任务, */ /* 找到存在任务的最高优先级就绪态任务列表后,退出遍历 */ while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) { configASSERT( uxTopPriority ); --uxTopPriority;} /* 从找到了就绪态任务列表中取下一个任务, */ /* 让 pxCurrentTCB 指向这个任务的任务控制块 */ listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); /* 更新任务优先级记录 */ uxTopReadyPriority = uxTopPriority;
}
- 硬件方式实现
taskSELECT_HIGHEST_PRIORITY_TASK()
函数
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{ UBaseType_t uxTopPriority; /* 使用硬件方式从任务优先级记录中获取最高的任务优先等级 */ portGET_HIGHEST_PRIORITY(uxTopPriority,uxTopReadyPriority); configASSERT(listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[uxTopPriority])) > 0); /* 从获取的任务优先级对应的就绪态任务列表中取下一个任务 */ /* 让 pxCurrentTCB 指向这个任务的任务控制块 */ listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority]));
}