本章旨在使读者更好地理解:
读者还应该对以下内容有充分的了解:
本章介绍的概念是了解如何使用FreeRTOS以及FreeRTOS应用程序如何行为的基础。 因此,这是本书中最详细的章节。
任务被实现为C函数。 它们的唯一特殊之处在于它们的原型,该原型必须返回void并采用void指针参数。 清单11演示了该原型。
每个任务本身就是一个小程序。 它具有入口点,通常将在无限循环内永久运行,并且不会退出。 清单12显示了典型任务的结构。
不允许FreeRTOS任务以任何方式从其实现函数中返回-它们必须不包含“ return”语句,也不允许其在函数末尾执行。 如果不再需要任务,则应将其明确删除。 清单12中也对此进行了演示。
单个任务函数定义可用于创建任意数量的任务-每个创建的任务都是一个单独的执行实例,具有自己的堆栈以及在任务本身内定义的任何自动(堆栈)变量的副本。
一个应用程序可以包含许多任务。 如果运行应用程序的处理器包含单个内核,则在任何给定时间只能执行一个任务。 这意味着任务可以以两种状态之一存在:正在运行和未运行。 首先考虑此简化模型,但请记住,这过于简化。 在本章的后面,将显示“未运行”状态实际上包含许多子状态。
当任务处于“运行”状态时,处理器将执行任务的代码。 当任务处于“未运行”状态时,该任务处于休眠状态,其状态已保存,准备好在计划程序下次决定进入“运行”状态时恢复执行。 当任务恢复执行时,它会从上一次离开运行状态之前要执行的指令开始执行。
从“未运行”状态转换为“正在运行”状态的任务被称为“已切入”或“已交换”。 相反,从“正在运行”状态转换为“未运行”状态的任务被称为“切换出”或“交换出”。 FreeRTOS调度程序是唯一可以切入和切出任务的实体。
FreeRTOS V9.0.0还包括xTaskCreateStatic()函数,该函数分配创建内存所需的内存。
在编译时静态地执行task任务:使用FreeRTOS xTaskCreate()API函数创建任务。 这可能是所有API函数中最复杂的函数,因此很遗憾,它是最先遇到的,但是必须首先掌握任务,因为它们是多任务系统的最基本组成部分。 本书随附的所有示例都使用xTaskCreate()函数,因此有很多示例可供参考。
第1.5节“数据类型和编码样式指南”介绍了所使用的数据类型和命名约定。
清单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章提供了有关堆内存管理的更多信息。 |
本示例演示了创建两个简单任务,然后开始执行任务所需的步骤。 任务只是简单地定期打印出一个字符串,使用粗略的空循环来创建周期延迟。 这两个任务在相同的优先级下创建,并且除了打印出的字符串外都是相同的,有关它们各自的实现,请参见清单14和清单15。
清单14.示例1中使用的第一个任务的实现
清单15.示例1中使用的第二个任务的实现
main()函数在启动调度程序之前会创建任务-有关其实现,请参见清单16。
图10.执行示例1时产生的输出1
图10显示了两个任务似乎同时执行。 但是,由于两个任务都在同一处理器内核上执行,因此情况并非如此。 实际上,这两个任务都正在迅速进入和退出“运行”状态。 两项任务都以相同的优先级运行,因此在同一处理器内核上共享时间。 它们的实际执行模式如图11所示。
沿图11底部的箭头表示从时间t1开始经过的时间。 彩色线表示在每个时间点正在执行哪个任务,例如,任务1在时间t1和时间t2之间正在执行。
任何时候都只能在“运行”状态下存在一项任务。 因此,当一个任务进入“运行中”状态(任务已接通)时,另一任务进入“非运行中”状态(任务已断开)。
图11.两个示例1任务的实际执行模式
示例1在启动调度程序之前从main()内部创建了两个任务。 也可以从另一个任务中创建一个任务。 例如,可以从任务1中创建任务2,如清单17所示。
清单17.调度程序启动后,从另一个任务中创建一个任务
示例1中创建的两个任务几乎相同,它们之间的唯一区别是它们打印出的文本字符串。 可以通过创建单个任务实现的两个实例来删除此重复项。 然后可以使用task参数将应该打印的字符串传递给每个任务。
清单18包含示例2使用的单个任务函数(vTaskFunction)的代码。此单个函数替换了示例1使用的两个任务函数(vTask1和vTask2)。请注意,如何将task参数强制转换为char *以获取 字符串,任务应打印出来。
即使现在只有一个任务实现(vTaskFunction),也可以创建已定义任务的多个实例。 每个创建的实例将在FreeRTOS调度程序的控制下独立执行。
清单19显示了如何使用xTaskCreate()函数的pvParameters参数将文本字符串传递到任务中。
清单19.示例2的main()函数
示例2的输出与图10中示例1的输出完全相同。
xTaskCreate()API函数的uxPriority参数为正在创建的任务分配初始优先级。 使用vTaskPrioritySet()API函数启动调度程序后,可以更改优先级。
可用的最大优先级数由FreeRTOSConfig.h中应用程序定义的configMAX_PRIORITIES编译时间配置常量设置。 低优先级数值表示低优先级任务,优先级0是可能的最低优先级。 因此,可用优先级的范围是0至(configMAX_PRIORITIES-1)。 任何数量的任务都可以共享相同的优先级-确保最大的设计灵活性。
FreeRTOS调度程序可以使用两种方法之一来确定哪个任务将处于“运行”状态。 可以将configMAX_PRIORITIES设置为最大值取决于所使用的方法:
通用方法
通用方法在C语言中实现,并且可以与所有FreeRTOS体系结构端口一起使用。
当使用通用方法时,FreeRTOS不会限制configMAX_PRIORITIES可以设置的最大值。 但是,始终建议将configMAX_PRIORITIES值保持在必要的最小值,因为它的值越高,消耗的RAM就越多,最坏情况下的执行时间也就越长。
如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为0,或者未定义configUSE_PORT_OPTIMISED_TASK_SELECTION,或者如果该通用方法是为使用的FreeRTOS端口提供的唯一方法,则将使用该通用方法。
架构优化方法
体系结构优化的方法使用少量的汇编代码,并且比通用方法要快。 configMAX_PRIORITIES设置不会影响最坏情况的执行时间。
如果使用体系结构优化方法,则configMAX_PRIORITIES不能大于32。与通用方法一样,建议将configMAX_PRIORITIES保持在必要的最小值,因为其值越高,将消耗更多的RAM。
如果在FreeRTOSConfig.h中将configUSE_PORT_OPTIMISED_TASK_SELECTION设置为1,则将使用体系结构优化方法。
并非所有的FreeRTOS端口都提供架构优化的方法。
FreeRTOS调度程序将始终确保能够运行的优先级最高的任务是选择进入“运行”状态的任务。 如果可以运行多个具有相同优先级的任务,则调度程序将依次将每个任务转换为“运行”状态或从“运行”状态移出。
第3.12节“调度算法”介绍了一种称为“时间分片”的可选功能。 到目前为止的示例中都使用了时间切片,时间切片是它们产生的输出中观察到的行为。 在示例中,两个任务的创建优先级相同,并且两个任务始终能够运行。 因此,每个任务都针对一个“时间片”执行,在时间片开始时进入运行状态,在时间片结束时退出运行状态。 在图11中,t1和t2之间的时间等于一个时间片。
为了能够选择要运行的下一个任务,调度程序本身必须在每个时间片1的末尾执行。 为此,使用了一个周期性的中断(称为“滴答中断”)。 时间片的长度由滴答中断频率有效设置,该滴答中断频率由FreeRTOSConfig.h中应用程序定义的configTICK_RATE_HZ编译时间配置常量配置。 例如,如果configTICK_RATE_HZ设置为100(Hz),则时间片将为10毫秒。 两个滴答中断之间的时间称为“滴答周期”。 一个时间片等于一个刻度周期。
可以扩展图11以按执行顺序显示调度程序本身的执行。 如图12所示,其中顶行显示了调度程序的执行时间,细箭头显示了从任务到滴答中断,然后是从滴答中断回到另一个任务的执行顺序。
configTICK_RATE_HZ的最佳值取决于正在开发的应用程序,尽管典型值为100。
FreeRTOS API调用始终以滴答周期的倍数指定时间,通常将其简称为“滴答”。 pdMS_TO_TICKS()宏将以毫秒为单位指定的时间转换为以刻度为单位的时间。 可用分辨率取决于定义的滴答频率,如果滴答频率高于1KHz(如果configTICK_RATE_HZ大于1000),则无法使用pdMS_TO_TICKS()。 清单20显示了如何使用pdMS_TO_TICKS()将指定为200毫秒的时间转换为以秒为单位指定的等效时间。
清单20.使用pdMS_TO_TICKS()宏将200毫秒转换为以滴答周期表示的等效时间
注意:不建议直接在应用程序中以滴答为单位指定时间,而是使用pdMS_TO_TICKS()宏以毫秒为单位指定时间,这样做可以确保如果滴答频率改变了,则应用程序中指定的时间不会更改。
“滴答计数”值是自从调度程序启动以来已经发生的滴答中断总数,假设滴答计数没有溢出。 在指定延迟时间时,用户应用程序不必考虑溢出,因为时间一致性是由FreeRTOS内部管理的。
第3.12节“调度算法”描述了配置常数,这些常数会影响调度程序何时选择要运行的新任务以及滴答中断何时执行。
调度程序将始终确保能够运行的优先级最高的任务是选择进入“运行”状态的任务。 到目前为止,在我们的示例中,已经以相同的优先级创建了两个任务,因此依次进入和退出了“运行”状态。 本示例研究更改示例2中创建的两个任务之一的优先级时发生的情况。 这次,将以优先级1创建第二个任务,并以优先级2创建第二个任务。创建任务的代码如清单21所示。 它仍然只是简单地定期打印出一个字符串,使用空循环来创建延迟。
清单21.创建具有不同优先级的两个任务
例3产生的输出如图13所示。
调度程序将始终选择能够运行的最高优先级任务。 任务2的优先级高于任务1,并且始终可以运行。 因此,任务2是唯一进入“运行”状态的任务。 由于任务1从不进入运行状态,因此它从不打印其字符串。 任务1被认为是任务2的“饥饿”处理时间。
图13.以不同优先级运行两个任务
任务2始终能够运行,因为它不必等待任何东西-它要么绕着空循环循环,要么打印到终端。
图14显示了示例3的执行顺序。
图14.一个任务比另一个任务具有更高优先级时的执行模式
到目前为止,已创建的任务始终具有要执行的处理能力,并且从未等待任何内容–因为它们从未等待任何内容,所以它们始终能够进入“运行”状态。 这种“连续处理”任务的用途有限,因为只能以最低的优先级创建它们。 如果它们以任何其他优先级运行,那么它们将根本阻止优先级较低的任务运行。
为了使任务有用,必须将它们重写为事件驱动的。 事件驱动的任务只有在触发事件的事件发生后才能执行工作(处理),并且在该事件发生之前不能进入运行状态。 调度程序始终选择能够运行的最高优先级任务。 高优先级任务无法运行意味着调度程序无法选择它们,而必须选择能够运行的低优先级任务。 因此,使用事件驱动的任务意味着可以在不同的优先级下创建任务,而不会使最高优先级的任务占用所有较低优先级的处理时间任务。
据说正在等待事件的任务处于“已阻止”状态,这是“未运行”状态的子状态。
任务可以进入“阻塞”状态以等待两种不同类型的事件:
FreeRTOS队列,二进制信号量,计数信号量,互斥量,递归互斥量,事件组以及直接到任务通知都可以用来创建同步事件。 所有这些功能都将在本书的后续章节中介绍。
任务可能会超时阻止同步事件,从而有效地同时阻止两种类型的事件。 例如,一个任务可能选择等待最多10毫秒,以使数据到达队列。 如果数据在10毫秒内到达,或者在10毫秒内没有数据到达,则任务将退出“阻塞”状态。
“挂起”也是“未运行”的子状态。 处于“挂起”状态的任务不可用于调度器。 进入Suspended状态的唯一方法是通过调用vTaskSuspend()API函数,唯一的方法是通过调用vTaskResume()或xTaskResumeFromISR()API函数。 大多数应用程序不使用Suspended状态。
处于“未运行”状态但未被阻止或挂起的任务被称为“就绪”状态。 它们能够运行,因此可以“运行”,但目前尚未处于“运行”状态。
图15在先前的过度简化状态图的基础上进行了扩展,以包括本节中描述的所有“未运行”子状态。 到目前为止,在示例中创建的任务尚未使用“已阻止”或“已暂停”状态。 它们仅在“就绪”状态和“运行”状态之间转换,由图15中的粗线突出显示。
图15.全任务状态机
例子4.使用Blocked状态创建一个延迟
到目前为止,在示例中创建的所有任务都是“周期性的”,它们已经延迟了一段时间并打印了字符串,然后再延迟一次,依此类推。 延迟是使用空循环非常粗略地生成的-任务有效地轮询了递增循环计数器,直到达到固定值为止。 实施例3清楚地表明了该方法的缺点。 优先级较高的任务在执行空循环时仍处于“运行”状态,使任何处理时间的优先级较低的任务“饥饿”。
任何形式的轮询还有其他几个缺点,尤其是它效率低下。 在轮询过程中,该任务实际上没有任何工作要做,但是它仍然使用最大的处理时间,因此浪费了处理器周期。 示例4通过使用对vTaskDelay()API函数的调用替换轮询null循环来纠正此行为,清单22给出了该函数的原型。清单23显示了新的任务定义。请注意,vTaskDelay()API函数 仅在FreeRTOSConfig.h中将INCLUDE_vTaskDelay设置为1时可用。
vTaskDelay()将调用任务置于Blocked状态,以固定数量的滴答中断。 处于“阻塞”状态时,该任务不使用任何处理时间,因此该任务仅在实际有待完成的工作时才使用处理时间。
参数名称 | 描述 |
---|---|
xTicksToDelay | 滴答中断的数量,即在转换回“就绪”状态之前,调用任务将保持在“阻塞”状态。例如,如果在滴答计数为10,000时名为vTaskDelay(100)的任务,则它将立即进入Blocked状态,并保持Blocked状态,直到滴答计数达到10,100。宏pdMS_TO_TICKS()可用于将以毫秒为单位的时间转换为以刻度为单位的时间。 例如,调用vTaskDelay(pdMS_TO_TICKS(100))将导致调用任务在Blocked状态下保持100毫秒。 |
清单23.空循环延迟之后的示例任务的源代码已由对vTaskDelay()的调用替换
即使仍以不同的优先级创建两个任务,两个任务现在都将运行。 图16所示的示例4的输出确认了预期的行为。
图16.执行示例4时产生的输出
图17中所示的执行顺序说明了为什么两个任务都运行,即使它们是在不同的优先级下创建的也是如此。 为简单起见,省略了调度程序本身的执行。
启动调度程序时,将自动创建空闲任务,以确保始终有至少一个任务能够运行(至少一个任务处于“就绪”状态)。 第3.8节“空闲任务和空闲任务挂钩”详细描述了空闲任务。
图17.任务使用vTaskDelay()代替NULL循环时的执行顺序
仅两个任务的实现已更改,其功能未更改。 将图17与图12进行比较可以清楚地表明,该功能正在以一种更加有效的方式实现。
图12显示了任务使用空循环来创建延迟的任务时始终可以运行的执行模式,因此在任务之间使用了可用处理器时间的百分之一百。 图17显示了任务在整个延迟周期内进入“阻塞”状态时的执行模式,因此仅当它们实际有需要执行的工作时才使用处理器时间(在这种情况下,只是打印一条消息),并且 结果,只使用了可用处理时间的一小部分。
在图17场景中,每次任务离开“阻塞”状态时,它们都会执行一个滴答周期的一小部分,然后再重新进入“阻塞”状态。 大多数情况下,没有能够运行的应用程序任务(没有处于“就绪”状态的应用程序任务),因此,没有可以选择进入“运行”状态的应用程序任务。 在这种情况下,空闲任务将运行。 分配给空闲的处理时间量是系统中备用处理能力的度量。 仅通过允许应用程序完全由事件驱动,使用RTOS即可显着增加备用处理能力。
图18中的粗线显示了示例4中的任务执行的转换,每个转换现在都在返回“就绪”状态之前通过“已阻塞”状态进行转换。
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()可用于将以毫秒为单位的时间转换为以刻度为单位的时间。 |
示例4中创建的两个任务是周期性任务,但是使用vTaskDelay()不能保证它们的运行频率是固定的,因为任务离开Blocked状态的时间与调用vTaskDelay()的时间有关。 将任务转换为使用vTaskDelayUntil()而不是vTaskDelay()可以解决此潜在问题。
清单25.使用vTaskDelayUntil()实现示例任务
示例5产生的输出与图16中示例4所示的输出完全相同。
前面的示例分别检查了轮询和阻止任务的行为。 本示例通过演示将两种方案组合在一起时的执行顺序来增强规定的预期系统行为,如下所示。
定期任务的源如清单27所示。
清单26.示例6中使用的连续处理任务
清单27.示例6中使用的定期任务
图19显示了示例6产生的输出,并通过图20所示的执行序列对观察到的行为进行了解释。
图20.示例6的执行模式
在示例4中创建的任务大部分时间都处于“阻塞”状态。 在这种状态下,它们将无法运行,因此无法由调度程序选择。
必须始终至少有一项任务可以进入“运行”状态2。 为确保这种情况,调度程序在调用vTaskStartScheduler()时会自动创建一个空闲任务。 空闲任务所做的仅是坐在循环中,因此,就像最初的第一个示例中的任务一样,它始终可以运行。
空闲任务具有最低的优先级(优先级为零),以确保它永远不会阻止更高优先级的应用程序任务进入“运行”状态-尽管没有什么可以阻止应用程序设计人员以空闲任务优先级创建并因此共享空闲任务优先级的任务, 如果需要的话。 FreeRTOSConfig.h中的configIDLE_SHOULD_YIELD编译时间配置常量可用于防止Idle任务消耗处理时间,而该处理时间可以更有效地分配给应用程序任务。 configIDLE_SHOULD_YIELD在3.12节,调度算法中介绍。
以最低优先级运行时,确保较高优先级的任务进入就绪状态后,空闲任务便会从运行状态过渡出来。 这可以在图17中的时间tn处看到,其中,空闲任务立即换出以允许任务2在任务2离开阻塞状态的瞬间执行。 据说任务2已抢占了空闲任务。 抢占自动发生,并且不知道该任务被抢占。
注意:如果应用程序使用vTaskDelete()API函数,则必须使空闲任务没有处理时间。 这是因为空闲任务负责清理删除任务后的内核资源。
可以通过使用空闲钩子(或空闲回调)功能将功能特定的功能直接添加到空闲任务中,该功能由空闲任务每次迭代一次由空闲任务自动调用。
空闲任务钩子的常见用法包括:
空闲任务挂钩函数必须遵守以下规则。
空闲任务钩子函数绝不能尝试阻塞或挂起。
注意:以任何方式阻止空闲任务都可能导致以下情况:没有任务可进入运行状态。
如果应用程序使用vTaskDelete()API函数,则Idle任务挂钩必须始终在合理的时间段内返回给其调用者。 这是因为空闲任务负责删除任务后清理内核资源。 如果空闲任务永久保留在“空闲”钩子函数中,则无法进行此清理。
空闲任务钩子函数必须具有清单28所示的名称和原型。
清单28.空闲任务钩子函数的名称和原型
示例4中使用阻塞vTaskDelay()API调用创建了许多空闲时间,即空闲任务执行的时间,因为两个应用程序任务都处于“阻塞”状态。 例7通过添加一个Idle hook函数来利用这个空闲时间,清单29给出了它的源文件。
清单29.一个非常简单的Idle hook函数
必须在FreeRTOSConfig.h中将configUSE_IDLE_HOOK设置为1,才能调用空闲挂钩函数。
稍微修改了实现创建的任务的函数,以打印出ulIdleCycleCount值,如清单30所示。
清单30.现在,示例任务的源代码将打印出ulIdleCycleCount值
实例7产生的输出如图21所示。该图显示了在应用程序任务的每次迭代之间,空闲任务挂钩函数被调用了大约400万次(迭代次数取决于演示所基于的硬件的速度)。
调度程序启动后,可以使用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函数可用于查询任务的优先级。 请注意,只有在FreeRTOSConfig.h中将INCLUDE_uxTaskPriorityGet设置为1时,uxTaskPriorityGet()API函数才可用。
清单32. uxTaskPriorityGet()API函数原型
表12. uxTaskPriorityGet()参数和返回值
参数名称/返回值 | 描述 |
---|---|
pxTask | 要查询其优先级的任务(主题任务)的句柄-有关获取任务句柄的信息,请参见xTaskCreate()API函数的pxCreatedTask参数。任务可以通过传递NULL代替有效的任务句柄来查询其自身的优先级。 |
Returned value | 当前分配给要查询的任务的优先级。 |
调度程序将始终选择最高的“就绪”状态任务作为进入“运行”状态的任务。 示例8通过使用vTaskPrioritySet()API函数来更改两个任务相对于彼此的优先级,对此进行了演示。
示例8以两个不同的优先级创建了两个任务。 这两个任务都不会进行任何可能导致其进入“阻塞”状态的API函数调用,因此两者始终处于“就绪”状态或“运行”状态。 因此,具有最高相对优先级的任务将始终是调度程序选择的处于“运行”状态的任务。
示例8的行为如下:
每个任务都可以查询并设置自己的优先级,而无需使用有效的任务句柄,只需使用NULL即可。 仅当任务希望引用其自身以外的任务时(例如,任务1更改任务2的优先级时),才需要任务句柄。要允许任务1进行此操作,将在任务2被执行时获取并保存任务2的句柄。 创建,如清单35中的注释中突出显示。
清单35.示例8的main()的实现
图22演示了示例8任务的执行顺序,结果输出如图23所示。
图22.运行示例8时任务执行的顺序
图23.执行示例8时产生的输出
任务可以使用vTaskDelete()API函数删除自身或任何其他任务。 请注意,仅当在FreeRTOSConfig.h中将INCLUDE_vTaskDelete设置为1时,vTaskDelete()API函数才可用。
已删除的任务不再存在,并且无法再次进入“运行”状态。
空闲任务的责任是释放分配给此后已删除任务的内存。 因此,重要的是,使用vTaskDelete()API函数的应用程序不要完全耗尽所有处理时间的空闲任务。
注意:删除任务时,只有内核本身分配给任务的内存才会自动释放。 必须显式释放执行任务分配的任何内存或其他资源。
清单36. vTaskDelete()API函数原型
表13. vTaskDelete()参数
参数名称/返回值 | 描述 |
---|---|
pxTaskToDelete | 要删除的任务(主题任务)的句柄-有关获取任务句柄的信息,请参阅xTaskCreate()API函数的pxCreatedTask参数。任务可以通过传递NULL代替有效的任务句柄来删除自身。 |
这是一个非常简单的示例,其行为如下。
清单38.示例9的任务1的实现
清单39.示例9的任务2的实现
图24.执行示例9时产生的输出
待定。 本部分将在最终发布之前编写。
实际正在运行的任务(使用处理时间)处于“正在运行”状态。 在单个核心处理器上,任何给定时间只能有一个任务处于“运行”状态。
实际未运行但不在“阻塞”状态或“未挂起”状态的任务处于“就绪”状态。 计划程序可以选择处于“就绪”状态的任务作为进入“运行”状态的任务。 调度程序将始终选择优先级最高的“就绪”状态任务以进入“运行”状态。
任务可以在“阻止”状态下等待事件,并在事件发生时自动移回“就绪”状态。 时间事件在特定时间发生,例如,在某个块时间到期时,通常用于实现周期性或超时行为。 当任务或中断服务例程使用任务通知,队列,事件组或多种信号量之一发送信息时,就会发生同步事件。 它们通常用于发信号通知异步活动,例如到达外围设备的数据。
调度算法是决定将哪些就绪状态任务转换为运行状态的软件例程。
到目前为止,所有示例都使用相同的调度算法,但是可以使用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显示了当应用程序中的两个任务共享优先级时,选择任务进入“运行”状态的顺序。
图26.执行模式在一个假设的应用程序中突出显示了任务的优先级和抢占,在该应用程序中,每个任务都被分配了唯一的优先级
参考图26:
图27执行模式突出显示了一个假设应用程序中的任务优先级和时间分片,在该应用程序中两个任务以相同的优先级运行
参见图27:
图27显示了空闲任务与应用程序编写者创建的任务共享处理时间。 如果应用程序编写者创建的空闲优先级任务有工作要做,那么为空闲任务分配这么多的处理时间可能不是理想的,但是空闲任务则没有。 这
configIDLE_SHOULD_YIELD编译时间配置常量可用于更改空闲任务的计划方式:
当configIDLE_SHOULD_YIELD设置为0时,将观察到图27所示的执行模式。将configIDLE_SHOULD_YIELD设置为1时,在相同情况下将观察到图28所示的执行模式。
图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演示了这种情况。因此,在没有时间分片的情况下运行调度程序被认为是一项高级技术,只应由 经验丰富的用户。
图29执行模式演示了在不使用时间分割的情况下,具有相同优先级的任务如何获得截然不同的处理时间
参考图29,它假定configIDLE_SHOULD_YIELD设置为0:
本书侧重于抢先式调度,但是FreeRTOS也可以使用协作式调度。 表17显示了配置FreeRTOS调度程序以使用协作调度的FreeRTOSConfig.h设置。
表17. FreeRTOSConfig.h设置,用于将内核配置为使用协作调度
常数 | 值 |
---|---|
configUSE_PREEMPTION | 0 |
configUSE_TIME_SLICING | 任何值 |
使用协作式调度程序时,仅当“运行状态”任务进入“阻止”状态,或者“运行状态”任务通过调用taskYIELD()明确产生(手动请求重新计划)时,才会发生上下文切换。 任务永远不会被抢占,因此无法使用时间分割。
图30演示了合作调度程序的行为。 图30中的水平虚线显示了任务何时处于“就绪”状态。
图30执行模式演示了协作调度程序的行为
参考图30:
在多任务应用程序中,应用程序编写者必须注意多个任务不能同时访问资源,因为同时访问可能会破坏资源。 例如,请考虑以下情形,其中正在访问的资源是UART(串行端口)。 向UART写入字符串是两项任务。 任务1正在编写“ bcdefghijklmnop”? 任务2正在写“ 23456789”
在这种情况下,实际写入UART的是“ abcdefg123456789hijklmnop”。 任务1写入的字符串没有按预期的连续顺序写入UART,而是已损坏,因为任务2写入UART的字符串出现在其中。
与使用抢占式调度程序相比,使用协作式调度程序通常更容易避免因同时访问而引起的问题3:
如图30所示,与使用抢占式调度程序相比,使用协作式调度程序时系统的响应速度将更低:
需要特别注意的是,时间片的结束不是调度程序可以选择要运行的新任务的唯一位置。 正如整本书将要演示的那样,调度程序还将选择一个新任务,以在当前正在执行的任务进入“阻塞”状态后,或当中断将优先级较高的任务移至“就绪”状态时立即运行。 ↩︎ ↩︎
即使在使用FreeRTOS的特殊低功耗功能时也是如此,在这种情况下,如果应用程序创建的任务均无法执行,则执行FreeRTOS的微控制器将置于低功耗模式。 ↩︎
本书稍后将介绍在任务之间安全共享资源的方法。 FreeRTOS本身提供的资源(例如队列和信号量)始终可以在任务之间安全共享。 ↩︎