《Mastering the FreeRTOS Real Time Kernel》读书笔记(3)队列管理

发布时间 2023-10-13 10:42:42作者: BD4RTZ

4.队列管理

队列,在一些系统中被称为消息队列,可以理解为信息中转站。是任务和任务,任务和中断之间可以互相读和写的一个共享空间。

4.2 队列的特征

存储数据

队列本质上是一个先进先出的缓冲区(FIFO),所以可以存储一定容量的数据。
有两种方式可以实现FIFO队列:
1.将发送给队列的数据复制到队列中,队列中存储的是真实的数据。
2.仅将数据的指针存储在队列中。

FreeRTOS使用第一种方式,因为第一种方式更加稳定,它不用考虑2中,数据被改动或者消失的情况。当然,如果数据的确比较大,也可以将它的指针存入队列,但是一定要保证这些数据的有效性。

队列的存在一方面也是为了更好的管理数据和内存,其不会让一些无关任务随意地访问该数据。

支持多任务访问

任何被允许访问队列的任务都可以访问队列。在在实践中,一个队列有多个写入程序是很常见的,但一个队列中有多个读取程序就不那么常见了,这可能会导致先出的信息存在歧义而导致死循环。

队列被读取时的阻塞机制

当有任务需要读取队列时,队列可以根据队列内信息的多寡,对任务的状态进行设置。当队列为空,该任务被设置为阻塞状态,当队列内有数据可用,该任务被设置为就绪。

多任务读取时的优先级安排:一个队列可能会阻止多个任务等待数据。在这种情况下,当数据可用时,只有一个任务将被取消阻止。未阻止的任务将始终是等待数据的优先级最高的任务。如果被阻止的任务具有相同的优先级,则等待数据时间最长的任务将被取消阻止。

队列被写入时的阻塞机制

队列写入时的阻塞就像从队列读取时一样,任务可以在写入队列时指定阻塞时间。在这种情况下,阻塞时间是指如果队列已满,则任务应保持在“阻塞”状态以等待队列上的空间变为可用的最长时间。
队列可以有多个写入程序,因此一个完整的队列可能会阻塞多个任务等待完成发送操作。在这种情况下,当队列上的空间变为可用时,只有一个任务将被取消阻塞。未阻塞的任务将始终是等待空间的优先级最高的任务。如果被阻塞的任务具有相同的优先级,则等待空间最长的任务将被取消阻塞。

4.3 使用一个队列

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );

FreeRTOS V9.0.0还包括xQueueCreateStatic()函数,该函数在编译时分配静态创建队列所需的内存。
1.uxQueueLength:可以容纳的最大数据数。
2.uxltemSize:队列的位宽,每个数据的大小(以字节为单位)。
3.返回值:如果返回NULL,因为没有足够的堆内存可供FreeRTOS分配队列数据结构和存储区域。返回的非NULL值表示队列已成功创建。返回的值应存储为创建的队列的句柄。
创建队列后,可以使用xQueueReset()API函数将队列返回到其原始空状态。

发送数据

xQueueSendToBack()用于将数据发送到队列的后部(尾部),xQueQueueSendtoFront()用于向队列的前部(头部)发送数据。xQueueSend()等效于xQueueSendToBack(),并且与之完全相同。

注意:在中断中使用的是中断安全版本xQueueSendToFrontFromISR()和xQueueSendToBackFromISR()。

BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
                              const void * pvItemToQueue,
                              TickType_t xTicksToWait );

BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
                              const void * pvItemToQueue,
                              TickType_t xTicksToWait );

1.xQueue(队列名字):将数据发送(写入)到的队列的句柄。队列句柄将从用于创建队列的xQueueCreate调用中返回。
2.pvltemToQueue:指向要复制到队列中的数据的指针。队列可以容纳的每个项目的大小是在创建队列时设置的,因此这许多字节将从pvItemToQueue复制到队列存储区域中。
3.xTicksToWait:任务应保持在“阻塞”状态以等待队列上的空间变为可用的最长时间。
4.返回值:
(1)只有当数据成功发送到队列时,才会返回pdPASS。如果指定了块时间(xTicksToWait不为零),则在函数返回之前,调用任务可能被置于“阻塞”状态,以等待队列中的空间变为可用,但在块时间到期之前,数据已成功写入队列。
(2)如果由于队列已满而无法将数据写入队列,则将返回errQUEUE_FULL。如果指定了块时间(xTicksToWait不为零),则调用任务将被置于“阻塞”状态,以等待另一个任务或中断在队列中腾出空间,但指定的块时间在此之前已过期。

