vTaskSwitchContext
时间:2023-12-02 21:07:02
1. 任务切换相关API函数
函数 | 描述 |
---|---|
xPortPendSVHandler() | PendSV事实上,中断服务函数的函数原型是PendSV_Handler() |
vTaskSwitchContext() | 检查任务堆栈是否溢出,并找到下一个优先级高的任务。如果能够统计运行时间,则会计任务运行时间 |
2. 任务切换的基本知识
在FreeRTOS在任务管理中,最重要的目的是找到最高的线索优先级任务,然后执行任务切换,以保持最高的优先级任务被占用CPU资源。使用汇编代码编写任务切换部分程序,以达到最佳性能。
FreeRTOS触发任务切换的方法有两种:
- 中断系统节拍时钟(SysTick定时器)。切换过程参考。《FreeRTOS原理剖析:系统节拍时钟分析》
- 执行系统调用代码。使用普通任务taskYIELD()强制任务切换;中断服务程序使用;portYIELD_FROM_ISR()强制任务切换;也可以在应用程序中设置xYieldPending通知调度器切换任务。
其中:
// 对于普通任务 #define taskYIELD() portYIELD() #define portYIELD_WITHIN_API portYIELD
// 中断服务程序 #define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD() #define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
可此可见,最终执行的代码段为:
#define portYIELD() \ { \ /* \ * 通过向中断控制和状态寄存器ICSR第28位写入1,触发PendSV中断 \ * 地址为0xE000 ED04 \ */ \ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ \ /* dsb和isb 完成数据同步隔离和指令同步隔离 \ * 确保以前的存储器访问操作和指令完成 \ */ \ __dsb( portSY_FULL_READ_WRITE ); \ __isb( portSY_FULL_READ_WRITE ); \ }
通过向中断控制和状态寄存器ICSR(地址:0xE000 ED04)第28位写入1,触发PendSV中断执行任务切换。
3. 任务切换过程
3.1 PendSV中断服务函数分析
在FreeRTOS中,有:
#define xPortPendSVHandler PendSV_Handler
源代码如下:
__asm void xPortPendSVHandler( void ) { extern uxCriticalNesting; extern pxCurrentTCB; /* 它将永远指向当前激活的任务 */ extern vTaskSwitchContext;
PRESERVE8 mrs r0, psp /* 阅读过程栈指针PSP,保存在R0中,此时SP的值为MSP */ isb /* 指令同步隔离 */ /* 这两句使R2.保存当前激活的任务TCB首地址 */ ldr r3, =pxCurrentTCB /* 将pxCurrentTCB存储地址保存到R3,注意的是pxCurrentTCB存储地址固定不变,但方向可变 */ ldr r2, [r3] /* 将R3地址所在的数据保存R2,即TCB的首地址 */ /* * 判断前两句是否有效FPU,如果可以,手动操作s16~s31压入栈中 * 其中s0~s15和FPSCR自动完成硬件 */ tst r14, #0x10 it eq vstmdbeq r0!, {s16-s31} /* 将当前激活任务的寄存器值入堆栈,并更新R此外,硬件自动将xPSR、PC、LR、R12、R0~R3入栈 */ stmdb r0!, {r4-r11, r14} /* R0为PSP地址,R二是激活任务TCB地址,R0的值写入R2保存地址去,即TCB第一个成员指向线程堆栈指针,每次任务切换都会更新PSP */ str r0, [r2] stmdb sp!, {r3} /* 将R3临时压入堆栈,R3保存了pxCurrentTCB地址,函数调用后会使用,所以要进入栈保护 */ /* 中断优先级大于或等于configMAX_SYSCALL_INTERRUPT_PRIORITY中断将被屏蔽 */ mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 dsb /* 数据同步隔离 */ isb /* 指令同步隔离 */ bl vTaskSwitchContext /* 切换到vTaskSwitchContext,找下一个任务 */ /* 开中断 */ mov r0, #0 msr basepri, r0 ldmia sp!, {r3} /* 恢复R3,R3保存了pxCurrentTCB的地址,这里pxCurrentTCB地址是固定的,但改变了 */ /* 当前激活的TCB栈顶值存入R0 */ ldr r1, [r3] /* 将pxCurrentTCB指向地址赋值R1,即将TCB首地址赋值R1 */ ldr r0, [r1] /* 将R1地址所在的数据赋值R0,即当前激活任务TCB第一项的值赋予R0 */ ldmia r0!, {r4-r11, r14} /* 将寄存器R4~R11出栈,同时更新R0的值 */ /* * 判断前两句是否有效FPU,如果可以,手动恢复s16-s31浮点寄存器 * 其中s0~s15和FPSCR自动完成硬件 */ tst r14, #0x10 it eq vldmiaeq r0!, {s16-s31} msr psp, r0 /* 将最新任务堆栈的顶部赋值线程堆栈指针PSP */ isb /* 指令同步隔离,清流水线 */ #ifdef WORKAROUND_PMU_CM001 #if WORKAROUND_PMU_CM001 == 1 push { r14 } pop { pc } nop #endif #endif bx r14 /* 当调用 bx r14指令退出中断,堆栈指针PSP指向新任务堆栈的正确位置 */
}
说明:
- 在Cortex-M处理器中有两个栈指针,一个是主栈指针(Main Stack Pointer,即MSP),它可以用于线程模式,只能用于中断模式MSP;另一个是过程堆栈指针(Processor Stack Pointer,即PSP),PSP总是用于线程模式。它只能在任何时候使用。
- 复位后处于线程模式特权级,默认使用MSP。在FreeRTOS中,MSP用于OS内核和异常处理,PSP用于应用任务。
- 通过设置CONTROL寄存器的bit[1]选择使用哪个堆栈指针。CONTROL[1]=0选主堆栈指针;CONTROL[1]=1择过程堆栈指针。
3.2 函数vTaskSwitchContext()
该函数将更新当前任务运行时间,检查任务堆栈是否溢出,然后调用宏 taskSELECT_HIGHEST_PRIORITY_TASK()获得更高优先级的任务。
函数源代码如下:
void vTaskSwitchContext( void ) { /* 如果任务调度器已经挂起 */ if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) { xYieldPending = pdTRUE; /* 标记任务调度器不允许任务切换 */ } else { xYieldPending = pdFALSE; /* 任务调度器未挂起 */
traceTASK_SWITCHED_OUT(); /* * 设置运行时间统计功能configGENERATE_RUN_TIME_STATS为1 * 如果使用了该功能,以下两个宏: * portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() portGET_RUN_TIME_COUNTER_VALUE() */ #if ( configGENERATE_RUN_TIME_STATS == 1 ) { #ifdef portALT_GET_RUN_TIME_COUNTER_VALUE portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime ); #else ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE(); #endif /* * ulTotalRunTime记录系统的总运行时间,ulTaskSwitchedInTime记录任务切换的时间 * 如果系统节拍周期为1ms,则ulTotalRunTime要497天后才会溢出 * ulTotalRunTime < ulTaskSwitchedInTime表示可能溢出 */ if( ulTotalRunTime > ulTaskSwitchedInTime ) { /* 记录当前任务的运行时间 */ pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime ); } else { mtCOVERAGE_TEST_MARKER(); } /* 更新ulTaskSwitchedInTime,下个任务时间从这个值开始 */ ulTaskSwitchedInTime = ulTotalRunTime; } #endif /* configGENERATE_RUN_TIME_STATS */ /* 核查堆栈是否溢出 */ taskCHECK_FOR_STACK_OVERFLOW(); /* 寻找更高优先级的任务 */ taskSELECT_HIGHEST_PRIORITY_TASK(); traceTASK_SWITCHED_IN(); /* 如果使用Newlib运行库,你的操作系统资源不够,而不得不选择newlib,就必须打开该宏 */ #if ( configUSE_NEWLIB_REENTRANT == 1 ) { _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); } #endif /* configUSE_NEWLIB_REENTRANT */ }
}
3.2 寻找下一个任务的方式
PendSV中会调用vTaskSwitchContext(),最后调用函数taskSELECT_HIGHEST_PRIORITY_TASK()寻找优先级最高的任务。
对于FreeRTOS的调度器,它有两种方式寻找下一个最高优先级的任务,分别为特殊方式和常用方式,在FreeRTOSConfig.h中可通过宏定义设置,如下:
/* 0:使用常用方式来选择下一个要运行的任务;1:使用特殊方法来选择下一个要运行的任务 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
在FreeRTOS中,通用方法不依赖某些硬件等限制,适用于多种MCU中。特殊方式是使用了某些硬件的特性,只针对部分MCU而使用。
3.2.1 常用方法
uxTopReadyPriority 记录就绪态中最高优先级值,创建任务时会更新值,有任务添加到就绪表时也会更新值。这种方法对任务的数量无限制。
#define taskSELECT_HIGHEST_PRIORITY_TASK() \ { \ UBaseType_t uxTopPriority = uxTopReadyPriority; \ \ while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \ { \ configASSERT( uxTopPriority ); \ --uxTopPriority; \ } \ \ /* 获取优先级最高任务的任务控制块 */ \ listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
uxTopReadyPriority = uxTopPriority; \
}
3.2.2 特殊方式
使用此方法,uxTopReadyPriority 每个bit位表示一个优先级,bit0表示优先级0,bit31表示优先级31,使用此方式优先级最大只能是32个。
#define taskSELECT_HIGHEST_PRIORITY_TASK() \ { \ UBaseType_t uxTopPriority; \ \ /* 获取优先级最高的任务 */ \ portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \ configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
/* 获取优先级最高任务的任务控制块 */ listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
其中:
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
__clz( ( uxReadyPriorities ) 是计算uxReadyPriorities 的前导零个数,如:
二进制0001 1010 0101 1111的前导零个数为3,可以知道,最高优先级uxTopPriority 等于 31减去前导零个数。
知道最高优先级的优先级,则通过listGET_OWNER_OF_NEXT_ENTRY()对应最高优先级的列表项,将pxCurrentTCB指向对应的控制块。
参考资料:
【1】: 正点原子:《STM32F407 FreeRTOS开发手册V1.1》
【2】: 野火:《FreeRTOS 内核实现与应用开发实战指南》
【3】: 《Cortex M3权威指南(中文)》