现在创建任务(xTaskCreate)、启动调度器(vTaskStartScheduler),任务控制(xTaskDelay),以及Tick 中断(xPortSysTickHandler),都分析完成了,SysTick,PendSV 中断已经使能,接下来第一个任务便可以自由的奔跑;等待下一次 SysTick 来临(1ms 后),调度器工作;
1、xPortSysTickHandler
SysTick 触发后,会调用到它的 ISR 函数 xPortSysTickHandler,这个函数的实现和处理器体系架构相关,定义在 port.c:
-
void xPortSysTickHandler( void )
-
-
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
-
executes all interrupts must be unmasked. There is therefore no need to
-
save and then restore the interrupt mask value as its value is already
-
known - therefore the slightly faster vPortRaiseBASEPRI() function is used
-
in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
-
-
-
/* Increment the RTOS tick. */
-
if(
xTaskIncrementTick() != pdFALSE )
-
-
/* A context switch is required. Context switching is performed in
-
the PendSV interrupt. Pend the PendSV interrupt. */
-
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
-
-
-
vPortClearBASEPRIFromISR();
-
由于之前配置的 SysTick 的优先级为最低,所以此刻便有可能其他中断介入打断,所以这里配置一下 BASEPRI 寄存器,来防止被打断;在最后调用 vPortClearBASEPRIFromISR() 来恢复;
SysTick Handler 中调用 xTaskIncrementTick 来判断是否需要进行上下文切换,如果需要进行上下文切换(也就是返回 pdTRUE)的话,那么通过调用:
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT
往 NVIC 中手动拉起一个 PendSV 中断;
先看看 xTaskIncrementTick 的内部逻辑在《FreeRTOS --(11)任务管理之系统节拍》中已经详细描述,它返回的是一个是否需要调度的标志,如果返回了 pdTRUE,则代表需要调度,拉起 PendSV;
当然,不仅仅是 SysTick 会引发上下文切换,主动调用 portYIELD 也会拉起 PendSV 使得上下文切换:
-
/* Scheduler utilities. */
-
-
-
/* Set a PendSV to request a context switch. */ \
-
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
-
-
/* Barriers are normally not required but do ensure the code is completely \
-
within the specified behaviour for the architecture. */ \
-
__dsb( portSY_FULL_READ_WRITE ); \
-
__isb( portSY_FULL_READ_WRITE ); \
-
2、xPortPendSVHandler
在 PendSV 被拉起来后,如果当前没有其他中断正在执行的话,就会走到 xPortPendSVHandler,这个 ISR 在 port.c 中,我们以 Cortex-M3 为例,是一段汇编代码:
-
__
asm void xPortPendSVHandler( void )
-
-
extern uxCriticalNesting;
-
-
extern vTaskSwitchContext;
-
-
-
-
mrs r0, psp
/* 获取进入PendSV的ISR之前的任务的 PSP. */
-
-
-
ldr r3, =pxCurrentTCB
/* 获取进入PendSV的ISR之前的任务的 pxCurrentTCB */
-
-
-
stmdb r0!, {r4-r11}
/* Save the remaining registers. */
-
str r0, [r2]
/* Save the new top of stack into the first member of the TCB. */
-
-
-
mov r0,
#configMAX_SYSCALL_INTERRUPT_PRIORITY
-
-
-
-
-
-
-
-
-
-
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. */
-
-
-
-
-
还好,这里的汇编代码都是常用的指令,理解起来并不困难,只是有一些细节需要特别注意,那么接下来就逐行分析:
0、切记,这里已经是 PendSV 的 ISR,此刻,Cortex-M3 的硬件已经完成了对 xPSR、PC、LR、R12、R0-R3 的入栈;
1、首先还是 PRESERVE8 的 8字节对齐操作;
2、既然是 PendSV 的 ISR,那么所有的通用寄存器都可以被我们使用,首先使用 MRS 指令,获取到当前的 PSP;(注意,在 ISR 中使用的是 MSP 堆栈指针,此刻获得的 PSP 是什么样的呢?)
3、获取进入 ISR 之前的 pxCurrentTCB,并保存到 R2 中;
4、手动入栈 R4~R11,在异常发生(PendSV 的时候,硬件已经自动入栈了 xPSR、PC、LR、R12、R0-R3 ,不过此刻的 R4~R11 还未改变,我们通过 stmdb 指令,从 R0 开始,将 R4~R11 手动入栈,在上下文切换之前为任务保存完整的栈信息)
这里是以 R0 的位置开始(任务的 PSP),顺序保存 R4~R11,并增加 R0;所以在执行完这条指令后,任务的堆栈信息变为:
5、此刻 R0 以及被更新到了 R11 的位置,此刻的 R2 是 pxCurrentTCB,还记得么,pxCurrentTCB 的第一个指针叫做 pxTopOfStack 保存的是任务的堆栈栈顶的指针变量也就是 PSP,接下来通过 STR 指令,将之前的任务的 pxTopOfStack 更新到现在 R0 的位置,也就是 R11,完成了手动保存并更新了上一个任务的堆栈;
6、上一个任务已经处理完毕,接下来便是选择下一个任务了:
stmdb sp!, {r3, r14}
使用 stmdb 将 R3 和 R14(也就是 LR)入栈(注意,此刻的 sp 指的是 MSP,主堆栈指针);为啥只对这两个寄存器入栈呢?因为即将调用 C 函数,此刻的 R3 保存了 pxCurrentTCB 指针的地址,这个值在函数调用后还要用到,而 R14 就是 LR,在函数调用的时候,将被重写覆盖;
7、通过配置 CM3 的 BASEPRI 来开启临界区:
-
mov r0
,
#configMAX_SYSCALL_INTERRUPT_PRIORITY
-
8、通过 bl 跳转指令来调用 vTaskSwitchContext 函数:
-
void vTaskSwitchContext( void )
-
-
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
-
-
/* The scheduler is currently suspended - do not allow a context
-
-
-
-
-
-
-
traceTASK_SWITCHED_OUT();
-
-
#if ( configGENERATE_RUN_TIME_STATS == 1 )
-
-
#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
-
portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
-
-
ulTotalRunTime =
portGET_RUN_TIME_COUNTER_VALUE();
-
-
-
/* Add the amount of time the task has been running to the
-
accumulated time so far. The time the task started running was
-
stored in ulTaskSwitchedInTime. Note that there is no overflow
-
protection here so count values are only valid until the timer
-
overflows. The guard against negative values is to protect
-
against suspect run time stat counter implementations - which
-
are provided by the application, not the kernel. */
-
if( ulTotalRunTime > ulTaskSwitchedInTime )
-
-
pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
-
-
-
-
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;
-
-
-
-
/* Select a new task to run using either the generic C or port
-
-
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. */
-
-
-
/* After the new task is switched in, update the global errno. */
-
#if( configUSE_POSIX_ERRNO == 1 )
-
-
FreeRTOS_errno = pxCurrentTCB->iTaskErrno;
-
-
-
-
#if ( configUSE_NEWLIB_REENTRANT == 1 )
-
-
/* Switch Newlib's _impure_ptr variable to point to the _reent
-
structure specific to this task.
-
See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
-
for additional information. */
-
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
-
-
#endif /* configUSE_NEWLIB_REENTRANT */
-
-
在调度器没被挂起的情况下,这个函数中,主要通过调用 taskSELECT_HIGHEST_PRIORITY_TASK 来选取最高优先级的任务,这个函数有两个实现,主要是看是否有定义 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏
-
#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )
-
-
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
-
-
UBaseType_t uxTopPriority = uxTopReadyPriority; \
-
-
/* Find the highest priority queue that contains ready tasks. */ \
-
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
-
-
configASSERT( uxTopPriority ); \
-
-
-
-
/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of \
-
the same priority get an equal share of the processor time. */ \
-
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
-
uxTopReadyPriority = uxTopPriority; \
-
} /* taskSELECT_HIGHEST_PRIORITY_TASK */
-
-
/*-----------------------------------------------------------*/
-
-
#else /* configUSE_PORT_OPTIMISED_TASK_SELECTION */
-
-
#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 ] ) ); \
-
-
-
#endif /* configUSE_PORT_OPTIMISED_TASK_SELECTION */
在没有定义 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏的时候,选择最高优先级任务的方式是通过遍历的方式来获取最高优先级的任务;
如果定义了 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏的时候,就调用 portGET_HIGHEST_PRIORITY 来获得最高优先级任务链表,他是和处理器架构体系相关的,也就是说,你的硬件如果支持捷径,那么就用你的方法获取最高优先级;在 Cortex-M3 中,是有一个 CLZ 指令,这个指令用来计算一个变量从最高位开始的连续零的个数,比如,uxTopReadyPriority为 0x09(二进制为:0000 0000 0000 0000 0000 0000 0000 1001),即bit3和bit0为1,表示存在优先级为0和3的就绪任务。则__clz( (uxTopReadyPriority)的值为28,uxTopPriority =31-28=3,即优先级为3的任务是就绪态最高优先级任务。下面的代码跟通用方法一样,调用宏listGET_OWNER_OF_NEXT_ENTRY获取最高优先级列表中的下一个列表项,并从该列表项中获取任务TCB指针赋给变量pxCurrentTCB。
所以在 CM3 的 portmacro.h 中:
-
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority =
-
(
31UL - (
uint32_t ) __clz( ( uxReadyPriorities ) ) )
但是定义 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个需要注意一点,它不能够支持优先级的个数超过 32 个,因为 CLZ 最大就处理 32 bit,所以这个表示优先级的 bitmap 最多 32,也就是优先级限定在 32 个以内;
9、获取到最大优先级的任务后,将其赋值给了 pxCurrentTCB,返回汇编代码;
10、离开临界区
-
-
为何此时能够离开临界区呢?因为前一个任务已经完好的保存,下一个任务也已经选择出来;此刻被高优先级中断嵌套,其实不在有影响,只是上下文切换的时间被延时;
11、将 R3 和 R14 出栈,R3 存储了 pxCurrentTCB 地址,R14 存储了进入 ISR 那刻的 LR(0xFFFF_FFFD);
12、此刻 R3 指向的 pxCurrentTCB 地址,已经是更新到最高优先级的 TCB 的地址,TCB 的第一个元素是这个任务的栈顶指针 pxTopOfStack,获取该任务的栈顶指针到 R0:
-
-
13、既然获得到了下一个即将被调度的任务的栈顶指针,那么首先是将它的 R4~R11 出栈(这部分要手动做,因为硬件只会去做xPSR、PC、LR、R12、R0-R3);
ldmia r0!, {r4-r11}
此刻便可以更新 PSP了,更新 PSP 后,如果返回 ISR 的话,硬件会自动出栈 xPSR、PC、LR、R12、R0-R3;
-
-
最后调用 bx R14 返回中断服务程序,完成整个调度:
bx r14
此刻硬件便会将 xPSR、PC、LR、R12、R0-R3 自动出栈,下一个任务跑起来了;