队列简介
队列是一种任务到任务,任务到中断,中断到任务数据交流得一种机制。在队列中可以存储数量有限,大小固定得多个数据,队列中的每一个数据叫做队列项目,队列能够存储队列项目的最大数量称为队列的长度,在创建队列的时候就需要指定所创建的队列的长度以及队列项目的大小。因为队列是用来在任务与任务或任务与中断之间传递消息的一种机制,因此队列也叫做消息队列。
数据存储
队列通常采用FIFO(先进先出)的存储缓冲机制,当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据。但同时FreeRTOS的队列也支持将数据写入到队列的头部,并且可以指定是否覆盖先前已经在队列头部的数据(对应3种写入数据的方式)。
多任务访问
队列不属于某个特定的任务,可以在任何的任务或中断种往队列中写入消息,或者从队列中读取消息。
队列读取阻塞
在任务从队列读取消息时,可以指定一个阻塞超时时间。如果任务在读取队列时,队列为空,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列中有可用消息。当有其他任务或中断将消息写入队列中,因等待队列而阻塞任务将会被添加到就绪态任务列表中,并读取队列中可用的消息。如果任务因等待队列而阻塞超时时间超过指定的阻塞超时时间,那么任务也将自动被转移到就绪态任务列表中,但不再读取队列中的数据。
因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。
队列写入阻塞
与队列读取一样,在任务往队列写消息时,也可以指定一个阻塞超时时间。如果任务在写入队列时,队列已经满了,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列有空闲的位置可以写入消息。指定的阻塞超时时间为任务阻塞的最大时间,如果在阻塞超时时间到达之前,队列有空闲的位置,那么队列写入阻塞任务将会解除阻塞,并往队列中写入消息,如果达到指定的阻塞超时时间,队列依旧没有空闲的位置写入消息,那么队列写入阻塞任务将会自动转移到就绪态任务列表上,但不会往队列中写入消息。
因为队列可以被多个任务写入,因此可能会有多个任务因等待同一个任务,而被阻塞,在这种情况下,如果队列中有空闲的位置,那么也只有一个任务会被解除阻塞态并往队列中写入消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列写入阻塞任务。
队列的特性
常规特性
队列的简化操作如下图所示,从图中可以知道:
队列可以包含若干个数据,队列中有若干项,这被称为“长度(length)”
每个数据大小固定
创建队列时就要指定长度,数据大小
数据的操作采用先进先出的方法(FIFO,First In First Out);写数据时放到尾部,读数据时从头部读
也可以强制写队列头部,覆盖头部数据

更详细的操作如下图所示:

传输数据的两种方法
使用队列传输数据时有两种方法:
拷贝:把数据、把变量的值复制进队列里
引用:把数据,把变量的地址复制进队列里
FreeRTOS传输小数据的时候,可以采用值传递,传输大数据时采用引用的方式
局部变量的值可以发送到队列里,后续即使函数退出,局部变量被回收,也不会影响队列中的数据
无需分配buffer来保存数据,队列中有buffer
局部变量马上可以再次使用
发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
如果数据实在太大,你还是可以使用队列传输它的地址
队列的空间有FreeRTOS内核分配,无需任务操心
对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。
队列的阻塞访问
只要知道队列的句柄,谁都可以读,写该队列。任务,ISR都可读,写队列。可以多个任务读写队列。
任务读写队列时,简单地说:如果读写不成功,则阻塞,可以指定超时时间。口语化的说,就是说可以定个闹钟;如果能读写了就马上进入就绪态,否则就阻塞到超时。
某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞态;还可以指定阻塞时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。
既然读取 队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞态,有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?
优先级最高的任务
如果大家优先级相同,那等待时间最久的任务会进入就绪态
跟读队列时类似,一个任务要写队列时,如果队列满了,该任务可以进入阻塞态;还可以指定阻塞的时间,如果队列有空间了,则该阻塞的任务就会变为就绪态。如果一直都没有空间,则时间到之后它会进入就绪态。
既然写队列的任务个数没有限制,那么当多个任务写“满队列”时,这些任务都会进入阻塞态,有多个任务在等待同一个队列的空间,当队列中有空间时,哪个任务会进入就绪态?
优先级最高的任务
如果大家优先级相同,那等待时间最久的任务会进入就绪态
队列结构体
typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
int8_t * pcHead; /*< 存储区域的起始地址 */
int8_t * pcWriteTo; /*< 下一个写入的位置 */
union
{
QueuePointers_t xQueue; /*< Data required exclusively when this structure is used as a queue. */
SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */
} u;
List_t xTasksWaitingToSend; /*< 等待发送列表 */
List_t xTasksWaitingToReceive; /*< 等待接收列表 */
volatile UBaseType_t uxMessagesWaiting; /*< 非空闲队列项目的数量/
UBaseType_t uxLength; /*< 队列长度 */
UBaseType_t uxItemSize; /*< 队列项目的大小 */
volatile int8_t cRxLock; /*< 读取上锁计数器*/
volatile int8_t cTxLock; /*< 写入上锁计数器 */
#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
#endif
#if ( configUSE_QUEUE_SETS == 1 )
struct QueueDefinition * pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
/* The old xQUEUE name is maintained above then typedefed to the new Queue_t
* name below to enable the use of older kernel aware debuggers. */
typedef xQUEUE Queue_t;

