目录

1. 队列结构

2. 创建队列

2.1 动态创建队列

2.1.1 xQueueCreate函数

2.1.2 xQueueGenericCreate函数

2.1.3 xQueueGenericReset函数

2.2 静态创建队列

2.2.1 xQueueCreateStatic函数

2.2.2 xQueueGenericCreateStatic函数

3. 删除队列

4. 向队列发送消息

4.1 任务级入队

4.1.1 任务级入队函数

4.1.2 任务级通用入队函数

4.2 中断级入队

4.2.1 中断级入队函数

4.2.2 中断级通用入队函数

4.3 队列的上锁与解锁

4.3.1 队列上锁

4.3.2 队列解锁

5. 从队列读取消息

5.1 任务级出队

5.1.1 任务级出队函数

5.1.2 任务级通用出队函数

5.2 中断级出队

5.2.1 xQueueReceiveFromISR函数

5.2.2 xQueuePeekFromISR函数

6. 队列集实现分析

6.1 xQueueCreateSet函数实现

6.2 xQueueAddToSet函数实现

6.3 xQueueRemoveFromSet函数实现

6.4 prvNotifyQueueSetContainer函数实现

6.4.1 调用时机

6.4.2 函数实现

6.5 xQueueSelectFromSet函数实现


1. 队列结构

队列结构定义在queue.c中,目的也是实现信息隐藏

typedef struct QueueDefinition
{// 指向队列存储区起始位置int8_t *pcHead;// 指向队列存储区结束位置(最后一个有效字节的下一个字节)int8_t *pcTail;// 指向下一个队列项写入位置,即正常下一条消息的写入位置int8_t *pcWriteTo;union{// 指向下一个可读取消息的前一个位置// 即pcReadFrom + uxItemSize为下一个可读取的消息int8_t *pcReadFrom;// 用作递归互斥信号量时,记录信号量被持有的次数UBaseType_t uxRecursiveCallCount;} u;// 等待发送任务列表,TCB的事件列表项加入List_t xTasksWaitingToSend;// 等待接收任务列表,TCB的事件列表项加入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;#endif#if ( configUSE_QUEUE_SETS == 1 )// 支持队列集功能struct QueueDefinition *pxQueueSetContainer;#endif#if ( configUSE_TRACE_FACILITY == 1 )// 队列编号UBaseType_t uxQueueNumber;// 队列类型,因为FreeRTOS中信号量也通过队列实现uint8_t ucQueueType;#endif
} xQUEUE;typedef xQUEUE Queue_t;

说明1:队列使用概述

① 队列用于实现任务与任务、任务与中断之间的通信

② 队列中可以存储个数有限的,最大长度固定的消息,这些属性在创建队列时指定

③ 队列并不属于某个特定的任务,任何任务在持有句柄后,都可通过队列通信

说明2:下一个可读取位置pcReadFrom

以一个可容纳5个队列项,且已经有2条消息的队列为例,读写位置如下,

从上图可是,pcReadFrom需要先加上uxItemSize并处理绕回,才会读取到下一个可读取的消息

注意,如此实现并没有什么特别的好处,只要代码逻辑与设计意图匹配即可

说明3:队列上锁概述

① 队列上锁的概念与挂起调度器类似,实际上队列上锁都是伴随着调度器挂起使用的

② 队列上锁的目的,也是在不关闭中断的情况下在队列上进行耗时操作

③ 队列上锁要保护的资源是xTaskWaitingToSend & xTaskWaitingToReceive列表

④ 队列上锁带来的直接影响就是在ISR中不能访问被保护的资源,所以才引入了cTxLock & cRxLock计数器

⑤ 之所以在调度器挂起的基础上又引入队列上锁机制,就是因为需要对上锁期间的操作进行计数,而挂起调度器是没有这个专项功能的

2. 创建队列

2.1 动态创建队列

2.1.1 xQueueCreate函数

xQueueCreate函数只是一个宏,该宏增加一个队列类型的参数调用xQueueGenericCreate函数

#define xQueueCreate( uxQueueLength, uxItemSize ) \
xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ),\( queueQUEUE_TYPE_BASE ) )

说明:队列类型

需要区分队列类型是因为在FreeRTOS中,信号量也通过队列实现,目前支持的队列类型如下,

2.1.2 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;// 队列长度必须大于0configASSERT( uxQueueLength > ( UBaseType_t ) 0 );// 计算队列存储区长度if( uxItemSize == ( UBaseType_t ) 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;
}

说明: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指向队列结构地址// 这里不将pcHead设置为NULL// 因为将pcHead设置为NULL表示实现的是互斥信号量pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;}else{pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;}// 设置队列结构成员pxNewQueue->uxLength = uxQueueLength;pxNewQueue->uxItemSize = uxItemSize;( void ) xQueueGenericReset( pxNewQueue, pdTRUE );#if ( configUSE_TRACE_FACILITY == 1 ){// 设置队列类型pxNewQueue->ucQueueType = ucQueueType;}#endif /* configUSE_TRACE_FACILITY */
}

实际完成队列布局设置的是xQueueGenericReset函数,下面单列一节进行说明

2.1.3 xQueueGenericReset函数

该函数完成队列中队列存储区的布局设置

// xNewQueue:标识是否是reset新建列表
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 );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;// 如果不是reset新建队列,需要处理当前的xTaskWaitingToSend列表// 对于reset已有队列的场景,只需要唤醒阻塞在等待发送列表上的任务,// 而不需要唤醒阻塞在等待接收列表上的任务// 因为队列清空后,发送任务可以发送了,但接收任务依然没有消息可读取if( xNewQueue == pdFALSE ){// 唤醒等待发送列表上的任务if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) ==pdFALSE ){// 如果唤醒的任务优先级高于当前任务,还需要触发一次任务调度if( xTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}}}else{// 如果是新建队列,则初始化发送等待列表 & 接收等待列表vListInitialise( &( pxQueue->xTasksWaitingToSend ) );vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );}}// 退出临界段taskEXIT_CRITICAL();// 此处返回pdPASS是版本兼容性原因return pdPASS;
}

