FreeRTOS —— 3.任务管理

3.任务管理

3.1 本章介绍与适用范围

范围

本章旨在使读者更好地理解:

  • FreeRTOS如何为应用程序中的每个任务分配处理时间。
  • FreeRTOS如何选择应在任何给定时间执行的任务。
  • 每个任务的相对优先级如何影响系统行为。
  • 任务可以存在的状态。

读者还应该对以下内容有充分的了解:

  • 如何执行任务。
  • 如何创建一个或多个任务实例。
  • 如何使用任务参数。
  • 如何更改已创建任务的优先级。
  • 如何删除任务。
  • 如何使用任务实现定期处理(软件计时器将在下一章中讨论)。
  • 空闲任务何时执行以及如何使用。

本章介绍的概念是了解如何使用FreeRTOS以及FreeRTOS应用程序如何行为的基础。 因此,这是本书中最详细的章节

3.2任务函数

任务被实现为C函数。 它们的唯一特殊之处在于它们的原型,该原型必须返回void并采用void指针参数。 清单11演示了该原型。

在这里插入图片描述
清单11.任务函数原型

每个任务本身就是一个小程序。 它具有入口点,通常将在无限循环内永久运行,并且不会退出。 清单12显示了典型任务的结构。

不允许FreeRTOS任务以任何方式从其实现函数中返回-它们必须不包含“ return”语句,也不允许其在函数末尾执行。 如果不再需要任务,则应将其明确删除。 清单12中也对此进行了演示。

单个任务函数定义可用于创建任意数量的任务-每个创建的任务都是一个单独的执行实例,具有自己的堆栈以及在任务本身内定义的任何自动(堆栈)变量的副本。

FreeRTOS —— 3.任务管理_第1张图片
清单12.典型任务功能的结构

3.3顶层任务状态

一个应用程序可以包含许多任务。 如果运行应用程序的处理器包含单个内核,则在任何给定时间只能执行一个任务。 这意味着任务可以以两种状态之一存在:正在运行和未运行。 首先考虑此简化模型,但请记住,这过于简化。 在本章的后面,将显示“未运行”状态实际上包含许多子状态。

当任务处于“运行”状态时,处理器将执行任务的代码。 当任务处于“未运行”状态时,该任务处于休眠状态,其状态已保存,准备好在计划程序下次决定进入“运行”状态时恢复执行。 当任务恢复执行时,它会从上一次离开运行状态之前要执行的指令开始执行。

FreeRTOS —— 3.任务管理_第2张图片
图9.顶级任务状态和过渡

从“未运行”状态转换为“正在运行”状态的任务被称为“已切入”或“已交换”。 相反,从“正在运行”状态转换为“未运行”状态的任务被称为“切换出”或“交换出”。 FreeRTOS调度程序是唯一可以切入和切出任务的实体。

3.4创建任务

xTaskCreate()API函数

FreeRTOS V9.0.0还包括xTaskCreateStatic()函数,该函数分配创建内存所需的内存。
在编译时静态地执行task任务:使用FreeRTOS xTaskCreate()API函数创建任务。 这可能是所有API函数中最复杂的函数,因此很遗憾,它是最先遇到的,但是必须首先掌握任务,因为它们是多任务系统的最基本组成部分。 本书随附的所有示例都使用xTaskCreate()函数,因此有很多示例可供参考。

第1.5节“数据类型和编码样式指南”介绍了所使用的数据类型和命名约定。

FreeRTOS —— 3.任务管理_第3张图片

清单13. xTaskCreate()API函数原型

表8. xTaskCreate()参数和返回值

参数名称/返回值 描述
pvTaskCode 任务只是永不退出的C函数,因此通常将其实现为无限循环。 pvTaskCode参数只是指向实现任务的函数的指针(实际上只是函数的名称)。
pcName 任务的描述性名称。 FreeRTOS不会以任何方式使用它。 它纯粹是作为调试辅助工具包含在内的。 用人类可读的名称标识任务比尝试通过其句柄标识任务要简单得多。应用程序定义的常量configMAX_TASK_NAME_LEN定义任务名称可以采用的最大长度,包括NULL终止符。 提供的字符串超过此最大值将导致该字符串被无提示地截断。
usStackDepth 每个任务都有其自己的唯一堆栈,该堆栈在创建任务时由内核分配给任务。 usStackDepth值告诉内核创建堆栈的大小。该值指定堆栈可以容纳的字数,而不是字节数。 例如,如果堆栈为32位宽,并且usStackDepth作为100传入,那么将分配400字节的堆栈空间(100 * 4字节)。 堆栈深度乘以堆栈宽度不得超过uint16_t类型变量中可以包含的最大值。空闲任务使用的堆栈大小由应用程序定义的常量configMINIMAL_STACK_SIZE1定义。 对于正在使用的处理器体系结构,在FreeRTOS演示应用程序中分配给该常数的值是建议用于任何任务的最小值。 如果您的任务使用了大量的堆栈空间,那么您必须分配一个更大的值。没有简单的方法来确定任务所需的堆栈空间。 可以进行计算,但是大多数用户只会分配他们认为合理的值,然后使用FreeRTOS提供的功能来确保分配的空间确实足够,并且不会不必要地浪费RAM。 第12.3节“堆栈溢出”包含有关如何查询任务实际使用的最大堆栈空间的信息。
pvParameters 任务函数接受类型为void(void *)的指针的参数。 分配给pvParameters的值是传递给任务的值。 本书中的一些示例演示了如何使用参数。
uxPriority 定义任务执行的优先级。 优先级可以从最低优先级的0分配到(configMAX_PRIORITIES – 1),这是最高优先级。 configMAX_PRIORITIES是用户定义的常量,将在第3.5节中进行介绍。传递高于(configMAX_PRIORITIES – 1)的uxPriority值将导致分配给任务的优先级被无声限制为最大合法值。
pxCreatedTask pxCreatedTask可用于传递正在创建的任务的句柄。 然后,可以使用此句柄在API调用中引用任务,例如,更改任务优先级或删除任务。如果您的应用程序没有用于任务句柄,则可以将pxCreatedTask设置为NULL。
Returned value 有两个可能的返回值:1. pdPASS 这表明任务已成功创建。2. pdFAIL 这表明尚未创建任务,因为FreeRTOS没有足够的堆内存来分配足够的RAM来容纳任务数据结构和堆栈。第2章提供了有关堆内存管理的更多信息。

例子1.创建任务

