开源应用程序架构(卷 2)
FreeRTOS

克里斯托弗·斯维克

FreeRTOS(发音为“free-arr-toss”)是一个用于嵌入式系统的开源实时操作系统 (RTOS)。FreeRTOS 支持许多不同的架构和编译器工具链,并且旨在“小巧、简单且易于使用”。

FreeRTOS 处于积极开发中,并且自 2002 年理查德·巴里开始在其上工作以来一直如此。至于我,我不是 FreeRTOS 的开发人员或贡献者,我只是一个用户和粉丝。因此,本章将偏向于 FreeRTOS 架构的“是什么”和“如何”,而“为什么”的内容会比本书中的其他章节少。

与所有操作系统一样,FreeRTOS 的主要工作是运行任务。FreeRTOS 的大部分代码都涉及优先级排序、调度和运行用户定义的任务。与所有操作系统不同的是,FreeRTOS 是一种在嵌入式系统上运行的实时操作系统。

在本章结束时,我希望您能了解 FreeRTOS 的基本架构。FreeRTOS 的大部分内容都致力于运行任务,因此您将很好地了解 FreeRTOS 如何做到这一点。

如果这是您第一次深入了解操作系统的内部,我还希望您能学习任何操作系统的工作原理。FreeRTOS 相对简单,尤其是在与 Windows、Linux 或 OS X 相比时,但所有操作系统都共享相同的基本概念和目标,因此查看任何操作系统都具有启发性和趣味性。

3.1. 什么是“嵌入式”和“实时”?

“嵌入式”和“实时”对不同的人可能意味着不同的东西,所以让我们根据 FreeRTOS 的使用方式来定义它们。

嵌入式系统是一种旨在仅执行少量操作的计算机系统,例如电视遥控器、车载 GPS、电子手表或起搏器中的系统。嵌入式系统通常比通用计算机系统更小、更慢,并且通常成本更低。一个典型的低端嵌入式系统可能具有一个以 25MHz 运行的 8 位 CPU、几 KB 的 RAM 和大约 32KB 的闪存。高端嵌入式系统可能具有一个以 750MHz 运行的 32 位 CPU、1GB 的 RAM 和多个 GB 的闪存。

实时系统旨在在一定时间内完成某些操作;它们保证事情在应该发生时发生。

起搏器是实时嵌入式系统的绝佳示例。起搏器必须在正确的时间收缩心肌以维持您的生命;它不能太忙而无法及时响应。起搏器和其他实时嵌入式系统经过精心设计,可以始终按时执行其任务。

3.2. 架构概述

FreeRTOS 是一个相对较小的应用程序。FreeRTOS 的最小核心只有三个源 (.c) 文件和少量头文件,总共不到 9000 行代码,包括注释和空行。一个典型的二进制代码映像小于 10KB。

FreeRTOS 的代码分为三个主要区域:任务、通信和硬件接口。

硬件注意事项

硬件独立的 FreeRTOS 层位于硬件相关的层之上。此硬件相关的层知道如何与您选择的任何芯片架构进行通信。图 3.1 显示了 FreeRTOS 的层。

图 3.1:FreeRTOS 软件层

FreeRTOS 附带了所有硬件独立和硬件相关的代码,您需要这些代码才能使系统启动并运行。它支持许多编译器(CodeWarrior、GCC、IAR 等)以及许多处理器架构(ARM7、ARM Cortex-M3、各种 PIC、Silicon Labs 8051、x86 等)。请参阅 FreeRTOS 网站以获取受支持的架构和编译器的列表。

FreeRTOS 通过设计具有高度可配置性。FreeRTOS 可以构建为单 CPU、基本 RTOS,仅支持少量任务,也可以构建为具有 TCP/IP、文件系统和 USB 的功能强大的多核系统。

配置选项在 FreeRTOSConfig.h 中通过设置各种 #defines 进行选择。时钟速度、堆大小、互斥体和 API 子集都可以在此文件中配置,以及许多其他选项。以下是一些设置最大任务优先级级别数、CPU 频率、系统滴答频率、最小堆栈大小和总堆大小的示例

#define configMAX_PRIORITIES      ( ( unsigned portBASE_TYPE ) 5 )
#define configCPU_CLOCK_HZ        ( 12000000UL )
#define configTICK_RATE_HZ        ( ( portTickType ) 1000 )
#define configMINIMAL_STACK_SIZE  ( ( unsigned short ) 100 )
#define configTOTAL_HEAP_SIZE     ( ( size_t ) ( 4 * 1024 ) )