说明1:reset已创建队列

FreeRTOS中的xQueueReset宏用于实现reset已创建队列

#define xQueueReset( xQueue ) xQueueGenericReset( xQueue, pdFALSE )

注意:reset已创建队列时,只是唤醒了等待发送队列的首个任务,而非唤醒所有任务。这点已经在FreeRTOS论坛上提问,FreeRTOS的作者也进行了答复

https://forums.freertos.org/t/why-we-just-wake-up-one-task-when-reset-a-queue/10617

根据答复,在实际使用情景中,这样的处理是正确的,我们来简单分析一下

① 此处场景正确的根源在于FreeRTOS的消息等待发送列表是按照优先级排序的,而FreeRTOS任务调度目标又是时刻调度当前处于就绪态的优先级最高的任务

② 假设当前有2个任务阻塞在消息等待发送队列,当reset该队列时,高优先级的任务会被唤醒,即可以向消息队列发送消息。当该消息被接收时,接收函数会检查等待发送列表是否为空,进而将之前被阻塞的低优先级任务唤醒

说明2:queueUNLOCKED宏值

如上文所述,队列上锁时需要进行计数,在初始化cRxLock & cTxLock成员时,初始值为-1;而一旦上锁后,将值设置为0,则可以用于计数

说明3:xTaskRemoveFromEventList函数

xTaskRemoveFromEventList可同时用于等待发送列表 & 等待接收列表

// 因为会操作就绪列表与任务状态列表项
// xTaskRemoveFromEventList函数必须在临界段中被调用
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList )
{TCB_t *pxUnblockedTCB;BaseType_t xReturn;// 获取等待列表的首个任务,也就是等待发送或接收的任务中优先级最高的任务pxUnblockedTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxEventList);configASSERT( pxUnblockedTCB );// 将任务从等待列表删除( void ) uxListRemove( &( pxUnblockedTCB->xEventListItem ) );// 此时的任务状态列表项可能在延时列表,也可能在挂起列表(portMAX_DELAY等待)// 且二者必居其一if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){// 如果调度器没有被挂起,则直接将任务加入就绪列表( void ) uxListRemove( &( pxUnblockedTCB->xStateListItem ) );prvAddTaskToReadyList( pxUnblockedTCB );}else{// 如果调度器被挂起,则将任务加入挂起解除就绪列表// 由于会考虑调度器是否被挂起的状态,可见该函数可能在ISR中被调用vListInsertEnd( &( xPendingReadyList ),&( pxUnblockedTCB->xEventListItem ) );}// 如果被唤醒的任务优先级高于当前任务,则标识需要进行任务调度if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority ){xReturn = pdTRUE;// 此处在返回pdTRUE的情况下又设置xYieldPending标志位// 是防止调用者不触发任务调度(send or recv message From ISR)// 此时就可以在xTaskIncrementTick中标识需要触发任务调度xYieldPending = pdTRUE;}else{xReturn = pdFALSE;}#if( configUSE_TICKLESS_IDLE != 0 ){// 如果使能tickless模式,则重置下一任务唤醒时间// 因为此时等待队列的任务也可能在延时列表中// 如果没有使能tickless模式,则此处可以不重置// 从逻辑上都应该重置,只是不重置时,也不会导致问题,// xTaskIncrementTick函数可以处理// 根据源码注释,在tickless模式中重置,是为了能尽早进入idle状态// 因为从延时列表中移除任务后更新下一任务解锁时间,新的解锁时间一定// 大于等于旧的解锁时间,所以可以在idle中保持更长的时间prvResetNextTaskUnblockTime();}#endifreturn xReturn;
}

说明4:以一个长度为4,队列项大小为32B的队列为例,创建后内存布局如下,

2.2 静态创建队列

2.2.1 xQueueCreateStatic函数

#define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage,\
pxQueueBuffer ) \
xQueueGenericCreateStatic( ( uxQueueLength ), ( uxItemSize ), \
( pucQueueStorage ), ( pxQueueBuffer ), ( queueQUEUE_TYPE_BASE ) )

说明:静态创建队列时,需要调用者预先准备好队列存储区 & 队列结构内存

2.2.2 xQueueGenericCreateStatic函数

// pucQueueStorage:队列存储区内存起始地址
// 队列存储区需要 >= (uxQueueLength * uxItemSize)字节
QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize, uint8_t *pucQueueStorage,
StaticQueue_t *pxStaticQueue, const uint8_t ucQueueType )
{Queue_t *pxNewQueue;configASSERT( uxQueueLength > ( UBaseType_t ) 0 );configASSERT( pxStaticQueue != NULL );// 如果队列项大小为0,则无需提供队列存储区configASSERT( !( ( pucQueueStorage != NULL ) && ( uxItemSize == 0 ) ));// 如果队列项大小不为0,则必须提供队列存储区configASSERT( !( ( pucQueueStorage == NULL ) && ( uxItemSize != 0 ) ));pxNewQueue = ( Queue_t * ) pxStaticQueue;if( pxNewQueue != NULL ){#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ){// 标识队列为静态分配pxNewQueue->ucStaticallyAllocated = pdTRUE;}#endif /* configSUPPORT_DYNAMIC_ALLOCATION */// 初始化队列,与动态分配流程相同prvInitialiseNewQueue( uxQueueLength, uxItemSize,pucQueueStorage, ucQueueType, pxNewQueue );}return pxNewQueue;
}

说明:StaticQueue_t 类型

StaticQueue_t 类型定义在FreeRTOS.h中,并向用户实现了信息隐藏

