任务切换一共两种方式:系统节拍器中断、调用portYIELD。但是,最终都是产生PendSV中断。
/* SysTick异常处理入口 */
void xPortSysTickHandler(void)
{
/* 禁止不高于系统调用优先级的中断 */
vPortRaiseBASEPRI();
{
/* 提供系统节拍,并判断是否需要切换任务 */
if(xTaskIncrementTick() != pdFALSE)
{
/* 悬起PendSV */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
/* 打开中断 */
vPortClearBASEPRIFromISR();
}
/* 请求调度 */
#define portYIELD() \
{ \
/* 请求可悬起异常 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
__dsb(portSY_FULL_READ_WRITE); \
__isb(portSY_FULL_READ_WRITE); \
}
系统节拍器产生中断之后,调用xTaskIncrementTick函数检查下一个节拍是否有任务需要切换
xTaskIncrementTick函数主要功能:
在调度器没有被挂起的情况下
系统节拍加一
系统节拍溢出时将溢出延时列表切换成延时列表
将延时列表中的超时任务清除,挂接到就绪列表,如果优先级大于当前任务,请求切换
当前任务优先级就绪列表中任务数大于1个,请求切换下一个任务
在调度器被挂起的情况下
调度器挂起时间加一
如果前面有程序因为各种原因(如带中断保护的API解除某任务阻塞)要求延迟到现在切换,请求切换
/* 系统节拍加一 */
BaseType_t xTaskIncrementTick(void)
{
TCB_t *pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
traceTASK_INCREMENT_TICK(xTickCount);
/* 调度器没有被挂起 */
if(uxSchedulerSuspended == (UBaseType_t)pdFALSE)
{
/* 系统节拍计数器加一 */
const TickType_t xConstTickCount = xTickCount + (TickType_t)1;
xTickCount = xConstTickCount;
/* 系统节拍溢出 */
if(xConstTickCount == (TickType_t)0U)
{
/* 切换延时列表,更新下一次解除阻塞的时间 */
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 处理延时列表中超时的任务 */
if(xConstTickCount >= xNextTaskUnblockTime)
{
for(;;)
{
/* 如果延时列表已经为空,则将下一次解除时间设为最大 */
if(listLIST_IS_EMPTY(pxDelayedTaskList) != pdFALSE)
{
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
/* 延时列表不是空的 */
else
{
/* 获取延时列表中需要最先解除的任务 */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList);
xItemValue = listGET_LIST_ITEM_VALUE(&(pxTCB->xStateListItem));
/* 最先需要解除的任务都没有超时 */
if(xConstTickCount < xItemValue)
{
/* 更新下一次解除时间 */
xNextTaskUnblockTime = xItemValue;
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 将超时任务从延时列表中移除 */
(void)uxListRemove(&(pxTCB->xStateListItem));
/* 如果该任务被挂接到某个事件列表,还需要从事件列表中移除 */
if(listLIST_ITEM_CONTAINER(&(pxTCB->xEventListItem)) != NULL)
{
(void)uxListRemove(&(pxTCB->xEventListItem));
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 将任务加入就绪列表中 */
prvAddTaskToReadyList(pxTCB);
#if (configUSE_PREEMPTION == 1)
{
/* 任务优先级大于当前任务优先级 */
if(pxTCB->uxPriority >= pxCurrentTCB->uxPriority)
{
/* 请求切换任务 */
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
}
}
}
#if ((configUSE_PREEMPTION == 1) && (configUSE_TIME_SLICING == 1))
{
/* 当前任务优先级列表中如果不止一个任务 */
if(listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[pxCurrentTCB->uxPriority])) > (UBaseType_t)1)
{
/* 请求切换任务 */
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
#if (configUSE_TICK_HOOK == 1)
{
if(uxPendedTicks == (UBaseType_t)0U)
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
}
/* 调度器被挂起 */
else
{
/* 挂起时间加一 */
++uxPendedTicks;
#if (configUSE_TICK_HOOK == 1)
{
vApplicationTickHook();
}
#endif
}
#if (configUSE_PREEMPTION == 1)
{
/* 前面有程序因为各种原因,要求延迟到现在切换 */
if(xYieldPending != pdFALSE)
{
/* 请求切换任务 */
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
return xSwitchRequired;
}
在看PendSV异常处理源代码之前,先了解一下基础知识,参考《Cortex-M3 权威指南》的第九章(中断的具体行为)
发生异常之后会自动进行三个步骤:
1.入栈 2.取向量 3.选择堆栈指针MSP/PSP,更新堆栈指针SP,更新链接寄存器LR,更新程序计数器PC
异常返回之后会自动进行两个步骤:
1.出栈 2.更新NVIC寄存器
在进入异常服务程序后,LR的值被自动更新为特殊的EX_RETURN
需要注意的是,有部分寄存器是自动入栈和出栈的,但是这些寄存器并不包括r4-r11,因此需要手动入栈
PendSV的主要工作步骤:保护老任务现场,切换到新任务,恢复新任务现场,跳转到新任务
/* PendSV异常入口 */
__asm void xPortPendSVHandler(void)
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
/* r0=psp */
mrs r0, psp
isb
/* r3=&pxCurrentTCB */
ldr r3, =pxCurrentTCB
/* r2=pxCurrentTCB */
ldr r2, [r3]
/* 将r4-r11压入任务栈 */
stmdb r0!, {r4-r11}
/* 更新任务栈顶指针 */
str r0, [r2]
/* 将&pxCurrentTCB和LR压入MSP中 */
stmdb sp!, {r3, r14}
/* 禁止不高于系统调用优先级的中断 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/* 切换上下文 */
bl vTaskSwitchContext
/* 打开中断 */
mov r0, #0
msr basepri, r0
/* 将&pxCurrentTCB和LR从MSP中弹出 */
ldmia sp!, {r3, r14}
/* r1=pxCurrentTCB 注意:现在pxCurrentTCB已经变成新任务 */
ldr r1, [r3]
/* r0=pxTopOfStack */
ldr r0, [r1]
/* 将任务栈中的r4-r11弹出 */
ldmia r0!, {r4-r11}
/* 将任务栈设置为psp(进程堆栈) */
msr psp, r0
isb
/* 返回 */
bx r14
nop
}
切换上下文,其实就是从最高优先级的就绪任务列表中取出下一个需要执行的任务,并将该任务设置为当前任务
要注意:如果调度器被挂起了,暂时就不要切换上下文,通过xYieldPending将任务切换延迟到下一个节拍
/* 任务切换上下文 */
void vTaskSwitchContext(void)
{
/* 调度器被挂起 */
if(uxSchedulerSuspended != (UBaseType_t)pdFALSE)
{
/* 等到下一次节拍的时候再切换上下文 */
xYieldPending = pdTRUE;
}
/* 调度器没有挂起 */
else
{
/* 现在就切换上下文 */
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();
#if (configUSE_POSIX_ERRNO == 1)
{
pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
}
#endif
/* 从最高优先级就绪任务列表中获取下一个任务并设置为当前任务 */
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
#if (configUSE_POSIX_ERRNO == 1)
{
FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
}
#endif
#if (configUSE_NEWLIB_REENTRANT == 1)
{
_impure_ptr = &(pxCurrentTCB->xNewLib_reent);
}
#endif
}
}