FreeRTOS 内核基础知识 - FreeRTOS
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

FreeRTOS 内核基础知识

FreeRTOS 内核是一个实时操作系统,支持各种架构。它是构建嵌入式微控制器应用程序的理想之选。它提供了以下功能:

  • 多任务计划程序。

  • 多个内存分配选项(包括创建完全静态分配的系统的功能)。

  • 任务间协调基元,包括任务通知、消息队列、多种信号灯类型以及流和消息缓冲区。

  • 支持多核微控制器上的对称多处理 (SMP)。

FreeRTOS 内核在关键部分或中断内部从不执行非确定性操作,例如,遍历链接列表。FreeRTOS 内核包含一个高效的软件计时器实施,不使用任何 CPU 时间(除非计时器需要维护)。已阻止的任务不需要耗时的定期维护。“直接到任务”通知可实现快速的任务信号发送,几乎没有 RAM 开销。它们可用于大多数任务间信号发送以及“中断到任务”信号发送场景。

FreeRTOS 内核设计为小型、简单且易于使用。典型的 RTOS 内核二进制映像大小为 4000 到 9000 字节。

有关 FreeRTOS 内核的最新文档,请参阅 FreeRTOS.org。Freeertos.org 提供了一系列关于使用 FreeRTOS 内核的详细教程和指南,包括快速入门指南和更深入的掌握 FreeRTOS 实时内核

FreeRTOS 内核计划程序

采用 RTOS 的嵌入式应用程序可以结构化为一组独立的任务。每个任务都在自己的上下文中执行,独立于其他任务。在任何时间点,应用程序中都只有一个任务在运行。每个任务应当在何时运行由实时 RTOS 计划程序决定。每个任务都提供有自己的堆栈。当某个任务被换出以便运行另一个任务时,该任务的执行上下文将保存到任务堆栈,以便稍后在换回该任务恢复其运行时,可以还原执行上下文。

为提供确定性的实时行为,FreeRTOS 任务计划程序允许为任务分配严格的优先级。RTOS 可确保为能够执行的最高优先级任务分配处理时间。如果优先级相同的多个任务同时准备好运行,则这些任务需要共享处理时间。FreeRTOS 还会创建空闲任务,仅在没有其他任务准备好运行时执行它。

内存管理

本部分提供了有关内核内存分配和应用程序内存管理的信息。

内核内存分配

每次在创建任务、队列或其他 RTOS 对象时,RTOS 内核都需要 RAM。RAM 可采用以下分配方式:

  • 在编译时静态分配。

  • 由 RTOS API 对象创建函数从 RTOS 堆动态分配。

在动态创建 RTOS 对象时,使用标准 C 库 malloc()free() 函数并不始终恰当,原因如下:

  • 它们在嵌入式系统中可能不可用。

  • 它们占用了宝贵的代码空间。

  • 它们通常不是线程安全的。

  • 它们不是确定性的。

出于这些原因,FreeRTOS 会在其可移动层保留内存分配 API。可移动层位于实施核心 RTOS 功能的源文件外部,因此您可以针对正在开发的实时系统,提供特定于应用程序的适当实施。当 RTOS 内核需要 RAM 时,它会调用 pvPortMalloc(),而不是 malloc()()。在释放 RAM 时,RTOS 内核调用 vPortFree(),而不是 free()

应用程序内存管理

当应用程序需要内存时,可以从 FreeRTOS 堆进行分配。FreeRTOS 提供了多种堆管理方案,复杂性和功能各不相同。您也可以提供自己的堆实施。

FreeRTOS 内核包含以下五个堆实施:

heap_1

是最简单的实施。不允许释放内存。

heap_2

允许释放内存,但不合并相邻的空闲数据块。

heap_3

对标准的 malloc()free() 进行包装以确保线程安全。

heap_4

合并相邻的空闲数据块以避免碎片。包括绝对地址放置选项。

heap_5

类似于 heap_4。可以跨越多个非相邻内存区域中的堆。

任务间协调

本部分包含了有关 FreeRTOS 基元的信息。

Queues

队列是任务间通信的主要方式。可用于在任务之间以及中断与任务之间发送消息。大多数情况下,队列用作线程安全先进先出 (FIFO) 缓冲区,新数据将发送到队列的后面。(数据也可以发送到队列的前面。) 消息通过队列以复制方式发送,这意味着是将数据(可能是指向更大缓冲区的指针)本身复制到队列中,而不只是存储对数据的引用。