3. 删除队列

void vQueueDelete( QueueHandle_t xQueue )
{Queue_t * const pxQueue = ( Queue_t * ) xQueue;configASSERT( pxQueue );#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) &&\( configSUPPORT_STATIC_ALLOCATION == 0 ) ){// 如果系统中只支持动态分配,则直接释放内存vPortFree( pxQueue );}#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) &&\( configSUPPORT_STATIC_ALLOCATION == 1 ) ){// 如果系统中同时支持动态 & 静态分配,则根据创建队列时的属性处理if( pxQueue->ucStaticallyAllocated == ( uint8_t ) pdFALSE ){vPortFree( pxQueue );}}#else{( void ) pxQueue;}#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
}

说明:如果在调用vQueueDelete函数删除队列时,队列的等待发送或者等待接收列表上仍有被阻塞的任务,删除函数并未进行处理。因此在使用中需要注意,当仍有任务在等待时,不能删除队列

个人认为这是源码不严谨之处,只是限于FreeRTOS的实际应用场景不容易导致问题而已

4. 向队列发送消息

4.1 任务级入队

4.1.1 任务级入队函数

// xTicksToWait:发送消息时的阻塞时间,以tick数为单位
// 当设置为portMAX_DELAY时是期望实现死等,但是需要系统使能TaskSuspend功能
// 否则也只是延时portMAX_DELAY个tick
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), \
queueSEND_TO_BACK )#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), \
queueSEND_TO_BACK )#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), \
queueSEND_TO_FRONT )// 覆写方式入队是不需要设置延时时间的,因为一定会进行写入操作
// 必要时会覆盖掉旧的消息
#define xQueueOverwrite( xQueue, pvItemToQueue ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )

说明1:xQueueSend和xQueueSendToBack的实现是相同的,都是将新的消息加入队尾,也就是遵循FIFO的语义

说明2:xQueueSendToFront函数是将新的消息加入队首,遵循LIFO的语义,可以用于发送紧急消息

4.1.2 任务级通用入队函数

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;configASSERT( pxQueue );// 如果入队消息指针为空,则是用于信号量的场景// 此时队列项长度必须为0configASSERT( !( ( pvItemToQueue == NULL ) &&( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );// 从这条断言可知,覆盖写入功能仅仅用于队列长度为1的场景// 或者说这是设计者的一种预设configASSERT( !( ( xCopyPosition == queueOVERWRITE ) &&( pxQueue->uxLength != 1 ) ) );// 由于带超时的xQueueGenericSend函数可能导致任务被阻塞// 因此在调度器被关闭时,不能调用// 这条断言对此进行了判断#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || \( configUSE_TIMERS == 1 ) ){configASSERT( !( ( xTaskGetSchedulerState() ==taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );}#endif// 注意此处进入了循环for( ;; ){// 进入临界段// 保护队列中的成员taskENTER_CRITICAL();{// 判断当前队列是否还有空间可供写入消息// 如果是覆写模式,则总是可以写入if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||( xCopyPosition == queueOVERWRITE ) ){// 将消息写入队列xYieldRequired =prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition);#if ( configUSE_QUEUE_SETS == 1 ){// 队列集相关操作}#else /* configUSE_QUEUE_SETS */{if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) ==pdFALSE ){// 如果等待接收队列上有阻塞的任务,则唤醒队首任务// 也就是唤醒优先级最高的等待任务// 如果唤醒的任务优先级高于当前任务,需要触发一次任务调度if( xTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}}else if( xYieldRequired != pdFALSE ){// 根据源码注释,这种情况仅发生在一个任务按顺序持有多个信号量// 但是按照不同的顺序归还信号量queueYIELD_IF_USING_PREEMPTION();}}#endif /* configUSE_QUEUE_SETS */// 退出临界段,从循环中返回taskEXIT_CRITICAL();return pdPASS;}else{// 此时队列中没有空间可供写入,开始处理阻塞延时等待if( xTicksToWait == ( TickType_t ) 0 ){// 如果阻塞时间为0,则直接返回队列已满taskEXIT_CRITICAL();return errQUEUE_FULL;}else if( xEntryTimeSet == pdFALSE ){// 如果阻塞时间非0,则设置延时时间// 该操作仅需要执行一次,所以通过xEntryTimeSet变量控制vTaskSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}}}taskEXIT_CRITICAL();// 关闭调度器 + 队列上锁vTaskSuspendAll();prvLockQueue( pxQueue );// 如果阻塞等待尚未超时且队列已满,则阻塞任务if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){if( prvIsQueueFull( pxQueue ) != pdFALSE ){vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ),xTicksToWait );// 队列解锁prvUnlockQueue( pxQueue );// 恢复调度器if( xTaskResumeAll() == pdFALSE ){portYIELD_WITHIN_API();}}else{// 如果阻塞等待尚未超时,但是队列还有空间// 则在循环中再次尝试发送// 这种场景发生在,在调度器关闭后,在中断ISR中有从队列读走消息prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{// 如果阻塞等待已经超时,则解锁队列 + 恢复调度器// 并返回队列已满prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();return errQUEUE_FULL;}}
}

说明1:prvCopyDataToQueue函数