队列相关的API函数
使用队列的流程:创建队列,写队列,读队列、删除队列
创建
队列的创建有两种方法:动态内存分配,静态内存分配
动态内存分配:xQueueCreate,队列的内存在函数内部动态分配
函数原型如下:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 | 说明 |
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,内存不足 |
静态内存分配:xQueueCreateStatic,队列的内存要实现分配好
函数原型如下:
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorage,
StaticQueue_t *pxQueueBuffer
);
参数 | 说明 |
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:字节为单位 |
pucQueueStorag | 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组, 此数组大小至少为"uxQueueLength * uxItemSize" |
pxQueueBuffer | 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL |
示例代码:
// 示例代码
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint32_t )
// xQueueBuffer用来保存队列结构体
StaticQueue_t xQueueBuffer;
// ucQueueStorage 用来保存队列的数据
// 大小为:队列长度 * 数据大小
uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];
void vATask( void *pvParameters )
{
QueueHandle_t xQueue1;
// 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小是ITEM_SIZE
xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
ITEM_SIZE,
ucQueueStorage,
&xQueueBuffer );
}
复位
队列刚刚被创建时,里面没有数据;使用过程中可以调用xQueueReset()把队列恢复为初始状态,此函数原型为:
* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
删除
删除队列的函数为vQueueDelete(),只能删除使用动态方法创建的队列,它会释放内存。原型如下:
void vQueueDelete( QueueHandle_t xQueue );
写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
这些函数用到的参数是类似的,统一说明如下:
参数 | 说明 |
xQueue | 队列句柄,要写哪个队列 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了 |
读队列
使用xQueueReceive()函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
参数说明如下:
参数 | 说明 |
xQueue | 队列句柄,要读哪个队列 |
pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。 |
查询
可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
覆盖/偷看
当队列长度为1时,可以使用xQueueOverwrite()或xQueueOverwriteFromISR()来覆盖数据。 注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也以意味着这些函数不会被阻塞。 函数原型如下:
/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void * pvItemToQueue
);
BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥视",也就是xQueuePeek()或xQueuePeekFromISR()。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。 函数原型如下:
/* 偷看队列
* xQueue: 偷看哪个队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 没有数据的话阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);
总结
首先,队列的本质是任务与任务,任务与中断之间的一种数据交流的机制,队列的内存空间由两部分组成,一部分是队列结构体,一部分是队列存储区域,可以详细见队列结构体的图,对于理解队列的源码有很大帮助。使用队列的流程是:创建队列,写队列,读队列,删除队列,其中,队列是消耗性资源,一次只能唤醒一个阻塞的任务。
创建队列:主要负责为队列申请内存,然后调用函数prvInitialiseNewQueue()对队列进行初始化,也即初始化队列结构体的成员变量,其中还会对队列进行重置。
写队列:函数xQueueSend()等写队列函数都是调用xQueueGenericSend()函数,只是带入的参数不同,指定了不同的写入位置。
读队列:有两种,一种最常用的就是,读取队列项之后会删除队列项,”偷看“则不会删除队列项,二者都是从队列头开始读数据。