1 PendSV中断
任务切换的本质:就是CPU寄存器的切换。
假设当由任务A切换到任务B时,主要分两步:
第一步:需暂停任务A的执行,并将此时任务A的寄存器(即CPU寄存器)保存到任务堆栈,这个过程叫做保护现场。
第二步:将任务B的各个寄存器值(被存于堆栈中)恢复到CPU寄存器中,这个过程叫做恢复现场;
对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换
注意:任务切换是在PendSV中断服务函数里边完成的
PendSV中断的触发主要有两个途径:
1)SysTick嘀嗒定时器中断调用
2)执行FreeRTOS提供的相关API函数,如portYIELED()
但PendSV中断的触发本质是通过向中断控制和状态寄存器ICSR的bit28写入1挂起PendSV来启动PendSV中断
2 滴答定时器中断调用
#define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
void SysTick_Handler(void)
{
HAL_IncTick();
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
/*只要任务调度器开启了,就会调用该函数*/
}
}
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI();
/*屏蔽所有受FreeRTOS管理的中断,因为SysTick的中断优先级设置为最低的中断优先等级
*因此需要屏蔽所有受FreeRTOS管理的中断*/
{
if( xTaskIncrementTick() != pdFALSE )
/*处理系统时钟节拍,并决定是否进行任务切换*/
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
/*将ICSR寄存器的28位置1,因此触发PendSV中断*/
}
}
vPortClearBASEPRIFromISR();
/*取消中断屏蔽*/
}
3 FreeRTOS相关API函数
#define portYIELD()
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
/* 设置中断控制状态寄存器,以触发 PendSV 中断 */
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
}
4 PendSV中断服务函数
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
/*导入全局变量及函数*/
PRESERVE8
/*8字节对齐*/
mrs r0, psp
isb
/*r0为psp,即当前运行任务的任务栈指针*/
ldr r3, =pxCurrentTCB
ldr r2, [ r3 ]
/*r3为pxCurrentTCB的地址值,即指向当前运行任务控制块的指针*/
/*r2为pxCurrentTCB的值,即当前运行任务控制块的首地址*/
tst r14, #0x10
/*判断R14的第四位是否为1,如果是1,则代表不使用浮点数,如果是0代表使用浮点数,
*并且如果使用这些浮点数,那么还需要压栈,保存这些寄存器{s16-s31},s0_s15是自动保存和恢复
*的,s16-s31需要手动保存和恢复*/
it eq
vstmdbeq r0!, {s16-s31}
stmdb r0!, {r4-r11, r14}
/*压栈指令,将r4~r11入栈到当前运行任务控制块中*/
str r0, [ r2 ]
/*此时r0位于最底下的栈底,然后将r0的地址写入到r2所指向的内存里面,此时r2是栈顶指针*/
stmdb sp!, {r0, r3}
/*将r0~r3入栈到MSP指向的栈中*/
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb
isb
/*屏蔽所有受FreeRTOS管理的所有中断(中断优先级为5~15),即关中断*/
bl vTaskSwitchContext
/*跳转到函数vTaskSwitchContext主要用于更新pxCurrentTCB,
*使其指向最高优先级的就绪态任务*/
mov r0, #0
msr basepri, r0
/*使能所有中断*/
ldmia sp!, {r0, r3}
/*将r0、r3重新从MSP指向的栈中出栈*/
ldr r1, [ r3 ]
/*注意:r3为pxCurrentTCB的地址值,pxCurrentTCB已经在函数vTaskSwitchContext中更新为最高优
*先级的就绪态任务,因此r1为pxCurrentTCB的值,即当前最高优先级就绪态任务控制块的首地址*/
ldr r0, [ r1 ]
/*r0为最高优先级就绪态任务的任务指针*/
ldmia r0!, {r4-r11, r14}
/*从最高优先级就绪态任务的任务栈中出栈r4~r14
*注意:这里出栈的r14为EXC_RETURN,其保存了任务是否使用了浮点单元的信息*/
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}
/*此时r14为EXC_RETURN,通过判断EXC_RETURN的bit4是否为0,
*判断任务是否使用的浮点单元,
*若使用了浮点单元,则需要从任务栈中恢复出浮点寄存器的值*/
msr psp, r0
isb
/*更新psp为任务切换后的任务栈指针*/
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif
/*用于修改XMC4000的BUG,不用理会*/
bx r14
/*跳转到切换后的任务运行,执行此指令,CPU会自动从PSP指向的任务栈中,
*出栈r0 、r1 、r2 、r3 、r12 、lr 、pc 、xpsr寄存器,接着CPU就跳转到PC指向的代码位置
*运行,也就是任务上次切换时运行到的位置*/
}
注意:保存和恢复用的都是PSP
/*查找最高优先级的就绪态任务*/
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
/*判断任务调度器是否运行*/
{
xYieldPending = pdTRUE;
/*此全局变量用于在系统运行的任意时刻标记需要进行任务切换,会在SysTick的中断服务函数中
*统一处理,任务调度器没有运行,不允许任务切换,因此将xYieldPending设置为pdTRUE,那么
*系统会在SysTick的中断服务函数中持续发起任务切换,直到任务调度器运行*/
}
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();
/*检查任务栈是否溢出,未定义,不用理会*/
#if ( configUSE_POSIX_ERRNO == 1 )
/*此宏为POXI相关配置,不用理会*/
{
pxCurrentTCB->iTaskErrno = FreeRTOS_errno;
}
#endif
taskSELECT_HIGHEST_PRIORITY_TASK();
/*将pxCurrentTCB指向最高优先级的就绪态任务,两种方法:
*1、滴答定时器中断调用
*2、执行FreeRTOS提供的相关API函数*/
traceTASK_SWITCHED_IN();
/*用于调试,不用理会*/
#if ( configUSE_POSIX_ERRNO == 1 )
/*此宏为POXI相关配置,不用理会*/
{
FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
}
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
/* 此宏为 Newlib 相关配置,不用理会 */
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
}
}
函数vTaskSwitchContext()调用了函数taskSELECT_PRIORITY_TASK(),来将pxCurrentTCB设置为指向优先级最高的就绪态任务。
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{
UBaseType_t uxTopPriority;
/*全局变量uxTopReadyPriority以位图方式记录了系统中存在任务的优先级
*将遍历的起始优先级设置为全局变量,而无需从系统支持优先级的最大值开始遍历,
*可以节约一定的遍历时间*/
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );
/*获取当前最高优先级的任务*/
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );
/*根据当前任务的最高优先级获取当前最高优先级的任务控制块 */
}
5 前导置零指令
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )
uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
在使用硬件方式实现的函数taskSELECT_highest_prioty_task()中调用了函数portGET_HIGHEST_PRIORITY()来计算任务优先级记录中的最高任务优先级,宏portGET_HIGHEST_PRIORITY()使用了_clz这个硬件指定来计算uxReadyPriority的前导零,然后使用31(变量uxReadyPriority的最大比特位)减去得到的前导零,那么就得到了变量uxReadyPriority中,最高位1的比特位。使用此方法就限制了系统最大的优先级数量不能超多 32,即最高优先等级位 31,不过对于绝大多数的应用场合, 32 个任务优先级等级已经足够使用了。
所谓的前导零指令,可以简单的理解为计算一个32位,头部0的个数
通过前导零指令获得最高优先级。
/*通过该函数获取当前最高优先级任务的任务控制块*/
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
{
List_t * const pxConstList = ( pxList );
/*确定当前列表*/
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
/*将当前列表的P指针指向P指针指向的下一个列表项(如图为列表项1)*/
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )
/*判断当前P指针指向的是否为末尾列表项*/
{
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
/*若P指针当前指向的是末尾列表项,则将P指针指向P指针指向下一个列表项/
}
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
/*若P指针指向的不是末尾列表项,则将该列表项确定为当前任务控制块*/
}