// prvCopyDataToQueue函数是在临界段中被调用
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,
const void *pvItemToQueue, const BaseType_t xPosition )
{BaseType_t xReturn = pdFALSE;UBaseType_t uxMessagesWaiting;uxMessagesWaiting = pxQueue->uxMessagesWaiting;if( pxQueue->uxItemSize == ( UBaseType_t ) 0 ){// 队列项长度为0,为互斥信号量相关操作// 其中会处理继承优先级等问题#if ( configUSE_MUTEXES == 1 ){if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){xReturn = xTaskPriorityDisinherit(( void * ) pxQueue->pxMutexHolder );pxQueue->pxMutexHolder = NULL;}}#endif /* configUSE_MUTEXES */}else if( xPosition == queueSEND_TO_BACK ){// 如果写入位置为queueSEND_TO_BACK// 则将消息写入pcWriteTo指向的位置// 拷贝固定长度(uxItemSize)的数据( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue,( size_t ) pxQueue->uxItemSize );// pcWriteTo位置后移pxQueue->pcWriteTo += pxQueue->uxItemSize;// 处理写入位置绕回if( pxQueue->pcWriteTo >= pxQueue->pcTail )pxQueue->pcWriteTo = pxQueue->pcHead;}}else{// 如果写入位置不是queueSEDN_TO_BACK// 则将消息写入pcReadFrom指向的位置// 这也就确保了LIFO的语义// 拷贝固定长度(uxItemSize)的数据( void ) memcpy( ( void * ) pxQueue->u.pcReadFrom,pvItemToQueue, ( size_t ) pxQueue->uxItemSize );// pcReadFrom位置前移pxQueue->u.pcReadFrom -= pxQueue->uxItemSize;// 处理写入位置绕回if( pxQueue->u.pcReadFrom < pxQueue->pcHead ){pxQueue->u.pcReadFrom = ( pxQueue->pcTail -pxQueue->uxItemSize );}// 如果是覆写模式,且覆写时有未读取的消息// 也就是真的发生了消息覆盖// 则递减消息个数,目的是与后续的消息数量加1匹配if( xPosition == queueOVERWRITE ){if( uxMessagesWaiting > ( UBaseType_t ) 0 ){--uxMessagesWaiting;}}}// 队列中消息数量加1pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;return xReturn;
}

关于prvCopyDataToQueue函数,说明如下2点,

① 传递拷贝与传递地址

FreeRTOS消息队列传递的是数据的拷贝,优点是使用灵活,当数据加入队列之后,原先的数据buffer可改为他用;缺点是拷贝数据代价较大

因此在实际使用中,当通过消息队列传输少量数据时(e.g. 控制信息)直接传递拷贝;当传输大量数据时(e.g. 网卡接收到的数据)可以传输buffer的地址

② 关于覆写功能

FreeRTOS消息队列的覆写功能并不判断队列是否已满,而是直接写入队首。这主要是因为作者预设的场景,就是覆写功能仅用于长度为1的消息队列

说明2:vTaskSetTimeOutState函数

vTaskSetTimeOutState函数用于设置进入队列阻塞延时时的系统时间状态,用于判断是否还需要延时

void vTaskSetTimeOutState( TimeOut_t * const pxTimeOut )
{configASSERT( pxTimeOut );pxTimeOut->xOverflowCount = xNumOfOverflows;pxTimeOut->xTimeOnEntering = xTickCount;
}

说明3:xTaskCheckForTimeOut函数

作为vTaskSetTimeOutState函数的后续操作,xTaskCheckForTimeOut函数用于判断阻塞延时是否已超时

// xTaskCheckForTimeOut的返回值标识任务的阻塞延时是否已经超时
BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut,
TickType_t * const pxTicksToWait )
{BaseType_t xReturn;configASSERT( pxTimeOut );configASSERT( pxTicksToWait );// 进入临界段taskENTER_CRITICAL();{// 获取当前系统时间const TickType_t xConstTickCount = xTickCount;#if ( INCLUDE_vTaskSuspend == 1 )if( *pxTicksToWait == portMAX_DELAY ){// 如果是死等,则等待不会超时xReturn = pdFALSE;}else#endifif( ( xNumOfOverflows != pxTimeOut->xOverflowCount ) &&( xConstTickCount >= pxTimeOut->xTimeOnEntering ) )// 这种情况是明显已经超时,无需再等待xReturn = pdTRUE;}else if( ( ( TickType_t ) ( xConstTickCount -pxTimeOut->xTimeOnEntering ) ) < *pxTicksToWait ) {// 根据已经流逝的时间,修正实际要阻塞延时的tick数*pxTicksToWait -= ( xConstTickCount -pxTimeOut->xTimeOnEntering );vTaskSetTimeOutState( pxTimeOut );xReturn = pdFALSE;}else{xReturn = pdTRUE;}}// 退出临界段taskEXIT_CRITICAL();return xReturn;
}

下面给出与代码对应的时间关系,

为什么说下面这种情况一定已经超时了呢 ? 因为这里实际要延时的tick数已经超过了portMAX_DELAY,已经无法表示了

说明4:vTaskPlaceOnEventList函数

带着修正后的延时tick数,就可以将任务加入队列发送等待列表(任务事件列表项)和延时列表(任务状态列表项)了

void vTaskPlaceOnEventList( List_t * const pxEventList,
const TickType_t xTicksToWait )
{configASSERT( pxEventList );vListInsert( pxEventList, &( pxCurrentTCB->xEventListItem ) );prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
}

注意:vTaskPlaceOnEventList函数只能在临界段或者(调度器被关闭 + 队列上锁)的情况下被调用

说明5:何时需要判断xTaskResumeAll函数的返回值

在xQueueGenericSend函数中,有些情况下判断了xTaskResumeAll函数的返回值,并在xTaskResumeAll函数中没有触发任务调度的情况下,触发一次任务调度,比如当队列已满,需要将调用者阻塞时

而有些情况下则忽略了xTaskResumeAll函数的返回值

首先需要明确的是,xTaskResumeAll函数中,即使需要触发调度也可能无法触发,这需要编译系统时支持抢占功能

但是有些场景是必须要进行任务切换的,比如上文提到的需要阻塞消息发送任务的情况,此时就需要判断xTaskResumeAll函数的返回值,并确保任务调度被触发

说明6:队列集简介