硬件相关的代码位于每个编译器工具链和 CPU 架构的单独文件中。例如,如果您使用 IAR 编译器在 ARM Cortex-M3 芯片上工作,则硬件相关的代码位于 FreeRTOS/Source/portable/IAR/ARM_CM3/ 目录中。portmacro.h 声明所有硬件特定的函数,而 port.cportasm.s 包含所有实际的硬件相关的代码。硬件独立的头文件 portable.h 在编译时包含正确的 portmacro.h 文件。FreeRTOS 使用在 portmacro.h 中声明的 #defined 函数调用硬件特定的函数。

让我们看一个 FreeRTOS 如何调用硬件相关函数的示例。硬件独立文件 tasks.c 经常需要进入代码的关键部分以防止抢占。在不同架构上进入关键部分的方式不同,硬件独立的 tasks.c 不希望理解硬件相关的细节。因此,tasks.c 调用全局宏 portENTER_CRITICAL(),很高兴不知道它是如何实际工作的。假设我们正在 ARM Cortex-M3 芯片上使用 IAR 编译器,FreeRTOS 使用文件 FreeRTOS/Source/portable/IAR/ARM_CM3/portmacro.h 构建,该文件将 portENTER_CRITICAL() 定义如下

#define portENTER_CRITICAL()   vPortEnterCritical()

vPortEnterCritical() 实际上是在 FreeRTOS/Source/portable/IAR/ARM_CM3/port.c 中定义的。port.c 文件与硬件相关,包含了解 IAR 编译器和 Cortex-M3 芯片的代码。vPortEnterCritical() 使用此硬件特定知识进入关键部分,然后返回到硬件独立的 tasks.c

portmacro.h 文件还定义了架构的基本数据类型。对于 ARM Cortex-M3 芯片上的 IAR 编译器,基本整数变量、指针和系统定时器滴答数据类型的数据类型定义如下

#define portBASE_TYPE  long              // Basic integer variable type
#define portSTACK_TYPE unsigned long     // Pointers to memory locations
typedef unsigned portLONG portTickType;  // The system timer tick type

这种通过 #defines 的薄层使用数据类型和函数的方法可能看起来有点复杂,但它允许 FreeRTOS 通过仅更改硬件相关的文件来为完全不同的系统架构重新编译。如果您想在 FreeRTOS 当前不支持的架构上运行 FreeRTOS,您只需要实现硬件相关的功能,这比 FreeRTOS 的硬件独立部分小得多。

正如我们所见,FreeRTOS 使用 C 预处理器 #define 宏实现了硬件相关的功能。FreeRTOS 还将 #define 用于大量与硬件无关的代码。对于非嵌入式应用程序,这种频繁使用 #define 是一个严重错误,但在许多较小的嵌入式系统中,调用函数的开销不值得“真实”函数提供的优势。

3.3. 调度任务:快速概述

任务优先级和就绪列表

每个任务都有一个用户分配的优先级,介于 0(最低优先级)和 configMAX_PRIORITIES-1 的编译时值(最高优先级)之间。例如,如果 configMAX_PRIORITIES 设置为 5,则 FreeRTOS 将使用 5 个优先级级别:0(最低优先级)、1、2、3 和 4(最高优先级)。

FreeRTOS 使用“就绪列表”来跟踪当前已准备好运行的所有任务。它使用任务列表数组实现就绪列表,如下所示

static xList pxReadyTasksLists[ configMAX_PRIORITIES ];  /* Prioritised ready tasks.  */

pxReadyTasksLists[0] 是所有就绪优先级 0 任务的列表,pxReadyTasksLists[1] 是所有就绪优先级 1 任务的列表,依此类推,一直到 pxReadyTasksLists[configMAX_PRIORITIES-1]

系统滴答

FreeRTOS 系统的心跳称为系统滴答。FreeRTOS 配置系统以生成周期性滴答中断。用户可以配置滴答中断频率,通常在毫秒范围内。每次滴答中断触发时,都会调用 vTaskSwitchContext() 函数。vTaskSwitchContext() 选择最高优先级的就绪任务并将其放入 pxCurrentTCB 变量中,如下所示

