声明:本人水平有限,博客可能存在部分错误的地方,请广大读者谅解并向本人反馈错误。
本专栏博客参考《STM32Cube高效开发教程(高级篇)》,有意向的读者可以购买正版书籍辅助学习,本书籍由王维波老师、鄢志丹老师、王钊老师倾力打造,书籍内容干货满满。
一、任务相关的一些概念
一个嵌入式操作系统的核心功能就是多任务管理功能,FreeRTOS的任务调度器具有基于优先级的抢占式任务调度方法,能满足实时性的要求。在接下来三篇博客中,将介绍FreeRTOS的多任务运行原理,各种任务调度方法的特点和作用,以及任务管理相关函数的使用。
1.1 多任务运行基本机制
在FreeRTOS中,一个任务就是实现某种功能的一个函数,任务函数的内部一般有一个死循环结构。任何时候都不允许从任务函数退出,也就是不能出现return语句。如果需要结束任务,在任务函数里,可以跳出死循环,然后使用函数vTaskDelete()删除任务自己,也可以在其他任务里调用函数vTaskDelete()删除这个住务。
在FreeRTOS里,用户可以创建多个任务。每个任务需要分配一个栈(stack)空间和一个任务控制块(TaskControlBlock,TCB)空间。每个任务还需要设定一个优先级,优先级的数字越小,表优先级越低(这里要与中断优先级区分)。
在单核处理器上,任何时刻只能有一个任务占用CPU并运行。但是在RTOS系统上,运行多个任务时,运行起来却好像多个任务在同时运行,这是由于RTOS的任务调度使得多个任务对CPU实现了分时复用的功能。
下图所示的是最简单的基于时间片的多任务运行原理。这里假设只有2个任务,并且任务Task1和Task2具有相同的优先级。圆周表示CPU时间,如同钟表的一圈,RTOS将CPU时间分成基本的时间片(timeslice),例如,FreeRTOS默认的时间片长度是1ms,也就是SysTick定时器的定时周期。在一个时间片内,会有一个任务占用CPU并执行,假设当前运行的任务是Task1在一个过间结束时(实际就是SysTick定时器发生中断时)进行任务调度,由于Task1和Task2具有相同的优先级,RTOS会将CPU使用权交给Task2。Task1交出CPU使用权时,会将CPU的当前场景(CPU各个核心寄存器的值)压入自己的栈空间。而Task2获取CPU的使用权时,会用自已栈空间保存的数据恢复CPU场景,因而Task2可以从上次运行的状态继续运行。
基于时间片的多任务调度就是这样控制多个同等优先级任务实现CPU的分时复用,从而实现多任务运行的。因为时间片的长度很短(默认是1ms),任务切换的速度非常快,所以程序运行时,给用户的感觉就是多个任务在同时运行。
当多个任务的优先级不同时,FreeRTOS还会使用基于优先级的抢占式任务调度方法,每个任务获得的CPU使用时间长度可以是不一样的。任务优先级和抢占式任务调度的原理在后面会介绍。
1.2 任务的状态
由单核CPU的多任务运行机制可知,任何时刻,只能有一个任务占用CPU并运行,这个任务的状态称为运行(runing)状态,其他未占用CPU的任务的状态都可称为非运行(not runing)状态。非运行状态又可以细分为3个状态,任务的各个状态以及状态之间的转换如下图所示。
FreeRTOS任务调度有抢占式(pre-emptive)和合作式(co-operative)两种方式,一般使用基于任务优先级的抢占式任务调度方法。任务调度的各种方法在后面会详细介绍,这里我们以抢占式任务调度方法为例,说明上图所示的原理。
1.2.1.就绪状态
任务被创建之后就处于就绪(ready)状态。FreeRTOS的任务调度器在基础时钟每次中断时进行一次任务调度申请,根据抢占式任务调度的特点,任务调度的结果有以下几种情况。
- 如果当前没有其他处于运行状态的任务,处于就绪状态的任务进入运行状态。
- 如果就绪任务的优先级高于或等于当前运行任务的优先级,处于就绪状态的任务进入运行状态。
- 如果就绪任务的优先级低于当前运行任务的优先级,处于就绪状态的任务无法获得CPU使用权,继续处于就绪状态。
就绪的任务获取CPU的使用权,进入运行状态,这个过程叫做切入(switch in)。相应地,处于运行状态的任务被调度器调度为就绪状态,这个过程称为切出(switch out)。
1.2.2.运行状态
在单核处理器上,占有CPU并运行的任务就处于运行状态。处于运行状态的高优先级任务如果一直运行,将一直占用CPU,在任务调度时,低优先级的就绪任务就无法获得CPU的使用权,无法实现多任务的运行。因此,处于运行状态的任务,应该在空闲的时候让出CPU的使用权。
处于运行状态的任务,有两种主动让出CPU使用权的方法,一种是执行函数vTaskSuspend()进入挂起状态,另一种是执行阻塞式函数进入阻塞状态,这两种状态都是非运行状态,运行的任务就交出了CPU的使用权,任务调度器可以使其他就络状态的任务进入运行状态。
1.2.3.阻塞状态
阻塞(blocked)状态就是任务暂时让出CPU的使用权,处于等待的状态。运行状态的任务可以调用两类函数进入阻塞状态。
一类是时间延迟函数,如vTaskDelay()或yTaskDelayUntil()。处于运行状态的任务调用这类函数后,就进入阻状态,并延退指定的时间。延返时间到了后,又进入就绪状态,参与任务调度后,又可以进入运行状态。
另一类是用于进程间通信的事件请求函数,例如,请求信号量的函数xSemaphoreTake()处于运行状态的任务执行函数xSemaploreTmke()后,就进入阻塞状态,如果其他任务释放了信号量,或等待的超时时间到了,任务就从阻塞状态进入就绪状态。
在运行状态的任务中调用函数vTaskSuspend(),可以将一个处于阻塞状态的任务转入挂起状态。
1.2.4.挂起状态
挂起(suspended)状态的任务就是暂停的任务,不参与调度器的调度。其他3种状态的任务都可以通过函数vTaskSuspendO进入挂起状态。处于挂起状态的任务不能自动退出挂起状态,需要在其他任务里调用函数vTaskResume(),才能使一个挂起的任务变为就绪状态。
1.3 任务的优先级
在FreeRTOS中,每个任务都必须设置一个优先级。总的优先级个数由文件FreeRTOSConfig.h中的宏configMAX_PRIORITIES定义,默认值是56。优先级数字越小优先级越低,所以最低优先级是0,最高优先级是configMAX_PRIORITIES-1。在创建任务时,用户必须为任务设置初始的优先级,在任务运行起来后,还可以修改优先级。多个任务可以具有相同的优先级。
另外,参数configMAX_PRIORITIES可设置的最大值,以及调度器决定哪个就绪任务进入运行状态,还与参数configUSE_PORT_OPTIMISED_TASK_SELECTION的取值有关。根据这个参数的取值,任务调度器有两种方法。
(1)通用方法。若configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0,则为通用方法。通用方法是用C语言实现的,可以在所有的FreeRTOS移植版本上使用,configMAX_PRIORITIES的最大值也不受限制。
(2)架构优化的方法。若configUSE_PORT_OPTIMISEDTASKSELECTION设置为1,则为架构优化方法,部分代码是用汇编语言写的。运行速度比通用方法快。使用架构优化方法时,configMAX_PRIORITIES的最大值不能超过32。在使用Cortex-M0架构或CMSIS-RTOS V2接口时,不能使用架构优化方法。
本系列博客示例使用的开发板上的处理器是STM32F407,FreeRTOS的接口一般设置为CMSIS-RTOS V2,所以在CubeMX中,参数configUSE_PORT_OPTIMISED_TASK_SELECTION是不可修改的,总是Disabled。
1.4 空闲任务
在main()函数中,调用osKemcIStart()启动FreeRTOS的任务调度器时,FreeRTOS会自动创建一个空闲任务(idle task),空闲任务的优先级为0,也就是最低优先级。
在FreeRTOS中,任何时候都需要有一个任务占用CPU,处于运行状态。如果用户创建的任务都不处于运行状态,例如,都处于阻塞状态,空闭任务就占用CPU处于运行状态。
空闲任务是比较重要的,也有很多用途。与空闲任务相关的配置参数有如下几个。
- configUSE_TICK_HOOK,是否使用空闲任务的钩子函数,若配置为1,则可以利用空闲任务的钩子函数,在系统空闲时做一些处理。在后面博客中,会介绍如何利用空闲任务钩子函数使系统进入低功耗状态。
- configDLE_SHOULD_YIELD,空闲任务是否对同等优先级的用户任务主动让出CPU使用权,这会影响任务调度结果。
- configUSE_TICKLESSIDLE,是否使用tickless低功耗模式,若设置为1,可实现系统的低功耗。在后面博客中,我们会介绍tickiess低功模式的原理和作用。
1.5 基础时钟与喵嗒信号
FreeRTOS自动采用SxsTick定时器作为FreeRTOS的基础时钟。SysTick定时器只有定时中断功能,其定时频率由参数confgTICK_RATE_HZ指定,默认值为1000,也就是1ms中断一次。
在FreeRTOS中有一个全局变量xTickCount,在SysTick每次中断时,这个变量加1,也就是每1ms变化一次。所谓的FreeRTOS的嘀嗒信号,就是指全局变量xTickCount的值发生变化,所以嘀嗒信号的变化周期是1ms。通过函数xTaskGetTickCount()可以获得全局变量xTickCount的值,延时函数vIaskDelay()和vTaskDelayunil()就是通过嘀嗒信号实现毫秒级延时的。
SysTick定时器中断不仅用于产生嘀嗒信号,还用于产生任务切换申请,其原理在后面会介绍。
二、FreeRTOS的任务调度
2.1 任务调度方法概述
FreeRTOS有两种任务调度算法,基于优先级的抢占式(pre-emptive)调度算法和合作式(Co-oporaive)调度算法。其中,抢占式调度算法可以使用时间片,也可以不使用时间片。通过参数的设置,用户可以选择具体的调度算法。FreeRTOS的任务调度方法有3种其对应的参数名称、取值及特点见下表。
在FreeRTOS中,默认的是使用带有时间片的抢占式任务调度方法。在CubeMX中,用户不能设置参数configUSE_TIME_SLICING,其默认值为1。在后面的示例中,如果不特别说明,均采用使用时间片的抢占式任务调度方法。
2.2 使用时间片的抢占式调度方法
抢占式任务调度方法,是FreeRTOS主动进行任务调度,分为使用时间片和不使用时间片两种情况。
FreeRTOS基础时钟的一个定时周期称为一个时间片(time slice),FreeRTOS的基础时钟是SysTick定时器。基础时钟的定时周期由参数confgTICK_RATE_HZ决定,默认值为1000Hz,所以时间片长度为1ms。当使用时间片时,在基础时钟的每次中断里,系统会要求进行一次上下文切换(coutext switching)。文件port.c中的函数xPortSysTickHandler()就是SysTick定时中断的处理函数,其代码如下:
void xPortsysTickHanaler(void)
{
/*sysTick的抢占优先级是15,优先级最低*/
portDISABLE_INTERRUPTS(); //禁用质有中断
{
if(xTaskIncrementTick()!=pdEAISE) //增加RTOS嘀嗒计数器的值
{
/*将PendSV中断的挂起标志位置位,申请进行上下文切换,在PendSV中断里处理下文切换*/
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BITF;
}
}
POrtENABLE_INTERRUPTS();/使能中断
}
这个函数的功能就是将PendSV(Pendable request for system service,可挂起的系统服务请求)中断的挂起标志位置位,也就是发起上下文切换的请求,而进行上下文切换是在PendSV的中断服务函数里完成的。文件port.c中的函数xPortPendSVHandler()是FreeRTOS的PendSV中断服务函数,其功能就是根据任务调度计算的结果,选择下一个任务进入运行状态。这个函数的代码是用汇编语言写的,这里就不展示和分析其源代码了。
在CubeMX中,一个项目使用了FreeRTOS后,会自动对NVIC做一些设置。FreeRTOS在最低优先级PendSV的中断服务函数里进行上下文切换,所以,FreeRTOS的任务切换的优先级总是低于系统中断的优先级。
使用时间片的抢占式调度方法的特点如下:
- 在基础时钟每个中断里发起一次任务调度请求。
- 在PendSV中断服务函数里进行上下文切换。
- 在上下文切换时,高优先级的就绪任务获得CPU的使用权。
- 若多个就绪状态的任务的优先级相同,则将轮流获得CPU的使用权。
下图所示的是使用带时间片的抢占式任务调度方法时,3个任务运行的时序图。图中的横轴是时间轴,纵轴是系统中的任务。垂直方向的虚线表示发生任多复换的时间点,水平方向的实心矩形表示任务占据CPU处于运行状态的时间段,水平方向的虚线表示任务处于就绪状态
的时间段,水平方向的空自段表示任务处于阻塞状态或挂起状态的时间段。
上图可以说明带时间片的抢占式任务调度方法的特点。假设Task2具有高优先级,Task1具有正常优先级,且这两个任务的优先级都高于空闲任务的优先级。我们从这个时序图可以看到这3个任务的运行和任务切换的过程:
- t1时刻开始是空闲任务在运行,这时候系统里没有其他任务处于就绪状态。
- 在t2时刻进行调度时,Task1抢占CPU开始运行,因为Task1的优先级高于空闲任务。
- 在t3时刻,Task1进入阻塞状态,让出了CPU的使用权,空闲任务又进入运行状态。
- 在t4时刻,Task1又进入运行状态。
- 在t5时刻,更高优先级的Task2抢占了CPU开始运行,Task1进入就绪状态。
- 在t6时刻,Task2运行后进入阻塞状态,让出CPU使用权,Task1从就绪状态变为运 行状态。
- 在t7时刻,Task1进入阻塞状态,主动让出CPU使用权,空闲任务又进入运行状态。
从上图的多任务运行过程可以看出,在低优先级任务运行时,高优先级的任务能抢占获得CPU的使用权。在没有其他用户任务运行时,空闲任务处于运行状态,否则,空闲任务处于就绪状态。
当多个就绪状态的任务优先级相同时,它们将轮流获得CPU的使用权,每个任务占用CPU运行1个时间片的时间。如果就绪任务的优先级与空闲任务的优先级都相同,参数configDLE_SHOULD_YIELD就会影响任务调度的结果。
- 如果configDLE_SHOULD_YIELD设置为0,表示空闲任务不会主动让出CPU的使用权,空闲任务与其他优先级为0的就绪任务轮流使用CPU。
- 如果configDLE_SHOULD_YIELD设置为1,表示空闲任务会主动让出CPU的使用权,空闲任务不会占用CPU。
参数configIDLESHOULD_YIELD的默认值为1。设计用户任务时,用户任务的优先级一般要高于空闲任务。
2.3 不使用时间片的抢占式调度方法
当配置为不使用时间片的抢占式调度方法时,任务选择和抢占式的算法是相同的,只是对于相同优先级的任务,不再使用时间片平均分配CPU使用时间。
使用时间片的抢占式调度方法,在基础时钟每次中断时进行一次上下文切换请求,从而进行任务调度;而不使用时间片的抢占式调度算法,只在以下情况下才进行任务调度。
- 有更高优先级的任务进入就绪状态时。
- 运行状态的任务进入阻塞状态或挂起状态时。
所以,不使用时间片时,进行上下文切换的频率比使用时间片时低,从而可降低CPU的负担,但是,对于同等优先级的任务,可能会出现占用CPU时间相差很大的情况。
下图所示的的是不使用时间片的抢占式任务调度方法,存在同等优先级任务时的任务运行时序图。图中Task0与空闲任务优先级相同,且是连续运行的。参数configDLE_SHOULD_YIELD值为0(即空闲任务不会主动让出CPU的使用权给Task0)。
- 在t1时刻,空闲任务占用CPU,因为系统里没有其他处于就绪状态的任务。
- 在t2时刻,Task0进入就绪状态。但是Task0与空闲任务优先级相同,且调度算法不使用时间片,不会让Task0和空闲任务轮流使用CPU,所以Task0就保持就绪状态。
- 在t4时刻,高优先级的Task1l抢占CPU。
- 在t5时刻,Task1进入阻塞状态,系统进行一次任务调度,Task0获得CPU的使用权。
- 在t6时刻,Task1再次抢占CPU,Task0又进入就绪状态。
- 在t7时刻,Task1进入阻塞状态,系统进行一次任务调度,空闲任务获得CPU使用权。
之后没有发生任务调度的机会,所以Task0就一直处于就绪状态 。
2.4 合作式任务调度方法
使用合作式任务调度方法时,FreeRTOS不主动进行上下文切换 ,而是当运行状态的任务进入阻塞状态时,或运行状态的任务调用函数taskYIELD()时,才会进行一次上下文切换。任务不会发生抢占,所以也不使用时间片。函数taskYIELD()的作用就是主动申请进行一次上下文切换。
下图所示的是使用合作式任务调度方法时,3个不同优先级任务的运行时序图,可以体现合作式任务调度方法的特点:
- 在t1时刻,低优先级的Task1处于运行状态。
- 在t2时刻,中等优先级的Task2进入就绪状态,但不能抢占CPU。
- 在t3时刻,高优先级的Task3进入就绪状态,但是也不能抢占CPU 。
- 在t4时刻,Task1调用函数taskYIELD(),主动申请进行一次上下文切换,高优先级的 Task3获得CPU使用权。
- 在t5时刻,Task3进入阻塞状态,就绪的Task2获得CPU的使用权。
- 在t6时刻,Task2进入阻塞状态,Task1又获得CPU使用权。
三、往期回顾
STM32Cube高效开发教程<高级篇><FreeRTOS>(一)-----FreeRTOS基础
STM32Cube高效开发教程<高级篇><FreeRTOS>(二)-----FreeRTOS的文件组成和基本原理