摘要:文章介绍了 FreeRTOS 中的队列机制,包括队列的基本概念、创建与初始化方式、数据操作(发送、接收、查看等)、状态查询与管理、在互斥量与信号量中的应用以及队列集的相关操作
建议:作者在这里建议,结合queue.c源码来看效果会更好一下
FreeRTOS 队列:任务间通信与同步的高效利器
在嵌入式系统开发中,任务间的通信与同步是至关重要的环节。FreeRTOS 作为一款广泛应用的实时操作系统,其队列机制为任务间的数据传递和协调提供了强大而灵活的解决方案。
一、队列的基本概念与结构
FreeRTOS 中的队列是一种用于任务间数据传递的数据结构。队列中的数据项通过复制的方式进行存储,而非引用。其核心数据结构Queue_t
(在旧版本中为xQUEUE
)包含了多个重要成员:
pcHead
:指向队列存储区域的起始位置,当队列用于表示互斥量时,有特殊用途。pcTail
:指向队列存储区域的末尾字节,用于标记队列的边界。pcWriteTo
:指示下一个可写入数据的位置。u.pcReadFrom
(或u.uxRecursiveCallCount
,取决于队列用途):在队列用于存储数据时,指向最后一个读取数据的位置;当作为互斥量时,用于记录递归调用次数。xTasksWaitingToSend
和xTasksWaitingToReceive
:分别是等待发送数据到队列和等待从队列接收数据的任务列表,这些任务按照优先级顺序存储。uxMessagesWaiting
:记录当前队列中存储的数据项数量。uxLength
:队列的长度,即能够容纳的数据项数量。uxItemSize
:每个数据项的大小。cRxLock
和cTxLock
:用于记录队列在锁定状态下接收和发送数据项的数量,初始值为queueUNLOCKED
((int8_t) -1
)。
此外,还有一些与队列相关的宏定义,如queueUNLOCKED
表示队列未锁定,queueLOCKED_UNMODIFIED
表示队列锁定但未修改等,这些宏定义的使用是用来更清晰地表达队列的状态和操作逻辑。
二、队列的创建与初始化
FreeRTOS 提供了多种方式创建队列,以满足不同的内存分配需求。
- 当
configSUPPORT_STATIC_ALLOCATION
和configSUPPORT_DYNAMIC_ALLOCATION
都为1
时,可以使用xQueueGenericCreateStatic
函数进行静态创建队列。该函数要求传入队列长度uxQueueLength
、数据项大小uxItemSize
、队列存储区域指针pucQueueStorage
以及队列类型ucQueueType
等参数。在创建过程中,会对传入的参数进行严格的断言检查,确保参数的有效性。例如,队列长度必须大于0
,并且根据数据项大小是否为0
来确定是否提供队列存储区域。创建成功后,会调用prvInitialiseNewQueue
函数对队列进行初始化,设置队列的各项成员变量,包括pcHead
、uxLength
、uxItemSize
等,并根据configUSE_TRACE_FACILITY
的设置来配置队列的跟踪信息。 - 当
configSUPPORT_DYNAMIC_ALLOCATION
为1
时,可以使用xQueueGenericCreate
函数进行动态创建队列。该函数同样需要传入队列长度和数据项大小等参数。根据数据项大小计算所需的队列存储区域大小,如果数据项大小为0
,则不分配存储区域。然后使用pvPortMalloc
函数动态分配队列结构和存储区域的内存空间。成功分配后,也会调用prvInitialiseNewQueue
函数进行初始化,并设置ucStaticallyAllocated
成员变量为pdFALSE
,表示队列是动态分配的。
对于互斥量这种特殊类型的队列,当configUSE_MUTEXES
为1
时,创建互斥量队列的过程稍有不同。首先会创建一个普通队列,然后调用prvInitialiseMutex
函数对其进行配置。在prvInitialiseMutex
函数中,会将pxMutexHolder
设置为NULL
,uxQueueType
设置为queueQUEUE_IS_MUTEX
,并将u.uxRecursiveCallCount
初始化为0
,同时调用traceCREATE_MUTEX
函数进行跟踪记录,并通过xQueueGenericSend
函数将互斥量初始化为可用状态。
三、队列的数据操作
(一)数据发送
xQueueGenericSend
函数用于向队列发送数据。在发送数据之前,会先检查队列是否有空间。如果队列未满或者发送模式为覆盖(queueOVERWRITE
且队列长度为1
),则可以直接将数据复制到队列中。数据复制通过prvCopyDataToQueue
函数完成,该函数会根据队列的类型和发送位置进行相应的处理。如果是普通队列且发送到队列末尾(queueSEND_TO_BACK
),则将数据复制到pcWriteTo
指向的位置,并更新pcWriteTo
指针;如果是发送到队列头部,则将数据复制到u.pcReadFrom
指向的位置,并更新u.pcReadFrom
指针。在复制数据完成后,会检查队列是否属于某个队列集(configUSE_QUEUE_SETS
为1
且pxQueue->pxQueueSetContainer
不为NULL
),如果是,则调用prvNotifyQueueSetContainer
函数通知队列集。如果队列中有任务正在等待接收数据,且该任务的优先级高于当前任务,则会进行任务切换(通过queueYIELD_IF_USING_PREEMPTION
宏实现)。如果队列已满且设置了阻塞时间,则当前任务会进入阻塞状态,等待队列有空间或者阻塞时间超时。在阻塞期间,任务会被挂起,并加入到xTasksWaitingToSend
列表中。xQueueGenericSendFromISR
函数用于在中断服务程序中向队列发送数据。与xQueueGenericSend
函数类似,但不能阻塞。如果队列有空间,则直接复制数据到队列,并根据队列是否属于队列集以及是否有任务等待接收数据来决定是否进行任务切换(通过设置pxHigherPriorityTaskWoken
标志)。如果队列已满,则直接返回errQUEUE_FULL
。
(二)数据接收
xQueueReceive
函数用于从队列接收数据。首先检查队列中是否有数据,如果有数据,则调用prvCopyDataFromQueue
函数将数据从队列复制到接收缓冲区,并更新队列的相关状态,如uxMessagesWaiting
减1
。如果队列中有任务正在等待发送数据到队列,且该任务的优先级高于当前任务,则会进行任务切换。如果队列为空且设置了阻塞时间,则当前任务会进入阻塞状态,等待队列中有数据或者阻塞时间超时。在阻塞期间,任务会被挂起,并加入到xTasksWaitingToReceive
列表中。xQueueReceiveFromISR
函数用于在中断服务程序中从队列接收数据。该函数不能阻塞,直接检查队列中是否有数据,如果有数据,则复制数据到接收缓冲区,并根据队列是否锁定以及是否有任务等待发送数据来决定是否进行任务切换(通过设置pxHigherPriorityTaskWoken
标志)。如果队列为空,则返回pdFAIL
。
(三)其他操作
xQueuePeek
函数用于查看队列头部的数据,但不将其从队列中移除。其操作过程与xQueueReceive
函数类似,但在复制数据后会将读取指针恢复到原来的位置,以保证数据仍然留在队列中。xQueuePeekFromISR
函数用于在中断服务程序中查看队列头部的数据,同样不将数据移除。如果队列中有数据,则直接复制数据到接收缓冲区并返回pdPASS
,否则返回pdFAIL
。
四、队列的状态查询与管理
uxQueueMessagesWaiting
函数用于获取队列中当前等待处理的数据项数量。通过进入临界区,读取uxMessagesWaiting
成员变量的值并返回。taskENTER_CRITICAL
和taskEXIT_CRITICAL
的基本原理(临界区)- 在许多实时操作系统(如 FreeRTOS)中,
taskENTER_CRITICAL和
taskEXIT_CRITICAL
是用于保护临界区的宏或函数。它们的实现方式通常是通过关闭中断来实现临界区的保护。 - 当
taskENTER_CRITICAL
被调用时,系统会暂时禁止中断(具体的实现可能因操作系统和硬件平台而异)。这就防止了其他中断服务程序或任务切换等操作打断当前的关键代码序列。在这个例子中,从taskENTER_CRITICAL
到taskEXIT_CRITICAL
之间的代码就是临界区。 - 在临界区内,代码可以安全地访问和修改共享资源(如队列相关的数据结构),而不用担心被其他任务或中断干扰导致数据不一致。
- 在许多实时操作系统(如 FreeRTOS)中,
uxQueueSpacesAvailable
函数用于获取队列中剩余的可用空间数量。通过计算队列长度与uxMessagesWaiting
的差值得到。xQueueIsQueueEmptyFromISR
和xQueueIsQueueFullFromISR
函数分别用于在中断服务程序中检查队列是否为空和是否已满,直接判断uxMessagesWaiting
的值与0
或队列长度的关系并返回相应结果。vQueueDelete
函数用于删除队列。在删除队列之前,会先进行一些断言检查,确保队列指针有效。如果configQUEUE_REGISTRY_SIZE
大于0
,则会调用vQueueUnregisterQueue
函数将队列从队列注册表中移除。然后根据队列的内存分配方式,如果是动态分配且configSUPPORT_DYNAMIC_ALLOCATION
为1
,则使用vPortFree
函数释放队列占用的内存空间;如果是静态分配且configSUPPORT_STATIC_ALLOCATION
为1
,则根据ucStaticallyAllocated
成员变量的值来决定是否释放内存(如果为pdFALSE
则释放)。
五、队列在互斥量与信号量中的应用
(一)互斥量
互斥量在 FreeRTOS 中是一种特殊的队列,用于保护共享资源。当一个任务获取互斥量时,其他任务将无法获取该互斥量,从而保证了共享资源的互斥访问。在创建互斥量时,通过prvInitialiseMutex
函数对队列进行特殊配置,将pxMutexHolder
设置为NULL
表示互斥量未被持有,uxQueueType
设置为queueQUEUE_IS_MUTEX
,并初始化u.uxRecursiveCallCount
为0
。在任务获取互斥量时,会检查pxMutexHolder
是否为当前任务,如果是,则增加u.uxRecursiveCallCount
,表示递归获取互斥量;如果不是,则通过xQueueSemaphoreTake
函数尝试获取互斥量,并根据获取结果更新u.uxRecursiveCallCount
。当任务释放互斥量时,会检查u.uxRecursiveCallCount
的值,如果为0
,则表示互斥量可以被其他任务获取,通过xQueueGenericSend
函数将互斥量释放,并自动唤醒等待该互斥量的任务。
(二)信号量
信号量也是基于队列实现的一种同步机制。当configUSE_COUNTING_SEMAPHORES
为1
时,可以创建计数信号量。计数信号量的队列长度表示信号量的最大计数值,uxMessagesWaiting
成员变量表示当前信号量的计数值。通过xQueueGiveFromISR
和xQueueSemaphoreTake
等函数对信号量进行操作,实现任务间的同步和资源的计数管理。
六、队列集的操作
当configUSE_QUEUE_SETS
为1
时,FreeRTOS 支持队列集的操作。队列集可以将多个队列或信号量组合在一起,方便任务对多个事件源进行监听和处理。
xQueueCreateSet
函数用于创建队列集,其内部调用xQueueGenericCreate
函数创建一个队列,该队列的每个数据项为Queue_t *
类型,用于存储队列集成员的指针。xQueueAddToSet
函数用于将队列或信号量添加到队列集中。在添加之前,会检查队列或信号量是否已经属于其他队列集,以及其是否为空(uxMessagesWaiting
为0
)。如果满足条件,则将其pxQueueSetContainer
成员变量设置为队列集指针,表示该队列或信号量已加入队列集。xQueueRemoveFromSet
函数用于从队列集中移除队列或信号量。在移除之前,会检查队列或信号量是否属于指定的队列集,以及其是否为空。如果满足条件,则将其pxQueueSetContainer
成员变量设置为NULL
,表示该队列或信号量已从队列集中移除。xQueueSelectFromSet
和xQueueSelectFromSetFromISR
函数分别用于在任务和中断服务程序中从队列集中选择一个有事件发生的队列或信号量。这两个函数内部调用xQueueReceive
或xQueueReceiveFromISR
函数,从队列集中接收数据,并返回对应的队列或信号量句柄。
FreeRTOS 的队列机制提供了丰富而强大的功能,涵盖了任务间数据传递、同步以及资源保护等多个方面。无论是普通的数据队列,还是特殊的互斥量、信号量和队列集,都为嵌入式系统开发者提供了灵活高效的工具,有助于构建稳定、可靠且高效的嵌入式应用程序。通过深入理解和熟练运用 FreeRTOS 队列机制,可以更好地发挥 FreeRTOS 在嵌入式系统中的优势,满足各种复杂的任务间通信和同步需求。