/* Find the highest-priority queue that contains ready tasks. */
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) )
{
    configASSERT( uxTopReadyPriority );
    --uxTopReadyPriority;
}

/* listGET_OWNER_OF_NEXT_ENTRY walks through the list, so the tasks of the same 
priority get an equal share of the processor time. */
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) );

在 while 循环开始之前,uxTopReadyPriority 保证大于或等于最高优先级就绪任务的优先级。while() 循环从优先级 uxTopReadyPriority 开始,并向下遍历 pxReadyTasksLists[] 数组以查找具有就绪任务的最高优先级级别。然后,listGET_OWNER_OF_NEXT_ENTRY() 从该优先级级别的就绪列表中获取下一个就绪任务。

现在,pxCurrentTCB 指向最高优先级任务,当 vTaskSwitchContext() 返回时,硬件相关的代码开始运行该任务。

这九行代码是 FreeRTOS 的绝对核心。FreeRTOS 的其他 8900 多行代码是为了确保这九行代码足以使最高优先级任务保持运行。

图 3.2 是就绪列表的高级视图。此示例具有三个优先级级别,一个优先级 0 任务,没有优先级 1 任务,三个优先级 2 任务。此图片是准确的,但并不完整;它缺少一些细节,我们将在稍后补充。

图 3.2:FreeRTOS 就绪列表的基本视图

现在我们已经完成了高级概述,让我们深入了解细节。我们将研究三个主要的 FreeRTOS 数据结构:任务、列表和队列。

3.4. 任务

所有操作系统的主要工作是运行和协调用户任务。与许多操作系统一样,FreeRTOS 中工作的基本单位是任务。FreeRTOS 使用任务控制块 (TCB) 来表示每个任务。

任务控制块 (TCB)

TCB 在 tasks.c 中定义如下

typedef struct tskTaskControlBlock
{
  volatile portSTACK_TYPE *pxTopOfStack;                  /* Points to the location of
                                                             the last item placed on 
                                                             the tasks stack.  THIS 
                                                             MUST BE THE FIRST MEMBER 
                                                             OF THE STRUCT. */
                                                         
  xListItem    xGenericListItem;                          /* List item used to place 
                                                             the TCB in ready and 
                                                             blocked queues. */
  xListItem    xEventListItem;                            /* List item used to place 
                                                             the TCB in event lists.*/
  unsigned portBASE_TYPE uxPriority;                      /* The priority of the task
                                                             where 0 is the lowest 
                                                             priority. */
  portSTACK_TYPE *pxStack;                                /* Points to the start of 
                                                             the stack. */
  signed char    pcTaskName[ configMAX_TASK_NAME_LEN ];   /* Descriptive name given 
                                                             to the task when created.
                                                             Facilitates debugging 
                                                             only. */

  #if ( portSTACK_GROWTH > 0 )
    portSTACK_TYPE *pxEndOfStack;                         /* Used for stack overflow 
                                                             checking on architectures
                                                             where the stack grows up
                                                             from low memory. */
  #endif

  #if ( configUSE_MUTEXES == 1 )
    unsigned portBASE_TYPE uxBasePriority;                /* The priority last 
                                                             assigned to the task - 
                                                             used by the priority 
                                                             inheritance mechanism. */
  #endif

} tskTCB;

TCB 在 pxStack 中存储堆栈起始地址,在 pxTopOfStack 中存储当前堆栈顶部。它还在 pxEndOfStack 中存储指向堆栈末尾的指针,以检查堆栈溢出(如果堆栈“向上”增长到更高的地址)。如果堆栈“向下”增长到较低的地址,则通过将当前堆栈顶部与 pxStack 中的堆栈内存开头进行比较来检查堆栈溢出。

TCB 将任务的初始优先级存储在 uxPriorityuxBasePriority 中。任务在创建时会被分配一个优先级,并且任务的优先级可以更改。如果 FreeRTOS 实现了优先级继承,那么它将使用 uxBasePriority 来记住原始优先级,同时任务会临时提升到“继承的”优先级。(有关优先级继承的更多信息,请参阅下面关于互斥量的讨论。)

每个任务在 FreeRTOS 的各种调度列表中使用两个列表项。当任务插入列表时,FreeRTOS 不会直接插入指向 TCB 的指针。相反,它会插入指向 TCB 的 xGenericListItemxEventListItem 的指针。这些 xListItem 变量使 FreeRTOS 列表比仅仅保存指向 TCB 的指针更智能。稍后在讨论列表时,我们将看到一个示例。

