前言
通过阅读本文可以了解到:
- 如何创建队列
- 一个队列如何管理它包含的数据
- 如何发送数据至队列
- 如何从队列接收数据
- 阻塞队列意味着什么
- 如何阻塞多个队列
- 如何覆盖队列中的数据
- 如何清除一个队列
- 读取和写入一个队列对任务优先级的影响
队列的特征
一个队列能保存有限数量的固定大小的数据单元,每个队列数据单元的长度与大小是在创建队列时设置的。
队列通常是一个先入先出(FIFO)的缓冲区,即数据在队列末尾被写入,在队列前部移除,也可以写入队列的前端,并覆盖已位于队列前端的数据。下图演示了队列的创建及使用:
有两种方法可以实现队列的数据通信:
- 通过复制实现队列:复制队列是指将发送至队列的数据一个字节一个字节地复制到队列中。
- 通过引用实现队列:引用队列意味着队列只持有指向发送到队列的数据的指针,而不是数据本身。
FreeRTOS是通过使用复制方法实现队列。这是考虑到复制队列比引用队列更强大更容易使用: - 堆栈变量可以直接发送至队列,即使该变量将在声明它的函数退出后。
- 可以将数据发送到队列,而无需先分配缓冲区来保存数据,然后将数据复制到分配的缓冲区。
- 发送任务可以立即重用发送至队列的变量或缓冲区。
- 发送任务和接受任务是完全解耦的,开发人员不需要关心哪个任务拥有数据或者负责发布数据。
- 复制队列并不会阻止队列也被用于引用队列。例如,当正在排队的数据的大小使得数据复制到队列不切实际时,可以将指向数据的指针复制到队列中。
- RTOS完全负责分配用于存储数据的内存。
- 在受内存保护的系统中,任务可以访问的RAM将受到限制。这种情况下,只有当发送和接受任务都可以访问存储数据的RAM时,才可以使用引用队列。
多任务访问
队列本身就是对象,任何知道它们存在的任务或 ISR 都可以访问它们。任意数量的任务可以写入同一个队列,任意数量的任务也可以从同一个队列读取。在实践中,队列有多个写入者是非常常见的,但是队列有多个读取者就不那么常见了。
阻塞队列读取
当任务尝试从队列中读取时,可以选择指定阻塞时间。如果队列已经为空,则这是任务将保持在阻塞状态以等待队列中的数据的时间。当一个任务或中断将数据写入队列时,因为等待队列而阻塞的任务移至就绪态,如果指定的阻塞时间在数据可用之前到期,相应的任务也会移至就绪态。
队列可以有多个读取者,因此单个队列可能会阻塞多个任务,在这种情况下,只有一个任务在数据可用时将被解除阻塞。解除阻塞的任务始终是等待数据的最高优先级任务。如果被阻塞的任务具有相同优先级,那么等待数据最久的任务将被阻塞。
阻塞队列写入
与读取队列一样,任务也可以在向队列写入数据时指定阻塞时间。在这种情况下,如果队列已满,则阻塞时间是任务应该保持在阻塞状态的最长时间,在这期间中如果队列写入成功则退出阻塞状态。
阻塞多个队列
队列可被分组到集合中,允许任务进入阻塞状态来等待数据在集合的全部队列中变为可用。
使用队列
xQueueCreate()
创建队列的函数。创建队列后,可以使用xQueueReset()函数将队列返回其原始的空状态。
uxQueueLength
创建的队列一次可以容纳的最大项数。
uxItemSize
队列的每个项的字节大小。
返回值
如果返回NULL,则无法创建队列,因为FreeRTOS没哟足够的堆内存来分配队列数据结构和存储区域。返回的非空值代表创建成功,该返回值是这个队列的句柄,后续操作这个队列时需要用到。
xQueueSendToBack()与xQueueSendToFront()
xQueueSendToBack()用于将数据发送到队列的尾部,xQueueSendToFront()用于将数据发送到队列的头部。xQueueSend()与xQueueSendToBack()等价。
xQueueSendToFront() 或 xQueueSendToBack()不能在中断函数中调用。应该使用中断安全转换 xQueueSendToFrontFromISR() 和 xQueueSendToBackFromISR()。
xQueue
发送数据的队列的句柄。
pvItemToQueue
指向要复制到队列中的数据的指针。
xTicksToWait
如果队列已满,任务应该保持阻塞状态以等待队列上可用空间的最大时间量。如果为0,则立即返回;如果为portMAX_DELAY,则一直阻塞。
返回值
在指定的时间内没有成功写入会返回失败。
xQueueReceive()
从队列中接收一个元素,收到的元素将从队列中删除。
xQueue
需要接收数据的队列句柄。
pvBuffer
指向要将接收到的数据复制到内存的指针。pvBuffer指向的内存必须大于在创建队列设置了每个数据项的大小。
xTicksToWait
如果队列已空,则任务应保持阻塞状态等待数据的最长时间。如果为0,则立即返回;如果为portMAX_DELAY,则一直阻塞。
返回值
在指定的时间内没有成功写入会返回失败。
uxQueueMessagesWaiting()
用于查询当前队列的项数。
传输时引用指针
如果存储在队列中的数据量很大,最好使用队列将指针传输到数据,而不是将数据本身逐个字节得复制到队列中。引用指针在处理时间和创建队列所需的RAM量方面都更有效。在队列中引用指针时,以下几点需要确保:
- 指向的RAM必须是明确定义的。通过指针在任务间共享内存时,必须确保两个任务不会同时修改它。理想情况下,只允许发送访问存储器,直到指向存储器的指针已经排队,并且队列接收到指针后,只允许接收任务访问存储器。
- 如果指向的指针时已在任务堆栈上分配的数据,堆栈帧更改后,数据无效。比如队列引用了一个函数内的变量地址,这个函数结束后,这个变量的就是未知的了,所以这个一个非常规的操作。
使用队列发送不同类型和长度的数据
目前我们已经知道队列可以发送一个结构或者引用指针,将这两个组合起来就可以允许一个任务使用一个队列接收来自任何数据源的任何数据类型。
首先我们可以先定义一个结构,这个结构是用来描述需要发送的数据的特性的(例如类型、长度、存放数据的内存地址),与此同时要自定义一块内存来放需要发送的数据,里面的数据如何解析可以从定义的结构得知。
从多个队列接收
队列集
应用程序设计通常需要单个任务来接收不同大小、不同含义、不同来源的数据,而且同时需要这些队列都非空,这种情况可以使用“队列集”。
队列集允许任务从多个队列接收数据,而无需依次轮询每个队列是否有数据。与使用接收结构的单个队列实现相同功能的设计相比,使用队列集从多个源接收数据的设计更复杂,效率也更低,所以在设计是非必要不使用。
如何使用队列集:
- 创建队列集
- 向集合中添加队列。信号量也可以添加到队列集中。
- 从队列集读取数据,确定队列集中哪些队列包含数据。当;作为队列集合成员的队列接收数据时,接收队列的句柄被发送到队列集合中,当任务调用队列集读取的函数时返回。因此,如果从队列集中返回队列句柄,那么句柄引用的队列就包含数据,然后任务可以直接从队列中读取数据。
如果队列是队列集的成员,那么不要直接从队列中读取数据,除非队列的句柄已经从队列集中获取。
xQueueCreateSet()
创建队列集。
uxEventQueueLength
创建队列集时指定可以容纳的队列数量。
返回值
如果返回NULL,则无法创建队列集,因为没有足够的空间。如果返回非空值,则该返回值为队列集的句柄。
xQueueAddToSet()
将队列或信号量添加到队列集中。
xQueueOrSemaphore
正在添加到队列集中的队列或信号量的句柄。两种句柄可以互相转化。
xQueueSet
要添加队列或信号量的队列集的句柄。
返回值
成功或否。队列和二进制信号量只有在为空时才能添加到集合中。计数信号量只能在其计数为零时才能添加到集合中。队列和信号量一次只能是一个队列集的成员。
xQueueSelectFromSet()
从队列集中读取队列的句柄。当队列集的成员有接收到数据时,该接口可以返回队列成员的句柄,然后必须直接从队列成员中读取数据。用该接口读取成员句柄时,每次只能读取一个。从使用的感觉来看,有点像任务间的switch语句。
xQueueSet
队列集的句柄。
xTicksToWait
阻塞的超时时间。
返回值
如果成功的话该返回值是队列集成员的句柄。
使用队列创建邮箱
嵌入式社区内部对“邮箱”没有共识,在不同的操作系统中有不同的含义。在本文中,指的是一个长度为1的队列。队列之所以被描述为邮箱,是因为它再应用程序中的使用方式,而不是因为它与队列的功能不同:
- 队列用于将数据从一个任务发送到另一个任务,或者从中断服务函数发送到一个任务。发送方在队列中放置一个条目,接收方从队列中移除该条目。数据从发送发传递到接收方。
- 邮箱由于保存任何任务或中断服务函数都可以读取的数据。数据不会通过邮箱,而是保留在邮箱中,直到被覆盖。发件人会覆盖邮箱中的值,接收人会从邮箱中读取值,不会去删除。
xQueueOverwrite()
像队列发送数据,如果队列已满,则覆盖。
xQueuePeek()
从队列中接收数据,而不从队列中移除项目。