队列 API 允许指定阻止时间。如果任务尝试读取空队列,则该任务将置为“被阻止”状态,直到队列中有可用数据,或者阻止时间结束。处于“被阻止”状态的任务不会占用任何 CPU 时间,以便其他任务运行。同样,如果任务尝试写入已满队列,则该任务将置为“被阻止”状态,直到队列中有可用空间,或者阻止时间结束。如果对同一队列阻止了多个任务,则具有最高优先级的任务将首先取消阻止。

在许多常见的设计场景中,其他 FreeRTOS 基元(如“直接到任务”通知以及流和消息缓冲区)可作为队列的轻型替代方案。

信号灯和互斥对象

FreeRTOS 内核提供了二元信号灯、计数信号灯和互斥对象,以用于相互排斥和同步的情况。

二元信号灯只能有两个值。如果要在任务之间或任务与中断之间实施同步,二元信号灯是不错的选择。计数信号灯可以有两个以上的值。该信号灯允许多个任务共享资源或执行更复杂的同步操作。

互斥对象是包括优先级继承机制的二元信号灯。这意味着,如果高优先级任务在尝试获取当前由较低优先级任务所持有的互斥对象时遭到阻止,则持有令牌的任务的优先级将临时提升至被阻止任务的优先级。此机制旨在确保较高优先级任务尽可能在最短时间内处于“被阻止”状态,从而最大程度地减少优先级反转的发生。

“直接到任务”通知

通过任务通知,任务可以与其他任务进行交互,并与中断服务例程 (ISR) 同步,且无需单独的通信对象(如信号灯)。每个 RTOS 任务都有一个 32 位通知值,用于存储通知的内容(如果有)。RTOS 任务通知即直接发送给任务的事件,可以取消阻止接收的任务,以及有选择地更新所接收任务的通知值。

RTOS 任务通知可用作二元信号灯、计数信号灯和队列(在某些情况下)的更快速的轻型替代方案。相比于可用于执行同等功能的其他 FreeRTOS 基元,任务通知在速度和 RAM 开销两方面均具有优势。但是,任务通知只能用在事件接收方只能是一个任务的情况下。

流缓冲区

通过流缓冲区,可以将字节流从中断服务例程传递到任务,或者从一个任务传递到另一个。字节流可以是任意长度,且并不一定具有开头或结尾。可以一次写入任意数量的字节,也可以一次读取任意数量的字节。可通过将 stream_buffer.c 源文件包含在项目中来启用流缓冲区功能。

流缓冲区假定只有一个任务或中断写入缓冲区(即写入器),并且只从缓冲区读取一个任务或中断(即读取器)。写入器和读取器是不同的任务或中断服务例程才安全,具有多个写入器或读取器是不安全的。

流缓冲区实施采用的是“直接到任务”通知。因此,流缓冲区 API 将调用的任务置于“被阻止”状态,调用该 API 可以改变该调用任务的通知状态和通知值。

发送数据

xStreamBufferSend() 用于将数据发送到任务的流缓冲区。xStreamBufferSendFromISR() 用于将数据发送到中断服务例程 (ISR) 的流缓冲区。

xStreamBufferSend() 允许指定阻止时间。如果在调用 xStreamBufferSend() 时,将非零阻止时间写入流缓冲区且缓冲区已满,则任务将置为“被阻止”状态,直到有可用空间或者阻止时间结束。

sbSEND_COMPLETED()sbSEND_COMPLETED_FROM_ISR() 是将数据写入流缓冲区时调用的宏(由 FreeRTOS API 在内部调用)。它采用更新的流缓冲区的句柄。这两个宏会查看流缓冲区中是否有被阻止的任务等待数据,如果有,会从“被阻止”状态中删除该任务。

您可以在 FreeRTOSConfig.h 中提供自己的 sbSEND_COMPLETED() 实施,以更改此默认行为。如果利用流缓冲区在多核处理器的核心之间传递数据,该功能很有用。在此情况下,可以执行 sbSEND_COMPLETED() 在另一个 CPU 核心中生成中断,然后该中断的服务例程可以使用 xStreamBufferSendCompletedFromISR() API 进行检查,如有必要则取消阻止等待数据的任务。