任务可以处于四种状态之一:运行、就绪、挂起或阻塞。您可能期望每个任务都具有一 个变量来告诉 FreeRTOS 它处于什么状态,但事实并非如此。相反,FreeRTOS 通过将任务放入相应的列表(就绪列表、挂起列表等)来隐式跟踪任务状态。任务在特定列表中的存在表示任务的状态。当任务从一种状态更改为另一种状态时,FreeRTOS 只需将其从一个列表移动到另一个列表即可。

任务设置

我们已经讨论了如何使用 pxReadyTasksLists 数组选择和调度任务;现在让我们看看任务是如何最初创建的。当调用 xTaskCreate() 函数时,任务会被创建。FreeRTOS 使用新分配的 TCB 对象来存储任务的名称、优先级和其他详细信息,然后分配用户请求的堆栈大小(假设有足够的可用内存),并在 TCB 的 pxStack 成员中记住堆栈内存的起始位置。

堆栈被初始化为看起来好像新任务已经在运行并且被上下文切换中断。这样,调度程序就可以像对待运行了一段时间的任务一样对待新创建的任务;调度程序不需要任何特殊情况代码来处理新任务。

使任务的堆栈看起来像被上下文切换中断的方式取决于 FreeRTOS 运行的架构,但是这个 ARM Cortex-M3 处理器的实现是一个很好的例子。

unsigned int *pxPortInitialiseStack( unsigned int *pxTopOfStack, 
                                     pdTASK_CODE pxCode,
                                     void *pvParameters )
{
  /* Simulate the stack frame as it would be created by a context switch interrupt. */
  pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on 
                     entry/exit of interrupts. */
  *pxTopOfStack = portINITIAL_XPSR;  /* xPSR */
  pxTopOfStack--;
  *pxTopOfStack = ( portSTACK_TYPE ) pxCode;  /* PC */
  pxTopOfStack--;
  *pxTopOfStack = 0;  /* LR */
  pxTopOfStack -= 5;  /* R12, R3, R2 and R1. */
  *pxTopOfStack = ( portSTACK_TYPE ) pvParameters;  /* R0 */
  pxTopOfStack -= 8;  /* R11, R10, R9, R8, R7, R6, R5 and R4. */
  
  return pxTopOfStack;
}

当任务被中断时,ARM Cortex-M3 处理器会在堆栈上压入寄存器。pxPortInitialiseStack() 修改堆栈以使其看起来像寄存器已被压入,即使任务实际上还没有开始运行。已知值存储在 ARM 寄存器 xPSR、PC、LRR0 的堆栈中。其余寄存器 R1 -- R12 通过递减堆栈指针的顶部为它们分配堆栈空间,但没有将特定数据存储在这些寄存器的堆栈中。ARM 架构规定这些寄存器在复位时未定义,因此(非错误的)程序不会依赖于已知值。

堆栈准备就绪后,任务几乎就可以运行了。但是,首先,FreeRTOS 禁用中断:我们即将开始修改就绪列表和其他调度程序结构,我们不希望其他人在我们下面更改它们。

如果这是第一个创建的任务,则 FreeRTOS 会初始化调度程序的任务列表。FreeRTOS 的调度程序有一个就绪列表数组 pxReadyTasksLists[],它为每个可能的优先级级别提供一个就绪列表。FreeRTOS 还有一些其他列表用于跟踪已挂起、已终止和已延迟的任务。这些现在也都被初始化了。

完成任何首次初始化后,新任务将添加到其指定优先级级别的就绪列表中。重新启用中断,新任务创建完成。

3.5. 列表

在任务之后,FreeRTOS 使用最多的数据结构是列表。FreeRTOS 使用其列表结构来跟踪任务以进行调度,并实现队列。

图 3.3:FreeRTOS 就绪列表的完整视图

FreeRTOS 列表是一个标准的循环双向链表,并添加了一些有趣的补充。这是一个列表元素