接收数据

同上,分为普通版本和中断安全版本

BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait );

参数设置基本同上。

查询队列内数据数量

同上,分为普通版本和中断安全版本

UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );

xQueue:同上

例子10:二发一收的抢占机制

1.Receiver任务首先运行,因为它具有最高优先级。它尝试从队列中读取。队列为空,因此接收器进入阻止状态以等待数据可用。发送器2在接收器被阻止后运行。
2.之后由于发射器2和发射器1是相同优先级,所以他们会交替运行。接收器也可以交替接收。

4.4 一个队列接受多种数据源

一个简单的设计解决方案是使用单个队列来传输结构体,该结构体的字段中包含数据值和数据源。
例子11:接收器通过发送数据包中的数据源,来判断数据来源。
具体使用还需要结合实际情况,可以实现更多效果。

4.5 传输大数据或可变大小数据

1.数据过大,不易拷贝到队列:直接发送数据的指针。但是一定要保证该指针有效,该指针指向的数据是有效的。
2.对于可变数据(不定长)的接受与处理:之前的两种强大的方法,向队列发送结构以及向队列发送指针。将这些技术相结合,可以使任务使用单个队列从任何数据源接收任何数据类型。
FreeRTOS+TCPTCP/IP堆栈的实现提供了一个如何实现这一点的实际示例。在自己的任务中运行的TCP/IP堆栈必须处理来自许多不同来源的事件。不同的事件类型与不同类型和长度的数据相关联。在TCP/IP任务之外发生的所有事件都由IPStackEvent_t类型的结构描述,并发送到队列上的TCP/IP任务。

4.6 从多个队列接受数据(队列集)

可以使用“队列集”(queue set)。队列集允许任务从多个队列接收数据,而无需任务依次轮询每个队列以确定哪个队列(如果有的话)包含数据。
但是,使用队列集从多个源接收数据的设计不那么整洁,也不那么高效。因此,建议只有在设计约束绝对需要使用队列集的情况下才使用队列集。
1.正在创建队列集。
2.将队列添加到集合中。信号量也可以添加到队列集中。信号量在这本书后面有描述。
3.从队列集合中读取以确定集合中的哪些队列包含数据。当作为集合成员的队列接收数据时,接收队列的句柄将发送到队列集合,并在任务调用从队列集读取的函数时返回。因此,如果从队列集返回队列句柄,则该句柄引用的队列包含数据,然后任务可以直接从队列中读取。
通过在FreeRTOSConfig.h中将configUSE_Queue_SETS编译时配置常量设置为1,可以启用队列集功能。
队列集相关函数和例子12不予介绍,请自行查询Queue Set相关内容。

4.7 使用队列构建一个“邮箱”

“邮箱”在不同的RTOS中意味着不同的东西。在本书中,“邮箱”一词用于指长度为1的队列。“邮箱”使用方法如下:
邮箱用于保存任何任务或任何中断服务例程都可以读取的数据。数据不会通过邮箱,而是保留在邮箱中,直到覆盖为止。发件人将覆盖邮箱中的值。接收器从邮箱中读取值,但不会从邮箱中删除该值。

xQueueOverwrite()

BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void * pvItemToQueue );

与xQueueSendToBack()API函数一样,xQueueOverwrite()API函数将数据发送到队列。与xQueueSendToBack()不同,如果队列已满,则xQueueOverwrite()将覆盖队列中已存在的数据。xQueueOverwrite()只能用于长度为1的队列。如果队列已满,则该限制避免了函数实现对覆盖队列中的哪一项做出任意决定的需要。
1.xQueue:xQueueCreate()调用中返回的队列句柄。
2.pvltemToQueue:指向要复制到队列中的数据的指针。

xQueuePeek()

xQueuePeek()用于接收(读取)队列中的项目,而不从队列中删除该项目。

BaseType_t xQueuePeek( QueueHandle_t xQueue,
                        void * const pvBuffer,
                        TickType_t xTicksToWait );

参数设置与xQueueReceive()类似。