简介
FreeRTOS中的队列是一种用于实现【任务与任务】,【任务与中断】以及【中断与任务】之间的通信机制。
此外,任务从队列读数据或者写入数据到队列时,都可能被阻塞。这个特性使得任务可以被设计成基于事件驱动的运行模式,大大提高了CPU的执行效率。队列是实现FreeRTOS中其他特性的基础组件,像软件定时器,信号量,互斥锁都是基于队列而实现的。
队列的基本特性
队列是一种FIFO操作的数据结构,入队操作就是把一个新的元素放进队尾(tail),出队操作就是从队头(front)取出一个元素。FreeRTOS中也支持把一个元素放到队头的操作,这个操作会覆盖之前队头的元素。
备份入队策略
队列在设计的时候,主要有两种元素入队存储策略:Queue by copy 和 Queue by reference。FreeRTOS的队列使用的是Queue by copy存储策略,考虑到这种策略实现起来更加简单,灵活,且安全。
- Queue by copy:数据入队的时候,队列中存储的是此数据的一份拷贝
- Queue by reference:数据队列的时候,队列中存储的是数据的指针,而非数据本身
读队列时阻塞
一个任务在尝试从队列中读取数据时,可以指定一个阻塞时间,即任务在等待队列有元素可读前最长等待阻塞时间。当队列中有元素可读时,任务会自动从阻塞态转换为就绪态。如果在等待时间内队列一直没有数据可读,则等待时间到期后,任务也会自动从阻塞态转换为就绪态,但是它会返回一个读取失败的结果。
队列可能有多个reader Task,所以在等待队列有数据可读时,可能会有多个任务阻塞。当队列有数据可读时,调度器会从所有阻塞的任务中选取优先级最高的那个任务让其进入到就绪态。如果最高优先级的不止一个,则让等待最久的那个进入到就绪态。
写队列时阻塞
一个任务在尝试写入数据到队列时,可以指定一个阻塞时间,即任务在等待队列有空余空间(非满)可写前最长等待阻塞时间。当队列中有空间可写入时,任务会自动从阻塞态转换为就绪态。如果在等待时间内队列一直是满的,则等待时间到期后,任务也会自动从阻塞态转换为就绪态,但是它会返回一个写入失败的结果。
队列可能有多个writerTask,所以在等待队列有空闲空间时,可能会有多个任务阻塞。当队列有空闲空间可写时,调度器会从所有阻塞的任务中选取优先级最高的那个任务让其进入到就绪态。如果最高优先级的不止一个,则让等待最久的那个进入到就绪态。
队列的创建
在FreeRTOS中创建队列时要指定队列的长度和存储的元素的大小。
使用xQueueCreate()内核函数来创建一个队列。队列的存储空间从FreeRTOS heap中分配。在使用xQueueCreate()创建队列时,如果FreeRTOS heap中没有足够的存储空间分配给当前队列,则函数返回NULL。如果创建成功,则返回队列的句柄(QueueHandle_t类型)。
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 uxQueueLength:指定队列的长度,即最多可以存放的元素个数
参数 uxItemSize:队列中存储的元素的大小(占用的字节数)
返回值:返回NULL代表创建失败,没有足够的堆空间来创建当前队列;创建成功则返回队列的句柄。
元素入队操作
使用xQueueSendToBack()函数来向队尾存放一个元素,实现元素入队操作。xQueueSend()函数与xQueueSendToBack()等价,用法一样,只是名称不同。
BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
//xQueueSendToBack的定义
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
//xQueueSend的定义
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
参数xQueue:目标队列的句柄。
参数pvItemToQueue:入队元素的指针。队列将存储此指针指向的数据的备份。
参数xTicksToWait:指定等待队列有空间可以容纳新元素入队的最长等待(阻塞)时间,这种情况发生在队列已经满了的时候。如果需要等待,则任务会因为调用这个函数而进入阻塞状态,直到队列非满而能让这个任务写入数据或者指定的阻塞时间过期,才会转变为就绪态。如果此参数使用0,则当队列已经满了的时候,此函数立即返回而不阻塞。
使用portMAX_DELAY作为参数将使得等待时间为无限长,直到队列非满而能让这个任务写入数据,注意如果要使用portMAX_DELAY为参数,则必须在FreeRTOSConfig.h将INCLUDE_vTaskSuspend定义为1。
返回值:pdPASS,当元素成功入队时返回pdPASS。例如,在限定超时时间(xTicksToWait非0)过期前成功入队。
errQUEUE_FULL,因为队列满而无法入队时返回errQUEUE_FULL。例如,在限定超时时间(xTicksToWait非0)过期后,队列一直为满而无法入队新元素。
元素出队操作
使用xQueueReceive()内核函数来从队列中读取队头元素,读取成功后队列将删除这个元素,实现元素出队操作。
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
参数xQueue:目标队列的句柄。
参数pvBuffer:用于存放读取到的队列元素的缓冲区,队列将把出队的元素拷贝到此缓冲区中。
参数xTicksToWait:参见xQueueSendToBack()函数的解释。
返回值:pdPASS,当成功从队列读取到元素时返回pdPASS。例如,在限定超时时间(xTicksToWait非0)过期前,成功读取到元素。
errQUEUE_EMPTY,因为队列空而无法出队时返回errQUEUE_EMPTY。例如,在限定超时时间(xTicksToWait非0)过期后,队列一直为空而无法读取元素。
查询队列中元素个数
使用内核函数uxQueueMessagesWaiting()来获取队列中有多少个元素。
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue );
参数xQueue:队列的句柄
返回值:队列中的元素个数,返回0代表队列为空
处理来自多个数据源的队列数据
一个队列可以有多个向里写入数据的任务,也可以由多个从中读取数据的任务。 实际开发中,更常见的情况是一个或多个任务向队列中写入数据,另外一个任务从队列中读取数据。
有时候我们会遇到这样一种设计需求:只使用单个队列,一个任务作为队列的reader Task,多个任务作为队列的writer Task,那么reader Task如何区分数据的来源呢?毕竟来自不同数据源的数据有不同的意义,需要使用不同的解析手段。
这里介绍一种简单易行的办法。当 reader Task向队列中存放数据时,每个数据元素都附加一个数据来源ID字段,来标识数据是来自哪个任务,或者说具有何种意义的。如下图所示:
其中UART Task,AnOther Task和Keyboard Task都是writer Task,Process Task是reader Task。队列中来自UART Task的数据用数字1标识,来自AnOther Task的数据用数字2标识,来自Keyboard Task的数据用数字3标识。Process Task从队列中读取一个元素时,根据其数据源ID的不同,判断出数据的来源,并做出不同的处理动作。
使用队列存储大型数据元素
当需要存储在队列中的元素占用的字节数比较大时,最好在队列中存储指向这些元素所在空间的指针,而非数据元素本身,这样更加高效,也节省单片机SRAM,因为FreeRTOS的队列采用的是Queue by copy存储策略的。但是要注意一些细节。
- 要避免两个或者多个任务同时通过队列中的指针来修改指针指向的大型数据,这会导致内存数据不一致,而发生不被期望的结果。正确的做法是,writer Task应该准备好这个大型数据,在他没把这个大块数据的指针放入到队列中前, 他可以任意读写。但当他将大型数据的地址入队,随后就不要通过指针访问这个大型数据了。而reader Task只有在从队列中取出指针后,才能通过这个指针来访问(读/写)它指向的大型数据。
- 如果指针元素指向的大型数据内存是动态分配的,则必须有一个任务要负责释放这个内存,并且到内存被释放后,要保证没不会再访问这个无效的内存。
- 位于任务的栈空间或者其他函数的栈空间的数据,不能通过这种方式入队,因为他们可能会随着栈帧的变化而失效。也就是说,指针指向的大型数据应该存放在FreeRTOS heap中,或者是用户自定义的全局变量。
处理来自多个数据源的不定类型数据
如果把指针和数据来源类型标识结合起来使用,就可以实现reader Task用单个队列接收来自多个writer Task的不定类型不定长度的数据了。FreeRTOS的TCP扩展库就是一个例子。
TCP Task通过队列接收来自多个其他任务的事件,事件被封装为一个队列元素,队列元素包含了事件类型和事件携带的数据,这个数据是通过void*类型指针传输的,所以数据可以是任意类型和任意长度的。TCP Task从队列接收到事件后,根据事件类型,做出不同的解析。
//定义事件类型枚举
typedef enum
{
eNetworkDownEvent = 0,
eNetworkRxEvent,
eTCPAcceptEvent,
} eIPEvent_t;
//定义队列元素类型
typedef struct IP_TASK_COMMANDS
{
eIPEvent_t eEventType; //事件类型
void *pvData; //事件携带的数据
} IPStackEvent_t;
//事件发送者,queue writer
void vSendAcceptRequestToTheTCPTask( Socket_t xSocket )
{
IPStackEvent_t xEventStruct;
xEventStruct.eEventType = eTCPAcceptEvent; //指定事件类型
xEventStruct.pvData = ( void * ) xSocket; //事件携带的数据
xSendEventStructToIPTask( &xEventStruct ); //将事件发送到队列
}
//TCP Task,queue reader Task
xQueueReceive( xNetworkEventQueue, &xReceivedEvent, xNextIPSleep ); //从队列取元素
switch( xReceivedEvent.eEventType ) //根据元素的事件类型,做出相应的数据的解析
{
case eTCPAcceptEvent:
xSocket = ( FreeRTOS_Socket_t * ) ( xReceivedEvent.pvData );
xTCPCheckNewClient( pxSocket );
break;
}
接收来自多个队列的数据
忠告:一个任务接收来着多个队列的数据,这种设计模式在FreeRTOS中是不优雅的,不高效的,是不被推荐的,应该尽量让一个任务只接收来自一个队列的数据。如果应用程序迫不得已,必须让某一个任务接收来自多个队列的消息,那么可以使用队列集合来实现这种需求。
队列集合可以实现接收来自多个队列的数据,同时不需要开发者编写代码挨个查询这些目标队列是否有元素可读。此内容详见FreeRTOS官方手册。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)