无论是FreeRTOS还是ucos,其任务切换的实现都是用汇编来写的,在STM32上都是用的PendSV这个系统异常来进行任务切换的,参考权威手册121页有关SVC与PendSV的介绍。通过置位NVIC的ICSR中断控制及状态寄存器(131页)地址0xE000_ED04来悬起SVC及PendSV异常,在异常中进行任务切换工作。
CM3内核一共有R0-R15共16个寄存器加上四个特殊功能寄存器
其中R0-R12是用来存储程序运行时的临时存储值,R13是栈地址指针寄存器,R14是执行BL程序跳转时用来存储跳转处下一行代码地址用的寄存器,R15总是指向当前执行代码的下一行地址的寄存器。BASEPRI寄存器是用来按优先级来屏蔽中断的,向里面写入数值后会屏蔽大于该值的优先级的中断,FreeRTOS就是在这里来屏蔽中断进入临界区的,写0的话就不屏蔽。(ucos ii是用PRIMASK来屏蔽的,这个寄存器置1后会屏蔽所有可编程优先级的中断,只有几个负优先级的中断可以响应,这个屏蔽非常彻底。)
任务切换就是要先恢复任务最后运行时R0-R12寄存器的值,然后恢复栈指针的地址,xPSR状态寄存器的值,恢复LR寄存器的值,最后用BX LR来跳转到任务上次运行处的位置,此时上次运行后R0-R12的值都恢复原状,栈指针也被恢复,这样任务就会按原来的状态继续运行下去。这是任务切换的原理,无论是什么CPU(x86或者ARM),进行任务切换时,要做的事都大同小异。
CM3内核的PendSV异常是专为操作系统用来任务切换用的,无论是FreeRTOS还是ucos在CM3上进行任务切换的过程和代码都差不多的。
当CM3开始响应一个中断时,会在它看不见的体内奔涌起三股暗流:(见权威指南中文版135页)
- 入栈: 把8个寄存器的值压入栈
- 取向量:从向量表中找出对应的服务程序入口地址
- 选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC
FreeRTOS要进行任务切换时,是使用在portmacro.h里的portYIELD()宏来实现的
/* Scheduler utilities. */
#define portYIELD() \
{ \
/* Set a PendSV to request a context switch. */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
/* Barriers are normally not required but do ensure the code is completely \
within the specified behaviour for the architecture. */ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
其实所做的就是把NVIC的ICSR寄存器的PENDSET位置1来触发PendSV异常,由于FreeRTOS设置PendSV中断的优先级为最低的15级,如果此时没有响应其他异常,并且没有在BASEPRI寄存器里写入数值来屏蔽15级以下优先级的中断的话,系统就会响应PendSV异常,进入PendSV服务程序里,也就是跳转到port.c里的_asm void xPortPendSVHandler(void) 内联汇编程序块。
CM3内核在响应PendSV异常时,会暗地里进行上面所说的三个操作,首先是入栈
CM3内核是向下生长的,也就是入栈时栈指针会向低地址位移,响应中断时,会向当前栈指针地址入栈以上表格里的寄存器的值,一共8个字,新的SP地址变为SP-32。为啥袒护R0‐R3以及R12呢, R4‐R11就是下等公民?原来,在ARM上,有一套的C函数调用标准约定(《 C/C++ Procedure Call Standard for the ARM Architecture》,AAPCS, Ref5)。个中原因就在它上面:它使得中断服务例程能用C语言编写,编译器优先使用被入栈的寄存器来保存中间结果。当然,如果程序过大也可能要用到R4‐R11,此时编译器负责生成代码来push它们。
然后内核会取向量,也就是从中断向量表里找到PendSV服务代码的地址,之后会更新寄存器的值,具体内容参看权威指南(136页),不在这里贴出。在暗地里做完这些后,系统就会跳转到PendSV服务里执行。
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp /* 读取响应中断时的栈指针地址(实际要减去32, */
isb /* 因为响应中断时暗地里会入栈8个寄存器的值) */
ldr r3, =pxCurrentTCB /* 把当前任务控制块的指针的地址读取到r3寄存器 */
ldr r2, [r3] /* 从指针地址里读取到该指针所指向的TCB的地址 */
stmdb r0!, {r4-r11} /* 把其他未保存的寄存器的值也手动存入栈里 */
str r0, [r2] /* 把存入了各种寄存器的值后的新的栈的地址存入TCB的首个成员内存里 */
/* TCB是个结构体,其第一个成员就是该任务的栈顶地址 */
stmdb sp!, {r3, r14} /* 保存现场,把r3和r14寄存器的内容压入栈里,注意此时的栈是MSP而不是前面的PSP */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0 /* 向BASEPRI寄存器中写入0xbf,来屏蔽11级以下低优先级的中断 */
dsb
isb
bl vTaskSwitchContext /* 跳转到上下文切换函数 */
mov r0, #0
msr basepri, r0 /* 关闭中断屏蔽 */
ldmia sp!, {r3, r14} /* 恢复现场,读取出存入栈里的r3和r14的值 */
ldr r1, [r3] /* 再次去读当前任务块指针所指向的新的TCB地址 */
ldr r0, [r1] /* 从TCB地址读取首个成员值也就是上次任务被切换时保存的栈顶地址 */
ldmia r0!, {r4-r11} /* 从读取的栈地址里手动恢复r4-r11寄存器的值 */
msr psp, r0 /* 把刚刚恢复r4-r11值后的新的r0地址设为要切换的任务程序的栈地址PSP */
isb
bx r14 /* 中断返回 */
nop
}
在进入这个中断服务里之前,系统暗地里会完成8个寄存器的入栈,然后再在服务里手动把其他的寄存器也入栈,之后再把这个新的栈地址给保存在当前任务的任务块TCB用来保存栈顶地址的成员里(刚好也是TCB结构体的第一个成员,所以不需要加偏移量)。
vTaskSwitchContext函数是切换pxCurrentTCB作用的,右键查看其定义在task.c里,去除无效的宏定义后,有效代码只有:
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
/* The scheduler is currently suspended - do not allow a context
switch. */
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
/* Select a new task to run using either the generic C or port
optimised asm code. */
taskSELECT_HIGHEST_PRIORITY_TASK();
}
}
如果任务调度被挂起,即uxSchedulerSuspended>0的话,那么不会切换TCB,任务也不会被切换,PendSV执行后返回的还是原任务。当uxSchedulerSuspended=0的时候,会执行宏定义taskSELECT_HIGHEST_PRIORITY_TASK()
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{ \
UBaseType_t uxTopPriority; \
\
/* Find the highest priority list that contains ready tasks. */ \
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
这个宏的作用就是查询当前待命的任务中最高的优先级,然后从改优先级的就绪列表readylist中查出下一个要执行的任务的任务块TCB,将这个TCB的指针赋值给pxCurrentTCB。
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
这个宏里,__clz是使用CM3指令集的前导零计算指令CLZ,uxReadyPriorities是32位,每一位代表一个优先级,如果其值为0x100200c0,则代表优先级28,17,7,6四个优先级里有任务待命中,此时 __clz(uxReadyPriorities)得到的值为3,那个uxTopPriority = 28,可以得到此时已准备好的任务中最高优先级为28(FreeRTOS所定义的任务优先级是值越大优先级越高,并且最多32个优先级,从最低的0到最高的31)。
知道了待命任务的最高优先级后由listGET_OWNER_OF_NEXT_ENTRY这个宏来查找这个优先级的待命列表里的下一个任务的控制块并把其指针赋值给pxCurrentTCB,关于这个宏,后面分析list.c源代码时再说明。
在vTaskSwitchContext被执行后,pxCurrentTCB会指向新的任务控制块,这个控制块是优先级最高的待命任务的任务块,所以再次用ldr r1,[r3]来读取pxCurrentTCB所指向的TCB时会得到新的TCB,之后把新的TCB的首个成员读取出来(也就是栈顶地址),从这个地址里恢复上次任务切换时保存在栈里的r4-r11这个8个寄存器的值,然后把新的栈地址写入PSP中,再用BX r14来进行中断返回。参考136页页底的异常返回,在返回时,会暗地里从栈指针地址处开始出栈恢复在进入中断时入栈的8个寄存器的值,xPSR,LR,PC,R0-R3和R12这些寄存器。根据这个新的LR程序返回地址,中断返回后就会到指定的任务程序被调度出继续运行了。