【FreeRTOS】任务管理

在介绍本文之前,向大家推荐个非常容易入门的人工智能学习网站,建议点击收藏❤️

1. 前言

通过阅读本文可以了解到:

  • FreeRTOS是如何给每个任务分配CPU时间的
  • FreeRTOS是如何在任意时间选择运行某个任务的
  • 每个任务的优先级对系统的影响是什么
  • 任务存在哪些状态
  • 任务管理的操作有哪些
  • 当空闲任务执行时可以干什么

2. 任务简介

每个任务函数的定义是有规定的,有一个void *参数和返回一个void类型参数,原型如下:

void ATaskFunction( void *pvParameters );

每个任务都相当于是个小程序,有一个入口点,里面是个死循环,一直运行或者直到删除。FreeRTOS任务不允许从他们的实现函数内返回,不允许包含’return’语句,如果不需要了应该删除该任务。一个任务函数可以用来创建任意数量的任务,每个任务都有自己的堆栈。

3. 顶层任务状态

一个系统通常有多个任务,对于单核的处理器同一时刻内只能运行一个任务。这意味着任务可以存在两种状态:“运行”和“非运行”,其中“非运行”包含许多子状态。当任务处于运行状态时,处理器会运行该任务的代码。当任务处于“未运行”状态时,任务处于休眠状态。当一个任务恢复执行时,它会从上次离开运行状态之前执行的指令执行。

4. 创建任务

4.1 xTaskCreate()

	BaseType_t xTaskCreate(	TaskFunction_t pxTaskCode,
							const char * const pcName,
							const configSTACK_DEPTH_TYPE usStackDepth,
							void * const pvParameters,
							UBaseType_t uxPriority,
							TaskHandle_t * const pxCreatedTask )
  • pxTaskCode
    任务是个无限循环的函数,pxTaskCode参数只是指向实现任务函数的指针(实际上只是函数名称)。
  • pcName
    任务的描述性名称,configMAX_TASK_NAME_LEN定义了任务名称的最长长度,超过则自动截断。
  • usStackDepth
    每个任务都有自己的唯一堆栈,该堆栈在创建任务时由内核分配给任务。usStackDepth指定了任务堆栈的大小。该值指定堆栈可以容纳的字数,而不是字节数。比如usStackDepth等于100时,则将分配400字节的堆栈空间(1个字等于4个字节)。总字节数不能超过uint16_t类型变量的最大值。
    空闲任务使用的堆栈大小由应用车供需定义的常量configMINIMAL_STACK_SIZE决定。在源码中的DEMO中使用的值是官方建议的最小任务堆栈值,如果实际的任务需要大量的堆栈空间,则必须分配更大的值。
    没有比较好的方法来计算任务所需要的堆栈空间,但是大多数用户只需分配他们认为合理的值,然后通过提供的查看相关使用情况的接口来判断是否合理(不能少但是也不能浪费)。
  • pvParameters
    任务函数的参数。
  • uxPriority
    任务优先级,从0到(configMAX_PRIORITIES -1),数值越高优先级越高。
  • pxCreatedTask
    任务句柄。
  • 返回值
    有可能因为可用的堆内存不足导致失败,失败反馈pdFAIL。

5. 任务优先级

任务创建时可以通过参数来指定任务优先级。使用vTaskPrioritySet()可以在启动调度程序后更改优先级。任意数量任务可以共享相同的优先级,确保最大的设计灵活性。
FreeRTOS调度程序可以使用以下两种方法之一来确定哪个任务将处于运行状态,同时configMAX_PRIORITIES可以设置的最大值取决于使用的方法:

  • 通用方法
    通用方法在C中实现,可以与所有的FreeRTOS架构移植一起使用。使用通用方法时,FreeRTOS不会限制configMAX_PRIORITIES可以设置的最大值。但是建议越小越好,因为值越高,消耗的RAM越多,最坏情况下的执行时间越长。如果想设置成通用方法,需要配置头文件中的configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0或者不需要定义它。

  • 架构优化方法
    这种方法需要使用少量汇编代码,它的效率比通用方法更高。configMAX_PRIORITIES设置不会影响最坏执行时间,如果使用架构优化方法,那么configMAX_PRIORITIES 不能大于32。与通用方法一样,建议将configMAX_PRIORITIES保持必要的最小值,因为值越大,消耗的RAM越多。如果配置文件中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,则使用架构优化方法。备注:并非所有FreeRTOS移植都提供了架构方法。
    FreeRTOS调度程序将始终确保能够运行的最高优先级任务是选择进入运行状态的任务。如果能够运行多个具有相同优先级的任务,则调度程序将依次把每个任务转换为运行状态和退出运行状态。

