STM32F1+HAL库+FreeTOTS学习9——任务切换
上期我们学习了FreeRTOS的第一个任务的启动流程。本期我们来学习以下任务是如何切换的,期间发生了什么
1. 任务切换的本质
首先我们需要先达成一个共识:任务切换的本质就是CPU寄存器的切换。
在前的启动第一个个任务的学习工程,我们说到:Cortex‐M3 处理器拥有 R0‐R15 的寄存器组。这些寄存器里面存放的就是当前CPU正在执行任务相关的一些信息。且每一个任务,都会有一个对应的堆栈空间,存放任务相关的信息。
当我们需要进行任务切换时候,底层的操作就是,CPU将当前寄存器的信息,保存起来(这个过程叫压栈,有一部分寄存器需要手动压栈,另一部分会自行压栈),然后找到下一个需要运行的任务,将对应的任务栈读取到CPU寄存器(这个过程叫出栈,一部分寄存器需要手动出栈,另一部分会自行出栈)。这样便完成了任务的切换,这个过程也叫做 “ 上下文切换 ” 。
上下文切换的过程由PendSV中断服务函数完成,当需要进行上下文切换时,则会触发PendSV中断。
2. PendSV中断
PendSV(Pended Service Call,可挂起服务调用)是一个对 RTOS 非常重要的异常。PendSV 的中断优先级是可以编程的,在FreeRTOS里面被配置为最低优先级,同时是一个非实时的中断,运行在更高优先级的中断完成后再运行PendSV中断,可以通过向中断控制和状态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。
在FreeRTOS中断,PendSV的触发方式有如下:
- 滴答定时器中断调用
- 执行FreeRTOS提供的相关API函数:portYIELD() (函数名称可能不是这个,但内部都会调用这个函数)
这里我们需要搞明白这个两个问题?
- 前面启动第一个任务,我们是通过SVC中断来触发的,这里为什么不可以用SVC中断而要使用PendSV呢?
如果一个中断请求(IRQ)在 SysTick 中断产生之前产生,那么 SysTick 就可能抢占该中断请求,这就会导致该中断请求被延迟处理,这在实时操作系统中是不允许的,因为这将会影响到实时操作系统的实时性,如下图所示:
另一方面,SysTick完成上下文切换后需要返回另一个任务,这个时候应该是处于线程模式(使用PSP指针),但由于先前的IRQ未被处理,导致无法进入线程模式,将产生用法错误异常(Usage Fault)。
- 为什么需要在SysTick里面触发PendSV中断来进行上下文切换,直接在SysTick里面进行不好吗?
在FreeRTOS中我们会使用到时间片调度,时间片的长度是用户自定义的,那么就可能会有这样的情况。上一个SysTick的中断还未完成,下一个SysTick中断就来了,导致无法进行上下文切换。
- 为什么PendSV中断优先级最低,这样做有什么好处?
为的就是避免第一个问题中的情况:一个中断请求(IRQ)在 SysTick 中断产生之前产生,那么 SysTick 就可能抢占该中断请求,但由于SytTick里面只是触发PendSV中断,那么就可以在SysTick中断完后,先处理先前的中断请求(IRQ),最后处理完PendSV(完成上下文切换)后回到线程模式,避免产生产生用法错误异常(Usage Fault)。
看到这里,相信你对PendSV已经有了一定的了解,那么我们接下来看一下PendSV中断服务函数的源码:
3. PendSV 中断服务函数
__asm void xPortPendSVHandler( void )
{
/* 导入全局变量及函数 */
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
/* 八字节对齐 */
PRESERVE8
/* 让r0 = psp 指针,即当前任务的任务栈指针 */
/* 因为进入中断后使用的是MSP指针,psp指针不使用 */
mrs r0, psp
isb
/* r3 = pxCurrentTCB 的地址值,指向当前任务控制块的指针 */
/* r2 = pxCurrentTCB 的值,当前任务控制块的首地址(也是栈顶指针的地址) */
ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */
ldr r2, [ r3 ]
/* 将 R4~R11 压栈到当前运行任务的任务栈中 */
stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */
/* 压栈的过程中,r0会发生改变(栈顶发生变化),这里是更新栈顶指针的值 , R2 指向的地址为此时的任务栈指针*/
str r0, [ r2 ] /* Save the new top of stack into the first member of the TCB. */
/* 将 R3、R14 入栈到 MSP 指向的栈中,保存r3和r14 */
stmdb sp !, { r3, r14 }
/* 屏蔽受 FreeRTOS 管理的所有中断 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/* 跳转到函数 vTaskSeitchContext,更新 pxCurrentTCB,使其指向最高优先级的就绪态任务 */
bl vTaskSwitchContext
/* 使能所有中断 */
mov r0, #0
msr basepri, r0
/* 将 R3、R14 重新从 MSP 指向的栈中出栈 */
ldmia sp !, { r3, r14 }
/* 注意:R3 为 pxCurrentTCB 的地址值, pxCurrentTCB 已经在函数 vTaskSwitchContext 中更新为最高优先级的就绪态任务
因此 R1 为 pxCurrentTCB 的值,即当前最高优先级就绪态任务控制块的首地址 */
ldr r1, [ r3 ]
/* R0 为最高优先级就绪态任务的任务栈指针 */
ldr r0, [ r1 ] /* The first item in pxCurrentTCB is the task top of stack. */
/* 从最高优先级就绪态任务的任务栈中出栈 R4~R11 */
ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */
/* 更新 PSP 为任务切换后的任务栈指针 */
msr psp, r0
isb
/* 跳转到切换后的任务运行
* 执行此指令,CPU 会自动从 PSP 指向的任务栈中,
* 出栈 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,
* 接着 CPU 就跳转到 PC 指向的代码位置运行,
* 也就是任务上次切换时运行到的位置
*/
bx r14
nop
/* *INDENT-ON* */
}
以上就是PendSV中断的全部内容,完成的就是:保存现场、确定下一个需要运行的任务,恢复现场这三部曲。
但是确定下一个需要运行的任务,我们只是提到通过 vTaskSwitchContext函数完成,具体如何完成的呢?我们接着来看:
4. 确定下一个需要运行的任务
4.1 函数 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_VALUE
portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
#else
ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
#endif
if( 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
}
}
函数 vTaskSwitchContext()调用了函数 taskSELECT_HIGHEST_PRIORITY_TASK(),来将pxCurrentTCB 设置为指向优先级最高的就绪态任务。
4.2 函数 taskSELECT_HIGHEST_PRIORITY_TASK()
函数 taskSELECT_HIGHEST_PRIORITY_TASK()用于将 pcCurrentTCB 设置为优先级最高的就绪态任务,因此该函数会使用位图的方式在任务优先级记录中查找优先级最高任务优先等级,然后根据这个优先等级,到对应的就绪态任务列表在中取任务。
FreeRTOS 提供了两种从任务优先级记录中查找优先级最高任务优先等级的方式,一种是由纯 C 代码实现的,这种方式适用于所有运行 FreeRTOS 的 MCU;另外一种方式则是使用了硬件计算前导零的指令,因此这种方式并不适用于所有运行 FreeRTOS 的 MCU,而仅适用于具有有相应硬件指令的 MCU。
正点原子所有板卡所使用的 STM32 MCU 都支持以上两种方式,可以在FreeRTOSConfig.h里面配置宏:configUSE_PORT_OPTIMISED_TASK_SELECTION
下面我们来看一下两种实现方式:
- 软件方式
#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;
}
- 硬件方式
#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 ] ) );
}
注意:portGET_HIGHEST_PRIORITY()实际上是一个宏定义,在 portmacro.h 文件中有定义,具体的代码如下所示:
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )
uxTopPriority =
( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
__clz 是一个硬件指令:前导置零指令
具体原理如下:前面我们在介绍就绪列表的时候,FreeRTOS里面有32个就绪列表,分别对应32个优先级,当某个优先级的就绪列表中存在列表项时,uxReadyPriorities 的对应bit就会置1,通过前导零指令就可以快速的找到头部0的个数,进而得到最高优先级,快速获取最高优先级就绪任务的任务控制块