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的含义如下:

FreeRTOS调度器-LMLPHP

  • 指令dsbisb的含义如下:

FreeRTOS调度器-LMLPHP

  • 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,而对于r0r11需要用户手动出栈;

  • msr psp, r0进程栈指针PSP设置为任务的堆栈;

  • msrbasepri, 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;

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

05-18 11:14