6. 嘀嗒中断和时间测量

调度算法有“时间片”的可选功能,意思是每个任务都有相同的运行时间。为了能够选择要运行的下一个任务,调度程序本身必须在每个时间片的末尾执行,时间片的长度由嘀嗒中断频率设置,通过配置文件的configTICK_RATE_HZ决定。
下图可一演示调度算法在运行时的执行情况:
在这里插入图片描述
FreeRTOS API调用总是以嘀嗒周期的倍数指定时间,通常称为嘀嗒。pdMS_TO_TICKS()宏以毫秒为单位的时间切换为嘀嗒单位。可用的分辨率取决于所定义的嘀嗒频率,如果嘀嗒频率高于1KHz(configTICK_RATE_HZ大于1000),则不能使用pdMS_TO_TICKS()。

7. “非运行”态

为了使任务有用,必须使他们成为事件驱动的。事件驱动的任务只有在触发它的事件发生后才能执行工作,并且在该事件发生之前无法进入运行状态。调度程序始终选择能够运行的最高优先级任务,因此使用事件驱动任务意味着需要使用不同的优先级创建任务。

7.1 阻塞状态

正在等待事件的任务被称为阻塞状态,这是“未运行”的子状态。任务可以进入阻塞状态,等待两种不同的事件:

  • 时间:任务同调用了延时接口,该任务会阻塞延时接口指定的时间。
  • 同步事件:该事件来自另一个任务或中断。
    FreeRTOS的队列、二值信号量、计数信号量、互斥量、递归互斥体、事件组和任务通知都可用于创建同步事件。任务可以在阻塞时设置带有超时的同步事件,同时有效地阻塞两种类型的事件。

7.2 挂起状态

“挂起”状态是“非运行”的子状态。处于挂起态的任务对于调度程序是不可用的,进入挂起态的唯一方法是通过调用vTaskSuspend()函数,调用vTaskResume()或xTaskResumeFromISR()是退出挂起态的唯一出路。大多数应用程序不使用挂起状态。

7.3 就绪状态

处于“未运行”状态但未被阻塞或挂起的任务称处于“就绪状态”。他们等待运行,可以被调度器使用。

7.4 状态转换图

状态间的切换

7.5 vTaskDelay()

vTaskDelay()将调用它的任务置于阻塞状态,可以通过参数指定阻塞的时间。

  • xTicksToDelay
    指定调用任务在转换回就绪状态之前将保持在阻塞状态的嘀嗒中断数。比如调用xTicksToDelay(100)的任务在嘀嗒计数为10000时立即进入阻塞状态,直到嘀嗒计数达到100100。可以通过宏pdMS_TO_TICKS()将毫秒为单位的时间装换为int为单位的时间,例如调用xTicksToDelay(pdMS_TO_TICKS(100))将导致调用任务保持阻塞100毫秒。

7.6 vTaskDelayUntil()