struct xLIST_ITEM
{
  portTickType xItemValue;                   /* The value being listed.  In most cases
                                                this is used to sort the list in 
                                                descending order. */
  volatile struct xLIST_ITEM * pxNext;       /* Pointer to the next xListItem in the 
                                                list.  */
  volatile struct xLIST_ITEM * pxPrevious;   /* Pointer to the previous xListItem in 
                                                the list. */
  void * pvOwner;                            /* Pointer to the object (normally a TCB)
                                                that contains the list item.  There is
                                                therefore a two-way link between the 
                                                object containing the list item and 
                                                the list item itself. */
  void * pvContainer;                        /* Pointer to the list in which this list
                                                item is placed (if any). */
};

每个列表元素都包含一个数字 xItemValue,它通常是正在跟踪的任务的优先级或事件调度的计时器值。列表按从高到低的优先级顺序排列,这意味着最高优先级的 xItemValue(最大的数字)位于列表的前面,最低优先级的 xItemValue(最小的数字)位于列表的末尾。

pxNextpxPrevious 指针是标准的链接列表指针。pvOwner 是指向列表元素所有者的指针。这通常是指向任务的 TCB 对象的指针。pvOwner 用于在 vTaskSwitchContext() 中快速切换任务:一旦在 pxReadyTasksLists[] 中找到最高优先级任务的列表元素,该列表元素的 pvOwner 指针就会直接引导我们到调度任务所需的 TCB。

pvContainer 指向此项所在的列表。它用于快速确定列表项是否在特定列表中。每个列表元素都可以放入一个列表中,该列表定义为

typedef struct xLIST
{
  volatile unsigned portBASE_TYPE uxNumberOfItems;
  volatile xListItem * pxIndex;           /* Used to walk through the list.  Points to
                                             the last item returned by a call to 
                                             pvListGetOwnerOfNextEntry (). */
  volatile xMiniListItem xListEnd;        /* List item that contains the maximum 
                                             possible item value, meaning it is always
                                             at the end of the list and is therefore 
                                             used as a marker. */
} xList;

任何时候列表的大小都存储在 uxNumberOfItems 中,用于快速执行列表大小操作。所有新列表都初始化为包含单个元素:xListEnd 元素。xListEnd.xItemValue 是一个哨兵值,等于 xItemValue 变量的最大值:当 portTickType 为 16 位值时为 0xffff,当 portTickType 为 32 位值时为 0xffffffff。其他列表元素也可能具有相同的值;插入算法确保 xListEnd 始终是列表中的最后一项。

由于列表按从高到低的顺序排序,因此 xListEnd 元素用作列表开头的标记。并且由于列表是循环的,因此此 xListEnd 元素也是列表末尾的标记。

您可能使用过的大多数“传统”列表访问都将在单个 for() 循环或类似这样的函数调用中完成所有工作

for (listPtr = listStart; listPtr != NULL; listPtr = listPtr->next) {
  // Do something with listPtr here...
}

FreeRTOS 经常需要跨多个 for() 和 while() 循环以及函数调用访问列表,因此它使用操纵 pxIndex 指针以遍历列表的列表函数。列表函数 listGET_OWNER_OF_NEXT_ENTRY() 执行 pxIndex = pxIndex->pxNext; 并返回 pxIndex。(当然,它也执行正确的列表末尾环绕检测。)这样,列表本身负责跟踪“您在哪里”并在使用 pxIndex 遍历列表时,允许 FreeRTOS 的其余部分无需担心它。

图 3.4:系统计时器滴答后 FreeRTOS 就绪列表的完整视图

vTaskSwitchContext() 中完成的 pxReadyTasksLists[] 列表操作是如何使用 pxIndex 的一个很好的例子。假设我们只有一个优先级级别,优先级 0,并且在该优先级级别有三个任务。这类似于我们之前看到的简单就绪列表图片,但这次我们将包括所有数据结构和字段。

图 3.3 所示,pxCurrentTCB 指示我们当前正在运行任务 B。下次 vTaskSwitchContext() 运行时,它会调用 listGET_OWNER_OF_NEXT_ENTRY() 以获取要运行的下一个任务。此函数使用 pxIndex->pxNext 确定下一个任务是任务 C,现在 pxIndex 指向任务 C 的列表元素,pxCurrentTCB 指向任务 C 的 TCB,如 图 3.4 所示。

请注意,每个 struct xListItem 对象实际上是关联 TCB 中的 xGenericListItem 对象。

3.6. 队列

FreeRTOS 允许任务使用队列相互通信和同步。中断服务例程 (ISR) 也使用队列进行通信和同步。

基本队列数据结构为

