FreeRTOS任务切换学习
所谓任务切换,就是CPU寄存器的切换。假设当由任务A切换到任务B时,主要分为两步:
1:需暂停任务A的执行,并将此时任务A的寄存器保存到任务堆栈,这个过程叫做保存现场;
2:将任务B的各个寄存器值(被存于任务堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场;
对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换。下面要补充几个知识,以便更好理解任务切换。
PendSV异常
PendSV(可挂起的系统调用)异常对 OS 操作非常重要,其优先级可以通过编程设置。可以通过将中断控制和状态寄存器 ICSR 的 bit28,也就是 PendSV 的挂起位置 1 来触发 PendSV 中断。与 SVC 异常不同,它是不精确的,因此它的挂起状态可在更高优先级异常处理内设置,且
会在高优先级处理完成后执行。若将 PendSV 设置为最低的异常优先级,可以让 PendSV 异常处理在所有其他中断处理完成后执行,
下面我们直接来边解读程序边理解实现任务切换的过程:
__asm void xPortPendSVHandler( void )
{extern uxCriticalNesting;extern pxCurrentTCB;extern vTaskSwitchContext;/* *INDENT-OFF* */PRESERVE8mrs r0, psp//读取进程栈指针,保存在寄存器 R0 里面。isbldr r3, =pxCurrentTCB /* 得到正在运行指向任务控制块的指针的地址。*/ldr r2, [ r3 ]//得到任务控制块的地址stmdb r0 !, { r4 - r11 } /* 保存从R4到R11寄存器的值*/str r0, [ r2 ] /* 将此时的栈顶指针保存到任务控制块中的首个元素 */stmdb sp !, { r3, r14 }mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITYmsr basepri, r0dsbisbbl vTaskSwitchContextmov r0, #0msr basepri, r0ldmia sp !, { r3, r14 }ldr r1, [ r3 ]ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */msr psp, r0isbbx r14nop
/* *INDENT-ON* */
}
xPortPendSVHandler首先我们要明白这个函数PendSV中断。中断中使用的是MSP指针,中断外使用的是PSP指针。具体可以在手册Cortext-M3手册中找到:
所以自动压栈都是用的PSP指针。并且完成自动压栈后PSP指针指向的位置如下图所示:
mrs r0, psp
所以此时r0寄存器保存的此时指针的位置。
stmdb r0 !, { r4 - r11 }从r0指针指向的位置手动压栈将寄存器R4-R11寄存器的值保存起来。此时r0指针指向的地址如图中所示:
str r0, [ r2 ]
将此时的栈顶指针保存到任务控制块中的首个元素。以便后面从压栈后的最新指针出开始出栈。
stmdb sp !, { r3, r14 }
R14 是连接寄存器(LR)。在一个汇编程序中,你可以把它写作 both LR 和 R14。LR 用于在调用子程序时存储返回地址,R3为任务控制块的地址,为了防止 R3 和 R14 的值被改写,所以这里临时将 R3和 R14 的值先压栈。
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0
开启临界区,也就是关闭中断。
bl vTaskSwitchContext
调用这个函数得到下一个要运行的任务。下面具体来看一下这个函数是如何实现的:
void vTaskSwitchContext( void )
{if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){/* The scheduler is currently suspended - do not allow a context* switch. */xYieldPending = pdTRUE;}else{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 /* configGENERATE_RUN_TIME_STATS *//* Check for stack overflow, if configured. */taskCHECK_FOR_STACK_OVERFLOW();/* Before the currently running task is switched out, save its errno. */#if ( configUSE_POSIX_ERRNO == 1 ){pxCurrentTCB->iTaskErrno = FreeRTOS_errno;}#endif/* Select a new task to run using either the generic C or port* optimised asm code. */taskSELECT_HIGHEST_PRIORITY_TASK(); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */traceTASK_SWITCHED_IN();/* After the new task is switched in, update the global errno. */#if ( configUSE_POSIX_ERRNO == 1 ){FreeRTOS_errno = pxCurrentTCB->iTaskErrno;}#endif#if ( ( configUSE_NEWLIB_REENTRANT == 1 ) || ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 ) ){/* Switch C-Runtime's TLS Block to point to the TLS* Block specific to this task. */configSET_TLS_BLOCK( pxCurrentTCB->xTLSBlock );}#endif}
}
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() */
里面的实现主要又有2个函数,分别是portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority )
和listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) )
:
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
这个函数获取最高优先级是采用硬件的方法。也就是前导置零指令,这里也需要前面的一个知识点。
就绪表分为多个优先级,就绪表的每个优先级可以容纳多个任务。每个就绪列表都是一个结构体。想要了解这部分可以看之前写的列表和列表项的知识:列表和列表项的知识回顾
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) )
这个函数实现如下所示:
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \{ \List_t * const pxConstList = ( pxList ); \/* 指向List_t类型的常量指针pxConstList,并将其初始化为pxList的值。 */ \/* we don't return the marker used at the end of the list. */ \( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \{ \( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \} \( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \}
当列表中仅有一个任务时候,过程如下图所示:刚开始pxindex指向的是末尾列表项。( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
这句代码将指针指向列表项1。
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \{ \( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \}
if判断作用是用来略过末尾列表项的作用。( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
这句代码作用指向包含此列表项的对象的指针。通常,这个指针指向一个任务控制块(TCB),但也可以指向其他使用列表项的数据结构。这实现了对象和其所属列表项之间的双向链接。
所以此时就得到该任务的任务控制块的地址。
当该就绪列表有多个任务时候,就要时间片流转了。这部分知识等到学到的时候继续补充。
mov r0, #0 msr basepri, r0
接下来分析继续执行的汇编代码。这2句汇编代码打开中断。退出临界区。
ldmia sp !, { r3, r14 }
恢复寄存器 R3 和 R14 的值。注意,此时 pxCurrentTCB
的值已经改变了,所以读取 R3 所保存的地址处的数据就会发现其值改变了,成为了下一个要运行的任务的任务控制块的地址。
ldr r1, [ r3 ] ldr r0, [ r1 ] 因为R3所保存的是将要运行任务的任务控制块地址。所以r1中得到这个任务控制块,r0在得到栈顶指针。此时:
栈顶指针指向的位置如上图红色箭头所示:
ldmia r0 !, { r4 - r11 }
将栈保存的值加载R4-R11寄存器中。也就是即将运行的任务的现场。
msr psp, r0
更新进程栈指针 PSP 的值。此时R0指向的值为:
然后之后bx r14
跳转到要执行的函数。因为R14保存函数返回的地址。执行此行代码以后硬件自动恢复寄存器 R0~R3、R12、LR、PC 和 xPSR 的值,确定异常返回以后应该进入处理器模式还是进程模式,使用主栈指针(MSP)还是进程栈指针(PSP)。很明显这里会进入进程模式,并且使用进程栈指针(PSP),寄存器 PC 值会被恢复为即将运行的任务的任务函数,新的任务开始运行!至此,任务切换成功。