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 i和cpsie f的含义如下:
- 指令dsb和isb的含义如下:
- 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 }
上述代码整个过程的操作:
- 保存现场(当前正在运行的任务);
- 关中断,进入临界区;
- 调用vTaskSwitchContext()函数获取下一个要运行的任务,将pxCurrentTCB更新为这个要运行任务的任务控制块;
- 获取新任务的栈顶指针;
- 恢复现场(即将要运行的任务的现场);
- 更新进程栈指针PSP的值;
- 最后执行指令bx r14,新的任务开始运行;
查找下一个要运行的任务
调用vTaskSwitchContext()函数查找下一个要运行的任务,而在函数内部最终是调用taskSELECT_HIGHEST_PRIORITY_TASK()来完成的;
在FreeRTOS中查找下一个要运行的任务有两种方式,分别是通用的方式和硬件方式,具体使用哪一种需要通过是否配置configUSE_PORT_OPTIMISED_TASK_SELECTION这个宏来决定的,当这个宏为1时,使用的就是硬件方式,否则就是通用方式;
两种方式的区别如下:
通用方式
数组pxReadyTasksLists[]为就绪任务列表数组,一个优先级对应一个列表,同优先级的就绪任务都挂接在对应的列表上;
变量uxTopReadyPriority代表处于就绪态的最高优先级值,该值更新的情况有两种:
- 每次创建任务的时候会判断新任务的优先级是否大于uxTopReadyPriority当前值,如果大于那就将新任务的优先级值赋值给uxTopReadyPriority;
- 当有新的就绪任务被添加到就绪列表中时会判断和更新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;
时间片调度发生在滴答定时器的中断服务函数中;