函数 | 描述 |
---|---|
xPortPendSVHandler() | PendSV中断服务函数,其实函数原型为PendSV_Handler() |
vTaskSwitchContext() | 检查任务堆栈使用是否溢出,和查找下一个优先级高的任务,如果使能运行时间统计功能,会计算任务运行时间 |
在FreeRTOS任务管理中,最主要的目的就是找到就绪态优先级最高的任务,然后执行任务切换,从而能保持优先级最高的任务一直占用CPU资源。为了达到最优性能,任务切换部分程序使用汇编代码编写。
FreeRTOS有两种方法触发任务切换:
其中:
// 对于普通任务时
#define taskYIELD() portYIELD()
#define portYIELD_WITHIN_API portYIELD
// 对于中断服务程序时
#define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
可以看出,最终执行的代码段为:
#define portYIELD() \
{ \
/* \
* 通过向中断控制及状态寄存器ICSR的第28位写入1,触发PendSV中断 \
* 地址为0xE000 ED04 \
*/ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
/* dsb和isb 完成数据同步隔离和指令同步隔离 \
* 保证之前存储器访问操作和指令都执行完 \
*/ \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
通过向中断控制及状态寄存器ICSR(地址:0xE000 ED04)的第28位写入1,触发PendSV中断,从而执行任务切换。
在FreeRTOS中,有:
#define xPortPendSVHandler PendSV_Handler
源代码如下:
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB; /* 永远会指向当前激活的任务 */
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp /* 读取进程栈指针PSP,保存在R0中,此时SP的值为MSP */
isb /* 指令同步隔离 */
/* 这两句使R2中保存当前激活的任务TCB首地址 */
ldr r3, =pxCurrentTCB /* 将pxCurrentTCB储存的地址保存到R3,注意的是pxCurrentTCB的储存地址是固定不变的,但指向是可变的 */
ldr r2, [r3] /* 将R3地址所处数据保存在R2,即TCB的首地址 */
/*
* 前两句判断是否使能了FPU,如果使能了,则手动将s16~s31压入栈中
* 其中s0~s15和FPSCR硬件自动完成
*/
tst r14, #0x10
it eq
vstmdbeq r0!, {s16-s31}
/* 将当前激活任务的寄存器值入栈,并更新R0,另外硬件自动将xPSR、PC、LR、R12、R0~R3入栈 */
stmdb r0!, {r4-r11, r14}
/* R0为PSP地址,R2为激活任务的TCB地址,R0的值写入R2所保存的地址去,即TCB第一个成员指向线程堆栈指针,在每次任务切换最后都会更新PSP */
str r0, [r2]
stmdb sp!, {r3} /* 将R3临时压入堆栈,R3保存了pxCurrentTCB地址,函数调用后会用到,因此要入栈保护 */
/* 关中断,中断优先级号大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都会被屏蔽 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0
dsb /* 数据同步隔离 */
isb /* 指令同步隔离 */
bl vTaskSwitchContext /* 切换到vTaskSwitchContext,查找下一个任务 */
/* 开中断 */
mov r0, #0
msr basepri, r0
ldmia sp!, {r3} /* 恢复R3,R3保存了pxCurrentTCB的地址,这里pxCurrentTCB的地址固定,但指向改变了 */
/* 当前激活的TCB栈顶值存入R0 */
ldr r1, [r3] /* 将pxCurrentTCB指向的地址赋值给R1,即将TCB的首地址赋值给R1 */
ldr r0, [r1] /* 将R1地址所处的数据赋值给R0,即当前激活任务TCB的第一项的值赋给R0 */
ldmia r0!, {r4-r11, r14} /* 将寄存器R4~R11出栈,并同时更新R0的值 */
/*
* 前两句判断是否使能了FPU,如果使能了,手动恢复s16-s31浮点寄存器
* 其中s0~s15和FPSCR硬件自动完成
*/
tst r14, #0x10
it eq
vldmiaeq r0!, {s16-s31}
msr psp, r0 /* 将最新的任务堆栈栈顶赋值给线程堆栈指针PSP */
isb /* 指令同步隔离,清流水线 */
#ifdef WORKAROUND_PMU_CM001
#if WORKAROUND_PMU_CM001 == 1
push { r14 }
pop { pc }
nop
#endif
#endif
bx r14 /* 当调用 bx r14指令退出中断,堆栈指针PSP指向了新任务堆栈的正确位置 */
}
说明:
该函数会更新当前任务运行时间,检查任务堆栈使用是否溢出,然后调用宏 taskSELECT_HIGHEST_PRIORITY_TASK()获取更高优先级的任务。
函数源代码如下:
void vTaskSwitchContext( void )
{
/* 如果任务调度器已经挂起 */
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
xYieldPending = pdTRUE; /* 标记任务调度器挂起,不允许任务切换 */
}
else
{
xYieldPending = pdFALSE; /* 标记任务调度器没有挂起 */
traceTASK_SWITCHED_OUT();
/*
* 如果启用运行时间统计功能,设置configGENERATE_RUN_TIME_STATS为1
* 如果使用了该功能,要提供以下两个宏:
* portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
* portGET_RUN_TIME_COUNTER_VALUE()
*/
#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
/*
* ulTotalRunTime记录系统的总运行时间,ulTaskSwitchedInTime记录任务切换的时间
* 如果系统节拍周期为1ms,则ulTotalRunTime要497天后才会溢出
* ulTotalRunTime < ulTaskSwitchedInTime表示可能溢出
*/
if( ulTotalRunTime > ulTaskSwitchedInTime )
{
/* 记录当前任务的运行时间 */
pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 更新ulTaskSwitchedInTime,下个任务时间从这个值开始 */
ulTaskSwitchedInTime = ulTotalRunTime;
}
#endif /* configGENERATE_RUN_TIME_STATS */
/* 核查堆栈是否溢出 */
taskCHECK_FOR_STACK_OVERFLOW();
/* 寻找更高优先级的任务 */
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
/* 如果使用Newlib运行库,你的操作系统资源不够,而不得不选择newlib,就必须打开该宏 */
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
}
}
PendSV中会调用vTaskSwitchContext(),最后调用函数taskSELECT_HIGHEST_PRIORITY_TASK()寻找优先级最高的任务。
对于FreeRTOS的调度器,它有两种方式寻找下一个最高优先级的任务,分别为特殊方式和常用方式,在FreeRTOSConfig.h中可通过宏定义设置,如下:
/* 0:使用常用方式来选择下一个要运行的任务;1:使用特殊方法来选择下一个要运行的任务 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
在FreeRTOS中,通用方法不依赖某些硬件等限制,适用于多种MCU中。特殊方式是使用了某些硬件的特性,只针对部分MCU而使用。
uxTopReadyPriority 记录就绪态中最高优先级值,创建任务时会更新值,有任务添加到就绪表时也会更新值。这种方法对任务的数量无限制。
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority = uxTopReadyPriority; \
\
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
{ \
configASSERT( uxTopPriority ); \
--uxTopPriority; \
} \
\
/* 获取优先级最高任务的任务控制块 */ \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
uxTopReadyPriority = uxTopPriority; \
}
使用此方法,uxTopReadyPriority 每个bit位表示一个优先级,bit0表示优先级0,bit31表示优先级31,使用此方式优先级最大只能是32个。
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
/* 获取优先级最高的任务 */ \
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
/* 获取优先级最高任务的任务控制块 */
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
其中:
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
__clz( ( uxReadyPriorities ) 是计算uxReadyPriorities 的前导零个数,如:
二进制0001 1010 0101 1111的前导零个数为3,可以知道,最高优先级uxTopPriority 等于 31减去前导零个数。
知道最高优先级的优先级,则通过listGET_OWNER_OF_NEXT_ENTRY()对应最高优先级的列表项,将pxCurrentTCB指向对应的控制块。
【1】: 正点原子:《STM32F407 FreeRTOS开发手册V1.1》
【2】: 野火:《FreeRTOS 内核实现与应用开发实战指南》
【3】: 《Cortex M3权威指南(中文)》