队列是一种任务到任务、任务到中断、中断到任务数据交流的一种机制。在队列中可以存储数量有限、大小固定的多个数据,队列中的每一个数据叫做队列项目,队列能够存储队列项目的最大数量称为队列的长度,在创建队列的时候,就需要指定所创建队列的长度及队列项目的大小。因为队列是用来在任务与任务或任务于中断之间传递消息的一种机制,因此队列也叫做消息队列。
基于队列, FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、 递归互斥信号量。
一、消息队列基础
1. 数据存储
队列通常采用 FIFO(先进先出)的存储缓冲机制,当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据。但同时 FreeRTOS的队列也支持将数据写入到队列的头部, 并且还可以指定是否覆盖先前已经在队列头部的数据。
2. 多任务访问
队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。
3. 队列读取阻塞
在任务从队列读取消息时,可以指定一个阻塞超时时间。如果任务在读取队列时,队列为空,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列中有可用的消息。当有其他任务或中断将消息写入队列中, 因等待队列而阻塞任务将会被添加到就绪态任务列表中,并读取队列中可用的消息。如果任务因等待队列而阻塞的时间超过指定的阻塞超时时间,那么任务也将自动被转移到就绪态任务列表中,但不再读取队列中的数据。
因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。
4. 队列写入阻塞
与队列读取一样,在任务往队列写入消息时,也可以指定一个阻塞超时时间。如果任务在写入队列时,队列已经满了,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列有空闲的位置可以写入消息。指定的阻塞超时时间为任务阻塞的最大时间,如果在阻塞超时时间到达之前,队列有空闲的位置,那么队列写入阻塞任务将会解除阻塞,并往队列中写入消息,如果达到指定的阻塞超时时间,队列依旧没有空闲的位置写入消息,那么队列写入阻塞任务将会自动转移到就绪态任务列表中,但不会往队列中写入消息。
因为同一个队列可以被多个任务写入, 因此可有会有多个任务因等待统一个任务,而被阻塞,在这种情况下,如果队列中有空闲的位置,那么也之后一个任务会被解除阻塞并往队列中写入消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列写入阻塞任务。
5. 队列操作
下面简单介绍一下队列操作的过程,包括创建队列、往队列中写入消息、从队列中读取消息等操作。
(1) 创建队列:创建了一个用于任务 A 与任务 B 之间“沟通交流”的队列,这个队列最大可容纳 5 个队列项目,即队列的长度为 5。刚创建的队列是不包含内容的,因此这个队列为空。
(2) 往队列写入第一个消息:任务 A 将一个私有变量写入队列的尾部。由于在写入队列之前,队列是空的,因此新写入的消息,既是是队列的头部,也是队列的尾部。
(3) 往队列中写入第二个消息:任务 A 改变了私有变量的值,并将新值写入队列。现在队列中包含了队列 A写入的两个值,其中第一个写入的值在队列的头部,而新写入的值在队列的尾部。 这时队列还有 3 个空闲的位置。
(4) 从队列读取第一个消息:任务 B 从队列中读取消息,任务 B 读取的消息是处于队列头部的消息,这是任务 A 第一次往队列中写入的消息。在任务 B 从队列中读取消息后,队列中任务 A 第二次写入的消息,变成了队列的头部,因此下次任务 B 再次读取消息时,将读取到这个消息。此时队列中剩余 4 个空闲的位置。
二、队列 API 函数
1. 队列结构体
队列的结构体为 Queue_t,在 queue.c 文件中有定义,具体的代码如下所示:
typedef struct QueueDefinition
{
int8_t *pcHead; /* 存储区域的起始地址 */
int8_t *pcWriteTo; /* 下一个写入的位置 */
/* 信号量是由队列实现的,
* 此结构体能用于队列和信号量,
* 当用于队列时,使用联合体中的 xQueue,
* 当用于信号量时,使用联合体中的 xSemaphore
*/
union
{
QueuePointers_t xQueue;
SemaphoreData_t xSemaphore;
} u;
List_t xTasksWaitingToSend; /* 写入阻塞任务列表 */
List_t xTasksWaitingToReceive; /* 读取阻塞任务列表 */
volatile UBaseType_t uxMessagesWaiting; /* 非空闲项目的数量 */
UBaseType_t uxLength; /* 队列的长度 */
UBaseType_t uxItemSize; /* 队列项目的大小 */
/* 锁用于在任务因队列操作被阻塞前,防止中断或其他任务操作队列。
* 上锁期间,队列可以写入和读取消息,但不会操作队列阻塞任务列表,
* 当有消息写入时, cTxLock 加 1,当有消息被读取时, cRxLock 加 1,
* 在解锁时,会统一处理队列的阻塞任务列表
*/
volatile int8_t cRxLock; /* 读取上锁计数器 */
volatile int8_t cTxLock; /* 写入上锁计数器 */
/* 同时启用了静态和动态内存管理 */
#if ((configSUPPORT_STATIC_ALLOCATION == 1) && \
(configSUPPORT_DYNAMIC_ALLOCATION == 1))
uint8_t ucStaticallyAllocated; /* 静态创建标志 */
#endif
/* 此宏用于使能启用队列集 */
#if (configUSE_QUEUE_SETS == 1)
struct QueueDefinition *pxQueueSetContainer; /* 指向队列所在队列集 */
#endif
/* 此宏用于使能可视化跟踪调试 */
#if (configUSE_TRACE_FACILITY == 1)
/* 仅用于调试,不用理会 */
UBaseType_t uxQueueNumber;
/* 队列的类型
* 0: 队列或队列集
* 1: 互斥信号量
* 2: 计数型信号量
* 3: 二值信号量
* 4: 可递归信号量
*/
uint8_t ucQueueType;
#endif
} xQUEUE;
/* 重定义成 Queue_t */
typedef xQUEUE Queue_t;
FreeRTOS 基于队列实现了互斥信号量和递归互斥信号量功能,在队列的结构体中,就包含了一个联合体 u,当队列结构体用作队列时,使用联合体 u 中的 xQueue,其数据类型为 QueuePointers_t,在 queue.c 文件中有定义,具体的代码如下所示:
typedef struct QueuePointers
{
int8_t *pcTail; /* 存储区域的结束地址 */
int8_t *pcReadFrom; /* 最后一次读取队列的位置 */
} QueuePointers_t;
而当队列结构体用于互斥信号量和递归互斥信号量时,则是使用联合体 u 中的xSemaphore,其数据类型为 SemaphoreData_t,在 queue.c 文件中有定义,具体的代码如下所示:
typedef struct SemaphoreData
{
TaskHandle_t xMutexHolder; /* 互斥信号量的持有者 */
UBaseType_t uxRecursiveCallCount; /* 递归互斥信号量被递归获取计数器 */
} SemaphoreData_t;
2. 创建队列
FreeRTOS 中用于创建队列的 API 函数如下表所示:
函数 | 描述 |
xQueueCreate() | 动态方式创建队列 |
xQueueCreateStatic() | 静态方式创建队列 |
(1)函数 xQueueCreate()
此函数用于使用动态方式创建队列,队列所需的内存空间由 FreeRTOS 从 FreeRTOS 管理的堆中分配。函数 xQueueCreate()实际上是一个宏定义,在 queue.h 文件中有定义,具体的代码如下所示:
#define xQueueCreate(uxQueueLength, \
uxItemSize) \
xQueueGenericCreate((uxQueueLength), \ //队列长度
(uxItemSize), \ //队列项目的大小
(queueQUEUE_TYPE_BASE))
函数 xQueueCreate()的返回值,如下表所示:
NULL 队列创建失败
其他值 队列创建成功,返回队列的起始地址
函数 xQueueCreate()实际上是调用了函数 xQueueGenericCreate(),函数xQueueGenericCreate()用于使用动态方式创建指定类型的队列,前面说 FreeRTOS 基于队列实现了多种功能,每一种功能对应一种队列类型,队列类型的 queue.h 文件中有定义,具体的代码如下所示:
#define queueQUEUE_TYPE_BASE ((uint8_t)0U) /* 队列 */
#define queueQUEUE_TYPE_SET ((uint8_t)0U) /* 队列集 */
#define queueQUEUE_TYPE_MUTEX ((uint8_t)1U) /* 互斥信号量 */
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ((uint8_t)2U) /* 计数型信号量 */
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ((uint8_t)3U) /* 二值信号量 */
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ((uint8_t)4U) /* 递归互斥信号量 */
函数 xQueueGenericCreate()在 queue.c 文件中有定义,具体的代码如下所示:
QueueHandle_t xQueueGenericCreate(
const UBaseType_t uxQueueLength, /* 队列长度 */
const UBaseType_t uxItemSize, /* 队列项目的大小 */
const uint8_t ucQueueType) /* 队列类型 */
{
Queue_t *pxNewQueue = NULL;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
/* 队列长度大于 0 才有意义
* 检查参数设置
*/
if ((uxQueueLength > (UBaseType_t)0) &&
((SIZE_MAX / uxQueueLength) >= uxItemSize) &&
((SIZE_MAX - sizeof(Queue_t)) >= (uxQueueLength * uxItemSize)))
{
/* 计算队列存储空间需要的字节大小 */
xQueueSizeInBytes = (size_t)(uxQueueLength * uxItemSize);
/* 为队列申请内存空间
* 队列控制块+队列存储区域
*/
pxNewQueue = (Queue_t *)pvPortMalloc(sizeof(Queue_t) +
xQueueSizeInBytes);
/* 内存申请成功 */
if (pxNewQueue != NULL)
{
/* 获取队列存储区域的起始地址 */
pucQueueStorage = (uint8_t *)pxNewQueue;
pucQueueStorage += sizeof(Queue_t);
/* 此宏用于启用支持静态内存管理 */
#if (configSUPPORT_STATIC_ALLOCATION == 1)
{
/* 标记此队列为非静态申请内存 */
pxNewQueue->ucStaticallyAllocated = pdFALSE;
}
#endif
/* 初始化队列 */
prvInitialiseNewQueue(uxQueueLength,
uxItemSize,
pucQueueStorage,
ucQueueType,
pxNewQueue);
}
else
{
/* 用于调试,不用理会 */
traceQUEUE_CREATE_FAILED(ucQueueType);
mtCOVERAGE_TEST_MARKER();
}
}
else
{
configASSERT(pxNewQueue);
mtCOVERAGE_TEST_MARKER();
}
return pxNewQueue;
}
函数 xQueueGenericCreate()主要负责为队列申请内存,然后调用函数prvInitialiseNewQueue()对队列进行初始化,函数 prvInitialiseNewQueue()在 queue.c 文件中有定义。
(2)函数 xQueueCreateStatic()
此函数用于使用静态方式创建队列,队列所需的内存空间需要由用户手动分配并提供。函数 xQueueCreateStatic()实际上是一个宏定义,在 queue.h 文件中有定义,具体的代码如下所示:
#define xQueueCreateStatic(uxQueueLength, \
uxItemSize, \
pucQueueStorage, \
pxQueueBuffer) \
xQueueGenericCreateStatic((uxQueueLength), \
(uxItemSize), \
(pucQueueStorage), \
(pxQueueBuffer), \
(queueQUEUE_TYPE_BASE))
函数 xQueueCreateStatic()的形参描述,如下表所示:
形参 | 描述 |
uxQueueLength | 队列长度 |
uxItemSize | 队列项目的大小 |
pucQueueStorage | 队列存储区域的起始地址 |
pxQueueBuffer | 静态队列结构体 |
函数 xQueueCreateStatic()的返回值,如下表所示:
返回值 | 描述 |
非 NULL | 队列创建成功,返回队列的起始地址 |
函数 xQueueCreateStatic()实际上是调用了函数 xQueueGenericCreateStatic(),函数xQueueGenericCreateStatic()用于使用静态方式创建指定类型的队列。要注意的是,函数xQueueCreateStatic()的入参 pxQueueBuffer 的数据类型为 StaticQueue_t*,结构体StaticQueue_t本质上与前面将的队列结构体 Queue_t 是一样的,区别在于 Queue_t 是在 queue.c 文件中定义的,属于 FreeRTOS 的内部结构体,对于数据隐藏策略而言,用户在应用程序开发时,是无法访问 FreeRTOS 内部使用的结构体的,但是使用静态方式创建队列时,需要根据队列结构体的大小来分配内存,因此用户需要在不访问队列结构体的前提下,确定队列结构体的大小,因此FreeRTOS 在 Free RTOS.h 文件中提供了 StaticQueue_t 结构体。
3. 队列写入消息
FreeRTOS 中用于往队列中写入消息的 API 函数如下表所示:
xQueueSend() // 往队列的尾部写入消息
xQueueSendToBack() // 同 xQueueSend()
xQueueSendToFront() // 往队列的头部写入消息
xQueueOverwrite() // 覆写队列消息(只用于队列长度为 1 的情况)
xQueueSendFromISR() // 在中断中往队列的尾部写入消息
xQueueSendToBackFromISR() // 同 xQueueSendFromISR()
xQueueSendToFrontFromISR() // 在中断中往队列的头部写入消息
xQueueOverwriteFromISR() // 在中断中覆写队列消息(只用于队列长度为 1 的情况)
4. 队列读取消息
FreeRTOS 中用于从队列中读取消息的 API 函数如下表所示:
xQueueReceive() //从队列头部读取消息,并删除消息
xQueuePeek() //从队列头部读取消息
xQueueReceiveFromISR() //在中断中从队列头部读取消息,并删除消息
xQueuePeekFromISR() //在中断中从队列头部读取消息
5. 队列锁
讲解队列操作的函数时,提到了队列的上锁与解锁,通过在队列的结构提供,也包含了队列读取上锁计数器和队列写入上锁计数器。在队列被上锁后,可以往队列中写入消息和读取消息,但是队列消息的读取和写入不会影响到队列读取和写入阻塞任务列表中的任务阻塞,队列的写入和读取阻塞任务列表会在队列解锁后,统一处理。
队列上锁的函数为 prvLockQueue(),函数 prvLockQueue()实际上是一个宏定义,在 queue.c文件中有定义,具体的代码如下所示:
#define prvLockQueue(pxQueue) \
taskENTER_CRITICAL(); \
{ \
if ((pxQueue)->cRxLock == queueUNLOCKED) \
{ \
(pxQueue)->cRxLock = queueLOCKED_UNMODIFIED; \
} \
if ((pxQueue)->cTxLock == queueUNLOCKED) \
{ \
(pxQueue)->cTxLock = queueLOCKED_UNMODIFIED; \
} \
} \
taskEXIT_CRITICAL()
队列结构体中的 cRxLock 和 cTxLock 成员变量就是队列的读取和写入上锁计数器,这两个成员变量用来表示队列的上锁状态。队列解锁的函数为 prvUnlockQueue(),函数 prvUnlockQueue()实际上是一个宏定义,在queue.c 文件中有定义,具体的代码如下所示:
static void prvUnlockQueue(Queue_t *const pxQueue)
{
/* 进入临界区 */
taskENTER_CRITICAL();
{
/* 获取队列的写入上锁计数器 */
int8_t cTxLock = pxQueue->cTxLock;
/* 判断队列在上锁期间是否被写入消息 */
while (cTxLock > queueLOCKED_UNMODIFIED)
{
/* 此宏用于使能队列集 */
#if (configUSE_QUEUE_SETS == 1)
{
/* 判断队列是否存在队列集 */
if (pxQueue->pxQueueSetContainer != NULL)
{
/* 通知队列集 */
if (prvNotifyQueueSetContainer(pxQueue) != pdFALSE)
{
/* 根据需要进行任务切换 */
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 队列不存在队列集 */
else
{
/* 判断队列的读取阻塞任务列表是否不为空 */
if (listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToReceive)) ==
pdFALSE)
{
/* 将读取阻塞任务列表中的任务解除阻塞 */
if (xTaskRemoveFromEventList(
&(pxQueue->xTasksWaitingToReceive)) !=
pdFALSE)
{
/* 根据需要进行任务切换 */
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
break;
}
}
}
#else
/* 未使能队列集 */
{
/* 判断队列的读取阻塞任务列表是否不为空 */
if (listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToReceive)) ==
pdFALSE)
{
/* 将读取阻塞任务列表中的任务解除阻塞 */
if (xTaskRemoveFromEventList(
&(pxQueue->xTasksWaitingToReceive)) !=
pdFALSE)
{
/* 根据需要进行任务切换 */
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
break;
}
}
#endif
/* 处理完一个读取阻塞任务后,
* 更新队列写入上锁计数器,
* 直到写入解锁为止
*/
--cTxLock;
}
/* 设置队列写入解锁 */
pxQueue->cTxLock = queueUNLOCKED;
}
/* 退出临界区 */
taskEXIT_CRITICAL();
/* 进入临界区 */
taskENTER_CRITICAL();
{
/* 获取队列的读取上锁计数器 */
int8_t cRxLock = pxQueue->cRxLock;
/* 判断队列在上锁期间是否被读取消息 */
while (cRxLock > queueLOCKED_UNMODIFIED)
{
/* 判断队列的写入阻塞任务列表是否不为空 */
if (listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToSend)) ==
pdFALSE)
{
/* 将写入阻塞任务列表中的任务解除阻塞 */
if (xTaskRemoveFromEventList(
&(pxQueue->xTasksWaitingToSend)) !=
pdFALSE)
{
/* 根据需要进行任务切换 */
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 处理完一个写入阻塞任务后,
* 更新队列读取上锁计数器,
* 直到读取解锁位置
*/
--cRxLock;
}
else
{
break;
}
}
/* 设置队列读取解锁 */
pxQueue->cRxLock = queueUNLOCKED;
}
/* 退出临界区 */
taskEXIT_CRITICAL();
}