根本作用类似于vTaskDelay(), vTaskDelay()参数指定了调用任务从阻塞切换到就绪之间的嘀嗒数,任务离开阻塞的时间与调用vTaskDelay();vTaskDelayUntil()的参数指定调用任务应该从阻塞状态移动到就绪态的时间,vTaskDelayUntil()是需要在固定执行期间调用的,因为调用任务被解除阻塞的时间是绝对的,而不是相对调用函数时的时间点。。

  • pxPreviousWakeTime
    用于实现定期执行且具有固定频率的任务。在这种情况下,pxPreviousWakeTime保持任务最后一次离开阻塞状态的时间,以此时间作参考点,用于计算任务下次离开阻塞的时间。pxPreviousWakeTime指定的变量在函数中自动更新,它通常不会被应用程序代码修改,但必须在第一次使用之前初始化为当前嘀嗒计数。
  • xTimeIncrement
    用于实现定期执行且具有固定频率的任务,频率由xTimeIncrement指定,单位是嘀嗒数。
  • vTaskDelayUntil()举例使用
void vTaskFunction( void *pvParameters )
{
	char *pcTaskName;
	TickType_t xLastWakeTime;

    /* 要打印的字符串通过参数传入。将此转换为字符指针。 */
    pcTaskName = ( char * ) pvParameters;

    /* 需要使用当前滴针计数初始化 xLastWakeTime 变量。注意,这是唯一一次显式地写入变量。
    在此之后,xLastWakeTime 将在 vTaskDelayUntil() 中自动更新。 */
    xLastWakeTime = xTaskGetTickCount();

    /* 对于大多数任务,都是在一个无限循环中实现的。 */
    for( ;; )
    {
        /* 打印出此任务的名称。 */
        vPrintString( pcTaskName );

        /* 这个任务应该精确地每 250 毫秒执行一次。根据 vTaskDelay() 函数,时间是以滴答为
        单位度量的,pdMS_TO_TICKS() 宏用于将毫秒转换为滴答。xLastWakeTime 在 
        vTaskDelayUntil() 中自动更新,因此任务不会显式地更新 xLastWakeTime。 */
        vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ));
    }
}

8. 空闲任务和空闲任务钩子函数

8.1 空闲任务

FreeRTOS必须始终至少有一个任务进入运行状态。为确保这种情况,开启调度器时,调度器会自动创建一个空闲任务,空闲任务只是一个循环中。空闲任务具有可能的最低优先级,这样才不会阻塞更高优先级的应用程序进入运行状态。

8.2 空闲任务钩子函数

可以通过空闲任务函数将应用程序特定功能直接添加到空闲任务中:每次空闲任务运行时调用。
空闲任务钩子函数常用功能:

  • 执行低优先级、后台或连续处理功能。
  • 测量备用处理能力的数量。(当所有优先级较高的应用程序任务无法执行时,空闲任务将会运行;因此,测量分配给空闲任务的处理时间量可清楚得指示多少处理时间)
  • 将处理器置于低功耗模式。

8.3 对空闲任务钩子函数实现的限制

  • 绝对不能阻塞或挂起
  • 如果应用程序使用了xTaskDelete()函数,那么空闲任务钩子函数必须总是在合理的时间段内返回。这是因为空闲任务负责在删除任务之后清理内核资源。如果空闲任务永久地保留在空闲钩子函数中,则无法进行清理。
  • 以任何方式阻塞空闲任务都可能导致无法使用任何任务进入运行状态。

9. 更改任务的优先级

9.1 vTaskPrioritySet()

vTaskPrioritySet()函数可用于在调度程序启动后更改任何任务的优先级。

  • pxTask
    想要修改优先级的任务句柄。当传入NULL是代表修改当前任务的优先级。
  • uxNewPriority
    要设置的任务优先级。

9.2 uxTaskPriorityGet()

查询任务的优先级。

  • pxTask
    需要查询优先级的任务句柄。
  • 返回值
    查询到的任务优先级。

10. 删除任务

任务可以使用vTaskDelete()函数来删除自身或其他任务,被删除的任务将不再存在,且不能再进入运行模式。释放已删除的任务所分配的内存时空闲任务的职责。

11. 调度算法

11.1 任务状态和事件的回顾

在单核处理器上,在任意时间只能有一个任务处于运行态。未运行的任务,既没有处于阻塞,也没有处于挂起,那就是就绪态。调度程序可以选择处于就绪态的任务作为进入运行状态的任务,而且总是选择优先级最高的就绪态任务来进入运行态。
任务可以在阻塞态中等待事件,并在事件发生时自动移动回就绪态。

