CORTEX_M4F_STM32F407ZG-SK为例,参考书籍Cortex-m权威指南   

系统启动:    
/* Start the scheduler. */
vTaskStartScheduler()(main.c)----->  vTaskStartScheduler()(tasks.c,设置节拍定时器和启动第一个任务,开始系统正常运行调度)
--xPortStartScheduler(port.c)---->vPortStartFirstTask(port.c)--->

vPortStartFirstTask 启动第一个任务,函数重新初始化系统的栈指针,表示 freertos 开始接手平台控制,同时出发svc系统调用,运行第一个任务。

FreeRTOSv10.2.0\FreeRTOS\Source\tasks.c  void vTaskStartScheduler( void ) 

FreeRTOSv10.2.0\FreeRTOS\Source\portable\GCC\ARM_CM4F\port.c   汇编实现,如 xPortPendSVHandler  xPortStartScheduler( void ) 

PendSV异常:
1、在PendSV中断服务函数中实现任务切换具体过程。
2、将中断寄存器BIT28置1,产生PendSV中断。

BIT28置1后,将PendSV中断设置为挂起状态,PendSV优先级比较低,等到优先级高于PendSV的中断执行完后,PendSV中断服务程序才会执行,进行切换工作。

任务切换场合:
1、执行系统调用,如:taskYIELD(), vTaskDelay() -->portYIELD_WITHIN_API() -----> portYIELD()(设置中断寄存器BIT28置1),中断服务程序使用portYIELD_FROM_ISR强制切换任务。
2、systick中断服务函数。(系统滴答时钟)

#define portEND_SWITCHING_ISR( xSwitchRequired ) if( xSwitchRequired != pdFALSE ) portYIELD()      (portmacro.h,设置中断寄存器BIT28置1)
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )

异常处理函数:
FreeRTOSConfig.h
#define xPortPendSVHandler PendSV_Handler

FreeRTOSv10.2.0\\FreeRTOS\Demo\CORTEX_M4F_Infineon_XMC4000_Keil\startup_XMC4200.s
ExcpVector   PendSV_Handler             ; PendSV Handler
 
xPortPendSVHandler --->vTaskSwitchContext  --->taskSELECT_HIGHEST_PRIORITY_TASK

xPortPendSVHandler
1、判断是否使用FPU,如果使用的话将S16-S31入栈,EXC_RETURN 当处理异常的时候bit4会被CONTROl的FPCA位替代,判断EXC_RETURN的bit4是否为1.
2、R4-R11 R14入栈。
3、关闭中断
4、调用vTaskSwitchContext,获取下一个要运行任务。
4、打开中断
6、获取要切换任务的栈顶指针。
7、R4-R11,R14出栈。
8、判断是否使用FPU,如果使用的话S16-S31出栈。
9、任务切换完成。

总结:
msp和psp指针切换,堆栈保存,任务切换,入栈出栈,FPU浮点单元

void xPortPendSVHandler( void )
{
/* This is a naked function. */

__asm volatile
(
" mrs r0, psp \n"
" isb \n"
" \n"
//将pxCurrentTCBConst标签(指针变量pxCurrentTCB的内存地址为&pxCurrentTCB或者到map看)放到r3,此时r3=&pxCurrentTCB
" ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. */
" ldr r2, [r3] \n"
" \n"
" tst r14, #0x10 \n" /* Is the task using the FPU context? If so, push high vfp registers. */
" it eq \n"
" vstmdbeq r0!, {s16-s31} \n"
" \n"
" stmdb r0!, {r4-r11, r14} \n" /* Save the core registers. */
" str r0, [r2] \n" /* Save the new top of stack into the first member of the TCB. */ //将r0(psp)值放到r2内容(pxCurrentTCBConst)所指的地址
" \n"
" stmdb sp!, {r0, r3} \n"
" mov r0, %0 \n" //获取中断优先级到r0,%0对应后面的configMAX_SYSCALL_INTERRUPT_PRIORITY
" msr basepri, r0 \n" //屏蔽低于优先级的中断
" dsb \n"
" isb \n"
" bl vTaskSwitchContext \n" //跳到任务切换c函数,找到下一个任务控制块
" mov r0, #0 \n"
" msr basepri, r0 \n" //开中断
" ldmia sp!, {r0, r3} \n"
" \n"
将r3(存放的是任务控制块指针变量的地址&pxCurrentTCB)寄存器内容作为地址取内容放到r1(此时r1为新的任务控制块地址)
" ldr r1, [r3] \n" /* The first item in pxCurrentTCB is the task top of stack. */
" ldr r0, [r1] \n" 将r1寄存器内容作为地址取内容放到r0(此时r0为新任务的pxTopOfStack)
" \n"
" ldmia r0!, {r4-r11, r14} \n" /* Pop the core registers. */ 将新的任务堆栈顶指针出堆栈到r4-r11
" \n"
" tst r14, #0x10 \n" /* Is the task using the FPU context? If so, pop the high vfp registers too. */
" it eq \n"
" vldmiaeq r0!, {s16-s31} \n"
" \n"
" msr psp, r0 \n" 将新的任务堆栈顶指针放到psp
" isb \n"
" \n"
#ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata workaround. */
#if WORKAROUND_PMU_CM001 == 1
" push { r14 } \n"
" pop { pc } \n"
#endif
#endif
" \n"
" bx r14 \n" // //之后硬件会自动把PC指针出堆栈(因为此时psp为新任务的堆栈顶指针所以出堆栈也是新任务的寄存器)
" \n"
" .align 4 \n"
"pxCurrentTCBConst: .word pxCurrentTCB \n"
::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY)
);
}
/*-----------------------------------------------------------*/

