队列管理
章节介绍与范围
“队列” 提供了一个任务到任务,任务到中断和中断到任务的通讯机制。
范围
本章节的目标是告诉读者很好的理解:
如何创建队列。
一个队列是如何管理它所包含的数据。
如何发送数据到队列。
如何从队列接收数据。
阻塞队列意味着什么。
如何阻塞多个队列。
如何覆盖队列中的数据。
如何清除一个队列。
读取和写入一个队列对任务优先级的影响。
本章节只涵盖了任务到任务通讯。任务到中断与中断到任务通讯在第 6 章中说明。
队列的特征
数据存储
一个队列能保存有限数量的固定大小的数据单元。一个队列能保存单元的最大数量叫做 “长度”。每个队列数据单元的长度与大小是在创建队列时设置的。
队列通常是一个先入先出(FIFO)的缓冲区,即数据在队列末尾(tail)被写入,在队列前部(head)移出。图 31 展示了数据被写入和移出作为 FIFO 使用的队列。也可以写入队列的前端,并覆盖已位于队列前端的数据。
有两种方法可以实现队列的行为:
通过复制实现队列:复制队列是指将发送到队列的数据一个字节一个字节地复制到队列中。
通过引用实现队列:引用队列意味着队列只持有指向发送到队列的数据的指针,而不是数据本身。
FreeRTOS 是通过使用复制方法实现队列。这是考虑到复制队列比引用队列更强大,更容易使用,因为:
堆栈变量可以直接发送到队列,即使该变量将在声明它的函数退出后,不再存在。
可以将数据发送到队列,而无需先分配缓冲区来保存数据,然后将数据复制到分配的缓冲区中。
发送任务可以立即重用发送到队列的变量或缓冲区。
发送任务和接收任务是完全解耦的,应用程序设计人员不需要关心哪个任务拥有数据,或者哪个任务负责发布数据。
复制队列并不会阻止队列也被用于引用队列。例如,当正在排队的数据的大小使得将数据复制到队列不切实际时,可以将指向数据的指针复制到队列中。
RTOS 完全负责分配用于存储数据的内存。
在受内存保护的系统中,任务可以访问的 RAM 将受到限制。在这种情况下,只有当发送和接收任务都可以访问存储数据的 RAM 时,才可以使用引用排队。按复制排队不受此限制;内核总是以完全特权运行,允许使用队列跨内存保护边界传递数据。
多任务访问
队列本身就是对象,任何知道它们存在的任务或 ISR 都可以访问它们。任意数量的任务可以写入同一个队列,任意数量的任务也可以从同一个队列读取。在实践中,队列有多个写入者是非常常见的,但是队列有多个读取者就不那么常见了。
阻塞队列读取
当任务尝试从队列中读取时,它可以选择指定 “阻塞” 时间。 如果队列已经为空,则这是任务将保持在阻塞状态以等待队列中的数据可用的时间。 当另一个任务或中断将数据放入队列时,处于阻塞状态且等待数据从队列中变为可用的任务将自动移至就绪状态。 如果指定的阻塞时间在数据可用之前到期,则任务也将自动从 “阻塞” 状态移动到 “就绪” 状态。
队列可以有多个读取者,因此单个队列可能会由多个在其上阻塞等待数据的任务。 在这种情况下,只有一个任务在数据可用时将被解除阻塞。 取消阻塞的任务始终是等待数据的最高优先级任务。 如果被阻塞的任务具有相同的优先级,那么等待数据最长的任务将被阻塞。
阻塞队列写入
与从队列读取数据时一样,任务也可以在向队列写入数据时指定阻塞时间。在这种情况下,如果队列已经满了,则阻塞时间是任务应该保持在阻塞状态以等待队列上可用空间的最长时间。
队列可以有多个写入者,因此对于一个完整的队列,可能有多个任务阻塞在队列上,等待完成发送操作。在这种情况下,当队列上的空间可用时,只有一个任务将被解除阻塞。未阻塞的任务总是等待空间的最高优先级任务。如果阻塞的任务具有相同的优先级,那么等待空间最长的任务将被解除阻塞。
阻塞多个队列
队列可被分组到集合中,允许任务进入阻塞状态来等待数据在集合的任何队列中变为可用。队列集合在第 4.6 章节 “从多个队列接收” 中展示。
使用队列
xQueueCreate() API 函数
一个队列在使用前必须被显式的创建。
队列被句柄引用,句柄是类型为 QueueHandle_t
类型的变量。xQueueCreate()
API 函数会创建一个队列,并给一个 QueueHandle_t
的变量来引用这个被创建的队列。
FreeRTOS V9.0.0 也包含了 xQueueCreateStatic()
函数,它创建队列是在编译时静态地分配内存。当一个队列创建时,FreeRTOS 是从 FreeRTOS 堆中分配所需 RAM。这一段 RAM 被用来保存队列数据结构和队列所含的各个单元。xQueueCreate()
在创建队列所需 RAM 不足时会返回 NULL
。第 2 章提供了 FreeRTOS 堆的更多信息。
清单 40. xQueueCreate()
API 函数原型
表 18. xQueueCreate()
参数和返回值
参数名
描述
uxQueueLength
正在创建的队列一次可以容纳的最大项数。
uxItemSize
可以存储在队列中的每个数据项的字节大小。
返回值
如果返回 NULL
,则无法创建队列,因为 FreeRTOS 没有足够的堆内存来分配队列数据结构和存储区域。返回的非空值表示队列已成功创建。返回的值应该存储为已创建队列的句柄。
创建队列后,可以使用 xQueueReset()
API 函数将队列返回到其原始的空状态。
xQueueSendToBack() 与 xQueueSendToFront() API 函数
正如所料,xQueueSendToBack()
用于将数据发送到队列的后端(尾部),xQueueSendToFront()
用于将数据发送到队列的前端(头部)。
xQueueSend()
与 xQueueSendToBack()
等价,并且完全相同。
永远不要从中断服务例程调用 xQueueSendToFront()
或 xQueueSendToBack()
。应该使用中断安全转换 xQueueSendToFrontFromISR()
和 xQueueSendToBackFromISR()
。这些将在第 6 章中描述。
清单 41. xQueueSendToFront()
API 函数原型
清单 42. xQueueSendToBack()
API 函数原型
表 19. xQueueSendToFront()
和 xQueueSendToSendToBack()
函数参数和返回值
参数名称/返回值
描述
xQueue
发送(写入)数据的队列的句柄。队列句柄将从用于创建队列的 xQueueCreate()
调用中返回。
pvItemToQueue
指向要复制到队列中的数据的指针。
在创建队列时,将设置队列可以容纳的每个项目的大小,因此这多个字节将从 pvItemToQueue
复制到队列存储区域中。
xTicksToWait
如果队列已经满了,任务应该保持阻塞状态以等待队列上可用空间的最大时间量。
如果 xTicksToWait
为零且队列已满,则 xQueueSendToFront()
和 xQueueSendToBack()
都将立即返回。
阻塞时间以滴答周期指定,因此它所表示的绝对时间依赖于滴答频率。宏 pdMS TO TICKS()
可用于将以毫秒为单位的时间转换为以节拍为单位的时间。
如果在 FreeRTOSConfig.h
中将 INCLUDE_vTaskSuspend
设置为 1,则将 xTicksToWait
设置为 portMAX_DELAY
将导致任务无限期地等待(没有超时)。
返回值
有两种可能的返回值:
pdPASS:仅当数据成功发送到队列时,才会返回
pdPASS
。如果指定了阻塞时间(xTicksToWait
不为零),那么调用任务可能被置于Blocked状态,等待空间在队列中变为可用,在函数返回之前,但数据已成功写入队列 在阻止时间到期之前。errQUEUE_FULL:如果由于队列已满,无法将数据写入队列,将返回
errQUEUE_FULL
。如果指定了阻塞时间(xTicksToWait
不为零),则调用任务将被置于阻塞状态以等待另一个任务或中断在队列中腾出空间,但指定的阻塞时间在该状态之前到期。
xQueueReceive() 是用来从队列中接收(读取)一个元素。收到的元素将从队列中删除。
切勿从中断服务程序调用 xQueueReceive()
。 中断安全 xQueueReceiveFromISR()
API 函数在第 6 章中描述。
清单 43. xQueueReceive()
API 函数原型
表 20. xQueueReceive()
函数参数和返回值
参数名称/返回值
描述
xQueue
正在接收(读取)数据的队列句柄。
将从用于创建队列的 xQueueCreate()
调用返回队列句柄。
pvBuffer
指向要将接收到的数据复制到的内存的指针。
在创建队列时设置队列保存的每个数据项的大小。 pvBuffer
指向的内存必须至少足以容纳那么多字节。
xTicksToWait
如果队列已经为空,则任务应保持在阻塞状态以等待数据的最长时间在队列中可用。
如果 xTicksToWait
为零,那么如果队列已经为空,则 xQueueReceive()
将立即返回。
阻塞时间在滴答周期中指定,因此它表示的绝对时间取决于滴答频率。 宏 pdMS_TO_TICKS()
可用于将以毫秒为单位指定的时间转换为刻度中指定的时间。
将 xTicksToWait
设置为 portMAX_DELAY
会导致任务无限期地等待(没有超时),前提是 FreeRTOSConfig.h
中的 INCLUDE_vTaskSuspend
设置为 1。
返回值
有两种可能的返回值:
pdPASS:仅当从队列中成功读取数据时才会返回
pdPASS
。 如果指定了阻塞时间(xTicksToWait
不为零),那么调用任务可能被置于阻塞状态,等待数据在队列中可用,但是在阻塞时间到期之前已成功从队列中读取数据。errQUEUE_EMPTY:如果由于队列已经为空而无法从队列中读取数据,则将返回
errQUEUE_EMPTY
。如果指定了阻塞时间(xTicksToWait
不为零),那么调用任务将被置于阻塞状态以等待另一个任务或中断将数据发送到队列,但阻塞时间在该时间之前到期。
uxQueueMessagesWaiting()
用于查询当前队列中的项目数。
切勿从中断服务程序调用 uxQueueMessagesWaiting()
。 应该在其位置使用中断安全 uxQueueMessagesWaitingFromISR()
。
清单 44. uxQueueMessagesWaiting()
API 函数原型
表 21. uxQueueMessagesWaiting()
函数参数或返回值
参数名称/返回值
描述
xQueue
正在查询队列的句柄。 将从用于创建队列的 xQueueCreate()
调用返回队列句柄。
返回值
正在查询的队列当前持有的项目数。 如果返回零,则队列为空。
示例 10. 从队列接收时阻塞
此示例演示了正在创建的队列,从多个任务发送到队列的数据以及从队列中接收的数据。 创建队列以保存 int32_t
类型的数据项。 发送到队列的任务不指定阻塞时间,从队列接收的任务执行。
发送到队列的任务的优先级低于从队列接收的任务的优先级。 这意味着队列永远不应包含多个项目,因为只要数据被发送到队列,接收任务就会解锁,抢占发送任务,并删除数据 - 再次将队列留空。
清单 45 显示了写入队列的任务的实现。 创建此任务的两个实例,一个将值 100 连续写入队列,另一个将值 200 连续写入同一队列。 任务参数用于将这些值传递到每个任务实例中。
清单 45. 示例 10 中使用的发送任务的实现。
清单 46 显示了从队列接收数据的任务的实现。 接收任务指定块时间为 100 毫秒,因此将进入阻塞状态以等待数据变为可用。 当队列中的数据可用时,它将离开阻塞状态,或者在没有数据可用的情况下,它将离开 100 毫秒。 在此示例中,100 毫秒超时应该永不过期,因为有两个任务连续写入队列。
清单 46. 示例 10 接受任务的实现
清单 47 包含 main()
函数的定义。 这只是在启动调度程序之前创建队列和三个任务。 创建队列以最多保存五个 int32_t
值,即使设置了任务的优先级,使得队列一次也不会包含多个项目。
清单 47. 例 10 中 main() 的实现
发送到队列的两个任务都具有相同的优先级。 这导致两个发送任务依次将数据发送到队列。 例 10 中产生的输出如图 32 所示。
图 33 展示了执行的顺序
从多个源接收数据
FreeRTOS 设计中常见的任务是从多个源接收数据,所以接收任务需要知道数据来自何处以确定如何处理数据。 一个简单的设计解决方案是使用单个队列来传输具有数据值和结构域中包含的数据源的结构。 该方案如图 34 所示。
图 34 参考:
一个以
Data_t
结构创建的队列。这个结构成员允许包含数据值和一个枚举类型来指示一个消息发送到队列。中央的 Controller 任务用于执行主系统功能。 这必须对输入和对队列中与其通信的系统状态的更改作出反应。
一个 CAN 总线任务用于封装 CAN 总线接口功能。当 CAN 总线任务接收并解码消息时,它将已解码的消息发送到
Data_t
结构中的 Controller 任务。 传输结构的eDataID
成员用于让 Controller 任务知道数据是什么 —— 在描述中它是电机速度值。 传输结构的lDataValue
成员用于让 Controller 任务知道实际的电机速度值。人机界面(HMI)任务用于封装所有 HMI 功能。机器操作员可能以多种方式输入命令和查询值,这些方式必须在 HMI 任务中检测和解释。输入新命令时,HMI 任务将命令以一个
Data_t
的结构发送到 Controller 任务。传输结构的eDataID
成员用于让 Controller 任务知道数据是什么 —— 在描述中它是一个新的调定点的值。 传递结构的lDataValue
成员用于让 Controller 任务知道实际调定点的值。
示例 11. 发送到队列和发送队列结构时的阻塞
示例 11 与示例 10 类似,但任务优先级相反,因此接收任务的优先级低于发送任务。 此外,队列用于传递结构,而不是整数。
清单 48 显示了示例 11 使用的结构的定义。
清单 48. 要在队列上传递的结构的定义,以及由示例使用的两个变量的声明
在示例 10 中,接收任务具有最高优先级,因此队列中永远不会存在多个元素。 这是因为一旦数据被放入队列中,接收任务就会抢占发送任务。 在示例 11 中,发送任务具有更高的优先级,因此队列通常是满的。 这是因为,一旦接收任务从队列中删除了一个项目,它就会被其中一个发送任务抢占,然后立即重新填充队列。 然后,发送任务重新进入阻塞状态,等待空间再次在队列中可用。
清单 49 显示了发送任务的实现。 发送任务指定 100 毫秒的阻塞时间,因此每次队列变满时,它都会进入阻塞状态以等待由可用空间。当队列中有空间可用时,或者没有空间可用的情况下超过 100 毫秒时,它就会离开阻塞状态。在这个例子中,100 毫秒超时应该永不过期,因为接受任务通过从队列中删除元素来不断地腾出空间。
清单 49. 示例 11 发送任务的实现
接收任务的优先级最低,所以只有当两个发送任务都处于阻塞状态时,接收任务才会运行。发送任务仅在队列满时才进入阻塞状态,因此接收任务仅在队列满时才会执行。因此,即使没有指定阻塞时间,它也总是期望接收数据。
清单 50 显示了接收任务的实现。
清单 50. 示例 11 接收任务的定义
main()
仅比前一个示例略有变化。 创建队列以容纳三个 Data_t
结构,并且发送和接收任务的优先级相反。 main()
的实现如清单 51 所示。
清单 51. 示例 11 main()
的实现
示例 11 生成的输出如图 35 所示。
图 36 显示了由于发送任务的优先级高于接收任务的优先级而导致的执行顺序。 表 22 提供了对图 36 的进一步说明,并描述了前四个消息是否来自同一任务。
表 22. 图 36 的关键点
时刻
描述
t1
任务发送方 1 执行并向队列发送 3 个数据项。
t2
队列已满,因此发送方 1 进入阻塞状态,等待下一次发送完成。任务发送方 2 现在是能够运行的最高优先级任务,因此进入运行状态。
t3
任务发送者 2 发现队列已经满了,因此进入阻塞状态,等待第一次发送完成。任务接收者现在是能够运行的最高优先级任务,因此进入运行状态。
t4
优先级高于接收任务优先级的两个任务正在等待队列中的空间可用,从而导致任务接收者在从队列中删除一个项目后立即被抢占。 任务发送者 1 和发送者 2 具有相同的优先级,因此调度程序选择等待时间最长的任务作为将进入运行状态的任务 —— 在这种情况下是任务发送者 1。
t5
任务发送者 1 将另一个数据项发送到队列。 队列中只有一个空间,因此任务发送者 1 进入阻塞状态以等待下一次发送完成。 任务接收器再次是能够运行的最高优先级任务,因此进入运行状态。
任务发送者 1 现在已向队列发送了四个项目,任务发送者 2 仍在等待将其第一个项目发送到队列。
t6
优先级高于接收任务优先级的两个任务正在等待队列中的空间可用,因此任务接收者一旦从队列中删除了一个项目就会被抢占。 此时发送者 2 等待的时间比发送者 1 长,因此发送者 2 进入运行状态。
t7
任务发送者 2 将数据项发送到队列。 队列中只有一个空格,因此发件人 2 进入阻止状态以等待下一次发送完成。 发送者 1 和发送者 2 都在等待队列中的空间可用,因此任务接收者是唯一可以进入运行状态的任务。
排队指针
如果存储在队列中的数据大小很大,则最好使用队列将指针传输到数据,而不是将数据本身逐字节地复制到队列中。 传输指针在处理时间和创建队列所需的 RAM 量方面都更有效。 但是,在队列指针时,必须特别注意确保:
指向的 RAM 的所有者是明确定义的。通过指针在任务之间共享内存时,必须确保两个任务不会同时修改内存内容,或采取任何其他可能导致内存内容无效或不一致的操作。 理想情况下,只允许发送任务访问存储器,直到指向存储器的指针已经排队,并且在从队列接收到指针之后,只允许接收任务访问存储器。
指向的 RAM 仍然有效。如果指向的内存是动态分配的,或者是从预先分配的缓冲区池中获取的,则完全一个任务应该负责释放内存。 任务完成后,任何任务都不应尝试访问内存。永远不应该使用指针来访问已在任务堆栈上分配的数据。 堆栈帧更改后,数据无效。
举例来说,清单 52,清单 53 和清单 54 演示了如何使用队列从一个任务向另一个任务发送指向缓冲区的指针:
清单 52 创建了一个最多可以容纳 5 个指针的队列。
清单 53 分配缓冲区,将字符串写入缓冲区,然后将指向缓冲区的指针发送到队列。
清单 54 从队列中接收指向缓冲区的指针,然后将包含在缓冲区中的字符串打印出来。
清单 52. 创建一个包含指针的队列
清单 53. 使用队列发送指向缓冲区的指针
清单 54. 使用队列接收指向缓冲区的指针
使用队列发送不同类型和长度的数据
前面几节已经证明了两个强大的设计模式:发送结构到一个队列,发送指针到一个队列。 组合这两个技术就可以允许一个任务使用一个队列接收来自任何数据源的任何数据类型。 FreeRTOS+TCP TCP/IP 协议栈的实现提供了如何这样实现的实际例子。
在自己的任务中运行的 TCP/IP 协议栈必须处理许多来自不同源的事件。 不同的事件类型与不同类型和长度的数据相关联。 在 TCP/IP 任务之外发生的所有事件都由 IPStackEvent_t
类型的结构描述,并发送到队列上的 TCP/IP 任务。 IPStackEvent_t
结构如清单 55 所示,IPStackEvent_t
结构的 pvData
成员是一个指针,可用于直接保存值或指向缓冲区。
清单 55. 用于将事件发送到 FreeRTOS+TCP上的 TCP/IP 协议栈任务结构
TCP/IP 事件及其相关数据的示例包括:
eNetworkRxEvent:数据的分组已经从网络接收到。从网络接收的数据使用类型为
IPStackEvent_t
的结构发送到 TCP/IP 任务,该结构的eEventType
成员设置为eNetworkRxEvent
,该结构的pvData
成员用于指向包含接收数据的缓冲区。清单 56 显示了一个伪代码示例。eTCPAcceptEvent:套接字是接受或等待来自客户端的连接。接受事件是使用
IPStackEvent_t
类型的结构从调用FreeRTOS_accept()
的任务发送到 TCP/IP 任务的。该结构的eEventType
成员设置为eTCPAcceptEvent
,该结构的pvData
成员设置为接受连接的套接字的句柄。清单 57 显示了一个伪代码示例。eNetworkDownEvent:网络需要连接或重新连接。网络中断事件是使用
IPStackEvent_t
类型的结构从网络接口发送到 TCP/IP 任务的。该结构的eEventType
成员设置为eNetworkDownEvent
。网络关闭事件不与任何数据相关联,因此不使用该结构的pvData
成员。清单 58 显示了一个伪代码示例。
清单 56. 伪代码,显示如何使用 IPStackEvent_t
结构将从网络接收的数据发送到 TCP/IP 任务
清单 57. 伪代码,显示如何使用 IPStackEvent_t
结构发送接受到 TCP/IP 任务的连接的套接字句柄
清单 58. 伪代码,显示如何使用 IPStackEvent_t
结构向 TCP 发送网络关闭事件
清单 59 中显示了在 TCP/IP 任务中接收和处理这些事件的代码。可以看出,从队列接收的 IPStackEvent_t
结构的 eEventType
成员用于确定如何解释 pvData
成员。
清单 59. 显示如何接收和处理 IPStackEvent_t 结构的伪代码
从多个队列接收
队列集
应用程序设计通常需要单个任务来接收不同大小的数据、不同含义的数据以及来自不同来源的数据。上一节演示了如何使用接收结构的单个队列以简洁高效的方式实现这一点。然而,有时应用程序的设计者正在使用限制其设计选择的约束,这就需要对某些数据源使用单独的队列。例如,集成到设计中的第三方代码可能假设存在专用队列。在这种情况下,可以使用 “队列集”。
队列设置允许所有任务从多个队列接收数据,而无需任务依次轮询每个队列以确定哪个队列(如果有)包含数据。
与使用接收结构的单个队列实现相同功能的设计相比,使用队列集从多个源接收数据的设计不那么整洁,效率也较低。因此,建议仅在设计约束绝对必要时才使用队列集。
以下部分描述了如何使用由以下人员设置的队列:
创建队列集。
向集合中添加队列。信号量也可以添加到队列集中。信号量将在本书后面描述。
从队列集中读取数据,以确定队列集中哪些队列包含数据。当作为队列集合成员的队列接收数据时,接收队列的句柄被发送到队列集合,当任务调用从队列集合读取的函数时返回。因此,如果从队列集中返回队列句柄,那么句柄引用的队列就知道包含数据,然后任务就可以直接从队列中读取。
如果队列是队列集的成员,那么不要从队列中读取数据,除非队列的句柄已经首先从队列集中读取。
队列集功能是通过在 FreeRTOSConfig.h
中将 configUSE_QUEUE_SETS
编译时配置常数设置为 1 来启用的。
xQueueCreateSet() API函数
必须先显式创建队列集,然后才能使用它。
队列集由句柄引用,句柄是 QueueSetHandle_t
类型的变量。xQueueCreateSet()
API 函数创建一个队列集,并返回一个引用它创建的队列集的 QueueSetHandle_t
。
清单 60. xQueueCreateSet()
函数原型
表格 23. xQueueCreateSet()
参数与返回值
参数名
描述
uxEventQueueLength
当队列集合中的一个队列接收数据时,接收队列的句柄被发送到队列集合。 uxEventQueueLength
定义了正在创建的队列集在任何时候可以容纳的队列句柄的最大数量。
队列句柄仅在队列集中的队列接收数据时发送给队列集。如果队列已满,则无法接收数据,因此如果队列集中的所有队列都已满,则无法向队列集发送队列句柄。因此,队列集中一次必须容纳的最大项目数是该组中每个队列长度的总和。
例如,如果集合中有三个空队列,每个队列的长度为五,那么集合中的队列总共可以在集合中的所有队列都满之前接收十五个项目(三个队列乘以五个项目)。在该示例中,uxEventQueueLength
必须设置为 15,以保证队列集可以接收发送给它的每个项目。
信号量也可以添加到队列集中。二进制和计数信号量将在本书后面介绍。为了计算必要的uxEventQueueLength,二进制信号量的长度为 1,计数信号量的长度由信号量的最大计数值给出。
作为另一个例子,如果队列集将包含长度为 3 的队列和长度为 1 的二进制信号量,uxEventQueueLength
必须设置为 4 (3+1)。
返回值
如果返回空值,则无法创建队列集,因为空闲操作系统没有足够的堆内存来分配队列集数据结构和存储区域。
返回的非空值表示队列集已成功创建。返回值应该存储为创建的队列集的句柄。
xQueueAddToSet()
将队列或信号量添加到队列集中。信号量将在本书后面描述。
清单 61. xQueueAddToSet()
API 函数原型
表格 24. xQueueAddToSet()
参数与返回值
参数名
描述
xQueueOrSemaphore
正在添加到队列集中的队列或信号量的句柄。
队列句柄和信号量句柄都可以转换为 QueueSetMemberHandle_t
类型。
xQueueSet
要添加队列或信号量的队列集的句柄。
返回值
有两种可能的返回值:
pdPASS: 只有当队列或信号量成功添加到队列集中时,才会返回 pdPASS。
pdFAIL: 如果队列或信号量无法添加到队列集中,将返回 pdFAIL。队列和二进制信号量只有在为空时才能添加到集合中。计数信号量只能在其计数为零时添加到集合中。队列和信号量一次只能是一个集合的成员。
xQueueSelectFromSet()
从队列集中读取队列句柄。
当作为集合成员的队列或信号量接收数据时,接收队列或信号量的句柄被发送到队列集合,并在任务调用 xQueueSelectFromSet()
时返回。如果从对 xQueueSelectFromSet()
的调用中返回句柄,则句柄引用的队列或信号量已知包含数据,然后调用任务必须直接从队列或信号量中读取。
注意: 除非队列或信号量的句柄是从对 xQueueSelectFromSet()
的调用中首先返回的,否则不要从属于集合成员的队列或信号量中读取数据。每次调用 xQueueSelectFromSet()
返回队列句柄或信号量句柄时,只从队列或信号量中读取一个项目。
清单 62. xQueueSelectFromSet()
API 函数原型
表格 25. xQueueSelectFromSet()
参数与返回值
参数名
描述
xQueueSet
队列集的句柄,从中接收(读取)队列句柄或信号量句柄。对用于创建队列集的 xQueueCreateSet()
的调用将返回队列集句柄。
xTicksToWait
如果队列集中的所有队列和信号量都为空,则调用任务应保持在阻塞状态以等待从队列集中接收队列或信号量句柄的最长时间。如果 xTicksToWait
为零,那么如果集合中的所有队列和信号量都为空,xQueueSelectFromSet()
将立即返回。
阻塞时间以刻度周期指定,因此它表示的绝对时间取决于刻度频率。宏 pdMS_TO_TICKS()
可用于将以毫秒为单位指定的时间转换为以刻度为单位指定的时间。
将 xTicksToWait
设置为 portMAXDELAY
将导致任务无限期等待(无超时),前提是在 FreeRTOSConfig.h
中将 INCLUDE_vTaskSuspend
设置为 1。
返回值
非空的返回值将是已知包含数据的队列或信号量的句柄。如果指定了阻塞时间(xTicksToWait
不为零),那么调用任务可能被置于阻塞状态,以等待数据从集合中的队列或信号量变得可用,但是在阻塞时间到期之前,从队列集合中成功读取了句柄。句柄以 QueueSetMemberHandle_t
类型返回,可以转换为 QueueHandle_t
类型或 SemaphoreHandle_t
类型。
如果返回值为空,则无法从队列集中读取句柄。如果指定了阻塞时间(xTicksToWait
不为零),则调用任务将被置于阻塞状态,以等待另一个任务或中断向集合中的一个或多个信号量发送数据,但阻塞时间在此之前已经过期。
本示例创建两个发送任务和一个接收任务。发送任务通过两个独立的队列向接收任务发送数据,每个任务一个队列。这两个队列被添加到队列集中,接收任务从队列集中读取以确定这两个队列中的哪一个包含数据。
任务、队列和队列集都是在 main()
中创建的,请参见清单 63 了解其实现。
清单 63. 示例 12 main()
实现
第一个发送任务使用 xQueue1
每 100 毫秒向接收任务发送一个字符指针。第二个发送任务使用 xQueue2
每 200 毫秒向接收任务发送一个字符指针。字符指针被设置为指向标识发送任务的字符串。清单 64 显示了两个发送任务的实现。
清单 64. 示例 12 中使用的发送任务
发送任务写入的队列是同一队列集的成员。每次任务发送到其中一个队列时,队列的句柄都会被发送到队列集中。接收任务调用 xQueueSelectFromSet()
从队列集中读取队列句柄。接收任务从集合中接收到队列句柄后,它知道接收句柄引用的队列包含数据,因此直接从队列中读取数据。它从队列中读取的数据是指向字符串的指针,接收任务将打印出该字符串。
如果对 xQueueSelectFromSet()
的调用超时,则它将返回空值。在示例 12 中,xQueueSelectFromSet()
是以不确定的块时间调用的,因此永远不会超时,并且只能返回有效的队列句柄。因此,在使用返回值之前,接收任务不需要检查 xQueueSelectFromSet()
是否返回空值。
xQueueSelectFromSet()
只有在句柄引用的队列包含数据时才会返回队列句柄,因此从队列中读取时没有必要使用块时间。
清单 65 显示了接收任务的实现。
清单 65. 示例 12 使用的接受任务
图 37 显示了示例 12 产生的输出。可以看出,接收任务从两个发送任务接收字符串。vSenderTask1()
使用的阻塞时间是 vSenderTask2()
使用的块时间的一半,导致 vSenderTask1()
发送的字符串打印频率是 vSenderTask2()
发送的字符串的两倍。
更实际的队列集用例
例 12 展示了一个非常简单的例子;队列集只包含队列,它包含的两个队列都用于发送字符指针。在真实的应用程序中,队列集可能包含队列和信号量,并且队列可能不都包含相同的数据类型。在这种情况下,在使用返回值之前,有必要测试 xQueueSelectFromSet()
返回的值。清单 66 演示了当集合具有以下成员时,如何使用从 xQueueSelectFromSet()
返回的值:
二进制信号量。
从中读取字符指针的队列。
从中读取
uint32_t
值的队列。
清单 66 假设队列和信号量已经被创建并添加到队列集中。
清单 66. 使用包含队列和信号量的队列集
使用队列创建邮箱
嵌入式社区内部对术语没有共识,“邮箱”在不同的实时操作系统中将有不同的含义。在这本书里,术语邮箱是指一个长度为1的队列。队列之所以被描述为邮箱,是因为它在应用程序中的使用方式,而不是因为它与队列的功能不同:
队列用于将数据从一个任务发送到另一个任务,或者从中断服务例程发送到一个任务。发送方在队列中放置一个条目,接收方从队列中移除该条目。数据通过队列从发送方传递到接收方。
邮箱用于保存任何任务或任何中断服务例程都可以读取的数据。数据不会通过邮箱,而是保留在邮箱中,直到被覆盖。发件人会覆盖邮箱中的值。接收器从邮箱中读取该值,但不从邮箱中删除该值。
本章描述了允许队列用作邮箱的两个队列应用编程接口函数。
清单 67 显示了一个被创建用作邮箱的队列。
清单 67. 正在创建用作邮箱的队列
xQueueOverwrite() API 函数
像 xQueueSendToBack()
应用编程接口函数一样,xQueueOverwrite()
应用编程接口函数向队列发送数据。与 xQueueSendToBack()
不同,如果队列已经满了,那么 xQueueOverwrite()
将覆盖队列中已经存在的数据。
xQueueOverwrite()
只能用于长度为1的队列。这种限制避免了函数的实现在队列已满时任意决定要覆盖队列中的哪个项目。
注意: 永远不要从中断服务例程调用 xQueueOverwrite()
。应该使用中断安全版本xQueueOverwriteFromISR()
来代替它。
清单 68. xQueueOverwrite()
API 函数原型
表 26. xQueueOverwrite()
参数和返回值
参数名/返回值
描述
xQueue
数据被发送(写入)到的队列的句柄。队列句柄将从用于创建队列的 xQueueCreate()
调用中返回。
pvItemToQueue
指向要复制到队列中的数据的指针。
队列可以容纳的每个项目的大小是在创建队列时设置的,因此这些字节将从 pvItemToQueue
复制到队列存储区域。
返回值
xQueueOverwrite()
将写入队列,即使队列已满,因此 pdPASS
是唯一可能的返回值。
清单 69. 使用 xQueueOverwrite() API 函数
xQueuePeek() API 函数
xQueuePeek()
用于从队列中接收(读取)项目,而不从队列中移除项目。xQueuePeek()
从队列头接收数据,而不修改存储在队列中的数据或数据在队列中的存储顺序。
注意: 永远不要从中断服务例程调用 xQueuePeek()
。应该使用中断安全版本 xQueuePeekFromISR()
来代替它。
xQueuePeek()
具有与 xQueueReceive()
相同的函数参数和返回值。
清单 70. xQueuePeek()
API 函数原型
清单 71 显示了 xQueuePeek() 用于接收发布到清单 69 中邮箱(队列)的项目
清单71. 使用 xQueuePeek() API 函数
Last updated