简介
队列是为了任务与任务、任务与通信之间准备的
先进先出(FIFO)的存储缓冲机制
数据发送到队列中会导致数据拷贝,在队列中存储的是数据的原始值,值传递
freeRTOS中的使用一个结构体来描述队列
队列操作过程
- 创建队列
task A 要向task B 发送消息,内容为变量x的值。首先创建一个队列,指定长度与大小,这里创建一个大小为4,长度为sizeof(int)的队列。 - 发送第一个数据
task A 中的变量x=10发送到队列中,此时队列长度为3,因为数据发送到队列的方式是值传递,那么x的值就可以再次使用,赋其他的值。 - 发送第二个数据
task A 继续发送变量x的值到队列,此时队列的长度为2。 - 从队列中读取消息
task B 从队列中读取消息,并将读取到的消息的值赋给 y,task B 从队列中读取到消息之后可以选择清除消息或者不清除。当清除了这个消息的时候其他任务或中断就不能获取这个消息了,并且队列的剩余大小就会加1,如果不清除的话,其他任务或中断也可以获取这个消息。
队列结构体
队列有一个重要的结构体 Queue_t,用来描述队列,此结构体在 queue.c 中定义如下:
typedef struct QueueDefinition
{
int8_t *pcHead; // 指向队列存储区开始地址
int8_t *pcTail; // 指向队列存储区最后一个字节
int8_t *pcWriteTo; // 指向存储区中下一个空闲区域
union
{
int8_t *pcReadFrom; //当用作队列的时候指向最后一个出队的队列项首地址
UBaseType_t uxRecursiveCallCount; //当用作递归互斥量的时候用来记录递归互斥量被调用的次数
} u;
List_t xTasksWaitingToSend; //等待发送任务列表,那些因为队列满导致入队失败而进入阻塞态的任务就会挂到此列表上
List_t xTasksWaitingToReceive; //等待接收任务列表,那些因为队列空导致出队失败而进入阻塞态的任务就会挂到此列表上
volatile UBaseType_t uxMessagesWaiting; //队列中当前队列项数量,也就是消息数
UBaseType_t uxLength; //创建队列时指定的队列长度,也就是队列中最大允许的队列项(消息)数量
UBaseType_t uxItemSize; //创建队列时指定的每个队列项(消息)最大长度,单位字节
volatile int8_t cRxLock; //队列上锁以后统计从队列中接收到的队列项数量(出队的队列项数量),无锁时为 queueUNLOCKED
volatile int8_t cTxLock; //队列上锁以后统计发送到队列中的队列项数量(入队的队列项数量),无锁时为 queueUNLOCKED
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) &&( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated;//如果使用静态存储的话此字段设置为 pdTURE
#endif
#if ( configUSE_QUEUE_SETS == 1 ) //队列集相关宏
struct QueueDefinition *pxQueueSetContainer;
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关宏
UBaseType_t uxQueueNumber;
uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;
// 老版本的 FreeRTOS 中队列可能会使用 xQUEUE 这个名字,新版本 FreeRTOS 中队列的名字都使用 Queue_t
队列创建
队列的创建分为动态创建和静态创建,不同之处在于内存是否需要用户自己分配,队列的动态 / 静态创建函数分别为 xQueueCreate() 和 xQueueCreateStatic() 。 这两个函数本质上都是宏,真正完成队列创建的函数是 xQueueGenericCreate()
和 xQueueGenericCreateStatic() 来看看这两个函数的内容
xQueueGenericCreate()
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
{
Queue_t *pxNewQueue;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
configASSERT( uxQueueLength > ( UBaseType_t ) 0 );
if( uxItemSize == ( UBaseType_t ) 0 ) { // 队列项大小为 0,就不需要存储区
xQueueSizeInBytes = ( size_t ) 0;
} else {
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
}
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );
if( pxNewQueue != NULL ) // 内存申请成功
{
pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) {
pxNewQueue->ucStaticallyAllocated = pdFALSE;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
}
return pxNewQueue;
}
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节。
ucQueueType: 队列类型,由于 FreeRTOS 中的信号量等也是通过队列来实现的,创建信号量的函数最终也是使用此函数的,因此在创建的时候需要指定此队列的用途,也就是队列类型,一共有六种类型:
(1)queueQUEUE_TYPE_BASE 普通的消息队列
(2)queueQUEUE_TYPE_SET 队列集
(3)queueQUEUE_TYPE_MUTEX 互斥信号量
(4)queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
(5)queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
(6)queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
分析一下函数内容:由于队列需要存储消息,所以需要一定的存储空间。参数 uxQueueLength 和 uxItemSize 的乘积就是消息存储区的大小,如果队列项大小为 0 那就不需要存储空间。调用 pvPortMalloc 给队列分配内存,内存大小是队列结构体和消息存储区的总内存,分配成功后,计算出消息存储区的首地址。最后调用 prvInitialiseNewQueue 函数来初始化队列。函数的主要做的工作就是分配内存,成功后进行初始化。
xQueueGenericCreateStatic()
QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
uint8_t * pucQueueStorage,
StaticQueue_t * pxStaticQueue,
const uint8_t ucQueueType )
uxQueueLength: 要创建的队列的队列长度,这里是队列的项目数。
uxItemSize: 队列中每个项目(消息)的长度,单位为字节
pucQueueStorage: 指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等于(uxQueueLength * uxItemsSize)字节。
pxStaticQueue:此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
ucQueueType: 队列类型。
队列初始化函数
prvInitialiseNewQueue()用于队列的初始化
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //队列长度
const UBaseType_t uxItemSize, //队列项目长度
uint8_t * pucQueueStorage, //队列项目存储区
const uint8_t ucQueueType, //队列类型
Queue_t * pxNewQueue ) //队列结构体
{
//防止编译器报错
( void ) ucQueueType;
if( uxItemSize == ( UBaseType_t ) 0 ){ //如果队列项长度为0,没有队列存储区,这里将 pcHead 指向队列开始地址
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
} else { //设置 pcHead 指向队列项存储区首地址
pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage; ****************(1)
}
/* 初始化队列结构体相关成员变量 */
pxNewQueue->uxLength = uxQueueLength; ****************(2)
pxNewQueue->uxItemSize = uxItemSize;
( void ) xQueueGenericReset( pxNewQueue, pdTRUE ); ****************(3)
traceQUEUE_CREATE( pxNewQueue );
}
(1) 队列结构体中的成员变量 pcHead 指向队列存储区中首地址。
(2) 初始化队列结构体中的成员变量 uxQueueLength 和 uxItemSize,这两个成员变量保存队列的最大队列项目和每个队列项大小。
(3) 调用函数 xQueueGenericReset()复位队列。
队列复位函数 xQueueGenericReset()
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
configASSERT( pxQueue );
taskENTER_CRITICAL();
{
//初始化队列相关成员变量
pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize ); ************(1)
pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;
pxQueue->pcWriteTo = pxQueue->pcHead;
pxQueue->u.pcReadFrom = pxQueue->pcHead+((pxQueue->uxLength-(UBaseType_t)1U)*pxQueue->uxItemSize);
pxQueue->cRxLock = queueUNLOCKED;
pxQueue->cTxLock = queueUNLOCKED;
if( xNewQueue == pdFALSE ) { **************************(2)
//由于复位队列以后队列依旧是空的,所以对于那些由于出队(从队列中读取消息)而阻塞的任务就依旧保持阻塞壮态。但是对于那些由
//于入队(向队列中发送消息)而阻塞的任务就不同了,这些任务要解除阻塞壮态,从队列的相应列表中移除。
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ){
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){
queueYIELD_IF_USING_PREEMPTION();
}
else{
mtCOVERAGE_TEST_MARKER();
}
}
else{
mtCOVERAGE_TEST_MARKER();
}
} else { //初始化队列中的列表
vListInitialise( &( pxQueue->xTasksWaitingToSend ) ); ********************** (3)
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );}
}
taskEXIT_CRITICAL();
return pdPASS;
}
(1)、初始化队列中的相关成员变量。
(2)、根据参数 xNewQueue 确定要复位的队列是否是新创建的队列,如果不是的话还需要做其他的处理
(3)、 初始化队列中的列表 xTasksWaitingToSend 和 xTasksWaitingToReceive。
至此一个队列被成功创建,以 4个队列项,每个队列项的长度为 32字节队列为例,其内容如下:
向队列发送消息(入队)
- 任务级入队函数最终都调用了一个
xQueueGenericSend()
函数(真…复杂昂)
参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。
pvItemToQueue:指向要发送的消息,发送时候会将这个消息拷贝到队列中。
xTicksToWait: 阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大时间。如果为 0 的话当队列满的时候就立即返回; 当为 portMAX_DELAY 的话就会一直等待,直到队列有空闲的队列 项,也就是死等,但是宏INCLUDE_vTaskSuspend 必须为 1。
xCopyPosition: 入队方式,有三种入队方式:
queueSEND_TO_BACK: 后向入队
queueSEND_TO_FRONT: 前向入队
queueOVERWRITE: 覆写入队
BaseType_t xQueueGenericSend(QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait,
const BaseType_t xCopyPosition)
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired; TimeOut_t xTimeOut;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
for( ;; ){
taskENTER_CRITICAL();
{
/* 比较当前消息数与队列长度,检查队列是否满,当队列未满或者是覆写入队的话就可以将消息入队了 */
if((pxQueue->uxMessagesWaiting < pxQueue->uxLength) || ( xCopyPosition == queueOVERWRITE ) )
{
traceQUEUE_SEND( pxQueue );
/* 调用函数 prvCopyDataToQueue()将消息拷贝到队列中 后向入队、前向入队和覆写入队 */
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
#if ( configUSE_QUEUE_SETS == 1 )
{ /* 省略与队列集有关代码 */}
#else /* configUSE_QUEUE_SETS */ {
/* 检查是否有任务由于等待消息进入阻塞态,阻塞任务会挂在 xTasksWaitingToReceive 列表上 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){
/* 有任务由于请求消息而阻塞 */
if(xTaskRemoveFromEventList(&(pxQueue->xTasksWaitingToReceive)) != pdFALSE ){
/* 调用函数 xTaskRemoveFromEventList()将阻塞的任务从列表 xTasksWaitingToReceive 上移除,并且
把 prvCopyDataToQueue() 的任务添加到就绪列表中。如果取消阻塞的任务优先级比当前正在运行的任务优
先级高还要标记需要进行任务切换 */
queueYIELD_IF_USING_PREEMPTION();
}else{mtCOVERAGE_TEST_MARKER();}
}
else if( xYieldRequired != pdFALSE ){
queueYIELD_IF_USING_PREEMPTION();
}else{mtCOVERAGE_TEST_MARKER();}
}
#endif /* configUSE_QUEUE_SETS */
taskEXIT_CRITICAL();
return pdPASS; /* 返回 pdPASS,标记入队成功 */
}else{ /* 队列满 */
/* 首先判断设置的阻塞时间是否为 0 */
if( xTicksToWait == ( TickType_t ) 0 ) {
taskEXIT_CRITICAL();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL; /* 阻塞时间为 0,返回 errQUEUE_FULL,标记队列满 */
}
else if( xEntryTimeSet == pdFALSE ) {
/* 如果阻塞时间不为 0 并且时间结构体还没有初始化的话就初始化一次超时结构体变量 */
/* 调用函数 vTaskSetTimeOutState()完成超时结构体变量 xTimeOut 的初始化 */
vTaskSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else {mtCOVERAGE_TEST_MARKER();}
}
}
taskEXIT_CRITICAL();
vTaskSuspendAll(); /* 任务调度器上锁, 代码执行到这里说明当前的状况是队列已满了,而且设置了不为0的阻塞时间。*/
prvLockQueue( pxQueue ); /* 队列上锁 */
/* 调用函数 xTaskCheckForTimeOut()更新超时结构体变量 xTimeOut,并且检查阻塞时间是否到了 */
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
{ /* 阻塞时间还没到,那就检查队列是否还是满的 */
if(prvIsQueueFull(pxQueue) != pdFALSE) {
/* 阻塞时间没到,而且队列依旧是满的,那就调用函数 vTaskPlaceOnEventList() 将任务添加到队列的
xTasksWaitingToSend 列表中和延时列表中,并且任务从就绪列表中移除 */
traceBLOCKING_ON_QUEUE_SEND(pxQueue);
vTaskPlaceOnEventList(&(pxQueue->xTasksWaitingToSend), xTicksToWait);
prvUnlockQueue(pxQueue);
if( xTaskResumeAll() == pdFALSE ){ /* 调用函数 xTaskResumeAll()恢复任务调度器 */
portYIELD_WITHIN_API();
}
}else{
/* 阻塞时间还没到,但是队列现在有空闲的队列项,那么就在重试一次 */
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
}
}else{
/* 阻塞时间到了!那么任务就不用添加到那些列表中了,那就解锁队列,恢复任务调度器。*/
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL; /* 返回 errQUEUE_FULL,表示队列满了 */
}
}
}
总结一下:
(1)判断队列是否满?
(2)如果队列未满,使用函数 prvCopyDataToQueue() 拷贝数据到队列
(3)检查是否有任务因为队列为空而进入阻塞态,如果有需要解除阻塞态。此功能通过函数 xTaskRemoveFromEventList () 来完成。有两种处理方式,如果任务调度器没有挂起,从相应的事件列表和状态列表中移除,并且将任务添加到就绪列表中。如果任务调度器挂起的话,不会从转态列表中移除,并将任务添加到 xPendingReadyList 列表中。当通过函数 xTaskResumeAll() 恢复任务调度器时,添加到 xPendingReadyList 列表中的任务就会被处理。
(4)如果队列满,阻塞时间为0,返回 errQUEUE_FULL
(5)如果队列满,且阻塞时间不为0,通过函数 vTaskPlaceOnEventList() 将任务添加到相应的事件列表和延时列表中
or
- 将数据拷贝到存储区中
- 判断是否有任务因为队列为空而进入阻塞态,如果有需要解除
- 如果队列为满则入队失败,入股哦设置了入队阻塞时间则将入队任务阻塞
- 中断级入队函数最后都调用了
xQueueGenericSendFromISR()
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, const void * const pvItemToQueue,
BaseType_t * const pxHigherPriorityTaskWoken, const BaseType_t xCopyPosition )
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = ( Queue_t * ) xQueue;
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
{
/* 判断队列是否已经满或者以覆写的方式入队 */
if((pxQueue->uxMessagesWaiting < pxQueue->uxLength)||(xCopyPosition == queueOVERWRITE)){
const int8_t cTxLock = pxQueue->cTxLock; /* 读取队列的成员变量 xTxLock,用于判断队列是否上锁 */
traceQUEUE_SEND_FROM_ISR(pxQueue);
(void)prvCopyDataToQueue(pxQueue,pvItemToQueue,xCopyPosition); /* 将数据拷贝到队列中 */
if( cTxLock == queueUNLOCKED ){
#if ( configUSE_QUEUE_SETS == 1 )
{}
#else /* configUSE_QUEUE_SETS */
{ /* 判断队列列表 xTasksWaitingToReceive 是否为空,如果不为空的话说明有任务在请求消息的时候被阻塞了 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){
/* 将相应的任务从列表 xTasksWaitingToReceive 上移除 */
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){
if( pxHigherPriorityTaskWoken != NULL ){
/* 如果刚刚从列表 xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高,那么标记
pxHigherPriorityTaskWoken 为 pdTRUE,表示要进行任务切换。*/
*pxHigherPriorityTaskWoken = pdTRUE;
}else{mtCOVERAGE_TEST_MARKER();}
}else{mtCOVERAGE_TEST_MARKER();}
}else{mtCOVERAGE_TEST_MARKER();}
}
#endif /* configUSE_QUEUE_SETS */
} else {
/* 如果队列上锁的话那就将队列成员变量 cTxLock +1 ,表示进行了一次入队操作,在队列解锁的时候会对其做相应的处理。*/
pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
}
xReturn = pdPASS; /* 返回 pdPASS,表示入队完成 */
} else {
traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );
xReturn = errQUEUE_FULL; /* 如果队列满的话就直接返回 errQUEUE_FULL,表示队列满 */
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
return xReturn;
}
总结一下:
(1)判断队列是否满?
(2)如果队列未满,使用函数 prvCopyDataToQueue() 拷贝数据到队列
(3)判断队列是否上锁。如果没有上锁,按照任务级入队函数同样处理。检查是否有任务因为队列为空而进入阻塞态,如果有需要解除阻塞态。此功能通过函数 xTaskRemoveFromEventList () 来完成。有两种处理方式,如果任务调度器没有挂起,从相应的事件列表和状态列表中移除,并且将任务添加到就绪列表中。如果任务调度器挂起的话,不会从转态列表中移除,并将任务添加到 xPendingReadyList 列表中。当通过函数 xTaskResumeAll() 恢复任务调度器时,添加到 xPendingReadyList 列表中的任务就会被处理。
(4)如果队列上锁,cTxLock +1,表示队列上锁期间入队消息的数量。
(5)如果队列满了,直接返回 errQUEUE_FULL,表示队列满,原因很简单,中断服务函数不任务,所以没有任务添加到列表这一个说法。
从队列读取数据(出队)
函数详细分析在第三章,互斥信号量中介绍。
队列上锁和解锁
- 上锁API函数:
prvLockQueue()
#define prvLockQueue( pxQueue ) \
taskENTER_CRITICAL(); \
{ \
if( ( pxQueue )->cRxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED; \
} \
if( ( pxQueue )->cTxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED; \
} \
} \
taskEXIT_CRITICAL()
prvLockQueue() 函数很简单,就是将队列中的成员变量 cRxLock 和 cTxLock 设置为 queueLOCKED_UNMODIFIED
- 解锁API函数:
prvUnlockQueue()
static void prvUnlockQueue( Queue_t * const pxQueue )
{
taskENTER_CRITICAL();
{
int8_t cTxLock = pxQueue->cTxLock;
while( cTxLock > queueLOCKED_UNMODIFIED ){ ---------------------(1)
#if ( configUSE_QUEUE_SETS == 1 ){
/* 省略部分代码 */
}
#else {
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive))==pdFALSE) { ------------(2)
if(xTaskRemoveFromEventList(&(pxQueue->xTasksWaitingToReceive))!= pdFALSE){ ------(3)
vTaskMissedYield(); ------------------------(4)
}else{mtCOVERAGE_TEST_MARKER();}
}else{break;}
}
#endif
--cTxLock; -------------(5)
}
pxQueue->cTxLock = queueUNLOCKED; ------------(6)
}
taskEXIT_CRITICAL();
taskENTER_CRITICAL();
{ /* Do the same for the Rx lock. */ }
taskEXIT_CRITICAL();
}
解锁函数稍复杂一点
(1) 判断是否有中断向队列发送了消息,因为队列上锁时向队列发送消息成功之后会将入队计数器 cTxLock +1
(2) 判断列表 xTasksWaitingToReceive 是否为空,如果不为空的话就要将相应的任务从列表中移除。
(3) 将任务从列表 xTasksWaitingToReceive 中移除。
(4) 如果刚刚从列表 xTasksWaitingToReceive 中移除的任务优先级比当前任务的优先级高,那么就要标记需要进行任务切换。这里调用函数 vTaskMissedYield() 来完成此任务,函数 vTaskMissedYield() 只是简单的将全局变量 xYieldPending 设置为 pdTRUE。那么真正的任务切换是在哪里完成的呢?在时钟节拍处理函数 xTaskIncrementTick()中,此函数会判断 xYieldPending
的值,从而决定是否进行任务切换
(5) 每处理完一条就将 cTxLock 减一,直到处理完所有的
(6) 当处理完以后标记 cTxLock 为 queueUNLOCKED