4.1 本章介绍与适用范围
“队列”提供了任务到任务,任务到中断以及中断到任务的通信机制。
范围
本章旨在使读者更好地理解:
- 如何创建队列。
- 队列如何管理其包含的数据。
- 如何将数据发送到队列。
- 如何从队列接收数据。
- 阻塞队列意味着什么。
- 如何在多个队列上进行阻止。
- 如何覆盖队列中的数据。
- 如何清除队列。
- 写入队列和从队列读取时任务优先级的影响。
本章仅介绍任务之间的通信。 第6章介绍了任务到中断和中断到任务的通信。
4.2队列的特征
数据存储
队列可以容纳有限数量的固定大小的数据项。 队列可以容纳的最大项目数称为“长度”。 创建队列时,将设置每个数据项的长度和大小。
队列通常用作先进先出(FIFO)缓冲区,其中数据被写入队列的末尾(尾部),并从队列的前部(头)删除。 图31演示了正在被用作FIFO的队列中写入数据和从中读取数据。 还可以写到队列的最前面,并覆盖已经在队列的最前面的数据。
图31.对队列进行写入和读取的示例序列
可以通过两种方式来实现队列行为:
- 按复制队列
通过复制队列意味着将发送到队列的数据逐字节复制到队列中。 - 按引用队列
通过引用队列意味着队列仅保存指向发送到队列的数据的指针,而不是数据本身。
FreeRTOS使用复制队列。 与按引用队列相比,按复制队列被认为同时更强大,更容易使用,因为:
- 堆栈变量可以直接发送到队列,即使在声明该变量的函数退出后该变量将不存在。
- 可以将数据发送到队列,而无需先分配一个缓冲区来保存数据,然后将数据复制到分配的缓冲区中。
- 发送任务可以立即重新使用发送到队列的变量或缓冲区。
- 发送任务和接收任务是完全分离的-应用程序设计人员无需担心“拥有”数据的任务或负责释放数据的任务。
- 按幅值队列不会阻止该队列也被用于按引用队列。 例如,当要排队的数据的大小使将数据复制到队列中变得不切实际时,可以将指向数据的指针复制到队列中。
- RTOS完全负责分配用于存储数据的内存。
- 在受内存保护的系统中,任务可以访问的RAM将受到限制。 在那种情况下,仅当发送和接收任务都可以访问存储数据的RAM时,才可以使用按引用队列。 复制队列并不强加该限制; 内核始终以完全特权运行,从而允许使用队列跨内存保护边界传递数据。
通过多个任务进行访问
队列本身就是对象,知道其存在的任何任务或ISR都可以访问队列。 可以将任意数量的任务写入同一队列,并且可以从同一队列中读取任意数量的任务。 在实践中,队列中有多个编写者是很常见的,但是队列中有多个读者则很少见。
阻塞队列读取
当任务尝试从队列中读取时,可以选择指定“阻塞”时间。 如果队列已经为空,这是将任务保持在“阻塞”状态以等待队列中的数据可用的时间。 当另一个任务或中断将数据放入队列时,处于等待状态的任务(等待数据从队列中可用)将自动移至“就绪”状态。 如果在数据可用之前指定的阻塞时间到期,则任务还将自动从“阻塞”状态移至“就绪”状态。
队列可以具有多个读取器,因此单个队列可能有多个阻塞的任务在等待数据。 在这种情况下,当数据可用时,只有一个任务将被解除阻止。 解除阻止的任务将始终是等待数据的优先级最高的任务。 如果被阻止的任务具有相同的优先级,则一直等待数据时间最长的任务将被解除阻止。
阻塞写队列
就像从队列中读取时一样,任务可以选择在写入队列时指定阻塞时间。 在这种情况下,阻塞时间是任务应保持在“阻塞”状态以等待队列上可用的空间(如果队列已满)的最长时间。
队列可以有多个编写器,因此一个完整的队列可能会阻塞一个以上的任务,以等待完成发送操作。 在这种情况下,当队列上的空间可用时,将仅解锁一项任务。 解除阻止的任务将始终是等待空间的优先级最高的任务。 如果被阻止的任务具有相同的优先级,那么一直在等待最长空间的任务将被解除阻止。
阻塞多个队列
队列可以分为几组,从而允许任务进入“已阻止”状态,以等待数据在该组中的任何队列上可用。 在第4.6节“从多个队列接收”中演示了队列集。
4.3使用队列
xQueueCreate()API函数
必须先显式创建一个队列,然后才能使用它。
队列由句柄引用,句柄是QueueHandle_t类型的变量。 xQueueCreate()API函数创建一个队列,并返回引用它创建的队列的QueueHandle_t。
FreeRTOS V9.0.0还包括xQueueCreateStatic()函数,该函数分配创建所需的内存
在编译时静态地创建一个队列:当创建队列时,FreeRTOS从FreeRTOS堆中分配RAM。 RAM用于保存队列数据结构和队列中包含的项目。 如果没有足够的堆RAM来创建队列,则xQueueCreate()将返回NULL。 第2章提供了有关FreeRTOS堆的更多信息。
清单40. xQueueCreate()API函数原型
表18. xQueueCreate()参数和返回值
参数名称 | 描述 |
---|
uxQueueLength | 正在创建的队列可以同时容纳的最大项目数。 |
uxItemSize | 可以存储在队列中的每个数据项的大小(以字节为单位)。 |
Return Value | 如果返回NULL,则无法创建队列,因为FreeRTOS没有足够的堆内存来分配队列数据结构和存储区域。返回的非NULL值表示队列已成功创建。 返回的值应存储为所创建队列的句柄。 |
创建队列后,可以使用xQueueReset()API函数将队列返回到其原始的空状态。
xQueueSendToBack()和xQueueSendToFront()API函数
可以预期,xQueueSendToBack()用于将数据发送到队列的后部(尾部),xQueueSendToFront()用于将数据发送到队列的前部(头)。
xQueueSend()等效于xQueueSendToBack()并与之完全相同。
注意:切勿从中断服务调用xQueueSendToFront()或xQueueSendToBack()
常规。 应在其位置使用中断安全版本xQueueSendToFrontFromISR()和xQueueSendToBackFromISR()。 这些将在第6章中进行描述。
清单41. xQueueSendToFront()API函数原型
清单42. xQueueSendToBack()API函数原型
表19. xQueueSendToFront()和xQueueSendToBack()函数参数和返回值
参数名称/返回值 | 描述 |
---|
xQueue | 将数据发送(写入)到的队列的句柄。 队列句柄将从用于创建队列的xQueueCreate()调用返回。 |
pvItemToQueue | 指向要复制到队列中的数据的指针。创建队列时,将设置队列可容纳的每个项目的大小,因此会将这许多字节从pvItemToQueue复制到队列存储区域。 |
xTicksToWait | 如果队列已满,则任务应保持在“阻塞”状态以等待队列上的空间可用的最长时间。如果xTicksToWait为零且队列已满,则xQueueSendToFront()和xQueueSendToBack()都将立即返回。阻塞时间以滴答周期指定,因此它表示的绝对时间取决于滴答频率。 宏pdMS_TO_TICKS()可用于将以毫秒为单位的时间转换为以刻度为单位的时间。如果在FreeRTOSConfig.h中将INCLUDE_vTaskSuspend设置为1,则将xTicksToWait设置为portMAX_DELAY将导致任务无限期等待(无超时)。 |
Returned value | 有两个可能的返回值:1.pdPASS 仅当数据成功发送到队列时,才会返回pdPASS。如果指定了阻塞时间(xTicksToWait不为零),则将调用任务置于“阻止”状态,以等待队列中的空间可用,然后函数返回,但数据已成功写入 在阻止时间到期之前排队。2.errQUEUE_FULL 如果由于队列已满而无法将数据写入队列,则将返回errQUEUE_FULL。如果指定了阻塞时间(xTicksToWait不为零),则将调用任务置于“阻止”状态,以等待另一个任务或中断以在队列中腾出空间,并且制定的阻塞时间已经超时 |
xQueueReceive()API函数
xQueueReceive()用于接收(读取)队列中的项目。 从队列中删除接收到的项目。
注意:切勿从中断服务程序中调用xQueueReceive()。 中断安全的xQueueReceiveFromISR()API函数在第6章中进行了描述。
清单43. xQueueReceive()API函数原型
表20. xQueueReceive()函数参数和返回值
参数名称/返回值 | 描述 |
---|
xQueue | 从中接收(读取)数据的队列的句柄。 队列句柄将从用于创建队列的xQueueCreate()调用返回。 |
pvBuffer | 指向将接收到的数据复制到的内存的指针。创建队列时,将设置队列保留的每个数据项的大小。 pvBuffer指向的内存必须至少足够大以容纳那么多字节。 |
xTicksToWait | 如果队列已经为空,则任务应保持在“阻塞”状态以等待数据在队列上可用的最长时间。如果xTicksToWait为零,则如果队列已经为空,则xQueueReceive()将立即返回。阻塞时间以滴答周期指定,因此它表示的绝对时间取决于滴答频率。 宏pdMS_TO_TICKS()可用于将以毫秒为单位的时间转换为以刻度为单位的时间。如果在FreeRTOSConfig.h中将INCLUDE_vTaskSuspend设置为1,则将xTicksToWait设置为portMAX_DELAY将导致任务无限期等待(无超时)。 |
Returned value | 有两个可能的返回值:1. pdPASS 仅当成功从队列中读取数据时,才会返回pdPASS。如果指定了阻止时间(xTicksToWait不为零),则可能将调用任务置于“阻止”状态,以等待数据在队列中可用,但是在阻止时间之前已成功从队列中读取数据 已到期。2.errQUEUE_EMPTY 如果由于队列已经为空而无法从队列中读取数据,则将返回errQUEUE_EMPTY。如果指定了阻塞时间(xTicksToWait不为零),则调用任务将被置于“阻塞”状态,以等待另一个任务或中断将数据发送到队列,但是阻塞时间在此之前已过期。 |
uxQueueMessagesWaiting()API函数
uxQueueMessagesWaiting()用于查询队列中当前的项目数。
注意:切勿从中断服务程序调用uxQueueMessagesWaiting()。 应在其位置使用中断安全的uxQueueMessagesWaitingFromISR()。
清单44. uxQueueMessagesWaiting()API函数原型
参数名称/返回值 | 描述 |
---|
xQueue | 正在查询的队列的句柄。 队列句柄将从用于创建队列的xQueueCreate()调用返回。 |
Returned value | 当前正在查询的队列中保留的项目数。 如果返回零,则队列为空。 |
例子10.从队列接收时阻塞
此示例演示了正在创建的队列,正在从多个任务发送到队列的数据以及正在从队列接收的数据。 创建该队列以保存int32_t类型的数据项。 发送到队列的任务没有指定阻塞时间,而从队列接收的任务却指定了阻塞时间。
发送到队列的任务的优先级低于从队列接收的任务的优先级。 这意味着队列中决不能包含多个项目,因为一旦将数据发送到队列,接收任务将立即解除阻塞,抢占发送任务并删除数据,从而再次使队列变空。
清单45显示了写入队列的任务的实现。 创建了此任务的两个实例,一个实例将值100连续写入队列,另一个实例将值200连续写入同一队列。 task参数用于将这些值传递到每个任务实例中。
清单45.示例10中使用的发送任务的实现
清单46显示了从队列接收数据的任务的实现。 接收任务指定100毫秒的阻止时间,因此将进入“阻止”状态以等待数据可用。 当队列中有可用数据或经过100毫秒而没有可用数据时,它将退出“阻塞”状态。 在此示例中,100毫秒的超时永远不会到期,因为有两个任务连续写入队列。
清单46.示例10的接收器任务的实现
清单47包含main()函数的定义。 这只是在启动调度程序之前创建队列和三个任务。 即使已设置任务的优先级,使得队列一次最多只能包含一个项目,但队列的创建最多可容纳五个int32_t值。
清单47.示例10中main()的实现
发送到队列的两个任务具有相同的优先级。 这将导致两个发送任务将数据依次发送到队列。 例10产生的输出如图32所示。
图32.执行示例10时产生的输出
图33演示了执行顺序。
图33.示例10产生的执行顺序
4.4从多个来源接收数据
在FreeRTOS设计中,一项任务通常是从多个来源接收数据。 接收任务需要知道数据来自何处,以确定应如何处理数据。 一种简单的设计解决方案是使用单个队列来传输结构和字段中包含的数据值和数据源两者的结构。 图34中演示了该方案。
图34.在队列上发送结构的示例场景
参考图34:
- 将创建一个队列,其中包含Data_t类型的结构体。 结构体成员同时允许数据值和枚举类型,该类型和枚举类型指示在一条消息中将数据表示发送到队列的含义。
- 中央控制器任务用于执行主要系统功能。 这必须对输入做出反应,并更改队列中传达给它的系统状态。
- 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结构体,并且发送和接收任务的优先级相反。 清单51显示了main()的实现。
例11产生的输出如图35所示。
图35示例11产生的输出
图36.示例11产生的执行顺序
Time | Description |
---|
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都在等待队列中的可用空间,因此任务接收器是唯一可以进入运行状态的任务。 |
4.5处理大型或可变大小的数据
队列指针
如果要存储在队列中的数据量很大,那么最好使用队列将指针转移到数据,而不是逐字节地将数据本身复制到队列中和从队列中复制出来。 传输指针在处理时间和创建队列所需的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任务。 清单55中显示了IPStackEvent_t结构。IPStackEvent_t结构的pvData成员是一个指针,可用于直接保存值或指向缓冲区。
清单55.用于将事件发送到FreeRTOS + TCP中的TCP / IP堆栈任务的结构体
示例TCP / IP事件及其相关数据包括:
- eNetworkRxEvent:已从网络接收到一个数据包。
使用IPStackEvent_t类型的结构体将从网络接收的数据发送到TCP / IP任务。 结构体“ eEventType成员”设置为eNetworkRxEvent,结构体“ pvData成员”用于指向包含接收到的数据的缓冲区。 清单56显示了一个伪代码示例。
清单56.伪代码显示如何使用IPStackEvent_t结构将从网络接收的数据发送到TCP / IP任务
- eTCPAcceptEvent:套接字用于接受或等待来自客户端的连接。
使用IPStackEvent_t类型的结构,将接受事件从名为FreeRTOS_accept()的任务发送到TCP / IP任务。 结构的eEventType成员设置为eTCPAcceptEvent,结构的pvData成员设置为接受连接的套接字的句柄。 清单57显示了一个伪代码示例。
清单57.伪代码显示如何使用IPStackEvent_t结构发送接受与TCP / IP任务的连接的套接字的句柄
- eNetworkDownEvent:网络需要连接或重新连接。
网络中断事件使用IPStackEvent_t类型的结构从网络接口发送到TCP / IP任务。 该结构的eEventType成员设置为eNetworkDownEvent。 网络中断事件不与任何数据相关联,因此不使用结构的pvData成员。 清单58显示了一个伪代码示例。
清单58.伪代码显示如何使用IPStackEvent_t结构将网络中断事件发送到TCP / IP任务
清单59中显示了在TCP / IP任务中接收和处理这些事件的代码。可以看出,从队列接收到的IPStackEvent_t结构的eEventType成员用于确定如何解释pvData成员。
清单59.显示如何接收和处理IPStackEvent_t结构的伪代码
4.6从多个队列接收
队列集
通常,应用程序设计需要一项任务来接收大小不同的数据,含义不同的数据以及来自不同来源的数据。 上一节演示了如何使用接收结构的单个队列以简洁有效的方式实现这一目标。 但是,有时应用程序的设计人员正在使用限制其设计选择的约束,从而需要对某些数据源使用单独的队列。 例如,集成到设计中的第三方代码可能会假定存在专用队列。 在这种情况下,可以使用“队列集”。
队列集使任务可以从多个队列中接收数据,而无需依次轮询每个队列以确定哪个队列包含数据。
与使用单个接收结构的队列实现相同功能的设计相比,使用队列集从多个源接收数据的设计更简洁,效率更低。 因此,建议仅在设计约束使其绝对必要时使用队列集。
以下各节描述如何使用以下方式设置的队列:
1.创建队列集。
2.将队列添加到集合中。
信号量也可以添加到队列集中。 信号量将在本书的后面进行介绍。
3.从队列集中进行读取,以确定该集中的哪些队列包含数据。 当作为集合成员的队列接收数据时,接收队列的句柄将发送到该队列集合,并在任务调用从该队列集合读取的函数时返回。 因此,如果从队列集中返回了队列句柄,则已知该句柄引用的队列包含数据,然后可以直接从队列中读取任务。
注意:如果队列是队列集的成员,则不要从队列中读取数据,除非首先从队列集中读取了队列的句柄。
通过在FreeRTOSConfig.h中将configUSE_QUEUE_SETS编译时间配置常量设置为1来启用队列集功能。
xQueueCreateSet()API函数
必须先显式创建一个队列集,然后才能使用它。
队列集由句柄引用,句柄是QueueSetHandle_t类型的变量。 xQueueCreateSet()API函数创建一个队列集,并返回一个QueueSetHandle_t,该QueueSetHandle_t引用了它创建的队列集。
清单60. xQueueCreateSet()API函数原型
参数名称 | 描述 |
---|
uxEventQueueLength | 当作为队列集成员的队列接收数据时,接收队列的句柄将发送到该队列集。 uxEventQueueLength定义正在创建的队列集可以在任何一次保存的最大队列句柄数。 队列句柄仅在集合中的队列接收到数据时才发送到该队列集合。 如果队列已满,则队列无法接收数据,因此,如果队列中的所有队列都已满,则无法将队列句柄发送到该队列集。 因此,队列集一次必须保存的最大项目数是该集合中每个队列的长度之和。 例如,如果集合中有三个空队列,并且每个队列的长度为5,则队列集在满之前总共可以接收十五个项目(三个队列乘以五个,每个队列乘以五个项目)。 在该示例中,必须将uxEventQueueLength设置为15,以确保队列集可以接收发送给它的每个项目。 信号量也可以添加到队列集中。 本书稍后将介绍二进制和计数信号灯。 为了计算必要的uxEventQueueLength,二进制信号量的长度为1,计数信号量的长度由信号量的最大计数值给出。 作为另一个示例,如果队列集将包含长度为3的队列和二进制信号量(长度为1),则uxEventQueueLength必须设置为4(3加1)。 |
Return Value | 如果返回NULL,则无法创建队列集,因为FreeRTOS没有足够的堆内存来分配队列集的数据结构和存储区域。返回的非NULL值表示已成功创建队列集。 返回的值应存储为创建的队列集的句柄。 |
xQueueAddToSet()API函数
xQueueAddToSet()将队列或信号量添加到队列集。 信号量将在本书的后面进行介绍。
清单61. xQueueAddToSet()API函数原型
表24. xQueueAddToSet()参数和返回值
参数名称 | 描述 |
---|
xQueueOrSemaphore | 正在添加到队列集中的队列或信号量的句柄。 队列句柄和信号量句柄都可以转换为QueueSetMemberHandle_t类型。 |
xQueueSet | 将队列或信号灯添加到的队列集的句柄。 |
Return Value | 有两个可能的返回值:1. pdPASS 仅当队列或信号量已成功添加到队列集中时,才会返回pdPASS。 2. pdFAIL 如果无法将队列或信号量添加到队列集中,则将返回pdFAIL。m 队列和二进制信号量只有在它们为空时才能添加到集合中。 计数信号量只能在计数为零时添加到集合中。 队列和信号量一次只能是一个集合的成员。 |
xQueueSelectFromSet()API函数
xQueueSelectFromSet()从队列集中读取队列句柄。
当属于集合成员的队列或信号量接收数据时,接收队列或信号量的句柄将发送到队列集,并在任务调用xQueueSelectFromSet()时返回。 如果从对xQueueSelectFromSet()的调用返回了一个句柄,则已知该句柄引用的队列或信号包含数据,然后调用任务必须直接从队列或信号中读取。
注意:除非首先从对xQueueSelectFromSet()的调用中返回了队列或信号的句柄,否则请不要从作为集合成员的队列或信号中读取数据。 每次从xQueueSelectFromSet()的调用返回队列句柄或信号量句柄时,仅从队列或信号量中读取一项。
清单62. xQueueSelectFromSet()API函数原型
表25. xQueueSelectFromSet()参数和返回值
参数名称 | 描述 |
---|
xQueueSet | 从中接收(读取)队列句柄或信号量句柄的队列集的句柄。 队列集句柄将从用于创建队列集的xQueueCreateSet()的调用返回。 |
xTicksToWait | 如果集合中的所有队列和信号灯都为空,则调用任务应保持在“阻塞”状态以等待从队列集中接收队列或信号灯句柄的最长时间。如果xTicksToWait为零,则如果集合中的所有队列和信号灯都为空,则xQueueSelectFromSet()将立即返回。阻塞时间以滴答周期指定,因此它表示的绝对时间取决于滴答频率。 宏pdMS_TO_TICKS()可用于将以毫秒为单位的时间转换为以滴答数为单位的时间。如果在FreeRTOSConfig.h中将INCLUDE_vTaskSuspend设置为1,则将xTicksToWait设置为portMAX_DELAY将导致任务无限期等待(无超时)。 |
Return Value | 不为NULL的返回值将是已知包含数据的队列或信号的句柄。 如果指定了阻止时间(xTicksToWait不为零),则可能会将调用任务置于“阻止”状态,以等待数据从集合中的队列或信号灯变为可用,但已成功从中读取了句柄 阻止时间到期之前设置的队列。 将句柄作为QueueSetMemberHandle_t类型返回,可以将其强制转换为QueueHandle_t类型或SemaphoreHandle_t类型。如果返回值为NULL,则无法从队列集中读取句柄。 如果指定了阻塞时间(xTicksToWait不为零),则调用任务将被置于“阻塞”状态,以等待另一个任务或中断将数据发送到集合中的队列或信号量,但是阻塞时间在此之前已过期 |
例子12.使用队列集
本示例创建两个发送任务和一个接收任务。 发送任务在两个单独的队列上将数据发送到接收任务,每个任务一个队列。 这两个队列被添加到一个队列集中,接收任务从该队列集中读取以确定两个队列中的哪个包含数据。
任务,队列和队列集都在main()中创建-有关其实现,请参见清单63。
清单63.示例12的main()的实现
第一个发送任务使用xQueue1每100毫秒向接收任务发送一个字符指针。 第二个发送任务使用xQueue2每200毫秒向接收任务发送一个字符指针。 字符指针设置为指向标识发送任务的字符串。 清单64显示了这两个发送任务的实现。
清单64.示例12中使用的发送任务
发送任务写入的队列是同一队列集的成员。 每次任务发送到一个队列时,该队列的句柄都将发送到该队列集。 接收任务调用xQueueSelectFromSet()从队列集中读取队列句柄。 接收任务从集合中接收到队列句柄后,它知道接收到的句柄引用的队列包含数据,因此直接从队列中读取数据。 它从队列中读取的数据是指向字符串的指针,接收任务会打印出该字符串。
如果对xQueueSelectFromSet()的调用超时,则它将返回NULL。 在示例12中,使用不确定的堵塞时间调用xQueueSelectFromSet(),因此永远不会超时,并且只能返回有效的队列句柄。 因此,在使用返回值之前,接收任务无需检查xQueueSelectFromSet()是否返回NULL。
xQueueSelectFromSet()仅在由句柄引用的队列包含数据时才返回队列句柄,因此从队列中读取时不必使用阻塞时间。
清单65显示了receive任务的实现。
图37显示了示例12产生的输出。可以看出,接收任务从两个发送任务接收字符串。 vSenderTask1()使用的阻止时间是vSenderTask2()使用的阻止时间的一半,这导致vSenderTask1()发送的字符串的打印时间是vSenderTask2()发送的字符串的两倍。
图37执行示例12时产生的输出
更现实的队列集用例
例12展示了一个非常简单的情况。 队列集仅包含队列,队列中包含的两个队列都用于发送字符指针。 在实际的应用程序中,队列集可能同时包含队列和信号量,并且这些队列可能并不都拥有相同的数据类型。 在这种情况下,有必要在使用返回值之前测试xQueueSelectFromSet()返回的值。 清单66演示了当集合具有以下成员时如何使用从xQueueSelectFromSet()返回的值:
1.二进制信号量。
2.从中读取字符指针的队列。
3.从中读取uint32_t值的队列。
清单66假定已经创建了队列和信号量并将其添加到队列集中。
清单66.使用包含队列和信号量的队列集
4.7使用队列创建邮箱
嵌入式社区内部尚未就术语达成共识,“邮箱”在不同的RTOS中将具有不同的含义。 在本书中,术语邮箱用于指代长度为1的队列。 队列可能被描述为邮箱,这是因为它在应用程序中的使用方式,而不是因为它与队列在功能上有所不同:
- 队列用于将数据从一个任务发送到另一任务,或从中断服务例程发送到任务。 发送方将项目放入队列,接收方将项目从队列中删除。 数据通过队列从发送方到达接收方。
- 邮箱用于保存可由任何任务或任何中断服务例程读取的数据。 数据不通过邮箱,而是保留在邮箱中直到被覆盖。 发件人覆盖邮箱中的值。 接收方从邮箱中读取值,但不会从邮箱中删除该值。
本章介绍了两个队列API函数,这些函数允许将队列用作邮箱。
清单67显示了正在创建的用作邮箱的队列。
清单67.正在创建用作邮箱的队列
xQueueOverwrite()API函数
与xQueueSendToBack()API函数类似,xQueueOverwrite()API函数将数据发送到队列。 与xQueueSendToBack()不同,如果队列已满,则xQueueOverwrite()将覆盖队列中已经存在的数据。
xQueueOverwrite()仅应与长度为1的队列一起使用。 该限制避免了函数的实现需要确定队列是否已满的情况下,要决定要覆盖队列中的哪个项目的任意决定。
注意:切勿从中断服务程序中调用xQueueOverwrite()。 应在其位置使用中断安全版本xQueueOverwriteFromISR()。
清单68. xQueueOverwrite()API函数原型
表26. xQueueOverwrite()参数和返回值
参数名称/返回值 | Description |
---|
xQueue | 将数据发送(写入)到的队列的句柄。 队列句柄将从用于创建队列的xQueueCreate()调用返回。 |
pvItemToQueue | 指向要复制到队列中的数据的指针。创建队列时,将设置队列可容纳的每个项目的大小,因此会将这么多字节从pvItemToQueue复制到队列存储区域。 |
Returned value | 即使队列已满,xQueueOverwrite()也会写入队列,因此pdPASS是唯一可能的返回值。 |
清单69显示了xQueueOverwrite(),它用于写入清单67中创建的邮箱(队列)。
清单69.使用xQueueOverwrite()API函数
xQueuePeek()API函数
xQueuePeek()用于从队列接收(读取)项目,而不会从队列中删除该项目。 xQueuePeek()从队列的头接收数据,而无需修改存储在队列中的数据或数据在队列中的存储顺序。
注意:切勿从中断服务程序中调用xQueuePeek()。 应在其位置使用中断安全版本xQueuePeekFromISR()。
xQueuePeek()具有与xQueueReceive()相同的函数参数和返回值。
清单70. xQueuePeek()API函数原型
清单71显示了xQueuePeek(),用于接收清单69中发布到邮箱(队列)的项目。
清单71.使用xQueuePeek()API函数
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)