本示例演示了创建两个简单任务,然后开始执行任务所需的步骤。 任务只是简单地定期打印出一个字符串,使用粗略的空循环来创建周期延迟。 这两个任务在相同的优先级下创建,并且除了打印出的字符串外都是相同的,有关它们各自的实现,请参见清单14和清单15。
FreeRTOS —— 3.任务管理_第4张图片

清单14.示例1中使用的第一个任务的实现

FreeRTOS —— 3.任务管理_第5张图片

清单15.示例1中使用的第二个任务的实现

main()函数在启动调度程序之前会创建任务-有关其实现,请参见清单16。

FreeRTOS —— 3.任务管理_第6张图片
清单16.启动示例1的任务

执行该示例将产生如图10所示的输出。
FreeRTOS —— 3.任务管理_第7张图片

图10.执行示例1时产生的输出1

图10显示了两个任务似乎同时执行。 但是,由于两个任务都在同一处理器内核上执行,因此情况并非如此。 实际上,这两个任务都正在迅速进入和退出“运行”状态。 两项任务都以相同的优先级运行,因此在同一处理器内核上共享时间。 它们的实际执行模式如图11所示。

沿图11底部的箭头表示从时间t1开始经过的时间。 彩色线表示在每个时间点正在执行哪个任务,例如,任务1在时间t1和时间t2之间正在执行。

任何时候都只能在“运行”状态下存在一项任务。 因此,当一个任务进入“运行中”状态(任务已接通)时,另一任务进入“非运行中”状态(任务已断开)。

FreeRTOS —— 3.任务管理_第8张图片

图11.两个示例1任务的实际执行模式

示例1在启动调度程序之前从main()内部创建了两个任务。 也可以从另一个任务中创建一个任务。 例如,可以从任务1中创建任务2,如清单17所示。

FreeRTOS —— 3.任务管理_第9张图片

清单17.调度程序启动后,从另一个任务中创建一个任务

例子2.使用任务参数

示例1中创建的两个任务几乎相同,它们之间的唯一区别是它们打印出的文本字符串。 可以通过创建单个任务实现的两个实例来删除此重复项。 然后可以使用task参数将应该打印的字符串传递给每个任务。

清单18包含示例2使用的单个任务函数(vTaskFunction)的代码。此单个函数替换了示例1使用的两个任务函数(vTask1和vTask2)。请注意,如何将task参数强制转换为char *以获取 字符串,任务应打印出来。

FreeRTOS —— 3.任务管理_第10张图片
清单18.示例2中用于创建两个任务的单个任务函数

即使现在只有一个任务实现(vTaskFunction),也可以创建已定义任务的多个实例。 每个创建的实例将在FreeRTOS调度程序的控制下独立执行。

清单19显示了如何使用xTaskCreate()函数的pvParameters参数将文本字符串传递到任务中。

FreeRTOS —— 3.任务管理_第11张图片

清单19.示例2的main()函数

示例2的输出与图10中示例1的输出完全相同。

3.5任务优先级

xTaskCreate()API函数的uxPriority参数为正在创建的任务分配初始优先级。 使用vTaskPrioritySet()API函数启动调度程序后,可以更改优先级。

可用的最大优先级数由FreeRTOSConfig.h中应用程序定义的configMAX_PRIORITIES编译时间配置常量设置。 低优先级数值表示低优先级任务,优先级0是可能的最低优先级。 因此,可用优先级的范围是0至(configMAX_PRIORITIES-1)。 任何数量的任务都可以共享相同的优先级-确保最大的设计灵活性。

FreeRTOS调度程序可以使用两种方法之一来确定哪个任务将处于“运行”状态。 可以将configMAX_PRIORITIES设置为最大值取决于所使用的方法:

  1. 通用方法
    通用方法在C语言中实现,并且可以与所有FreeRTOS体系结构端口一起使用。
    当使用通用方法时,FreeRTOS不会限制configMAX_PRIORITIES可以设置的最大值。 但是,始终建议将configMAX_PRIORITIES值保持在必要的最小值,因为它的值越高,消耗的RAM就越多,最坏情况下的执行时间也就越长。
    如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0,或者未定义configUSE_PORT_OPTIMISED_TASK_SELECTION,或者如果该通用方法是为使用的FreeRTOS端口提供的唯一方法,则将使用该通用方法。

  2. 架构优化方法
    体系结构优化的方法使用少量的汇编代码,并且比通用方法要快。 configMAX_PRIORITIES设置不会影响最坏情况的执行时间。
    如果使用体系结构优化方法,则configMAX_PRIORITIES不能大于32。与通用方法一样,建议将configMAX_PRIORITIES保持在必要的最小值,因为其值越高,将消耗更多的RAM。
    如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,则将使用体系结构优化方法。
    并非所有的FreeRTOS端口都提供架构优化的方法。

FreeRTOS调度程序将始终确保能够运行的优先级最高的任务是选择进入“运行”状态的任务。 如果可以运行多个具有相同优先级的任务,则调度程序将依次将每个任务转换为“运行”状态或从“运行”状态移出。

3.6时间测量和滴答中断

第3.12节“调度算法”介绍了一种称为“时间分片”的可选功能。 到目前为止的示例中都使用了时间切片,时间切片是它们产生的输出中观察到的行为。 在示例中,两个任务的创建优先级相同,并且两个任务始终能够运行。 因此,每个任务都针对一个“时间片”执行,在时间片开始时进入运行状态,在时间片结束时退出运行状态。 在图11中,t1和t2之间的时间等于一个时间片。

为了能够选择要运行的下一个任务,调度程序本身必须在每个时间片1的末尾执行。 为此,使用了一个周期性的中断(称为“滴答中断”)。 时间片的长度由滴答中断频率有效设置,该滴答中断频率由FreeRTOSConfig.h中应用程序定义的configTICK_RATE_HZ编译时间配置常量配置。 例如,如果configTICK_RATE_HZ设置为100(Hz),则时间片将为10毫秒。 两个滴答中断之间的时间称为“滴答周期”。 一个时间片等于一个刻度周期。

可以扩展图11以按执行顺序显示调度程序本身的执行。 如图12所示,其中顶行显示了调度程序的执行时间,细箭头显示了从任务到滴答中断,然后是从滴答中断回到另一个任务的执行顺序。

configTICK_RATE_HZ的最佳值取决于正在开发的应用程序,尽管典型值为100。

FreeRTOS —— 3.任务管理_第12张图片
图12.执行序列展开以显示滴答中断正在执行