typedef struct QueueDefinition
{
  signed char *pcHead;                      /* Points to the beginning of the queue 
                                               storage area. */
  signed char *pcTail;                      /* Points to the byte at the end of the 
                                               queue storage area. One more byte is 
                                               allocated than necessary to store the 
                                             queue items; this is used as a marker. */
  signed char *pcWriteTo;                   /* Points to the free next place in the 
                                               storage area. */
  signed char *pcReadFrom;                  /* Points to the last place that a queued 
                                               item was read from. */
                                           
  xList xTasksWaitingToSend;                /* List of tasks that are blocked waiting 
                                               to post onto this queue.  Stored in 
                                               priority order. */
  xList xTasksWaitingToReceive;             /* List of tasks that are blocked waiting 
                                               to read from this queue. Stored in 
                                               priority order. */

  volatile unsigned portBASE_TYPE uxMessagesWaiting;  /* The number of items currently
                                                         in the queue. */
  unsigned portBASE_TYPE uxLength;                    /* The length of the queue 
                                                         defined as the number of 
                                                         items it will hold, not the 
                                                         number of bytes. */
  unsigned portBASE_TYPE uxItemSize;                  /* The size of each items that 
                                                         the queue will hold. */
                                         
} xQUEUE;

这是一个相当标准的队列,具有头指针和尾指针,以及用于跟踪我们刚刚读取和写入位置的指针。

创建队列时,用户会指定队列的长度以及队列要跟踪的每个项目的尺寸。pcHeadpcTail 用于跟踪队列的内部存储。将项目添加到队列会将项目的深度副本复制到队列的内部存储中。

FreeRTOS 进行深度复制而不是存储指向项目的指针,因为插入项目的生命周期可能远短于队列的生命周期。例如,考虑一个简单的整数队列,使用跨多个函数调用的局部变量进行插入和删除。如果队列存储指向整数局部变量的指针,则一旦整数的局部变量超出范围并且局部变量的内存用于某些新值,这些指针就会失效。

用户选择要入队的对象。如果项目很小,例如前面段落中的简单整数示例,用户可以入队项目的副本,或者如果项目很大,用户可以入队指向项目的指针。请注意,在这两种情况下,FreeRTOS 都会进行深度复制:如果用户选择入队项目的副本,则队列会存储每个项目的深度副本;如果用户选择入队指针,则队列会存储指针的深度副本。当然,如果用户在队列中存储指针,则用户负责管理与指针关联的内存。队列不关心您在其中存储什么数据,它只需要知道数据的大小。

FreeRTOS 支持阻塞和非阻塞队列插入和删除。非阻塞操作会立即返回“队列插入是否成功?”或“队列删除是否成功?”状态。阻塞操作由超时指定。任务可以无限期地阻塞或阻塞有限的时间。

阻塞的任务(称为任务 A)将在其插入/删除操作无法完成且其超时(如果有)未过期的情况下保持阻塞状态。如果中断或其他任务修改队列以使任务 A 的操作能够完成,则任务 A 将被解除阻塞。如果任务 A 的队列操作在其实际运行时仍然可能,则任务 A 将完成其队列操作并返回“成功”。但是,在任务 A 实际运行时,有可能更高优先级的任务或中断对队列执行了另一个操作,从而阻止任务 A 执行其操作。在这种情况下,任务 A 将检查其超时,如果超时尚未过期,则恢复阻塞,或者返回队列操作“失败”状态。

需要注意的是,当任务在队列上阻塞时,系统的其余部分会继续运行;其他任务和中断会继续运行。这样,阻塞的任务就不会浪费本可以被其他任务和中断有效利用的 CPU 周期。

FreeRTOS 使用 xTasksWaitingToSend 列表来跟踪正在阻塞以插入队列的任务。每次从队列中删除一个元素时,都会检查 xTasksWaitingToSend 列表。如果某个任务正在该列表中等待,则该任务将被解除阻塞。

类似地,xTasksWaitingToReceive 用于跟踪正在阻塞以从队列中删除的任务。每次将新元素插入队列时,都会检查 xTasksWaitingToReceive 列表。如果某个任务正在该列表中等待,则该任务将被解除阻塞。

信号量和互斥量

FreeRTOS 使用其队列在任务之间和任务内部进行通信。FreeRTOS 还使用其队列来实现信号量和互斥量。

有什么区别?

