FreeRTOS----调度器

FreeRTOS----调度器

调度器的启动流程分析

当创建完任务之后,会调用vTaskStartScheduler()函数,启动任务调度器;

void vTaskStartScheduler( void )
{
/* 部分代码如下: */
BaseType_t xReturn;
    xReturn = xTaskCreate(  prvIdleTask,
                          configIDLE_TASK_NAME,
                          configMINIMAL_STACK_SIZE,
                          ( void * ) NULL,
                          portPRIVILEGE_BIT, 
                          &xIdleTaskHandle ); 

    #if ( configUSE_TIMERS == 1 )
    {
        if( xReturn == pdPASS )
        {
            xReturn = xTimerCreateTimerTask();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif /* configUSE_TIMERS */

    if( xReturn == pdPASS )
    {
        #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
        {
            freertos_tasks_c_additions_init();
        }
        #endif
        
        portDISABLE_INTERRUPTS();

        #if ( configUSE_NEWLIB_REENTRANT == 1 )
        {
            _impure_ptr = &( pxCurrentTCB->xNewLib_reent );
        }
        #endif /* configUSE_NEWLIB_REENTRANT */

        xNextTaskUnblockTime = portMAX_DELAY;
        xSchedulerRunning = pdTRUE;
        xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

        portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

        traceTASK_SWITCHED_IN();

        if( xPortStartScheduler() != pdFALSE )
        {
            /* Should not reach here as if the scheduler is running the
            function will not return. */
        }
        else
        {
            /* Should only reach here if a task calls xTaskEndScheduler(). */
        }
    }
    else
    {
        configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
    }

    /* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
    meaning xIdleTaskHandle is not used anywhere else. */
    ( void ) xIdleTaskHandle;
}
  • 创建空闲任务,如果使用静态内存,就使用函数xTaskCreateStatic()来创建,空闲任务的优先级为0,优先级最低;
  • 如果使用软件定时器的话,需要通过函数xTimerCreateTimerTask()来创建定时器服务任务;
  • 关闭中断;
  • 将变量xSchedulerRunning设置为pdTRUE,表示调度器开始运行;
  • 如果宏configGENERATE_RUN_TIME_STATS为1的时候,说明使能了时间统计功能,此时,需要用户实现宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,用来配置一个定时器/计数器;
  • 调用函数xPortStartScheduler()来初始化调度器启动有关的硬件;

FreeRTOS系统时钟是由滴答定时器来提供的,而且任务切换也会用到PendSV中断,而这些硬件的初始化由xPortStartScheduler()函数来完成,具体代码如下:

/* 部分重要代码如下: */
/* Make PendSV and SysTick the lowest priority interrupts. */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

/* Start the timer that generates the tick ISR.  Interrupts are disabled
here already. */
vPortSetupTimerInterrupt();

/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;

/* Start the first task. */
prvStartFirstTask();
  • 设置PendSV的中断优先级为最低优先级;
  • 设置SysTick的中断优先级为最低优先级;
  • 调用函数vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断;
  • 初始化临界区嵌套计数器为0;
  • 调用函数prvStartFirstTask()开启第一个任务;

启动第一个任务

函数prvStartFirstTask()用于启动第一个任务,函数源码如下:

__asm void prvStartFirstTask( void )
{
    PRESERVE8

    /* Use the NVIC offset register to locate the stack. */
    ldr r0, =0xE000ED08
    ldr r0, [r0]
    ldr r0, [r0]

    /* Set the msp back to the start of the stack. */
    msr msp, r0
    /* Globally enable interrupts. */
    cpsie i
    cpsie f
    dsb
    isb
    /* Call SVC to start the first task. */
    svc 0
    nop
    nop
}
  • PRESERVE8,伪指令,意为8字节对齐;

  • 一般来说向量表应该是从起始地址(0x00000000)开始存储的,不过有些应用可能需要在运行时修改或者重定义向量表,Cortex-M处理器为此提供了一个叫做向量表重定位的特性,即提供了一个名为向量表偏移寄存器(VTOR)的可编程寄存器,该寄存器的地址为0xE000ED08,通过这个寄存器可以重新定义向量表;

    在向量表的起始地址存储的是MSP(主栈指针)初始值,而下面几行代码的操作就是获取这个MSP初始值,并赋值给r0,紧接着将r0的值赋值给MSP,即将初始值给MSP,复位;

    ldr r0, =0xE000ED08
    ldr r0, [r0]
    ldr r0, [r0]
    msr msp, r0
  • 指令cpsie icpsie f的含义如下:

  • 指令dsbisb的含义如下:

  • svc 0,调用SVC指令触发SVC中断,SVC也叫做请求管理调用,SVC和PendSV异常对于OS的设计来说非常重要,而在FreeRTOS中仅仅使用SVC异常来启动第一个任务,后面的程序中就再也用不到SVC了

至此之后,将进入SVC异常中断处理函数,SVC中断服务函数应该为SVC_Handler(),但是FreeRTOSConfig.h中通常通过#define的方式重新定义为vPortSVCHandler(),如下:

#define vPortSVCHandler SVC_Handler

函数vPortSVCHandler()定义在port.c文件中,源码如下:

__asm void vPortSVCHandler( void )
{
    PRESERVE8

    ldr r3, =pxCurrentTCB   /* Restore the context. */
    ldr r1, [r3]            /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
    ldr r0, [r1]            /* The first item in pxCurrentTCB is the task top of stack. */
    ldmia r0!, {r4-r11}     /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
    msr psp, r0         /* Restore the task stack pointer. */
    isb
    mov r0, #0
    msr basepri, r0
    orr r14, #0xd
    bx r14
}
  • pxCurrentTCB是一个执行TCB_t的指针,这个指针永远指向正在运行的任务,而这里是获取这个指针存储的地址;下面指令一连串的操作就是,先获取pxCurrentTCB指针所指向的TCB地址,然后再通过这个地址获取到TCB的第一个字段,也就是任务堆栈的栈顶指针pxTopOfStack所指向的位置;

    ldr   r3, =pxCurrentTCB
    ldr r1, [r3]
    ldr r0, [r1]  

    最终的目的就是要获取第一个要运行的这个任务的任务栈顶指针

    因为任务所对应的寄存器值,也就是保存现场时存入的这些值,在任务切换时需要恢复现场,即恢复这些寄存器的值;

  • ldmia r0!, {r4-r11},LDMIA指令是多加载/存储指令,不过这里使用的是具有回写的多加载/存储指令,此处的作用就是,将r0寄存器中存储的地址及后面多个连续地址中的值赋值给寄存器r4-r11,而对于r0~r3、r12、PC、xPSR等这些寄存器会在退出中断的时候MCU自动出栈恢复的,r4~r11需要用户手动出栈;

  • msr psp, r0进程栈指针PSP设置为任务的堆栈;
  • msr basepri, r0即寄存器basepri=0,开启中断;
  • orr r14, #0xd,r14是连接寄存器(LR), r14最后四位按位或上0x0d ,表示退出异常以后CPU进入线程模式并且使用进程栈;
  • bx r14,该指令执行以后,硬件自动恢复寄存器r0~r3、r12、LR、PC和xPSR的值,堆栈使用进程栈PSP,然后执行寄存器PC中保存的任务函数;

至此,FreeRTOS的任务调度器正式开始运行;

任务的切换

RTOS系统的核心是任务管理,而任务管理的核心是任务切换,任务切换决定了任务的执行顺序,任务切换效率的高低也决定了一个RTOS的性能;

PendSV异常

PendSV(可挂起的系统调用)异常,其优先级可以通过编程设置,通过将中断控制和状态寄存器ICSR的bit28置1来触发PendSV中断;与SVC异常不同的是,它的挂起状态可在更高优先级异常处理内设置,且会在高优先级异常处理完成后执行,从而利用该特性,将PendSV设置为最低的异常优先级,可以让PendSV异常处理在所有其他中断处理完成后执行,这对于上下文的切换非常有用,也是各种OS设计中的关键;

FreeRTOS系统的任务切换最终都是在PendSV中断服务函数中完成的;

PendSV中断服务函数本应该为PendSV_Handler(),但是在FreeRTOS中重定义为如下:

#define xPortPendSVHandler  PendSV_Handler

函数xPortPendSVHandler()源码如下:

__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

    PRESERVE8

    mrs r0, psp
    isb

    ldr r3, =pxCurrentTCB       /* Get the location of the current TCB. */
    ldr r2, [r3]

    stmdb r0!, {r4-r11}         /* Save the remaining registers. */
    str r0, [r2]                /* Save the new top of stack into the first member of the TCB. */
    stmdb sp!, {r3, r14}
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    dsb
    isb
    bl vTaskSwitchContext
    mov r0, #0
    msr basepri, r0
    ldmia sp!, {r3, r14}

    ldr r1, [r3]
    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. */
    msr psp, r0
    isb
    bx r14
    nop
}

上述代码整个过程的操作:

  1. 保存现场(当前正在运行的任务);
  2. 关中断,进入临界区;
  3. 调用vTaskSwitchContext()函数获取下一个要运行的任务,将pxCurrentTCB更新为这个要运行任务的任务控制块;
  4. 获取新任务的栈顶指针;
  5. 恢复现场(即将要运行的任务的现场);
  6. 更新进程栈指针PSP的值;
  7. 最后执行指令bx r14,新的任务开始运行;

查找下一个要运行的任务

调用vTaskSwitchContext()函数查找下一个要运行的任务,而在函数内部最终是调用taskSELECT_HIGHEST_PRIORITY_TASK()来完成的;

在FreeRTOS中查找下一个要运行的任务有两种方式,分别是通用的方式和硬件方式,具体使用哪一种需要通过是否配置configUSE_PORT_OPTIMISED_TASK_SELECTION这个宏来决定的,当这个宏为1时,使用的就是硬件方式,否则就是通用方式;

两种方式的区别如下:

通用方式

数组pxReadyTasksLists[]为就绪任务列表数组,一个优先级对应一个列表,同优先级的就绪任务都挂接在对应的列表上;

变量uxTopReadyPriority代表处于就绪态的最高优先级值,该值更新的情况有两种:

  1. 每次创建任务的时候会判断新任务的优先级是否大于uxTopReadyPriority当前值,如果大于那就将新任务的优先级值赋值给uxTopReadyPriority;
  2. 当有新的就绪任务被添加到就绪列表中时会判断和更新uxTopReadyPriority的值;

在通用方式中,就是从uxTopReadyPriority指代的这个当前就绪态中最高优先级值开始判断,哪个列表不为空就说明哪个优先级有就绪的任务;

硬件方式

硬件方式是使用处理器自带的硬件指令来实现的,比如Cortex-M处理器就带有计算前导零个数指令:CLZ;

使用硬件方式时,uxTopReadyPriority变量就不是代表就绪态中的最高优先级了,而是使用该变量的每个bit代表一个优先级,bit0代表优先级0,bit31代表优先级31,当某个优先级有就绪任务的话就将每个位置1,因此,使用硬件方式时,最多只能有32个优先级;

CLZ指令用于计算前导零个数,也就是从最高位开始到第一个为1的bit位,其中间0的个数,之后,再用31减去这个个数得到的就是处于就绪态的最高优先级值;

获取到就绪态中最高优先级之后,使用listGET_OWNER_OF_NEXT_ENTRY()从对应的列表中找出下一个列表项,将该列表项对应的任务块赋值给pxCurrentTCB,这样就确定了下一个要运行的任务;

任务切换

在两种情况下会触发任务切换:执行系统调用和滴答定时器中断;

执行系统调用

执行系统调用就是执行FreeRTOS系统提供的相关API,比如任务切换函数taskYIELD()和其他间接调用taskYIELD()的API;

函数taskYIELD()其实是个宏,其定义如下:

#define taskYIELD()  portYIELD()

/* Scheduler utilities. */
#define portYIELD()                                                             \
{                                                                               \
    /* 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 );                                            \
}

上述源码最终的操作就是通过向中断控制和状态寄存器ICSR的bit28写入1,挂起PendSV来触发PendSV中断,这样就可以在PendSV中断服务函数中进行任务切换;

中断级的任务切换函数为portYIELD_FROM_ISR(),最终也是通过函数portYIELD()来完成的;

滴答定时器中断

需要修改滴答定时器中断服务函数如下:

void SysTick_Handler(void)
{
    if(xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
    {
        xPortSysTickHandler();
    }
}

在滴答定时器服务函数中调用xPortSysTickHandler(),此函数源码如下:

void xPortSysTickHandler( void )
{
    vPortRaiseBASEPRI(); //关中断
    {
        if( xTaskIncrementTick() != pdFALSE ) //增加时钟计数器xTickCount的值
        {
            //向中断控制和状态寄存器ICSR的bit28写入1,挂起PendSV来触发PendSV中断
            portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
        }
    }
    vPortClearBASEPRIFromISR(); //打开中断
}

时间片调度

FreeRTOS支持多个任务同时拥有同一个优先级,即同一个优先级中的一个任务在运行一个时间片(一个时钟节拍的长度)后让出CPU的使用权,让有同优先级的下一个任务运行;

要使用时间片调度的话,必须将宏configUSE_PREEMPTION和宏configUSE_TIME_SLICING配置为1;

时间片的长度由宏configTICK_RATE_HZ来确定,一个时间片的长度就是滴答定时器的中断周期,比如configTICK_RATE_HZ值设置为1000,那么一个时间片的长度就是1ms;

时间片调度发生在滴答定时器的中断服务函数中;

猜你喜欢

转载自www.cnblogs.com/jasontian996/p/11957853.html