接收数据

xStreamBufferReceive() 用于从任务的流缓冲区读取数据。xStreamBufferReceiveFromISR() 用于从中断服务例程 (ISR) 的流缓冲区读取数据。

xStreamBufferReceive() 允许指定阻止时间。如果在调用 xStreamBufferReceive() 时,从流缓冲区读取非零阻止时间但缓冲区为空,则任务将置为“被阻止”状态,直到流缓冲区中有可用的指定数据量,或者阻止时间结束。

在取消阻止任务之前流缓冲区中必须具备的数据量,称为流缓冲区的触发级别。如果被阻止的任务触发级别为 10,则当至少 10 字节写入缓冲区或者任务的阻止时间结束时,将取消阻止该任务。如果读取任务尚未达到触发级别但其阻止时间已经到期,则该任务将接收写入缓冲区的任何数据。任务的触发级别必须设置为介于 1 与流缓冲区大小之间的值。流缓冲区的触发级别在调用 xStreamBufferCreate() 时设置。可以通过调用 xStreamBufferSetTriggerLevel() 进行更改。

sbRECEIVE_COMPLETED()sbRECEIVE_COMPLETED_FROM_ISR() 是从流缓冲区读取数据时调用的宏(由 FreeRTOS API 内部调用)。宏会查看流缓冲区中是否有被阻止的任务等待缓冲区中有可用空间,如果有,它们会从“被阻止”状态中删除该任务。您可以在 FreeRTOSConfig.h 中提供其他实施来更改 sbRECEIVE_COMPLETED() 的默认行为。

消息缓冲区

通过消息缓冲区,可以将可变长度的离散消息从中断服务例程传递到任务,或者从一个任务传递到另一个。例如,可以将长度为 10、20 和 123 字节的消息写入消息缓冲区,或者从同一消息缓冲区中读取这些消息。10 字节的消息只能以 10 字节消息而不是单独字节的形式读取。消息缓冲区构建在流缓冲区实施之上。您可以通过将 stream_buffer.c 源文件包含在项目中来启用消息缓冲区功能。

消息缓冲区假定只有一个任务或中断写入缓冲区(即写入器),并且只从缓冲区读取一个任务或中断(即读取器)。写入器和读取器是不同的任务或中断服务例程才安全,具有多个写入器或读取器是不安全的。

消息缓冲区实施采用的是“直接到任务”通知。因此,流缓冲区 API 将调用的任务置于“被阻止”状态,调用该 API 可以改变该调用任务的通知状态和通知值。

要启用消息缓冲区来处理可变大小的消息,应先将每条消息的长度写入消息缓冲区,然后再写入消息本身。长度存储在类型为 size_t 的变量中,在 32 字节架构上通常为 4 字节。因此,将一条 10 字节消息写入消息缓冲区时,实际占用的缓冲区空间为 14 字节。同样,将一条 100 字节消息写入消息缓冲区时,实际使用的缓冲区空间为 104 字节。

发送数据

xMessageBufferSend() 用于将数据从任务发送到消息缓冲区。xMessageBufferSendFromISR() 用于将数据从中断服务例程 (ISR) 发送到消息缓冲区。

xMessageBufferSend() 允许指定阻止时间。如果在调用 xMessageBufferSend() 时,将非零阻止时间写入消息缓冲区且缓冲区已满,则任务将置为“被阻止”状态,直到消息缓冲区中有可用空间,或者阻止时间结束。

sbSEND_COMPLETED()sbSEND_COMPLETED_FROM_ISR() 是将数据写入流缓冲区时调用的宏(由 FreeRTOS API 在内部调用)。宏采用单个参数,即更新的流缓冲区的句柄。这两个宏会查看流缓冲区中是否有被阻止的任务等待数据,如果有,会从“被阻止”状态中删除该任务。

您可以在 FreeRTOSConfig.h 中提供自己的 sbSEND_COMPLETED() 实施,以更改此默认行为。如果利用流缓冲区在多核处理器的核心之间传递数据,该功能很有用。在此情况下,可以执行 sbSEND_COMPLETED() 在另一个 CPU 核心中生成中断,然后该中断的服务例程可以使用 xStreamBufferSendCompletedFromISR() API 进行检查,如有必要则取消阻止等待数据的任务。

接收数据