信号量和互斥量听起来可能是一样的东西,但它们不是。FreeRTOS 以类似的方式实现它们,但它们旨在以不同的方式使用。它们应该如何以不同的方式使用?嵌入式系统专家 Michael Barr 在他的文章“互斥量和信号量揭秘”中对此进行了最佳阐述。

信号量的正确用法是从一个任务向另一个任务发送信号。互斥量旨在由使用它所保护的共享资源的每个任务按顺序获取和释放。相比之下,使用信号量的任务要么发送信号(在 FreeRTOS 术语中为“发送”),要么等待(在 FreeRTOS 术语中为“接收”)——而不是两者都做。

互斥量用于保护共享资源。一个任务获取互斥量,使用共享资源,然后释放互斥量。当互斥量被另一个任务持有时,任何任务都不能获取该互斥量。这保证了在任何时间只有一个任务被允许使用共享资源。

信号量用于一个任务向另一个任务发送信号。引用 Barr 的文章

例如,任务 1 可能包含在按下“电源”按钮时发布(即发送信号或递增)特定信号量的代码,而任务 2(唤醒显示器)则挂起在同一信号量上。在这种情况下,一个任务是事件信号的生产者;另一个是消费者。

如果您对信号量和互斥量有任何疑问,请查看 Michael 的文章。

实现

FreeRTOS 将 N 元素信号量实现为一个可以容纳 N 个项目的队列。它不存储队列项目中的任何实际数据;信号量只关心当前有多少个队列条目被占用,这在队列的 uxMessagesWaiting 字段中跟踪。它正在执行“纯同步”,如 FreeRTOS 头文件 semphr.h 中所述。因此,队列的项目大小为零字节(uxItemSize == 0)。每次信号量访问都会递增或递减 uxMessagesWaiting 字段;不需要任何项目或数据复制。

与信号量类似,互斥量也实现为一个队列,但 xQUEUE 结构体的几个字段使用 #defines 重载。

/* Effectively make a union out of the xQUEUE structure. */
#define uxQueueType           pcHead
#define pxMutexHolder         pcTail

由于互斥量不在队列中存储任何数据,因此它不需要任何内部存储,因此不需要 pcHeadpcTail 字段。FreeRTOS 将 uxQueueType 字段(实际上是 pcHead 字段)设置为 0 以指示此队列正在用于互斥量。FreeRTOS 使用重载的 pcTail 字段来实现互斥量的优先级继承。

如果您不熟悉优先级继承,我将再次引用 Michael Barr 的话来定义它,这次来自他的文章,“优先级反转简介

[优先级继承] 要求低优先级任务继承任何挂起在它们共享的资源上的高优先级任务的优先级。此优先级更改应在高优先级任务开始挂起时立即发生;当释放资源时,它应结束。

FreeRTOS 使用 pxMutexHolder 字段(实际上只是 #define 重载的 pcTail 字段)实现优先级继承。FreeRTOS 在 pxMutexHolder 字段中记录持有互斥量的任务。当发现高优先级任务正在等待当前由低优先级任务持有的互斥量时,FreeRTOS 会将低优先级任务“升级”到高优先级任务的优先级,直到互斥量再次可用。

3.7. 结论

我们已经完成了对 FreeRTOS 架构的了解。希望您现在对 FreeRTOS 的任务如何运行和通信有了很好的了解。如果您以前从未查看过任何操作系统的内部结构,我希望您现在对它们的工作原理有了基本的了解。

显然,本章没有涵盖 FreeRTOS 的所有架构。值得注意的是,我没有提到内存分配、ISR、调试或 MPU 支持。本章也没有讨论如何设置或使用 FreeRTOS。Richard Barry 撰写了一本优秀的书籍,使用 FreeRTOS 实时内核:实用指南,其中详细讨论了这一点;如果您打算使用 FreeRTOS,我强烈推荐它。

3.8. 致谢

我要感谢 Richard Barry 创建和维护 FreeRTOS,并选择将其开源。Richard 在撰写本章方面提供了很大的帮助,提供了 FreeRTOS 的一些历史以及非常有价值的技术审查。

还要感谢 Amy Brown 和 Greg Wilson 将整个 AOSA 事情整合在一起。

最后也是最重要的(与“并非最不重要”相反),感谢我的妻子 Sarah 与我分享了本章的研究和写作。幸运的是,她嫁给我的时候就知道我是一个极客!