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,而对于r0r3、r12、PC、xPSR等这些寄存器会在退出中断的时候MCU自动出栈恢复的,r4r11需要用户手动出栈;

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

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

FreeRTOS调度器的更多相关文章

  1. FreeRTOS - 调度器

    原文地址:http://www.cnblogs.com/god-of-death/p/6942641.html 绝大多数情况下,调度器的配置如下: 下面的说明基于上面的调度器配置: 如果有更高优先级的 ...

  2. FreeRTOS 任务与调度器(2)

    在上一篇我们介绍了FreeRTOS任务的一些基本操作和功能,今天我们会介绍一个很好很强大的功能——任务通知 任务通知可以在不同任务之间传递信息,它可以取代二值信号量.计数信号量.事件标志组.深度为1的 ...

  3. FreeRTOS 任务与调度器(1)

    前言: Task.c和Task.h文件内是FreeRTOS的核心内容,所有任务和调度器相关的API函数都在这个文件中,它包括下图这些内容FreeRTOS文件如下: Task.c和Task.h文件内是F ...

  4. 【freertos】005-启动调度器分析

    前言 本节主要讲解启动调度器. 这些都是与硬件相关,所以会分两条线走:posix和cortex m3. 原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/160 ...

  5. FreeRTOS --(9)任务管理之启动调度器

    转载自 https://blog.csdn.net/zhoutaopower/article/details/107057528 在使用 FreeRTOS 的时候,一般的,先创建若干任务,但此刻任务并 ...

  6. FreeRTOS 调度锁,任务锁和中断锁

    以下转载自安富莱电子: http://forum.armfly.com/forum.php 调度锁调度锁就是 RTOS 提供的调度器开关函数,如果某个任务调用了调度锁开关函数,处于调度锁开和调度锁关之 ...

  7. 大数据之Yarn——Capacity调度器概念以及配置

    试想一下,你现在所在的公司有一个hadoop的集群.但是A项目组经常做一些定时的BI报表,B项目组则经常使用一些软件做一些临时需求.那么他们肯定会遇到同时提交任务的场景,这个时候到底如何分配资源满足这 ...

  8. [Spring]支持注解的Spring调度器

    概述 如果想在Spring中使用任务调度功能,除了集成调度框架Quartz这种方式,也可以使用Spring自己的调度任务框架. 使用Spring的调度框架,优点是:支持注解(@Scheduler),可 ...

  9. 编写简单的ramdisk(选择IO调度器)

    前言 目前linux中包含anticipatory.cfq.deadline和noop这4个I/O调度器.2.6.18之前的linux默认使用anticipatory,而之后的默认使用cfq.我们在前 ...

随机推荐

  1. 转载【oracle切换表空间】

    http://blog.itpub.net/28939273/viewspace-1061476/ [root@yoon ~]# more /etc/oracle-releaseOracle Linu ...

  2. 利用 AWS 无服务架构之语音合成

    目录 一.架构图 二.服务部署 2.1.创建 DynamoDB 表 2.2.创建 S3 2.2.1 静态网页存储桶 2.2.2.音频存储桶 2.3.创建 SNS Topic 2.4.为 Lambda ...

  3. Ubuntu16.04下KeepAlived+Nginx 布署

    前言         网上已经有很多相关文章,对各种概念介绍的比较清楚,也有各种详细的步骤,这里主要记录本要在ubuntu16.04下的布署过程,主要记录编译安装keepalived时遇到的坑及解决办 ...

  4. SPSS 2019年10月31日 20:20:53今日学习总结

    ◆描述性统计分析 概念:描述性统计分析方法是指应用分类.制表.图形及概括性数据指标(去均值,方差等)来概括数据分布特征的方法. 而推断性统计分析方法则是通过随机抽样,应用统计方法把从样本数据得到的结论 ...

  5. Python进入后台界面(admin)设定

    前言 用过Django框架的童鞋肯定都知道,在创建完Django项目后,每个app下,都会有一个urls.py文件,里边会有如下几行: ※特别要注意下面标红颜色的部分[] 一般情况下不需要修改什么东西 ...

  6. 20175316 盛茂淞 2018-2019-2 《Java程序设计》实验五 《网络安全与编程》 实验报告

    20175316 盛茂淞 2018-2019-2 <Java程序设计>实验五 <网络安全与编程> 实验报告 一.实验报告封面 课程:Java程序设计 班级:1753班 姓名:盛 ...

  7. windows下图形学视觉基本库安装不完全指南

    安装各种库东奔西走...... GLUT(英文全写:OpenGL Utility Toolkit)是一个处理OpenGL程式的工具库,负责处理和底层操作系统的呼叫以及I/O,并包括了以下常见的功能: ...

  8. Timsort

    https://archive.codeplex.com/?p=timsort4net#117964 download archive https://codeplexarchive.blob.cor ...

  9. java源码 -- LinkedHashSet

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

  10. Ubuntu将自带的python3升级

    一.这里演示的是将python3.5升级到python3.6 1.添加安装源,在命令行输入如下命令: sudo add-apt-repository ppa:jonathonf/python-3.6 ...