① 队列集也是通过通用的队列结构实现的,其中标识队列类型的枚举值为queueQUEUE_TYPE_SET

② 队列集的作用是任务可以监听一组队列,即可以在一组队列上进行等待,当有其中一个队列有数据时,任务即可被唤醒

③ 由于队列集并非分析重点,此处仅给出相关API,如有需要,后续再分析

// 创建队列集
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );// 将队列(或信号量)加入队列集
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );// 将队列(或信号量)移出队列集
BaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );// 监听队列集
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait );

4.2 中断级入队

4.2.1 中断级入队函数

#define xQueueSendFromISR(xQueue, pvItemToQueue, \
pxHigherPriorityTaskWoken) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), \
( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, \
pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), \
( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, \
pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), \
( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )#define xQueueOverwriteFromISR( xQueue, pvItemToQueue, \
pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), \
( pxHigherPriorityTaskWoken ), queueOVERWRITE )

说明:与任务级入队函数类似,中断级入队函数也均实现为宏,实际调用xQueueGenericSendFromISR函数实现入队

与任务级入队相比,中断级入队增加了一个出参pxHigherPriorityTaskWoken,该参数用于标识在调用完FromISR入队函数后,是否需要触发一次任务调度

4.2.2 中断级通用入队函数

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;configASSERT( pxQueue );configASSERT( !( ( pvItemToQueue == NULL ) &&( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );configASSERT( !( ( xCopyPosition == queueOVERWRITE ) &&( pxQueue->uxLength != 1 ) ) );// 进入临界段uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();{if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||( xCopyPosition == queueOVERWRITE ) ){// 队列中仍有空间可供写入const int8_t cTxLock = pxQueue->cTxLock;// 将消息写入消息队列( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );if( cTxLock == queueUNLOCKED ){// 只有当队列未上锁时,才能在ISR中操作队列中的等待发送 / 接收列表#if ( configUSE_QUEUE_SETS == 1 ){// 队列集相关操作}#else /* configUSE_QUEUE_SETS */{if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) ==pdFALSE ){if( xTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){if( pxHigherPriorityTaskWoken != NULL ){// 如果被唤醒的任务优先级高于当前任务// 则设置pxHigherPriorityTaskWoken,标识需要触发任务调度*pxHigherPriorityTaskWoken = pdTRUE;}}}}#endif /* configUSE_QUEUE_SETS */}else{// 如果队列已上锁,则增加cTxLock计数,而不能操作等待发送列表// 即在队列上锁期间,发生了多少次入队操作pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );}xReturn = pdPASS;}else{// 中断级入队不允许阻塞,所以当队列没有空间时,直接返回队列已满xReturn = errQUEUE_FULL;}}// 退出临界段portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );return xReturn;
}

说明:中断级入队函数调用示例

void testISR(void)
{BaseType_t xHigherPriorityTaskWoken = pdFALSE;chat cIN = 0xaa;xQueueSendFrom(xTestQueue, &cIN, &xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

这里给出portYIELD_FROM_ISR宏的实现

4.3 队列的上锁与解锁

4.3.1 队列上锁

#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成员的状态,在计数值上就是将其设置为0,为后续计数做准备

4.3.2 队列解锁

// 该函数必须在调度器被关闭的情况下被调用(源码注释)
static void prvUnlockQueue( Queue_t * const pxQueue )
{// 进入临界段,保护队列计数taskENTER_CRITICAL();{// 先处理入队计数int8_t cTxLock = pxQueue->cTxLock;while( cTxLock > queueLOCKED_UNMODIFIED ){#if ( configUSE_QUEUE_SETS == 1 ){// 队列集相关操作}#else /* configUSE_QUEUE_SETS */{// 逐个唤醒等待接收列表上的任务// 直到等待接收队列为空,或者cTxLock计数值为0if( listLIST_IS_EMPTY(&( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){if( xTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){vTaskMissedYield();}}else{break;}}#endif /* configUSE_QUEUE_SETS */--cTxLock;}// 此处cTxLock就解锁了// 可见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();}--cRxLock;}else{break;}}pxQueue->cRxLock = queueUNLOCKED;}taskEXIT_CRITICAL();
}

说明:vTaskMissedYield函数

vTaskMissedYield函数只是设置了xYieldPending全局变量,在SysTick中断的xTaskIncrementTick函数中会根据该变量触发任务调度

void vTaskMissedYield( void )
{xYieldPending = pdTRUE;
}

5. 从队列读取消息

5.1 任务级出队

5.1.1 任务级出队函数

#define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) \
xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdFALSE )#define xQueuePeek( xQueue, pvBuffer, xTicksToWait ) \
xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdTRUE )

xQueueReceive和xQueuePeek的差别,就是后者不会删除读取到的消息

5.1.2 任务级通用出队函数

有了分析入队函数的基础,出队函数相当于一个镜像的操作

BaseType_t xQueueGenericReceive( QueueHandle_t xQueue,
void * const pvBuffer, TickType_t xTicksToWait,
const BaseType_t xJustPeeking )
{BaseType_t xEntryTimeSet = pdFALSE;TimeOut_t xTimeOut;int8_t *pcOriginalReadPosition;Queue_t * const pxQueue = ( Queue_t * ) xQueue;// 这里的一些列断言与入队函数是一样的configASSERT( pxQueue );configASSERT( !( ( pvBuffer == NULL ) &&( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || \( configUSE_TIMERS == 1)){configASSERT( !( ( xTaskGetSchedulerState() ==taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );}#endif// 进入循环for( ;; ){// 进入临界段taskENTER_CRITICAL();{const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;// 判断是否有消息可读if( uxMessagesWaiting > ( UBaseType_t ) 0 ){pcOriginalReadPosition = pxQueue->u.pcReadFrom;// 读取消息// 读取消息时,会修改pcReadFrom指针prvCopyDataFromQueue( pxQueue, pvBuffer );if( xJustPeeking == pdFALSE ){// 如果不是peek操作,则消费一条消息pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;#if ( configUSE_MUTEXES == 1 ){// 互斥信号量操作if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){pxQueue->pxMutexHolder = ( int8_t * ) pvTaskIncrementMutexHeldCount();}}#endif /* configUSE_MUTEXES */// 如果等待发送列表非空,则唤醒队首任务if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) )== pdFALSE ){// 如果唤醒任务的优先级高于当前任务,则触发任务调度if( xTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}}}else{// 如果是peek操作,需要恢复pcReadFrom指针的位置pxQueue->u.pcReadFrom = pcOriginalReadPosition;// 特别注意:这里是唤醒等待接收队列的队首任务// 因为peek之后的数据还在队列中,可能有其他的任务也需要这份数据// 毕竟这份数据必须有人消费掉才是合理的// 也就是说,此时队列中仍然有可供读取的消息,所以要检查一次// 等待接收列表if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) )== pdFALSE ){if( xTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}}}taskEXIT_CRITICAL();return pdPASS;}else{if( xTicksToWait == ( TickType_t ) 0 ){// 如果阻塞时间为0,则直接返回队列为空taskEXIT_CRITICAL();return errQUEUE_EMPTY;}else if( xEntryTimeSet == pdFALSE ){// 如果阻塞时间非0,则设置延时时间// 该操作仅需要执行一次,所以通过xEntryTimeSet变量控制vTaskSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}}}taskEXIT_CRITICAL();// 关闭调度器vTaskSuspendAll();// 队列上锁prvLockQueue( pxQueue );// 检查延时是否已经超时if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){if( prvIsQueueEmpty( pxQueue ) != pdFALSE ){// 如果需要延时等待,对于互斥信号量要进行优先级继承操作#if ( configUSE_MUTEXES == 1 ){if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ){taskENTER_CRITICAL();{vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );}taskEXIT_CRITICAL();}}#endif// 将任务加入等待接收列表vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ),xTicksToWait );prvUnlockQueue( pxQueue );if( xTaskResumeAll() == pdFALSE ){portYIELD_WITHIN_API();}}else{// 如果队列不为空,则再次尝试读取消息prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{// 如果等待超时,则返回队列为空prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();if( prvIsQueueEmpty( pxQueue ) != pdFALSE ){return errQUEUE_EMPTY;}}}
}

说明:prvCopyDataFromQueue函数

static void prvCopyDataFromQueue( Queue_t * const pxQueue,
void * const pvBuffer )
{if( pxQueue->uxItemSize != ( UBaseType_t ) 0 ){// pcReadFrom位置后移pxQueue->u.pcReadFrom += pxQueue->uxItemSize;// 处理读取位置绕回if( pxQueue->u.pcReadFrom >= pxQueue->pcTail ){pxQueue->u.pcReadFrom = pxQueue->pcHead;}// 固定拷贝队列项(uxItemSize)大小( void ) memcpy( ( void * ) pvBuffer,( void * ) pxQueue->u.pcReadFrom, ( size_t ) pxQueue->uxItemSize );}
}

此处要注意的是先后移pcReadFrom的位置,后拷贝消息

5.2 中断级出队

5.2.1 xQueueReceiveFromISR函数

BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken )
{BaseType_t xReturn;UBaseType_t uxSavedInterruptStatus;Queue_t * const pxQueue = ( Queue_t * ) xQueue;configASSERT( pxQueue );configASSERT( !( ( pvBuffer == NULL ) && \( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );// 关中断,进入临界段uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();{const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;// 判断是否有消息可读if( uxMessagesWaiting > ( UBaseType_t ) 0 ){const int8_t cRxLock = pxQueue->cRxLock;// 读取消息prvCopyDataFromQueue( pxQueue, pvBuffer );pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;// 如果cRxLock没有上锁,也就是说可以操作等待发送列表// 则唤醒等待发送列表的队首任务if( cRxLock == queueUNLOCKED ){if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) )== pdFALSE ){if( xTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ){// 如果唤醒任务的优先级高于当前任务// 则通过出参标记if( pxHigherPriorityTaskWoken != NULL ){*pxHigherPriorityTaskWoken = pdTRUE;}}}}else{// 如果cRxLock上锁,则增加其计数值pxQueue->cRxLock = ( int8_t ) ( cRxLock + 1 );}xReturn = pdPASS;}else{// 如果没有消息可读,直接返回读取失败xReturn = pdFAIL;}}// 恢复中断状态portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );return xReturn;
}

注意:在调用xQueueReceiveFromISR接收消息后,也需要判断pxHigherPriorityTaskWoken指向变量的值,确定是否需要触发任务调度

5.2.2 xQueuePeekFromISR函数

BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,
void * const pvBuffer )
{BaseType_t xReturn;UBaseType_t uxSavedInterruptStatus;int8_t *pcOriginalReadPosition;Queue_t * const pxQueue = ( Queue_t * ) xQueue;configASSERT( pxQueue );configASSERT( !( ( pvBuffer == NULL ) && \( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );configASSERT( pxQueue->uxItemSize != 0 ); /* Can't peek a semaphore. */// 关闭中断,进入临界段uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();{// 判断是否有消息可读if( pxQueue->uxMessagesWaiting > ( UBaseType_t ) 0 ){// 读取消息,并恢复pcReadFrom指针位置pcOriginalReadPosition = pxQueue->u.pcReadFrom;prvCopyDataFromQueue( pxQueue, pvBuffer );pxQueue->u.pcReadFrom = pcOriginalReadPosition;xReturn = pdPASS;}else{// 没有消息可供读取,直接返回读取失败xReturn = pdFAIL;}}portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );return xReturn;
}

6. 队列集实现分析

6.1 xQueueCreateSet函数实现

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
{QueueSetHandle_t pxQueue;pxQueue = xQueueGenericCreate( uxEventQueueLength,sizeof( Queue_t * ), queueQUEUE_TYPE_SET );return pxQueue;
}

说明1:调用xQueueCreateSet函数生成队列集合时,只需要指定队列长度即可,队列项成员内置为Queue_t *类型

也就是从队列集中取出的结果是已经有消息可读取的消息队列句柄

说明2:对队列集的处理

在xQueueGenericCreate函数调用的prvInitialiseNewQueue函数中,会将队列结构的pxQueueSetContainer成员置为NULL,后续将用于标识队列是否已加入队列集

6.2 xQueueAddToSet函数实现

// xQueueOrSemaphore:要加入队列集的队列或信号量
// xQueueSet:要加入的目标队列集
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet )
{BaseType_t xReturn;taskENTER_CRITICAL();{if( ( ( Queue_t * ) xQueueOrSemaphore )->pxQueueSetContainer!= NULL ){// pxQueueSetContainer不为NULL,说明该队列已经加入某个队列集// 而一个队列不能加入多个队列集xReturn = pdFAIL;}else if( ( ( Queue_t * ) xQueueOrSemaphore )->uxMessagesWaiting!= ( UBaseType_t ) 0 ){// 如果队列中还有消息没有读取,也不允许加入队列集xReturn = pdFAIL;}else{// 设置队列从属的队列集( ( Queue_t * ) xQueueOrSemaphore )->pxQueueSetContainer= xQueueSet;xReturn = pdPASS;}}taskEXIT_CRITICAL();return xReturn;
}

设置队列集的目的就是使得任务或中断可以监听多个队列,从实现分析,队列集除了可以监听队列,还可以监听信号量

6.3 xQueueRemoveFromSet函数实现

// xQueueOrSemaphore:要从队列集移除的队列或信号量
// xQueueSet:目标队列集
BaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet )
{BaseType_t xReturn;Queue_t * const pxQueueOrSemaphore = ( Queue_t * ) xQueueOrSemaphore;// 首先判断要移除的队列是否属于目标队列集,如果不是则返回报错if( pxQueueOrSemaphore->pxQueueSetContainer != xQueueSet ){xReturn = pdFAIL;}else if( pxQueueOrSemaphore->uxMessagesWaiting != ( UBaseType_t ) 0 ){// 如果队列中仍有消息没有读取,也不允许移除xReturn = pdFAIL;}else{taskENTER_CRITICAL();{// 移除队列pxQueueOrSemaphore->pxQueueSetContainer = NULL;}taskEXIT_CRITICAL();xReturn = pdPASS;}return xReturn;
}

6.4 prvNotifyQueueSetContainer函数实现

队列集操作的关键就是prvNotifyQueueSetContainer函数,从函数名可知,该函数用于通知队列集的监听者有消息可供读取

6.4.1 调用时机

该函数被如下4个函数调用,可见均是在有消息可供读取时通知队列集

prvUnlockQueue
xQueueGenericSend
xQueueGenericSendFromISR // 说明该函数可以在中断处理函数中被调用
xQueueGiveFromISR

我们以xQueueGenericSend函数中的调用为例进行分析,其余场景类似

// 将消息拷贝到消息队列
xYieldRequired = prvCopyDataToQueue( pxQueue,
pvItemToQueue, xCopyPosition );
#if ( configUSE_QUEUE_SETS == 1 )
{// 如果pxQueueSetContainer不为NULL,说明该队列或信号量从属于一个队列集// 因此需要通知该队列集if( pxQueue->pxQueueSetContainer != NULL ){if( prvNotifyQueueSetContainer( pxQueue, xCopyPosition )!= pdFALSE ){// 如果唤醒的任务优先级更高,需要触发一次任务调度queueYIELD_IF_USING_PREEMPTION();}}
}
#endif

6.4.2 函数实现

static BaseType_t prvNotifyQueueSetContainer(
const Queue_t * const pxQueue, const BaseType_t xCopyPosition )
{// 先获取队列所述的队列集Queue_t *pxQueueSetContainer = pxQueue->pxQueueSetContainer;BaseType_t xReturn = pdFALSE;// 源码注释:该函数必须在临界段中调用// 队列集不能为空configASSERT( pxQueueSetContainer );// 队列集必须仍可写入消息configASSERT( pxQueueSetContainer->uxMessagesWaiting <pxQueueSetContainer->uxLength );if( pxQueueSetContainer->uxMessagesWaiting <pxQueueSetContainer->uxLength ){const int8_t cTxLock = pxQueueSetContainer->cTxLock;// 写入队列集的消息,就是有消息可供读取的队列句柄// 此处传递&pxQueue,向队列集写入的消息就是pxQueue变量的值,// 也就是队列句柄xReturn = prvCopyDataToQueue( pxQueueSetContainer, &pxQueue,xCopyPosition );if( cTxLock == queueUNLOCKED ){// 如果队列没有上锁,且有任务等待接收消息,则唤醒之if( listLIST_IS_EMPTY(&( pxQueueSetContainer->xTasksWaitingToReceive ) ) == pdFALSE){if( xTaskRemoveFromEventList(&( pxQueueSetContainer->xTasksWaitingToReceive ) )!= pdFALSE ){xReturn = pdTRUE;}}}else{// 如果队列上锁,则递增队列上锁计数pxQueueSetContainer->cTxLock = ( int8_t ) ( cTxLock + 1 );}}return xReturn;
}

6.5 xQueueSelectFromSet函数实现

QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait )
{QueueSetMemberHandle_t xReturn = NULL;( void ) xQueueGenericReceive( ( QueueHandle_t ) xQueueSet,&xReturn, xTicksToWait, pdFALSE );return xReturn;
}

说明1:xQueueSelectFromSet函数返回的,是有消息可供读取的消息队列的句柄,调用者需要在此句柄上调用出队函数才能获取实际消息

说明2:如果没有消息队列的数据就位,xQueueSelectFromSet函数会返回NULL,即xQeueuGenericReceive函数不会操作传递进来的xReturn变量

说明3:对应中断版本为xQueueSelectFromSetFromISR,实现如下,

QueueSetMemberHandle_t xQueueSelectFromSetFromISR(
QueueSetHandle_t xQueueSet )
{QueueSetMemberHandle_t xReturn = NULL;( void ) xQueueReceiveFromISR( ( QueueHandle_t ) xQueueSet,&xReturn, NULL );return xReturn;
}

FreeRTOS源码分析与应用开发04:消息队列相关推荐

  1. FreeRTOS源码分析与应用开发02:任务管理

    目录 1. 任务概述 1.1 任务表示 1.2 任务状态 1.2.1 运行态 1.2.2 就绪态 1.2.3 阻塞态 1.2.4 挂起态 1.3 任务优先级 1.3.1 FreeRTOS优先级配置 1 ...

  2. FreeRTOS源码分析与应用开发01:中断配置与临界段

    目录 1. 异常与中断的基本概念 1.1 异常分类 1.2 中断概述 1.2.1 中断处理宜短暂 1.2.2 临界段影响中断实时性 1.3 中断硬件基础 1.3.1 外设 1.3.2 中断控制器 1. ...

  3. FreeRTOS源码分析与应用开发07:事件标志组

    目录 1. 概述 2. 事件标志组类型 3. 创建事件标志组 4. 删除事件标志组 5. 设置事件标志位 5.1 任务级设置 5.2 中断级设置 6. 清除事件标志位 6.1 任务级清除 6.2 中断 ...

  4. FreeRTOS源码分析与应用开发08:任务通知

    目录 1. 概述 1.1 任务通知概念 1.2 任务通知控制结构 2. 发送任务通知 2.1 任务级发送 2.2 中断级发送 2.2.1 xTaskNotifyFromISR函数 2.2.2 vTas ...

  5. FreeRTOS源码分析与应用开发06:软件定时器

    目录 1. 概述 1.1 软件定时器 & 硬件定时器 1.2 软件定时器精度 1.3 单次模式 & 周期模式 2. 软件定时器组件 2.1 定时器任务 2.2 定时器列表 2.3 定时 ...

  6. FreeRTOS源码分析与应用开发05:信号量

    目录 1. 信号量概述 1.1 信号量概念 1.2 4种信号量 1.2.1 二值信号量 1.2.2 计数信号量 1.2.3 互斥信号量 1.2.4 递归互斥信号量 1.3 信号量相关控制结构 1.3. ...

  7. FreeRTOS源码分析与应用开发09:低功耗Tickless模式

    目录 1. STM32F4低功耗模式简介 2. Tickless模式详解 2.1 如何降低功耗 2.2 关闭SysTick的问题与解决方案 2.2.1 关闭SysTick导致系统节拍计数器停止 2.2 ...

  8. FreeRTOS源码分析与应用开发11(完):编译、链接与部署

    目录 1. 存储设备布局 2. 链接器脚本 2.1 链接器脚本生成 2.2 链接器脚本分析 2.2.1 分散加载文件 2.2.2 加载区 & 运行区 2.2.3 ER_IROM1运行区分析 2 ...

  9. FreeRTOS源码分析与应用开发10:内存管理

    目录 1. 概述 1.1 RTOS中内存分配特点 1.2 内存堆(heap space)来源 1.2.1 ucHeap数组 1.2.2 链接器设置的堆 1.2.3 多个非连续内存堆 1.3 关于字节对 ...

最新文章

  1. 确定多重选择列表控件 (List Control) 中的选定内容
  2. java配置文件报错_java使用spring框架配置文件时遇到的错误——Referenced file contains errors...
  3. 最常用最好记Linux命令
  4. System.Threading.Timer 定时器的用法
  5. spark mapWithState 实现
  6. 样条曲面_用SolidWorks画一个自然、光顺过渡的曲面
  7. FFmpeg源代码简单分析:configure
  8. Linux学习笔记1-在CentOS 7中安装配置JDK8
  9. FastAPI(56)- 使用 Websocket 打造一个迷你聊天室
  10. 东北大学毕业设计(论文)全程解析
  11. 2022 SPSSPRO杯A|B|C题思路分享【认证杯】
  12. 浅谈微信域名防封 微信域名检测工作原理
  13. 如何在海量元素中(例如 10 亿无序、不定长、不重复)快速判断一个元素是否存在?
  14. 检验新买内存条的真假
  15. 企业微信账号异常,解决攻略
  16. 程序员转行都去干嘛了?产品经理很正常,这位卖烧饼的也太强了
  17. 再生核希尔伯特空间(RKHS)
  18. 浅谈MYSQL增量备份
  19. 浅谈Arrays.asList()方法
  20. dubbo实现esb_为什么你并不需要企业服务总线(ESB)

热门文章

  1. redis 是哪个厂家的_redis 基本数据类型-字符串(String)
  2. 客户和顾客是一个意思吗_履约保证金和投标保证金是一个意思吗?
  3. 牛客网训练赛26D(xor)
  4. Android 反编译快手APP,gksvideourla
  5. Maven技巧和窍门:高级Reactor选项
  6. Oracle数据库中IN参数个数超过1000的问题
  7. Kotlin 1.2 有哪些新特性
  8. Android开发笔记(一百二十四)自定义相册
  9. html5好看的大方框,这个样式导致HTML5的视频中的按钮变成一个方框。求解决…...
  10. 内核管理 之 内核管理概述