FreeRTOS API调用始终以滴答周期的倍数指定时间,通常将其简称为“滴答”。 pdMS_TO_TICKS()宏将以毫秒为单位指定的时间转换为以刻度为单位的时间。 可用分辨率取决于定义的滴答频率,如果滴答频率高于1KHz(如果configTICK_RATE_HZ大于1000),则无法使用pdMS_TO_TICKS()。 清单20显示了如何使用pdMS_TO_TICKS()将指定为200毫秒的时间转换为以秒为单位指定的等效时间。

FreeRTOS —— 3.任务管理_第13张图片
清单20.使用pdMS_TO_TICKS()宏将200毫秒转换为以滴答周期表示的等效时间

注意:不建议直接在应用程序中以滴答为单位指定时间,而是使用pdMS_TO_TICKS()宏以毫秒为单位指定时间,这样做可以确保如果滴答频率改变了,则应用程序中指定的时间不会更改。

“滴答计数”值是自从调度程序启动以来已经发生的滴答中断总数,假设滴答计数没有溢出。 在指定延迟时间时,用户应用程序不必考虑溢出,因为时间一致性是由FreeRTOS内部管理的。

第3.12节“调度算法”描述了配置常数,这些常数会影响调度程序何时选择要运行的新任务以及滴答中断何时执行。

例子3.试验优先级

调度程序将始终确保能够运行的优先级最高的任务是选择进入“运行”状态的任务。 到目前为止,在我们的示例中,已经以相同的优先级创建了两个任务,因此依次进入和退出了“运行”状态。 本示例研究更改示例2中创建的两个任务之一的优先级时发生的情况。 这次,将以优先级1创建第二个任务,并以优先级2创建第二个任务。创建任务的代码如清单21所示。 它仍然只是简单地定期打印出一个字符串,使用空循环来创建延迟。

FreeRTOS —— 3.任务管理_第14张图片

清单21.创建具有不同优先级的两个任务

例3产生的输出如图13所示。

调度程序将始终选择能够运行的最高优先级任务。 任务2的优先级高于任务1,并且始终可以运行。 因此,任务2是唯一进入“运行”状态的任务。 由于任务1从不进入运行状态,因此它从不打印其字符串。 任务1被认为是任务2的“饥饿”处理时间。

FreeRTOS —— 3.任务管理_第15张图片

图13.以不同优先级运行两个任务

任务2始终能够运行,因为它不必等待任何东西-它要么绕着空循环循环,要么打印到终端。

图14显示了示例3的执行顺序。

FreeRTOS —— 3.任务管理_第16张图片

图14.一个任务比另一个任务具有更高优先级时的执行模式

3.7扩展“未运行”状态

到目前为止,已创建的任务始终具有要执行的处理能力,并且从未等待任何内容–因为它们从未等待任何内容,所以它们始终能够进入“运行”状态。 这种“连续处理”任务的用途有限,因为只能以最低的优先级创建它们。 如果它们以任何其他优先级运行,那么它们将根本阻止优先级较低的任务运行。

为了使任务有用,必须将它们重写为事件驱动的。 事件驱动的任务只有在触发事件的事件发生后才能执行工作(处理),并且在该事件发生之前不能进入运行状态。 调度程序始终选择能够运行的最高优先级任务。 高优先级任务无法运行意味着调度程序无法选择它们,而必须选择能够运行的低优先级任务。 因此,使用事件驱动的任务意味着可以在不同的优先级下创建任务,而不会使最高优先级的任务占用所有较低优先级的处理时间任务。

阻塞状态

据说正在等待事件的任务处于“已阻止”状态,这是“未运行”状态的子状态。

任务可以进入“阻塞”状态以等待两种不同类型的事件:

  1. 时间(与时间有关)事件-该事件是延迟期到期或达到绝对时间。 例如,任务可能会进入“阻塞”状态,以等待10毫秒过去。
  2. 同步事件-这些事件源自另一个任务或中断。 例如,任务可能会进入“阻塞”状态,以等待数据到达队列。 同步事件涵盖了广泛的事件类型。

FreeRTOS队列,二进制信号量,计数信号量,互斥量,递归互斥量,事件组以及直接到任务通知都可以用来创建同步事件。 所有这些功能都将在本书的后续章节中介绍。

任务可能会超时阻止同步事件,从而有效地同时阻止两种类型的事件。 例如,一个任务可能选择等待最多10毫秒,以使数据到达队列。 如果数据在10毫秒内到达,或者在10毫秒内没有数据到达,则任务将退出“阻塞”状态。

挂起状态

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

就绪状态

处于“未运行”状态但未被阻止或挂起的任务被称为“就绪”状态。 它们能够运行,因此可以“运行”,但目前尚未处于“运行”状态。

完成状态转换图

图15在先前的过度简化状态图的基础上进行了扩展,以包括本节中描述的所有“未运行”子状态。 到目前为止,在示例中创建的任务尚未使用“已阻止”或“已暂停”状态。 它们仅在“就绪”状态和“运行”状态之间转换,由图15中的粗线突出显示。

FreeRTOS —— 3.任务管理_第17张图片

图15.全任务状态机

例子4.使用Blocked状态创建一个延迟

到目前为止,在示例中创建的所有任务都是“周期性的”,它们已经延迟了一段时间并打印了字符串,然后再延迟一次,依此类推。 延迟是使用空循环非常粗略地生成的-任务有效地轮询了递增循环计数器,直到达到固定值为止。 实施例3清楚地表明了该方法的缺点。 优先级较高的任务在执行空循环时仍处于“运行”状态,使任何处理时间的优先级较低的任务“饥饿”。

任何形式的轮询还有其他几个缺点,尤其是它效率低下。 在轮询过程中,该任务实际上没有任何工作要做,但是它仍然使用最大的处理时间,因此浪费了处理器周期。 示例4通过使用对vTaskDelay()API函数的调用替换轮询null循环来纠正此行为,清单22给出了该函数的原型。清单23显示了新的任务定义。请注意,vTaskDelay()API函数 仅在FreeRTOSConfig.h中将INCLUDE_vTaskDelay设置为1时可用。

vTaskDelay()将调用任务置于Blocked状态,以固定数量的滴答中断。 处于“阻塞”状态时,该任务不使用任何处理时间,因此该任务仅在实际有待完成的工作时才使用处理时间。

在这里插入图片描述
清单22. vTaskDelay()API函数原型

参数名称 描述
xTicksToDelay 滴答中断的数量,即在转换回“就绪”状态之前,调用任务将保持在“阻塞”状态。例如,如果在滴答计数为10,000时名为vTaskDelay(100)的任务,则它将立即进入Blocked状态,并保持Blocked状态,直到滴答计数达到10,100。宏pdMS_TO_TICKS()可用于将以毫秒为单位的时间转换为以刻度为单位的时间。 例如,调用vTaskDelay(pdMS_TO_TICKS(100))将导致调用任务在Blocked状态下保持100毫秒。