xMessageBufferReceive() 用于将数据从消息缓冲区读取到任务中。xMessageBufferReceiveFromISR() 用于将数据从消息缓冲区读取到中断服务例程 (ISR) 中。xMessageBufferReceive() 允许指定阻止时间。如果在调用 xMessageBufferReceive() 时,从消息缓冲区读取非零阻止时间但缓冲区为空,则任务将置为“被阻止”状态,直到有可用数据或者阻止时间结束。

sbRECEIVE_COMPLETED()sbRECEIVE_COMPLETED_FROM_ISR() 是从流缓冲区读取数据时调用的宏(由 FreeRTOS API 内部调用)。宏会查看流缓冲区中是否有被阻止的任务等待缓冲区中有可用空间,如果有,它们会从“被阻止”状态中删除该任务。您可以在 FreeRTOSConfig.h 中提供其他实施来更改 sbRECEIVE_COMPLETED() 的默认行为。

对称多处理 (SMP) 支持

FreeRTOS 内核支持 SMP,使 FreeRTOS 的一个内核实例能够在多个相同的处理器内核上调度任务。核心架构必须相同且共享相同的内存。

修改应用程序以使用 FreeRTOS-SMP 内核

除了这些额外的 API 之外,单核与 SMP 版本之间的 FreeRTOS API 基本相同。因此,为 FreeRTOS 单核版本编写的应用程序应使用 SMP 版本进行编译,这样工作量就会很少。但是,可能会存在一些功能问题,因为一些适用于单核应用程序的假设可能不适用于多核应用程序。

一个常见的假设是,当优先级较高的任务正在运行时,优先级较低的任务无法运行。虽然在单核系统上确实如此,但多核系统则不然,因为多个任务可以同时运行。如果应用程序依靠相对任务优先级来提供互斥性,则它可能会在多核环境中观察到意外结果。

另一个常见的假设是 ISR 不能相互或与其他任务同步运行。在多核环境中,情况不再是这样。应用程序编写者在访问任务和 ISR 之间共享的数据时需要确保适当的互斥性。

软件计时器

采用软件计时器,可以在未来的设定时间执行函数。由计时器执行的函数称为计时器的回调函数。从启动计时器到执行计时器回调函数之间的时间称为计时器的周期。FreeRTOS 内核提供了高效的软件计时器实施,原因如下:

  • 它不会从中断上下文内执行计时器回调函数。

  • 它不会占用任何处理时间,除非计时器实际上已过期。

  • 它不会给滴答中断增加任何处理开销。

  • 中断处于禁用状态时,它不会搜索任何链接列表结构。

低功耗支持

与大多数嵌入式操作系统一样,FreeRTOS 内核使用硬件计时器来生成周期性的滴答中断,以用于测量时间。常规硬件计时器实施的节能受限于必须定期退出然后重新进入低功耗状态来处理滴答中断。如果滴答中断的频率太高,则为每次滴答中断进入和退出低功耗状态所消耗的能量和时间,会超过除最轻节能模式之外其他任何可能的节能收益。

为解决此限制问题,FreeRTOS 为低功耗应用程序提供了非滴答计时器模式。FreeRTOS 非滴答空闲模式在空闲时间段(即不存在可执行的应用程序任务的时间段)内将停止周期性的滴答中断,然后在重新启动滴答中断时对 RTOS 滴答计数值进行校正调整。通过停止滴答中断,微控制器可以维持在深度节能状态,直到中断发生,或者到了 RTOS 内核将任务转换为就绪状态的时间。

内核配置

您可以使用 FreeRTOSConfig.h 标头文件为特定主板和应用程序配置 FreeRTOS 内核。每个基于内核构建的应用程序都必须在其预处理程序包含路径中有一个 FreeRTOSConfig.h 标头文件。FreeRTOSConfig.h 是特定于应用程序的,并且应置于应用程序目录下,而不是置于 FreeRTOS 内核源代码目录中。

FreeRTOS 演示和测试应用程序的 FreeRTOSConfig.h 文件位于 freertos/vendors/vendor/boards/board/aws_demos/config_files/FreeRTOSConfig.hfreertos/vendors/vendor/boards/board/aws_tests/config_files/FreeRTOSConfig.h 中。

有关要在 FreeRTOSConfig.h 中指定的可用配置参数的列表,请参阅 FreeRTOS.org