目录
一个嵌入式操作系统的核心功能就是任务管理功能,尤其是多任务管理功能。
一、FreeRTOS任务管理基础
1、多任务运行基本机制
在FreeRTOS中,一个任务就是实现某种功能的一个函数,任务函数的内部一般有一个死循环结构。任何时候都不允许从任务函数退出,也就是不能出现return语句。如果需要结束任务,在任务函数里,可以跳出死循环,然后使用函数vTaskDelete()删除任务自己,也可以在其他任务里调用函数vTaskDelete()删除这个任务。
在FreeRTOS里,用户可以创建多个任务。每个任务需要分配一个栈(stack)空间和一个任务控制块(Task Control Block,TCB)空间。每个任务还需要设定一个优先级,优先级的数字越小,表示优先级越低(注:RTOS里的任务优先级和NVIC里的中断优先级是不同的概念,不要混为一谈)。
在单核处理器上,任何时刻只能有一个任务占用CPU并运行。但是在RTOS系统上,运行多外任务时,运行起来却好像多个任务在同时运行,这是由于RTOS的任务调度多个任务时,使得CPU实现了分时复用的功能。
最简单的基于时间片的多任务运行原理:假设只有2个任务,并且任务Taskl和Task2具有相同的优先级。圆周表示CPU时间,如同钟表的一圈,RTOS将CPU时间分成基本的时间片(time slice),例如,FreeRTOS默认的时间片长度是1ms,也就是SysTick定时器的定时周期。在一个时间片内,会有一个任务占用CPU并执行,假设当前运行的任务是Task1。一个时间片结束时(实际就是SysTick定时器发生中断时)进行任务调度,由于Taskl和Task2有相同的优先级,RTOS会将CPU使用权交给Task2。Task1交出CPU使用权时,会将CPU的当前场景(CPU各个核心寄存器的值)压入自己栈空间。而Task2获取CPU的使用权时,会用自己栈空间保存的数据恢复CPU场景,因而Task2可以从上次运行的状态继续运行。
基于时间片的多任务调度就是这样控制多个同等优先级任务实现CPU的分时复用,从而实现多任务运行的。因为时间片的长度很短(默认是1ms),任务切换的速度非常快,所以程序运行时,给用户的感觉就是多个任务在同时运行。
当多个任务的优先级不同时,FreeRTOS还会使用基于优先级的抢占式任务调度方法,每个任务获得的CPU使用时间长度可以是不一样的。
2、RTOS任务管理中的任务状态
由单核CPU的多任务运行机制可知,任何时刻,只能有一个任务占用CPU并运行,这个任务的状态称为运行(running)状态,其他未占用CPU的任务的状态都可称为非运行(notrunning)状态。非运行状态又可以细分为3个状态,任务的各个状态以及状态之间的转换如图:
FreeRTOS任务调度有抢占式(pre-emptive)和合作式(co-operative)两种方式,一般使用基于任务优先级的抢占式任务调度方法。
(1)就绪状态
任务被创建之后就处于就绪(ready)状态。FreeRTOS的任务调度器在基础时钟每次中断时进行一次任务调度申请,根据抢占式任务调度的特点,任务调度的结果有以下几种情况。
- 如果当前没有其他处于运行状态的任务,处于就绪状态的任务进入运行状态。
- 如果就绪任务的优先级高于或等于当前运行任务的优先级,处于就绪状态的任务进入运行状态。
- 如果就绪任务的优先级低于当前运行任务的优先级,处于就绪状态的任务无法获得CPU使用权,继续处于就绪状态。
就绪的任务获取CPU的使用权,进入运行状态,这个过程称为切入(switch in)。相应地,处于运行状态的任务被调度器调度为就绪状态,这个过程称为切出(switch out)。
(2)运行状态
在单核处理器上,占有CPU并运行的任务就处于运行状态。处于运行状态的高优先级任务如果一直运行,将一直占用CPU,在任务调度时,低优先级的就绪任务就无法获得CPU的使用权,无法实现多任务的运行。因此,处于运行状态的任务,应该在空闲的时候让出CPU的使用权。
处于运行状态的任务,有两种主动让出CPU使用权的方法,一种是执行函数vTaskSuspend()进入挂起状态,另一种是执行阻塞式函数进入阻塞状态。这两种状态都是非运行状态,运行的任务就交出了CPU的使用权,任务调度器可以使其他就绪状态的任务进入运行状态。
(3)阻塞状态
阻塞(blocked)状态就是任务暂时让出CPU的使用权,处于等待的状态。运行状态的任务可以调用两类函数进入阻塞状态。
一类是时间延迟函数,如vTaskDelay()或vTaskDelayUntil()。处于运行状态的任务调用这类函数后,就进入阻塞状态,并延迟指定的时间。延迟时间到了后,又进入就绪状态,参与任务调度后,又可以进入运行状态。
另一类是用于进程间通信的事件请求函数,例如,请求信号量的函数xSemaphoreTake()。处于运行状态的任务执行函数xSemaphoreTake()后,就进入阻塞状态,如果其他任务释放了信号量,或等待的超时时间到了,任务就从阻塞状态进入就绪状态。
在运行状态的任务中调用函数vTaskSuspend(),可以将一个处于阻塞状态的任务转入挂起状态。
(4)挂起状态
挂起(suspended)状态的任务就是暂停的任务,不参与调度器的调度。其他3种状态的任务都可以通过函数vTaskSuspend()进入挂起状态。处于挂起状态的任务不能自动退出挂起状态,需要在其他任务里调用函数vTaskResume(),才能使一个挂起的任务变为就绪状态。
3、RTOS任务优先级
在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_OPTIMISED_TASK_SELECTION设置为1,则为架构优化方法,部分代码是用汇编语言写的,运行速度比通用方法快。使用架构优化方法时,configMAX_PRIORITIES的最大值不能超过32。(当处理器是STM32F407,FreeRTOS的接口设置为CMSIS-RTOSV2,参数USE_PORT_OPTIMISED_TASK_SELECTION是不可修改的,总是Disabled。)
4、空闲任务
在main()函数中,调用osKernelStart()启动FreeRTOS的任务调度器时,FreeRTOS会自动创建一个空闲任务(idle task),空闲任务的优先级为0,也就是最低优先级。
在FreeRTOS中,任何时候都需要有一个任务占用CPU,处于运行状态。如果用户创建的任务都不处于运行状态,例如,都处于阻塞状态,空闲任务就占用CPU处于运行状态。
空闲任务是比较重要的,也有很多用途。与空闲任务相关的配置参数有如下几个。
- configUSE_IDLE_HOOK,是否使用空闲任务的钩子函数,若配置为1,则可以利用空闲任务的钩子函数,在系统空闲时做一些处理。
- configIDLE_SHOULD_YIELD,空闲任务是否对同等优先级的任务主动让出CPU使用权,这会影响任务调度结果。
- configUSE_TICKLESS_IDLE,是否使用tickless低功耗模式,若设置为1,可实现系统的低功耗。
5、基础时钟与嘀嗒信号
FreeRTOS自动采用SysTick定时器作为FreeRTOS的基础时钟。SysTick定时器只有定时中断功能,其定时频率由参数configTICK_RATE_HZ指定,默认值为1000,也就是1ms中断一次。
在FreeRTOS中有一个全局变量xTickCount,在SysTick每次中断时,这个变量加1,也就是每1ms变化一次。所谓的FreeRTOS的嘀嗒信号,就是指全局变量xTickCount的值发生变化,所以嘀嗒信号的变化周期是1ms。通过函数xTaskGetTickCount()可以获得全局变量xTickCount的值,延时函数vTaskDelay()和vTaskDelayUntil()就是通过嘀嗒信号实现毫秒级延时的。
SysTick定时器中断不仅用于产生嘀嗒信号,还用于产生任务切换申请。
二、FreeRTOS的任务调度
FreeRTOS有两种任务调度算法,基于优先级的抢占式(pre-emptive)调度算法和合作式(co-operative)调度算法。其中,抢占式调度算法可以使用时间片,也可以不使用时间片。通过参数的设置,用户可以选择具体的调度算法。FreeRTOS的任务调度方法有3种,其对应的参数名称、取值及特点见表:
谓度方式 |
宏定义参数 |
取值 |
特点 |
抢占式 (使用时节 ) |
configUSE_PREEMPTION |
1 |
基于优先级的抢占式任务调度,同等优先级任 |
configUSE_TIME_SLICING |
1 |
||
抢占式 (不使用间片) |
configUSE_PREEMPTION |
1 |
基于优先级的抢占式任务调度,同等优先级任 |
configUSE_TIME_SLICING |
0 |
||
合作式 |
configUSE_PREEMPTION |
0 |
只有当运行状态的任务进入阻塞状态,或显式 |
configUSE_TIME_SLICING |
任意 |
在FreeRTOS中,默认的是使用带有时间片的抢占式任务调度方法。在CubeMX中,参数configUSE_TIME_SLICING的默认值为1。
2、使用时间片的抢占式调度方法
抢占式任务调度方法,是FreeRTOS主动进行任务调度,分为使用时间片和不使用时间片情况。
FreeRTOS基础时钟的一个定时周期称为一个时间片(timeslice),FreeRTOS的基础时钟是SysTick定时器。基础时钟的定时周期由参数configTICK_RATE_HZ决定,默认值为1000Hz,所以时间片长度为1ms。当使用时间片时,在基础时钟的每次中断里,系统会要求进行一次上下文切换(context switching)。文件port.c中的函数xPortSysTickHandler()就是SysTick定时中断的处理函数,其代码如下:
//该函数位于port.c中
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
executes all interrupts must be unmasked. There is therefore no need to
save and then restore the interrupt mask value as its value is already
known. */
portDISABLE_INTERRUPTS();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}
这个函数的功能就是将PendSV(Pendable request for system service,可挂起的系统服务请求)中断的挂起标志位置位,也就是发起上下文切换的请求,而进行上下文切换是在PendSV断服务函数里完成的。文件port.c中的函数xPortPendSVHandler()是FreeRTOS的PendSV中断服务函数,其功能就是根据任务调度计算的结果,选择下一个任务进入运行状态。这个函数的代码是用汇编语言写的。
在CubeMX中,一个项目使用了FreeRTOS后,会自动对NVIC做一些设置。系统默认地自动将优先级分组方案设置为4位全部用于抢占优先级,SysTick和PendSV中断的抢占优先级都是15,也就是最低优先级。FreeRTOS在最低优先级的PendSV的中断服务函数里进行上下文切换,所以,FreeRTOS的任务切换的优先级总是低于系统中断的优先级。
使用时间片的抢占式调度方法的特点如下:
- 在基础时钟每个中断里发起一次任务调度请求。
- 在PendSV中断服务函数里进行上下文切换。
- 在上下文切换时,高优先级的就绪任务获得CPU的使用权。
- 若多个就绪状态的任务的优先级相同,则将轮流获得CPU的使用权。
(1)举例说明时间片任务调度
例如,使用带时间片的抢占式任务调度方法时,3个任务运行的时序图。图中的横轴是时间轴,纵轴是系统中的任务。垂直方向的虚线表示发生任务切换的时间点,水平方向的实心矩形表示任务占据CPU处于运行状态的时间段,水平方向的虚线表示任务处于就绪状态的时间段,水平方向的空白段表示任务处于阻塞状态或挂起状态的时间段。
- t1时刻开始是空闲任务在运行,这时候系统里没有其他任务处于就绪状态。
- 在t2时刻进行调度时,Task1抢占CPU开始运行,因为Task1的优先级高于空闲任务。
- 在t3时刻,Task1进入阻塞状态,让出了CPU的使用权,空闲任务又进入运行状态。
- 在t4时刻,Task1又进入运行状态。
- 在t5时刻,更高优先级的Task2抢占了CPU开始运行,Taskl即使进入就绪状态也无权占有CPU。
- 在t6时刻,Task2运行后进入阻塞状态,让出CPU使用权,Task1从就绪状态变为运行状态。
- 在t7时刻,Task1进入阻塞状态,主动让出CPU使用权,空闲任务又进入运行状态。
从图中的多任务运行过程可以看出,在低优先级任务运行时,高优先级的任务能抢占获得CPU的使用权。在没有其他用户任务运行时,空闲任务处于运行状态,否则,空闲任务处于就绪状态。
当多个就绪状态的任务优先级相同时,它们将轮流获得CPU的使用权,每个任务占用CPU运行1个时间片的时间。如果就绪任务的优先级与空闲任务的优先级都相同,参数configIDLE_SHOULD_YIELD就会影响任务调度的结果。
- 如果configIDLE_SHOULD_YIELD设置为0,表示空闲任务不会主动让出CPU的使用权,空闲任务与其他优先级为0的就绪任务轮流使用CPU。
- 如果configIDLE_SHOULD_YIELD设置为1,表示空闲任务会主动让出CPU的使用权,空闲任务不会占用CPU。
参数configIDLE_SHOULD_YIELD的默认值为1。设计用户任务时,用户任务的优先级一般要高于空闲任务。
3、不使用时间片的抢占式调度方法
当配置为不使用时间片的抢占式调度方法时,任务选择和抢占式的算法是相同的,只是对于相同优先级的任务,不再使用时间片平均分配CPU使用时间。
使用时间片的抢占式调度方法,在基础时钟每次中断时进行一次上下文切换请求,从而进行任务调度;而不使用时间片的抢占式调度算法,只在以下情况下才进行任务调度。
- 有更高优先级的任务进入就绪状态时。
- 运行状态的任务进入阻塞状态或挂起状态时。
所以,不使用时间片时,进行上下文切换的频率比使用时间片时低,从而可降低CPU的负担。但是,对于同等优先级的任务,可能会出现占用CPU时间相差很大的情况。
(1)举例说明非时间片任务调度
Task1与空闲任务优先级相同,且是连续运行的。参数configIDLE_SHOULD_YIELD值为0。
- 在t1时刻,空闲任务占用CPU,因为系统里没有其他处于就绪状态的任务。
- 在t2时刻,Task1进入就绪状态。但是Task1与空闲任务优先级相同,且调度算法不使用时间片,不会让Task1和空闲任务轮流使用CPU,所以Task1就保持就绪状态。
- 在t4时刻,高优先级的Task2抢占CPU。
- 在t5时刻,Task2进入阻塞状态,系统进行一次任务调度,Task1获得CPU的使用权。
- 在t6时刻,Task2再次抢占CPU,Task1又进入就绪状态。
- 在t7时刻,Task2进入阻塞状态,系统进行一次任务调度,空闲任务获得CPU使用权。之后没有发生任务调度的机会,所以Task1就一直处于就绪状态。
4、合作式任务调度方法
使用合作式任务调度方法时,FreeRTOS不主动进行上下文切换,而是当运行状态的任务进入阻塞状态时,或运行状态的任务调用taskYIELD()时,才会进行一次上下文切换。任务不会发生抢占,所以也不使时间片。函数taskYIELD()的作用就是主动申请进行一次上下文切换。
(1)举例说明合作式任务调度
3个不同优先级任务采用合作式任务调度方法的运行时序图:
- 在t1时刻,低优先级的Task1处于运行状态。
- 在t2时刻,中等优先级的Task2进入就绪状态,但不能抢占CPU。
- 在t3时刻,高优先级的Task3进入就绪状态,但是也不能抢占CPU。
- 在t4时刻,Taskl调用函数taskYIELD(),主动申请进行一次上下文切换,高优先级的Task3获得CPU使用权。
- 在t5时刻,Task3进入阻塞状态,就绪的Task2获得CPU的使用权。
- 在t6时刻,Task2进入阻塞状态,Task1又获得CPU使用权。