vTaskSwitchContext()
1、判断 uxSchedulerSuspended 是否为pdFALSE,任务调度器有没有被挂起,如果被挂起xYieldPending赋值为pdTRUE。
2、如果任务调度器没有挂起,查找下一个要运行的任务。
3、调用taskSELECT_HIGHEST_PRIORITY_TASK获取下一个要运行的任务,通用方法或者硬件方法。

taskSelect_HIGHEST_PRIOPITY_TASK
通过configUSE_PORT_OPTIMISED_TASK_SELECTION宏选择软件还是硬件,
硬件方法:
硬件(设置为1)效率高,优先级数目32,需要体系结构支持。
1、调用portGET_HIGHEST_PRIORITY获取最高优先级任务。
2、listGET_OWNER_OF_NEXT_ENTRY获取下一个要运行任务的任务控制块,并将其保存在pxCurrentTCB.

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) ucPortCountLeadingZeros( ( uxReadyPriorities ) ) )
uxTopReadyPriority使用每一位来表示任务,bit0位1,表示存在优先级位0的就绪任务,32位整数最多32位,优先级0~31.

/* Generic helper function. */
__attribute__( ( always_inline ) ) static inline uint8_t ucPortCountLeadingZeros( uint32_t ulBitmap )
{
uint8_t ucReturn;

__asm volatile ( "clz %0, %1" : "=r" ( ucReturn ) : "r" ( ulBitmap ) : "memory" );
return ucReturn;
}

    
__clz( (uxTopReadyPriority) 是什么意思, __clz() 会被汇编指令CLZ替换掉,这个指令用来计算一个变量从最高位开始的连续零的个数。

软件方法:
1、获取处于就绪态的优先级最高的任务,直接读取 uxTopReadyPriority 即可。
2、listGET_OWNER_OF_NEXT_ENTRY 获取下一个要运行任务的任务控制块,并将其保存在 pxCurrentTCB。

#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 ); \
--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 */

中断退出后,需要做什么判断?
任务切换,执行寄存器PC中保存的任务函数。

抢占如何实现?
主动抢占:系统调用或者systick进行抢占,会查找运行高优先级的任务。获取cpu运行权。

被动抢占:systick调度器进行抢占。通过时间片调度。
任务阻塞,延迟、信号量等待等阻塞式 API来释放cpu运行权(此任务挂起),运行其他高优先级任务(当延迟时间到,获取到信号量等将会进行抢占,返回上一次任务)。

抢占式调度器,当前的任务要么被高优先级任务抢占,要么通过调用阻塞式 API 来释放 CPU 使用权让低优先级任务执行,没有用户任务执行时就执行空闲任务

系统在每一次节拍计数器中断服务程序xPortSysTickHandler(平台实现 port.c 中)  中调用处理函数 xTaskIncrementTick, 依据该函数返回值判断是否需要触发 PendSV 异常, 进行任务切换。
涉及任务时间片轮循, 任务阻塞超时, 以及结束以此实现的延时函数。

FreeRTOS会在关键区即taskENTER_CRITICAL()和taskEXIT_CRITICAL()包裹的区间中,执行进程切换。即在关闭中断的时候,进行进程切换。
我们已经知道,即便关闭中断,PowerPC的sc中断,还是可以得到响应。但是时钟中断呢?这是个外部中断,无法得到响应。
那么是何时打开的中断呢?新进程切入之后,立即打开中断?在多个TASK级别做开关中断配对?

触发任务切换的两种情况 : 
高优先级任务就绪抢占和同优先级任务时间共享(包括提前挂起)。 
系统中,时间延时和任务阻塞,时间片都以 Systick 为单位。

xTaskIncrementTick()
系统每次节拍中断服务程序中主要任务由函数 xTaskIncrementTick 完成。
在任务调度器没有挂起的情况下( xTaskIncrementTick !=  pdFALSE ),该函数主要完成 :

判断节拍计数器 xTickCount  是否溢出, 溢出轮换延时函数队列
判断是否有阻塞任务超时,取出插入就绪链表
同优先级任务时间片轮

而当任务调度器被挂起时, 该函数累加挂起时间计数器 uxPendedTicks, 调用用户钩子函数, 此时,正在运行的任务不会被切换, 一直运行。
当恢复调度时, 系统会先重复调用 xTaskIncrementTick 补偿 (uxPendedTicks次)。
不管, 系统调度器是否挂起, 每次节拍中断都会调用用户的钩子函数 vApplicationTickHook。 由于函数是中断中调用,不要在里面处理太复杂的事情!!