FreeRTOS任务切换


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。 由于函数是中断中调用,不要在里面处理太复杂的事情!!

你可能感兴趣的:(单片机)