11.2 配置调度算法

调度算法是决定哪个就绪态的任务变成运行态的程序。在配置文件中可以通过configUSE_PREEMPTION和configUSE_TIME_SLICING更改算法。configUSE_TICKLESS_IDLE也会影响调度算法,因为它的使用会导致嘀嗒中断在较长时间内完全关闭,configUSE_TICKLESS_IDLE 是个高级选项,专门用于必须最小化其功耗的应用程序。
在所有可能的配置中,FreeRTOS调度程序将确保选择共享优先级的任务依次进入运行状态,这种情况通常称为“循环调度”。循环调度算法不保证在相同优先级的任务之间平均分配时间。

11.3 具有时间片的优先级抢占式调度

当configUSE_PREEMPTION和configUSE_TIME_SLICING同时为1时,调度程序设置为“带有时间片的固定优先级预先调度”的调度算法。这是大多数小型RTOS应有程序使用的调度算法。

11.4 用于描述调度算法的相关术语

  • 固定优先级:该调度算法不会更改分配给正在调度的任务的优先级,但也不会阻止任务本身更改自己的优先级或其他任务的优先级。
  • 抢占:如果优先级高于运行态任务的任务进入就绪态,则抢占调度算法会立即抢占运行状态任务,则被抢占的任务进入就绪态。
  • 时间片:时间片由于相同优先级的任务之间共享CPU,时间片等于两次嘀嗒中断之间的时间。

11.5 空闲任务的调度方式

空闲任务有两种调度方式,通过宏configIDLE_SHOULD_YIELD来配置:

  • 当设置为0时,空闲任务将保持其整个时间片的运行状态,除非被更高优先级的任务抢占。
  • 当设置为1时,空闲任务将在其循环中自愿放弃分配的时间片的剩余部分,即使在就绪态下还有其他空闲优先级任务。
    在这里插入图片描述在这里插入图片描述

11.6 优先级抢占式调度(没有时间片)

没有时间片的优先级抢占式调度算法不使用时间片来共享相同优先级的任务之间的处理时间。设置方式如下:

  • configUSE_PREEMPTION = 1

  • configUSE_TIME_SLICING = 0
    如果使用时间分片,并且有多个能够运行最高优先级的就绪态任务,那么调度程序将选择一个新任务在每个RTOS节拍中断期间进入运行状态。如果未使用时间分片,则调度程序仅选择新任务在以下任一情况下进入运行状态:

  • 优先级较高的任务进入就绪态

  • 处于运行态的任务进入阻塞或挂起状态
    当不使用时间片时,任务上下文切换更少。因此关闭时间片可以让调度程序的处理开销减少,但是也会导致相同优先级的任务接收到大大不同的处理时间量,处于这个原因,运行调度程序没有时间片被认为是一种只有经验丰富的用户才能使用到的高级技术。
    在这里插入图片描述

11.7 协助式调度

FreeRTOS中一般用抢占式调度,但它也支持协作式调度,配置如下:

  • configUSE_PREEMPTION = 0

  • configUSE_TIME_SLICING = 任意值
    当使用协作式调度程序时,只有在运行任务进入阻塞状态,或者运行态任务通过taskYIELD()主动调度才会发生上下文切换。这种情况下,任务永远不会被抢占,因此不能使用时间片。
    在一个多任务应用程序中,必须注意同一个资源会被多个任务同时访问的情况,因为同时访问可能会对该资源造成破坏。通常,使用协作式调度比抢占式调度更容易避免由于多任务访问导致资源损坏:

  • 当使用抢占式调度时,可以在任何时刻抢占运行态的任务。

  • 当使用协作式调度时,程序编写者控制着任务合适切换到另一个任务。因此程序编写者能够确保资源处于不一致状态时不会被切换到其他任务。

协作式调度的系统响应速度低于抢占式调度的响应

12. 总结

一个应用程序可以有多个任务,每个任务相关于一个小程序。调度程序可以使多个任务很好地在一个系统上运行。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

强人电子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值