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 值会被恢复为即将运行的任务的任务函数,新的任务开始运行!至此,任务切换成功。