FreeRTOS —— 3.任务管理_第18张图片

清单23.空循环延迟之后的示例任务的源代码已由对vTaskDelay()的调用替换

即使仍以不同的优先级创建两个任务,两个任务现在都将运行。 图16所示的示例4的输出确认了预期的行为。
FreeRTOS —— 3.任务管理_第19张图片

图16.执行示例4时产生的输出

图17中所示的执行顺序说明了为什么两个任务都运行,即使它们是在不同的优先级下创建的也是如此。 为简单起见,省略了调度程序本身的执行。

启动调度程序时,将自动创建空闲任务,以确保始终有至少一个任务能够运行(至少一个任务处于“就绪”状态)。 第3.8节“空闲任务和空闲任务挂钩”详细描述了空闲任务。

FreeRTOS —— 3.任务管理_第20张图片
图17.任务使用vTaskDelay()代替NULL循环时的执行顺序

仅两个任务的实现已更改,其功能未更改。 将图17与图12进行比较可以清楚地表明,该功能正在以一种更加有效的方式实现。

图12显示了任务使用空循环来创建延迟的任务时始终可以运行的执行模式,因此在任务之间使用了可用处理器时间的百分之一百。 图17显示了任务在整个延迟周期内进入“阻塞”状态时的执行模式,因此仅当它们实际有需要执行的工作时才使用处理器时间(在这种情况下,只是打印一条消息),并且 结果,只使用了可用处理时间的一小部分。

在图17场景中,每次任务离开“阻塞”状态时,它们都会执行一个滴答周期的一小部分,然后再重新进入“阻塞”状态。 大多数情况下,没有能够运行的应用程序任务(没有处于“就绪”状态的应用程序任务),因此,没有可以选择进入“运行”状态的应用程序任务。 在这种情况下,空闲任务将运行。 分配给空闲的处理时间量是系统中备用处理能力的度量。 仅通过允许应用程序完全由事件驱动,使用RTOS即可显着增加备用处理能力。

图18中的粗线显示了示例4中的任务执行的转换,每个转换现在都在返回“就绪”状态之前通过“已阻塞”状态进行转换。

FreeRTOS —— 3.任务管理_第21张图片
图18.粗线指示示例4中的任务执行的状态转换

vTaskDelayUntil()API函数

vTaskDelayUntil()与vTaskDelay()类似。 如刚刚演示的,vTaskDelay()参数指定在调用vTaskDelay()的任务与同一任务再次移出“已阻止”状态之间应发生的滴答中断数。 任务保持在阻塞状态的时间长度由vTaskDelay()参数指定,但是任务离开阻塞状态的时间与调用vTaskDelay()的时间有关。

而是,vTaskDelayUntil()的参数指定了确切的滴答计数值,在该滴答计数值处,应将调用任务从“已阻止”状态转移到“就绪”状态。 vTaskDelayUntil()是API函数,当需要固定执行时间(您希望任务以固定频率定期执行)时,应使用该API函数,因为取消调用任务的时间是绝对的,而不是相对于 调用函数的时间(与vTaskDelay()一样)。

在这里插入图片描述
清单24. vTaskDelayUntil()API函数原型

Table 10. vTaskDelayUntil() parameters

参数名称 描述
pxPreviousWakeTime 假设使用vTaskDelayUntil()来执行以固定频率周期性执行的任务,则命名此参数。 在这种情况下,pxPreviousWakeTime保存任务最后一次离开“阻塞”状态(被“唤醒”)的时间。 该时间用作计算任务下一次离开“已阻止”状态的时间的参考点。pxPreviousWakeTime指向的变量在vTaskDelayUntil()函数中自动更新; 它通常不会被应用程序代码修改,但是必须在首次使用之前初始化为当前滴答计数。 清单25演示了如何执行初始化。
xTimeIncrement 在使用vTaskDelayUntil()来执行定期执行并以固定频率(由xTimeIncrement值设置频率)的任务的假设下,也可以命名此参数。xTimeIncrement在“滴答”中指定。 宏pdMS_TO_TICKS()可用于将以毫秒为单位的时间转换为以刻度为单位的时间。

例子5.转换例子任务以使用vTaskDelayUntil()

示例4中创建的两个任务是周期性任务,但是使用vTaskDelay()不能保证它们的运行频率是固定的,因为任务离开Blocked状态的时间与调用vTaskDelay()的时间有关。 将任务转换为使用vTaskDelayUntil()而不是vTaskDelay()可以解决此潜在问题。

FreeRTOS —— 3.任务管理_第22张图片
清单25.使用vTaskDelayUntil()实现示例任务

示例5产生的输出与图16中示例4所示的输出完全相同。

例子6.合并阻塞和非阻塞任务

前面的示例分别检查了轮询和阻止任务的行为。 本示例通过演示将两种方案组合在一起时的执行顺序来增强规定的预期系统行为,如下所示。

  1. 创建两个优先级为1的任务。除了连续打印出一个字符串外,这些任务不做任何事。
    这些任务永远不会进行任何可能导致它们进入“已阻止”状态的API函数调用,因此它们始终处于“就绪”或“运行”状态。 这种性质的任务称为“连续处理”任务,因为它们总是有工作要做(在这种情况下,虽然是微不足道的工作)。 清单26显示了连续处理任务的源。
  2. 然后,在优先级2处创建第三个任务,使其高于其他两个任务的优先级。 第三个任务也只是打印出一个字符串,但是这次是定期的,因此使用vTaskDelayUntil()API函数将自身置于每次打印迭代之间的“已阻止”状态。

定期任务的源如清单27所示。

FreeRTOS —— 3.任务管理_第23张图片

清单26.示例6中使用的连续处理任务

FreeRTOS —— 3.任务管理_第24张图片

清单27.示例6中使用的定期任务

图19显示了示例6产生的输出,并通过图20所示的执行序列对观察到的行为进行了解释。
FreeRTOS —— 3.任务管理_第25张图片
图20.示例6的执行模式

3.8空闲任务和空闲任务挂钩

在示例4中创建的任务大部分时间都处于“阻塞”状态。 在这种状态下,它们将无法运行,因此无法由调度程序选择。

必须始终至少有一项任务可以进入“运行”状态2。 为确保这种情况,调度程序在调用vTaskStartScheduler()时会自动创建一个空闲任务。 空闲任务所做的仅是坐在循环中,因此,就像最初的第一个示例中的任务一样,它始终可以运行。

空闲任务具有最低的优先级(优先级为零),以确保它永远不会阻止更高优先级的应用程序任务进入“运行”状态-尽管没有什么可以阻止应用程序设计人员以空闲任务优先级创建并因此共享空闲任务优先级的任务, 如果需要的话。 FreeRTOSConfig.h中的configIDLE_SHOULD_YIELD编译时间配置常量可用于防止Idle任务消耗处理时间,而该处理时间可以更有效地分配给应用程序任务。 configIDLE_SHOULD_YIELD在3.12节,调度算法中介绍。

以最低优先级运行时,确保较高优先级的任务进入就绪状态后,空闲任务便会从运行状态过渡出来。 这可以在图17中的时间tn处看到,其中,空闲任务立即换出以允许任务2在任务2离开阻塞状态的瞬间执行。 据说任务2已抢占了空闲任务。 抢占自动发生,并且不知道该任务被抢占。

注意:如果应用程序使用vTaskDelete()API函数,则必须使空闲任务没有处理时间。 这是因为空闲任务负责清理删除任务后的内核资源。

空闲任务钩子函数

可以通过使用空闲钩子(或空闲回调)功能将功能特定的功能直接添加到空闲任务中,该功能由空闲任务每次迭代一次由空闲任务自动调用。

空闲任务钩子的常见用法包括:

  • 执行低优先级,后台或连续处理功能。
  • 测量备用处理能力的数量。 (仅当所有优先级较高的应用程序任务都无法执行时,空闲任务才会运行;因此,测量分配给该空闲任务的处理时间量可以清楚地表明有多少处理时间是空闲的。)
  • 将处理器置于低功耗模式,可在不需要执行任何应用程序处理时提供一种简便,自动的省电方法(尽管使用此方法可节省的电量少于使用对勾所能节省的电量 第10章“低功耗支持”中所述的“ -less”空闲模式。

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

空闲任务挂钩函数必须遵守以下规则。

  1. 空闲任务钩子函数绝不能尝试阻塞或挂起。
    注意:以任何方式阻止空闲任务都可能导致以下情况:没有任务可进入运行状态。

  2. 如果应用程序使用vTaskDelete()API函数,则Idle任务挂钩必须始终在合理的时间段内返回给其调用者。 这是因为空闲任务负责删除任务后清理内核资源。 如果空闲任务永久保留在“空闲”钩子函数中,则无法进行此清理。

空闲任务钩子函数必须具有清单28所示的名称和原型。

在这里插入图片描述

清单28.空闲任务钩子函数的名称和原型

例子7.定义一个空闲的任务钩子函数

示例4中使用阻塞vTaskDelay()API调用创建了许多空闲时间,即空闲任务执行的时间,因为两个应用程序任务都处于“阻塞”状态。 例7通过添加一个Idle hook函数来利用这个空闲时间,清单29给出了它的源文件。

FreeRTOS —— 3.任务管理_第26张图片

清单29.一个非常简单的Idle hook函数

必须在FreeRTOSConfig.h中将configUSE_IDLE_HOOK设置为1,才能调用空闲挂钩函数。

稍微修改了实现创建的任务的函数,以打印出ulIdleCycleCount值,如清单30所示。

FreeRTOS —— 3.任务管理_第27张图片

清单30.现在,示例任务的源代码将打印出ulIdleCycleCount值

实例7产生的输出如图21所示。该图显示了在应用程序任务的每次迭代之间,空闲任务挂钩函数被调用了大约400万次(迭代次数取决于演示所基于的硬件的速度)。

FreeRTOS —— 3.任务管理_第28张图片
图21.执行示例7时产生的输出

3.9更改任务的优先级

vTaskPrioritySet()API函数

调度程序启动后,可以使用vTaskPrioritySet()API函数更改任何任务的优先级。 请注意,仅当在FreeRTOSConfig.h中将INCLUDE_vTaskPrioritySet设置为1时,vTaskPrioritySet()API函数才可用。

在这里插入图片描述
清单31. vTaskPrioritySet()API函数原型

表11. vTaskPrioritySet()参数

参数名称 描述
pxTask 优先级正在修改的任务(主题任务)的句柄—有关获取任务句柄的信息,请参见xTaskCreate()API函数的Created Task参数。任务可以通过传递NULL代替有效的任务句柄来更改其自身的优先级。
uxNewPriority 要设置主题任务的优先级。 这会自动设置为最大可用优先级(configMAX_PRIORITIES – 1),其中configMAX_PRIORITIES是FreeRTOSConfig.h头文件中设置的编译时间常数。

uxTaskPriorityGet()API函数

uxTaskPriorityGet()API函数可用于查询任务的优先级。 请注意,只有在FreeRTOSConfig.h中将INCLUDE_uxTaskPriorityGet设置为1时,uxTaskPriorityGet()API函数才可用。

在这里插入图片描述

清单32. uxTaskPriorityGet()API函数原型
表12. uxTaskPriorityGet()参数和返回值

参数名称/返回值 描述
pxTask 要查询其优先级的任务(主题任务)的句柄-有关获取任务句柄的信息,请参见xTaskCreate()API函数的pxCreatedTask参数。任务可以通过传递NULL代替有效的任务句柄来查询其自身的优先级。
Returned value 当前分配给要查询的任务的优先级。

例子8.改变任务优先级

调度程序将始终选择最高的“就绪”状态任务作为进入“运行”状态的任务。 示例8通过使用vTaskPrioritySet()API函数来更改两个任务相对于彼此的优先级,对此进行了演示。

示例8以两个不同的优先级创建了两个任务。 这两个任务都不会进行任何可能导致其进入“阻塞”状态的API函数调用,因此两者始终处于“就绪”状态或“运行”状态。 因此,具有最高相对优先级的任务将始终是调度程序选择的处于“运行”状态的任务。

示例8的行为如下:

  1. 任务1(清单33)以最高优先级创建,因此可以保证首先运行。 在将任务2(清单34)的优先级提高到其优先级之前,任务1将打印出几个字符串。
  2. 任务2具有最高的相对优先级后便立即开始运行(进入“运行”状态)。 任一时间只能有一个任务处于“运行”状态,因此当任务2处于“运行”状态时,任务1处于“就绪”状态。
  3. 任务2打印出一条消息,然后将其优先级重新设置为低于任务1的优先级。
  4. 降低任务2的优先级意味着任务1再次是最高优先级的任务,因此任务1重新进入运行状态,迫使任务2返回就绪状态。
    FreeRTOS —— 3.任务管理_第29张图片
    清单33.示例8中任务1的实现
    FreeRTOS —— 3.任务管理_第30张图片
    清单34.示例8中任务2的实现

每个任务都可以查询并设置自己的优先级,而无需使用有效的任务句柄,只需使用NULL即可。 仅当任务希望引用其自身以外的任务时(例如,任务1更改任务2的优先级时),才需要任务句柄。要允许任务1进行此操作,将在任务2被执行时获取并保存任务2的句柄。 创建,如清单35中的注释中突出显示。

FreeRTOS —— 3.任务管理_第31张图片

清单35.示例8的main()的实现

图22演示了示例8任务的执行顺序,结果输出如图23所示。

FreeRTOS —— 3.任务管理_第32张图片

图22.运行示例8时任务执行的顺序

FreeRTOS —— 3.任务管理_第33张图片

图23.执行示例8时产生的输出

3.10删除任务

vTaskDelete()API函数

任务可以使用vTaskDelete()API函数删除自身或任何其他任务。 请注意,仅当在FreeRTOSConfig.h中将INCLUDE_vTaskDelete设置为1时,vTaskDelete()API函数才可用。

已删除的任务不再存在,并且无法再次进入“运行”状态。

空闲任务的责任是释放分配给此后已删除任务的内存。 因此,重要的是,使用vTaskDelete()API函数的应用程序不要完全耗尽所有处理时间的空闲任务。

注意:删除任务时,只有内核本身分配给任务的内存才会自动释放。 必须显式释放执行任务分配的任何内存或其他资源。

在这里插入图片描述

清单36. vTaskDelete()API函数原型

表13. vTaskDelete()参数

参数名称/返回值 描述
pxTaskToDelete 要删除的任务(主题任务)的句柄-有关获取任务句柄的信息,请参阅xTaskCreate()API函数的pxCreatedTask参数。任务可以通过传递NULL代替有效的任务句柄来删除自身。

例子9.删除任务

这是一个非常简单的示例,其行为如下。

  1. 任务1由具有优先级1的main()创建。运行时,它以优先级2创建任务2。任务2现在是优先级最高的任务,因此它开始立即执行。 清单37中显示了main()的源,清单38中显示了任务1的源。
  2. 任务2除了删除自身外什么也不做。 它可以通过将NULL传递给vTaskDelete()来删除自身,但是出于演示目的,它使用了自己的任务句柄。 清单2显示了任务2的源代码。
  3. 删除任务2后,任务1仍然是优先级最高的任务,因此继续执行-此时它将调用vTaskDelay()进行短暂阻塞。
  4. 空闲任务在任务1处于阻塞状态时执行,并释放分配给现在已删除的任务2的内存。
  5. 当任务1离开阻止状态时,它再次成为最高优先级的就绪状态任务,因此抢占了空闲任务。 进入“运行”状态后,它将再次创建任务2,然后继续进行。
    FreeRTOS —— 3.任务管理_第34张图片
    清单37.示例9的main()的实现

FreeRTOS —— 3.任务管理_第35张图片
清单38.示例9的任务1的实现
FreeRTOS —— 3.任务管理_第36张图片
清单39.示例9的任务2的实现

FreeRTOS —— 3.任务管理_第37张图片

图24.执行示例9时产生的输出

FreeRTOS —— 3.任务管理_第38张图片
图25.示例9的执行顺序

3.11线程本地存储

待定。 本部分将在最终发布之前编写。

3.12调度算法

任务状态和事件回顾

实际正在运行的任务(使用处理时间)处于“正在运行”状态。 在单个核心处理器上,任何给定时间只能有一个任务处于“运行”状态。

实际未运行但不在“阻塞”状态或“未挂起”状态的任务处于“就绪”状态。 计划程序可以选择处于“就绪”状态的任务作为进入“运行”状态的任务。 调度程序将始终选择优先级最高的“就绪”状态任务以进入“运行”状态。

任务可以在“阻止”状态下等待事件,并在事件发生时自动移回“就绪”状态。 时间事件在特定时间发生,例如,在某个块时间到期时,通常用于实现周期性或超时行为。 当任务或中断服务例程使用任务通知,队列,事件组或多种信号量之一发送信息时,就会发生同步事件。 它们通常用于发信号通知异步活动,例如到达外围设备的数据。

配置调度算法

调度算法是决定将哪些就绪状态任务转换为运行状态的软件例程。

到目前为止,所有示例都使用相同的调度算法,但是可以使用configUSE_PREEMPTION和configUSE_TIME_SLICING配置常量来更改该算法。 这两个常量都在FreeRTOSConfig.h中定义。

第三个配置常量configUSE_TICKLESS_IDLE也影响调度算法,因为其使用可能导致滴答中断长时间完全关闭。 configUSE_TICKLESS_IDLE是高级选项,专门用于必须将其功耗降至最低的应用程序。 第10章,低功耗支持中介绍了configUSE_TICKLESS_IDLE。 本节提供的描述假定configUSE_TICKLESS_IDLE设置为0,如果未定义常量,则为默认设置。

在所有可能的配置中,FreeRTOS调度程序将确保选择共享优先级的任务以依次进入“运行”状态。 这种“轮流使用”政策通常称为“ Round Robin计划”。 Round Robin调度算法不能保证在优先级相同的任务之间平均分配时间,只有优先级相同的就绪状态任务会依次进入运行状态。

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

表14中所示的配置将FreeRTOS调度程序设置为使用调度算法,即“带时间分片的固定优先抢先调度”。 这是大多数小型RTOS应用程序使用的调度算法,也是本书到目前为止所提供的所有示例所使用的算法。 表15提供了该算法名称中使用的术语的描述。

表14. FreeRTOSConfig.h设置,该设置将内核配置为使用带时间分片的优先抢占式调度

常数
configUSE_PREEMPTION 1
configUSE_TIME_SLICING 1
术语 定义
Fixed Priority 称为“固定优先级”的调度算法不会更改分配给计划任务的优先级,也不会阻止任务本身更改自己的优先级或其他任务的优先级。
Pre-emptive 如果优先级高于“运行状态”任务的任务进入“就绪”状态,则抢占式调度算法将立即“清除”“运行状态”任务。 被抢占意味着被非自愿地(没有明确地屈服或阻止)移出运行状态并进入就绪状态,以允许其他任务进入运行状态。
Time Slicing 时间分片用于在优先级相同的任务之间共享处理时间,即使任务未明确产生或进入“已阻止”状态也是如此。 如果还有其他“就绪”状态任务的优先级与“正在运行”任务相同,则描述为使用“时间分片”的调度算法将选择一个新任务以在每个时间片结束时进入“正在运行”状态。 时间片等于两个RTOS滴答中断之间的时间。

图26和图27展示了使用带有时间分片算法的固定优先级抢先式调度时如何调度任务。 图26显示了当应用程序中的所有任务具有唯一优先级时选择任务进入“运行”状态的顺序。 图27显示了当应用程序中的两个任务共享优先级时,选择任务进入“运行”状态的顺序。

FreeRTOS —— 3.任务管理_第39张图片

图26.执行模式在一个假设的应用程序中突出显示了任务的优先级和抢占,在该应用程序中,每个任务都被分配了唯一的优先级

参考图26:

  1. 空闲任务
    空闲任务以最低优先级运行,因此,每当更高优先级的任务进入“就绪”状态时(例如,在时间t3,t5和t9时),它就会被抢占。
  2. 任务3
    任务3是事件驱动的任务,以较低的优先级但高于“空闲”优先级执行。它将大部分时间都花在“阻止”状态上以等待其感兴趣的事件,每次事件发生时都会从“阻止”状态转换为“就绪”状态。所有FreeRTOS任务间通信机制(任务通知,队列,信号量,事件组等)都可以通过这种方式用于信号事件和取消阻止任务。
    事件在时间t3和t5以及时间t9和t12之间发生。立即处理在时间t3和t5发生的事件,因为在这些时间,任务3是能够运行的最高优先级任务。在时间t9和t12之间某处发生的事件直到t12才得到处理,因为直到那时,更高优先级的任务Task 1和Task 2仍在执行。仅在时间t12时,任务1和任务2都处于“阻止”状态,使任务3成为最高优先级的“就绪”状态任务。
  3. 任务2
    任务2是一个周期性任务,其执行优先级高于任务3的优先级,但低于任务1的优先级。任务的时间间隔表示任务2希望在时间t1,t6和t9执行。
    在时间t6,任务3处于“运行”状态,但是任务2具有较高的相对优先级,因此抢占了任务3并立即开始执行。任务2完成其处理并在时间t7重新进入Blocked状态,此时,任务3可以重新进入Running状态以完成其处理。任务3本身在时间t8处阻塞。
  4. 任务1
    任务1也是事件驱动的任务。它以最高优先级执行,因此可以抢占系统中的任何其他任务。显示的唯一任务1事件发生在时间t10,此时任务1抢占了任务2。仅在任务1在时间t11重新进入“阻止”状态后,任务2才能完成其处理。

FreeRTOS —— 3.任务管理_第40张图片

图27执行模式突出显示了一个假设应用程序中的任务优先级和时间分片,在该应用程序中两个任务以相同的优先级运行

参见图27:

  1. 空闲任务和任务2
    空闲任务和任务2都是连续的处理任务,并且它们的优先级均为0(可能的最低优先级)。调度程序仅在没有更高的优先级任务可以运行时才将处理时间分配给优先级0的任务,并通过时间切片共享分配给优先级0的任务的时间。每个滴答中断都会开始一个新的时间片,在图27中的时间点为t1,t2,t3,t4,t5,t8,t9,t10和t11。
    空闲任务和任务2依次进入“运行”状态,这可能导致这两个任务在同一时间段的一部分处于“运行”状态,就像在时间t5和时间t8之间发生的那样。
  2. 任务1
    任务1的优先级高于空闲优先级。任务1是事件驱动的任务,其大部分时间都在“阻止”状态下等待其感兴趣的事件,每次事件发生时都会从“阻止”状态转换为“就绪”状态。
    感兴趣的事件在时间t6发生,因此任务1成为可以运行的最高优先级任务,因此任务1在整个时间片中抢占了空闲任务。事件的处理在时间t7结束,此时任务1重新进入阻塞状态。

图27显示了空闲任务与应用程序编写者创建的任务共享处理时间。 如果应用程序编写者创建的空闲优先级任务有工作要做,那么为空闲任务分配这么多的处理时间可能不是理想的,但是空闲任务则没有。 这
configIDLE_SHOULD_YIELD编译时间配置常量可用于更改空闲任务的计划方式:

  • 如果configIDLE_SHOULD_YIELD设置为0,则空闲任务将在整个时间段内保持“正在运行”状态,除非优先级更高的任务抢占了该任务。
  • 如果configIDLE_SHOULD_YIELD设置为1,则如果还有其他空闲优先级任务处于“就绪”状态,则空闲任务将在其循环的每次迭代中产生(自愿放弃分配的时间片的剩余部分)。

当configIDLE_SHOULD_YIELD设置为0时,将观察到图27所示的执行模式。将configIDLE_SHOULD_YIELD设置为1时,在相同情况下将观察到图28所示的执行模式。
FreeRTOS —— 3.任务管理_第41张图片

图28与图27所示相同场景的执行模式,但是这次将configIDLE_SHOULD_YIELD设置为1

图28还显示,当configIDLE_SHOULD_YIELD设置为1时,在空闲任务之后选择进入运行状态的任务不会在整个时间段内执行,而是在产生空闲任务的时间段内的任何剩余时间内执行 。

优先抢占式调度(无时间片)

无需时间分片的优先抢占式调度可保持与上一节所述相同的任务选择和抢占算法,但不使用时间分片来共享相同优先级的任务之间的处理时间。

表16显示了FreeRTOSConfig.h设置,该设置将FreeRTOS调度程序配置为使用优先的抢先式调度而不进行时间分片。

表16. FreeRTOSConfig.h设置,该设置将内核配置为使用不带时间分片的优先抢占式调度

常数
configUSE_PREEMPTION 1
configUSE_TIME_SLICING 0

如图27所示,如果使用了时间片,并且有多个可以运行的具有最高优先级的就绪状态任务,那么调度程序将选择一个新任务以在每个RTOS滴答中断期间进入运行状态 (一个滴答中断标记了一个时间片的结束)。 如果未使用时间分片,则调度程序将仅在以下任一情况下选择新任务以进入“运行”状态:

  • 更高优先级的任务进入“就绪”状态。
  • 处于“运行”状态的任务进入“阻塞”或“挂起”状态。

不使用时间切片时的任务上下文切换少于使用时间切片时的任务上下文切换。 因此,关闭时间分割可减少调度程序的处理开销。 但是,关闭时间分片也会导致优先级相同的任务接收到非常不同的处理时间,图29演示了这种情况。因此,在没有时间分片的情况下运行调度程序被认为是一项高级技术,只应由 经验丰富的用户。

FreeRTOS —— 3.任务管理_第42张图片

图29执行模式演示了在不使用时间分割的情况下,具有相同优先级的任务如何获得截然不同的处理时间

参考图29,它假定configIDLE_SHOULD_YIELD设置为0:

  1. 滴答中断
    滴答中断发生在时间t1,t2,t3,t4,t5,t8,t11,t12和t13。
  2. 任务1
    任务1是高优先级的事件驱动任务,其大部分时间都处于“阻塞”状态,以等待其感兴趣的事件。 每次事件发生时,任务1从“阻塞”状态转换为“就绪”状态(随后,由于它是优先级最高的“就绪”状态任务,因此进入“运行”状态)。 图29显示了任务1在时间t6和t7之间,然后在时间t9和t10之间再次处理事件。
  3. 空闲任务和任务2
    空闲任务和任务2都是连续的处理任务,并且它们的优先级均为0(空闲优先级)。 连续处理任务不会进入“阻止”状态。
    未使用时间片,因此处于“运行中”状态的空闲优先级任务将保持“运行中”状态,直到被更高优先级的任务1抢占为止。
    在图29中,空闲任务在时间t1开始运行,并一直处于运行状态,直到在时间t6被任务1抢占为止-这是进入运行状态后的四个完整滴答周期。
    任务2在时间t7开始运行,这是任务1重新进入“已阻止”状态以等待另一个事件的时间。 任务2保持运行状态,直到任务1在时间t9之前也将其抢占为止,该时间小于进入运行状态后的一个滴答周期。
    尽管已收到比任务2多四倍的处理时间,但在时间t10,空闲任务仍重新进入运行状态。

合作调度

本书侧重于抢先式调度,但是FreeRTOS也可以使用协作式调度。 表17显示了配置FreeRTOS调度程序以使用协作调度的FreeRTOSConfig.h设置。

表17. FreeRTOSConfig.h设置,用于将内核配置为使用协作调度

常数
configUSE_PREEMPTION 0
configUSE_TIME_SLICING 任何值

使用协作式调度程序时,仅当“运行状态”任务进入“阻止”状态,或者“运行状态”任务通过调用taskYIELD()明确产生(手动请求重新计划)时,才会发生上下文切换。 任务永远不会被抢占,因此无法使用时间分割。

图30演示了合作调度程序的行为。 图30中的水平虚线显示了任务何时处于“就绪”状态。

FreeRTOS —— 3.任务管理_第43张图片

图30执行模式演示了协作调度程序的行为

参考图30:

  1. 任务1
    任务1具有最高优先级。 它以“阻塞”状态开始,等待信号量。
    在时间t3,有一个中断发出信号量,导致任务1离开“阻塞”状态并进入“就绪”状态(第6章介绍了从中断中发出信号量)。
    在时间t3,任务1是最高优先级的就绪状态任务,如果使用了抢占式调度程序,则任务1将变为运行状态任务。 但是,由于正在使用协作调度程序,因此任务1保持就绪状态,直到时间t4为止(这是运行状态任务调用taskYIELD()的时间)。
  2. 任务2
    任务2的优先级在任务1和任务3的优先级之间。它以“阻止”状态开始,等待任务3在时间t2发送给它的消息。
    在时间t2,任务2是最高优先级的就绪状态任务,如果使用了抢占式调度程序,则任务2将变为运行状态任务。 但是,由于正在使用协作调度程序,因此任务2保持就绪状态,直到运行状态任务进入调用taskYIELD()的阻止状态为止。
    运行状态任务在时间t4调用taskYIELD(),但是此时任务1是优先级最高的就绪状态任务,因此,直到任务1在时间t5重新进入Blocked状态之前,任务2才真正成为运行状态任务。
    在时间t6,任务2重新进入阻止状态以等待下一条消息,此时,任务3再次成为优先级最高的就绪状态任务。

在多任务应用程序中,应用程序编写者必须注意多个任务不能同时访问资源,因为同时访问可能会破坏资源。 例如,请考虑以下情形,其中正在访问的资源是UART(串行端口)。 向UART写入字符串是两项任务。 任务1正在编写“ bcdefghijklmnop”? 任务2正在写“ 23456789”

  1. 任务1处于“运行”状态,并开始写入其字符串。 它将“ abcdefg”写入UART,但在写入任何其他字符之前保留“运行”状态。
  2. 任务2进入运行状态,并在退出运行状态之前将“ 123456789”写入UART。
  3. 任务1重新进入运行状态,并将其字符串的其余字符写入UART。

在这种情况下,实际写入UART的是“ abcdefg123456789hijklmnop”。 任务1写入的字符串没有按预期的连续顺序写入UART,而是已损坏,因为任务2写入UART的字符串出现在其中。

与使用抢占式调度程序相比,使用协作式调度程序通常更容易避免因同时访问而引起的问题3

  • 使用抢占式调度程序时,可以随时抢占“运行状态”任务,包括与另一个任务共享的资源处于不一致状态时。 如UART示例所示,将资源置于不一致状态会导致数据损坏。
  • 使用协作调度程序时,应用程序编写器控制何时可以切换到另一个任务。 因此,应用程序编写者可以确保在资源处于不一致状态时不会切换到另一个任务。
  • 在上面的UART示例中,应用程序编写器可以确保任务1在整个字符串写入UART之前不会离开运行状态,这样做可以避免字符串被另一个任务的激活损坏的可能性。

如图30所示,与使用抢占式调度程序相比,使用协作式调度程序时系统的响应速度将更低:

  • 使用抢占式计划程序时,计划程序将成为优先级最高的就绪状态任务,该计划程序将立即开始运行任务。 在必须在定义的时间段内响应高优先级事件的实时系统中,这通常是必不可少的。
  • 使用协作调度程序时,将切换到已成为最高优先级的任务,直到“运行状态”任务进入“阻止”状态或调用taskYIELD()为止,才不会执行“就绪”状态任务。

  1. 需要特别注意的是,时间片的结束不是调度程序可以选择要运行的新任务的唯一位置。 正如整本书将要演示的那样,调度程序还将选择一个新任务,以在当前正在执行的任务进入“阻塞”状态后,或当中断将优先级较高的任务移至“就绪”状态时立即运行。 ↩︎ ↩︎

  2. 即使在使用FreeRTOS的特殊低功耗功能时也是如此,在这种情况下,如果应用程序创建的任务均无法执行,则执行FreeRTOS的微控制器将置于低功耗模式。 ↩︎

  3. 本书稍后将介绍在任务之间安全共享资源的方法。 FreeRTOS本身提供的资源(例如队列和信号量)始终可以在任务之间安全共享。 ↩︎

你可能感兴趣的:(FreeRTOS,rtos,freertos,gd32,stm32,单片机)