✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转FreeRTOS
💬保持学习、保持热爱、认真分享、一起进步!!!
目录
前言
本文将详细全方位的讲解FreeRTOS的队列消息,其实在FreeRTOS中队列的重要性也不言而喻,与FreeRTOS任务调度同等重要,因为后面的各种信号量基本都是基于队列的。本文主要围绕(队列的创建、队列的出队、入队函数、队列的环形缓冲区的实现(数据的拷贝、先进先出、以及后进先出的实现)、队列锁、以及队列任务级函数与中断级函数的区别)队列的重要知识,本文都会以源码分析的形式进行解析。
一.消息队列的特点
由于队列的知识也比较多,这里先进行一个简单的总结,把我们即将要学的知识点进行简单的概括。
1.队列的基本概念
消息队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度
的消息(而这个消息可以是任意类型的数据),任务能够从队列里面读取消息,也能够向队列发送消息。
基于队列,FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、递归互斥信号量,所以掌握队列就显得十分重要。
如下图所示,一个队列可以很多任务来写队列也可以很多任务来读队列。但是并不能两个队列同时来写或读队列。就与全局变量不能多个任务来同时读写,那样会出问题,而队列就是通过关中断的方式来保证队列同一时间只能一个任务进行读写。因为关中断:任务无法切换,且一些中断也无法来干扰。(后面源码中会有体现)
队列的特点:
1.一般情况下队列消息是先进先出方式排队(当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据),但同时 FreeRTOS的队列也支持将数据写入到队列的头部,并且还可以指定是否覆盖先前已经在队列头部的数据
。(后面会详细讲解pcHead、pcTail、pcWriteTo、pcReadFrom的指向关系,明白了这些就明白了队列环形缓冲区)
2. 队列传输数据时有两种方法:1. 直接拷贝数据 2.拷贝数据的地址,然后根据地址读取数据。 第二种方法适合传输大数据比如一个大数组, 或者一个结构体变量。
3.队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。
因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。
4.读写队列均支持阻塞机制
(以读队列为例:在任务从队列读取消息时,可以指定一个阻塞超时时间。如果队列不为空则会读取队列中第一个消息(通过拷贝的方式: memcpy函数),如果队列为空,则看我们自己设置阻塞时间(1.阻塞时间为0:即刻返回队列空错误。2.阻塞时间不为0:假设阻塞时间为20ms,刚开始队列为空,则任务进入阻塞,如果在20ms内有消息入队了即该任务会被唤醒然后读取消息,如果在20ms内还没有消息则任务就不会再等待直接从阻塞态中唤醒,返回队列空错误。3.阻塞时间为最大:任务死等进入阻塞态,直到完成读取队列的消息。 写队列过程基本一致,不过一个的队列空一个是队列满。)
5.当在中断中读写队列时,如果队列空或满,不会进行阻塞,直接返回队列空或队列满错误,因为中断要的就是快进快出。(后面源码解析会详细阐述任务中使用队列与中断中使用队列的区别,以及与中断息息相关的队列锁)
上述队列特点只是大概介绍一下,更多的细节看下面源码分析,保证搞得透透彻彻、明明白白的。
2.队列结构体
typedef struct QueueDefinition { int8_t * pcHead; int8_t * pcWriteTo; union { QueuePointers_t xQueue; SemaphoreData_t xSemaphore; } 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; #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;
接下来分析一下各个结构体成员:
1.pcHead:指向队列消息存储区起始位置,即第一个消息空间。
2. pcWriteTo :指向队列消息存储区下一个可用消息空间。(一般就是从pcWriteTo 位置入队消息即尾插)。
3.一个联合体变量: 当用于队列时使用xQueue结构体变量,当用于信号量 时使用xSemaphore结构体变量,详情看下图。
这里只讲队列:
pcTail:队列存储区域的结束地址,与pcHead一样一个指向开始地址一个指向结束地址,他们只是一个一头一尾的标识,在入队出队的时候他们并不会改变。
pcReadFrom:最后一次读取队列的位置。(后面源码会详细解析,有个Read肯定与读队列(出队)有关)
4.xTasksWaitingToSend :发送消息阻塞列表,看英文意思也知道:等待发送的任务,也就是队列已满,任务想要发送消息到队列(入队),如果设定了阻塞时间,任务就会挂入该列表,表示任务已阻塞,任务会按照优先级进行排序(后面解除阻塞就是按照任务的优先级:当队列不为满了,xTasksWaitingToSend 列表中优先级高的就会先被唤醒)。
5.xTasksWaitingToReceive:等待消息阻塞列表,看英文意思也知道:等待接收的任务,也就是队列已空,任务想要从队列中读取消息(出队),如果设定了阻塞时间,任务就会挂入该列表,表示任务已阻塞,任务会按照优先级进行排序(后面解除阻塞就是按照任务的优先级:当队列不为空了,xTasksWaitingToReceive列表中优先级高的就会先被唤醒)。
其实这两条阻塞列表就是队列的核心之一,当然这里只是讲了个大概,更多细节看源码分析。
6.uxMessagesWaiting:用于记录当前
消息队列的消息个数,如果消息
队列被用于信号量的时候,这个值就表示有效信号量个数。
7.uxLength:表示队列的长度,表示一共能存放多少消息。
8.uxItemSize:表示单个消息的大小(单位为字节)。
9.cRxLock:队列上锁后,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED。
10.cTxLock:队列上锁后,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED。
队列为什么要上锁,上锁之后又为啥要记录入队出队个消息个数?关于这些问题后面会有解答。
11.ucQueueType:队列的类型,前面就说了那些信号量都是基于队列的。
介绍完队列结构体,如果是初学者我估计很多队列结构体还是没有搞明白,这是正常的,真正想完全搞懂,还是的去分析队列的那些API函数的源码,源码都被你搞明白了,概念还会不懂嘛?
二.队列API函数源码分析
1.队列创建函数 xQueueCreate()
队列创建与之前的任务创建都分为动态与静态创建,所谓静态创建就是队列所需要的内存需要自己来分配,而动态创建则由FreeRTOS动态分配。在队列这里就不讲静态创建了因为也基本不用,要支持动态创建需要将宏configSUPPORT_DYNAMIC_ALLOCATION 定义为1,队列所需要的内存分为两部分:1.队列结构体变量 2.队列的存储区域(环形缓存区),动态创建时会自动分配这两块内存,而且是连续的。
xQueueCreate函数有两个参数uxQueueLength,uxItemSize。
uxQueueLength:队列能够存储的最大消息数目,即队列长度。
uxItemSize:队列中消息的大小,以字节为单位
返回值:
如果创建成功则返回一个队列句柄(就是队列结构体的地址),用于访问创建的队列。如果创建不成功则返回NULL,可能原因是创建队列需要的 RAM 无法分配成功。
xQueueCreate()其实是一个宏,真正来创建队列的函数是xQueueGenericCreate()
发现xQueueGenericCreate()比xQueueCreate()多了一个参数ucQueueType队列类型,Generic代表的就是通用的创建函数,既可以创建队列,也可以创建各种信号量,就取决于ucQueueType这个参数。
xQueueCreate()则代入的是queueQUEUE_TYPE_BASE这个参数,表示创建队列。
接下来就是创建队列的源码分析:
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) 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; 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; }#endif
代码分析:
1.参数检查是否合法
2.计算队列环形存储空间需要的字节大小,环形存储区大小=消息的总个数*每个消息的大小(字节)。
3.为队列申请内存空间,队列所需内存就是队列结构体的大小+环形存储区的大小,然后一起分配出空间(前面作为队列结构体)。
4.当内存申请成功,获取存储区的起始地址,标记队列为动态申请,最后去调用prvInitialiseNewQueue()函数初始化队列(那些队列结构体成员的初始化)。
5.调用prvInitialiseNewQueue()函数进行队列的初始化:
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 ) { 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 #if ( configUSE_QUEUE_SETS == 1 ) { pxNewQueue->pxQueueSetContainer = NULL; } #endif traceQUEUE_CREATE( pxNewQueue );}
prvInitialiseNewQueue()函数前面比较简单就初始化pcHead、uxLength、uxItemSize直接看代码注释即可,最后调用了xQueueGenericReset()函数去重置队列(这里会有更多的结构体成员的初始化)。
xQueueGenericReset()函数原形:
xQueueGenericReset()函数源码分析:
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue ){ BaseType_t xReturn = pdPASS; Queue_t * const pxQueue = xQueue; configASSERT( pxQueue ); if( ( pxQueue != NULL ) && ( pxQueue->uxLength >= 1U ) && ( ( SIZE_MAX / pxQueue->uxLength ) >= pxQueue->uxItemSize ) ) { taskENTER_CRITICAL(); pxQueue->u.xQueue.pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize ); pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U; pxQueue->pcWriteTo = pxQueue->pcHead; pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - 1U ) * pxQueue->uxItemSize ); pxQueue->cRxLock = queueUNLOCKED; pxQueue->cTxLock = queueUNLOCKED; if( xNewQueue == pdFALSE ) { 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 ) ); vListInitialise( &( pxQueue->xTasksWaitingToReceive ) ); } taskEXIT_CRITICAL(); } else { xReturn = pdFAIL; } configASSERT( xReturn != pdFAIL ); return xReturn;}
1.初始化各个队列结构体变量
pcTail:pcHead存储区起始位置加上存储区的大小(总消息个数*一个消息大小),指向存储区结束位置。
uxMessagesWaiting:刚创建的队列里面无消息,即队列为空,则消息数量为0。
pcWriteTo:下一个入队的位置(从尾部入队),这里的尾部与pcTail是不一样的pcTail是始终指向存储区结束位置,而pcWriteTo是从队首开始第一个空位,所谓的尾部入队。
pcReadFrom:最后一次读取的位置,最开始是在队尾第一个消息的位置,后面出队源码分析的时候再来讲为什么在这个位置。
cRxLock,cTxLock设置为queueUNLOCKED等于-1,表示一开始队列不会上锁。(后面再来讲关于队列上锁)。
到这里队列结构体成员变量基本初始化完毕, 假设我们申请了4个队列项,每个队列项占用32字节存储空间(即uxLength=4、uxItemSize=32),则队列内存分布如下图所示。
后面讲解入队出队的源码时,会查看这张图,主要是看出入队时pcWriteTo、pcReadFrom指针的变化。
2.xQueueGenericReset()函数xNewQueue参数来区别新旧队列,有些人问我们创建队列不就是新队列嘛为啥区分,答:人家这个函数又不是只能重置新队列,这个函数也可以在其他地方调用重置已经创建的旧队列。
那么问题来了重置新队列与重置旧队列有啥区别?
- 1.重置旧队列:
也就是xNewQueue参数== pdFALSE,我们知道前面的重置操作是一样的,前面已经把队列置空了,记住这是前提条件
,所以:
1)如果旧队列之前有任务在等待写队列,此时我们就需要去唤醒阻塞链表优先级最高的一个任务,添加到就绪链表因为前面的重置操作已经让队列为空则允许写队列。
2)如果旧队列之前有任务在等待读队列,则不需要管他让任务继续阻塞,因为重置的队列还是空的不允许读队列。
唤醒任务函数xTaskRemoveFromEventList()源码解析:
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList ){ TCB_t * pxUnblockedTCB; BaseType_t xReturn; configASSERT( pxUnblockedTCB ); listREMOVE_ITEM( &( pxUnblockedTCB->xEventListItem ) ); if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ) { listREMOVE_ITEM( &( pxUnblockedTCB->xStateListItem ) ); prvAddTaskToReadyList( pxUnblockedTCB ); #if ( configUSE_TICKLESS_IDLE != 0 ) { prvResetNextTaskUnblockTime(); } #endif } else { listINSERT_END( &( xPendingReadyList ), &( pxUnblockedTCB->xEventListItem ) ); } if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority ) { xReturn = pdTRUE; xYieldPending = pdTRUE; } else { xReturn = pdFALSE; } return xReturn;}
唤醒任务xTaskRemoveFromEventList()函数:
1.首先将任务从事件列表(xTasksWaitingToSend或xTasksWaitingToReceive)中移除,该任务是列表中优先级最高的。因为事件列表是按优先级大小排序的(优先级最高的排第一个)。
为什么在任务插入事件列表是按照优先级从大到小排序?
看下图就明白了:
2.将任务从事件列表移除后,此时任务并没有被真正唤醒,因为任务还被挂入了延时列表中(如果不清楚后面入队出队函数会讲)。
先需要判断任务调度器是否被挂起(关于任务调度器的知识请参考->FreeRTOS-任务管理)
1.当任务调度器未被挂起时,此时可以操作就绪链表,则将任务从延时链表中移除,然后将任务添加到就绪链表,此时任务就被真正被唤醒。
2.当任务调度器挂起时,则无法访问就绪列表(因为都无法进行任务切换了),所以将任务先挂起到xPendingReadyList链表中,等到任务调度器解除挂起(调用xTaskResumeAll()函数)在该函数中任务会真正被唤醒(从延时链表中移除,然后将任务添加到就绪链表)。
当任务调度器被挂起的时候,就绪链表不能被访问,所以在调度器挂起期间有(某个在等待同步事件的任务等到了该事件就应该从阻塞态变成就绪态)但是调度器挂起无法挂起就绪链表则先将任务挂起到xPendingReadyList链表中等到调度器恢复时,再将xPendingReadyList链表任务一一添加到就绪链表中
恢复调度器函数xTaskResumeAll()
- 2.重置新队列:
新队列的话没有阻塞在事件列表中的任务,直接就初始化两个事件阻塞列表就行。
自此创建队列函数xQueueCreate()就全部讲完,基本上创建一个队列就是为队列分配内存,然后初始化队列结构体的成员变量。
2.向队列发送消息(出队)
FreeRTOS的队列入队提供很多接口函数,其实主要是xQueueGenericSend()与xQueueGenericSendFromISR()函数为实际执行函数,一个是在任务中调用的,一个是在中断中调用的,其中又因为入队的方式又分成几个函数(尾部入队,头部入队),最后还有一个特殊情况,覆写入队,这种方式下队列只能有一个消息,每次写入都会覆盖上一个消息的内容。
(1).队列入队(任务级)
在任务中往队列写入消息的函数有函数 xQueueSend() 、 xQueueSendToBack() 、xQueueSendToFront()、xQueueOverwrite(),虽然有四个,但是这些函数实际上都是宏定义,四个函数最终调用的是xQueueGenericSend()函数,只不过传入的参数不一样即入队的方式会有所差异而已,具体差异如下图所示。
从上图可以看出xQueueSend() 与xQueueSendToBack()函数传入的参数是一模一样的,则他们的功能相同都是队尾入队,xQueueSendToFront()为对头入队(就是所谓的紧急消息,可以后进先出),xQueueOverwrite()函数则为覆写入队(只有在队列的队列长度为 1 时,才能够使用,在xQueueGenericSend()函数会有体现)。
所以任务中入队我们只需要掌握一个函数xQueueGenericSend()即可,就是将四个入队函数搞定,只不过在xQueueGenericSend()函数需要将传入的参数分析一下即可。
xQueueGenericSend()函数介绍:
加粗样式
函数参数:
1.xQueue:要写入的队列
2.pvItemToQueue:要写入的消息(数据的地址)
3.xTicksToWait:阻塞超时时间(当队列为满,是否需要进行阻塞等待)
4.xCopyPosition:要写入的位置:(尾部入队、头部入队、覆写入队)
函数返回值:
1.pdTRUE:写入成功
2.errQUEUE_FULL:队列满,写入失败
xQueueGenericSend()函数源码分析:
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 = xQueue; configASSERT( pxQueue ); configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) ); configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) ); #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) ) { configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) ); } #endif for( ; ; ) { taskENTER_CRITICAL(); { if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) ) { traceQUEUE_SEND( pxQueue ); #if ( configUSE_QUEUE_SETS == 1 ) { } else {xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{mtCOVERAGE_TEST_MARKER();}}else if( xYieldRequired != pdFALSE ){queueYIELD_IF_USING_PREEMPTION();}else{ mtCOVERAGE_TEST_MARKER();} } #endif taskEXIT_CRITICAL();return pdPASS; } else {if( xTicksToWait == ( TickType_t ) 0 ){taskEXIT_CRITICAL();traceQUEUE_SEND_FAILED( pxQueue );return errQUEUE_FULL;}else if( xEntryTimeSet == pdFALSE ){vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();} }}taskEXIT_CRITICAL(); vTaskSuspendAll(); prvLockQueue( pxQueue ); if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) { if( prvIsQueueFull( pxQueue ) != pdFALSE ) { traceBLOCKING_ON_QUEUE_SEND( pxQueue ); vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), xTicksToWait ); prvUnlockQueue( pxQueue ); if( xTaskResumeAll() == pdFALSE ) { portYIELD_WITHIN_API(); } } else { prvUnlockQueue( pxQueue ); ( void ) xTaskResumeAll(); } } else { prvUnlockQueue( pxQueue ); ( void ) xTaskResumeAll(); traceQUEUE_SEND_FAILED( pxQueue ); return errQUEUE_FULL; } } }
xQueueGenericSend()函数代码比较,看起来比较复杂,其实我们只要搞懂这一个函数其他的(像出队函数、中断中的出队、入队函数、以及不同方式出队、入队)全部大同小异,所以我们只需认真分析xQueueGenericSend()函数即可,真的不难。
接下来就是代码分析:
代码分析的时候,请对照前面的完整代码一起理解
- (1).第一步肯定是检查参数的合法性
其中如果是以覆写的方式入队的话,队列的长度必须要为1(也就是队列的环形存储区总共只能容纳一个消息)
- (2).一进函数先关中断
为什么要关中断(保护临界资源)?
因为队列可以很多任务或者中断都可以读写,但是同一时间不能两个读写队列,就跟全局变量一样,同时读或写就会出问题,所以FreeRTOS通过关中断来解决这个问题:关中断可以防止任务切换、中断的发生,这样一来正在读或写队列的任务或中断就可以独占队列(没有其他任务或中断来打扰)。
- (3).判断是否能写队列
1.如果队列有空闲位置肯定可以写队列。
2.如果为覆写的方式也就是调用了xQueueOverwrite()函数,就算队列未满(队列消息为1),也可以直接覆写。
- (4).如果可以写队列,则拷贝消息到队列(包括: 尾部入队、头部入队、覆写入队)
prvCopyDataToQueue()函数源码分析:
代码分析:
函数中关于信号量部分以后再讲,函数主要分为尾部入队、头部入队、覆写入队三种入队方式,接下来一 一分析。
1.尾部入队
图像解析:
1.头部入队
图像解析:
为什么说头部入队的消息可以先出队,因为出队与PcReadFrom指针有关,到后面分析出队的函数时你们就知道了。
覆写入队:
覆写入队与头部入队的方式一样的,即覆写是头部入队,只不过看下图:
覆写入读:前提条件是队列长度只能为1,所以当队列满(针对覆写)即(队列消息数为1),则需要消息数uxMessagesWaiting-1,因为后面会加1,即保持uxMessagesWaiting的正确性。
5.入队后,队列消息数加1
- (5).判断等待接收列表是否有任务?
判断等待接收列表是否有任务,如果列表不为空,说明有任务在等待读取队列的消息(出队),此时需要将任务唤醒,因为上一步操作已经向队列中写入了消息,所以队列有消息可以出队(所以需要唤醒在等待读取队列的任务)。
唤醒任务:依靠xTaskRemoveFromEventList()函数将任务唤醒,该函数在队列创建函数中,已经详细讲解,详情请往回看。
其实唤醒任务,就是将任务在事件阻塞链表、延时列表中移除,然后添加到就绪列表,则任务就可以参加调度,唤醒完成,当然在xTaskRemoveFromEventList()函数会考虑到调度器是否挂起的情况,因为调度器挂起后则不能操作就绪列表,详情往回看。
当然如果被唤醒的任务比当前正在运行的任务的优先级更高则需要立即进行一次任务切换。
(6).如因为队列满(不能写入消息),则需要判断任务是否需要阻塞?
1.如果我们设置的阻塞时间为0,则立即返回队列满错误。(不阻塞)
2.如果我们设置阻塞时间大于0,则任务需要进行阻塞(挂入等待发送列表xTasksWaitingToSend)。
vTaskInternalSetTimeOutState()函数源码:
将系统计数器xTickCount的值与系统计数器溢出次数xNumOfOverflows记下,这两个变量在FreeRTOS-任务管理文章中有介绍我就不赘言了,后面需要计算任务是否真的需要阻塞。(是否超时)
7.判断要阻塞的任务是否发生了超时,任务是否还需要阻塞?
会用通过调用xTaskCheckForTimeOut()函数来判断是否发生超时,在此期间需要挂起调度器,并且给队列上锁,防止队列被其他任务或中断访问(至于为什么讲中断级入队函数再讲)。
xTaskCheckForTimeOut()函数传入两个参数
1.pxTimeOut :前面记录的系统计数器xTickCount的值与系统计数器溢出次数xNumOfOverflows所在的结构体。
2.pxTicksToWait:我们设置的超时时间。
xTaskCheckForTimeOut()函数官方描述:
任务因为等待某事件而进入阻塞状态,通常情况下任务会设置一个等待超时周期。如果在等待事件超时,任务会退出阻塞状态。
为什么要搞一个超时时间?
假设某任务等待一个事件而进入阻塞状态,但是事件迟迟不发生,超时后任务退出阻塞状态继续执行该任务。假设任务等待的事件仍然没有发生,则任务又会阻塞在该事件下。只要任务等待的事件一直不发生,这个任务进入阻塞然后超时退出阻塞,再进入阻塞的循环就会一直存在。
就比如说入队函数其实是一个大循环,假设我们是我阻塞时间为20,当时任务一个队列满而进入阻塞,当阻塞时间已过任务被唤醒,此时如果队列还为满,任务则又会被阻塞20,所以FreeRTOS设置一个超时时间20,也就是说假如现在的xTickCount的值为10,所以等到了10+20=30 这个时间点还不能写队列,则返回错误,不会再继续阻塞。
所以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; const TickType_t xElapsedTime = xConstTickCount - pxTimeOut->xTimeOnEntering; #if ( INCLUDE_xTaskAbortDelay == 1 ) if( pxCurrentTCB->ucDelayAborted != ( uint8_t ) pdFALSE ) { pxCurrentTCB->ucDelayAborted = pdFALSE; xReturn = pdTRUE; } else #endif #if ( INCLUDE_vTaskSuspend == 1 ) if( *pxTicksToWait == portMAX_DELAY ) { xReturn = pdFALSE; } else #endif if( ( xNumOfOverflows != pxTimeOut->xOverflowCount ) && ( xConstTickCount >= pxTimeOut->xTimeOnEntering ) ) { xReturn = pdTRUE; *pxTicksToWait = ( TickType_t ) 0; } else if( xElapsedTime < *pxTicksToWait ) { *pxTicksToWait -= xElapsedTime; vTaskInternalSetTimeOutState( pxTimeOut ); xReturn = pdFALSE; } else { *pxTicksToWait = ( TickType_t ) 0; xReturn = pdTRUE; } } taskEXIT_CRITICAL(); return xReturn; }
代码分析:
1.计算要阻塞的任务已经等待了多长时间(已经过去了多长时间)?
记录已经过去多长时间,这个时间指的是哪一段?
2.如果我们设置的超时时间是portMAX_DELAY(最大值),如果任务支持挂起则永远不会超时,因为任务会一直阻塞(任务进入挂起态)直到等到可以写队列。
3.当上次记录的系统溢出次数xNumOfOverflows不等于这次的溢出次数,则说明这次的溢出次数大于上一次的,当前系统计数器xTickCount的值大于上次保存的系统计数器的值,则说明时间已经过去了比portMAX_DELAY还有更久,所以任务也无需再进行阻塞,直接将我们开始设置的超时时间设置为0,表示已超时,任务不需要再等待。
4.如果已经过去的时间xElapsedTime小于我们设置的超时时间*pxTicksToWait,说明还未超时,任务还有时间需要阻塞(重新计算超时时间)。
最后其实你不理解xTaskCheckForTimeOut()这个函数也没有关系,反正记住假设一个任务需要阻塞20,当前时间为10 ,则时间到30的时候还未等待到事件发生(这里是等待发送队列),则任务即可解除阻塞,不需要再等了。而xTaskCheckForTimeOut()这个函数的作用就是防止任务一直去等待或者多等。
8.当任务真正需要阻塞的时候,则将任务阻塞
则主要是依靠vTaskPlaceOnEventList()函数使任务阻塞,注意传入的xTicksToWait是经过xTaskCheckForTimeOut()函数时间补偿过的。
vTaskPlaceOnEventList()函数源码分析:
函数中主要内容是:
1.将任务挂入等待发送列表xTasksWaitingToSend中。
2.将任务挂入阻塞列表中,阻塞时间为更新过的超时时间xTicksToWait。
将任务挂入延时列表中,依靠prvAddCurrentTaskToDelayedList()函数,我前面的文章有详细讲解。
队列入队总结:
至此通用入队函数xQueueGenericSend()的所以内容全部完整的讲了一遍,其实如果抛开一些细枝末节的东西,队列入队大致分为几个步骤:
1.首先要入队判断能否可以入队(队列有空闲位置或者覆写入队方式时即可入队)
2.若可以入队,则拷贝要入队的消息到指定的队列存储区的位置(入队方式不同,入队位置略有差异),然后判断是否有任务在等待读队列,如果有则立马唤醒它,如果唤醒的任务的优先级大于当前任务则立即切换任务,最后返回入队成功。
3.若不能入队(队列已满(不为覆写入队)),则需要判断任务是否需要阻塞:
(1): 若设置的超时时间为0,则即可返回队列已满错误(入队失败)。
(2):若设置的超时时间不为0(但不为最大值),则经过时间补偿,判断是否真的需要阻塞,需要则将任务阻塞,若过了超时时间队列还未有空闲位置,则即可返回队列已满错误(入队失败)。
(3):若设置的超时时间为portMAX_DELAY,则一直等待直到等到队列有空闲位置可以写入,即可唤醒。
其实通用入队函数讲完了,后面的出队其实原理是一模一样的,只不过一个判断队列是否为满,一个判断队列是否为满,一个判断是否唤醒在等到读队列的任务,一个是判断是否唤醒在等到写队列的任务,所以后面的出队函数不会讲的怎么细致,源码贴出来就看得懂。
(2).队列锁
关于队列锁初学者可能会有点懵,但是没关系让我细细道来:
- 第一个问题:在什么地方需要给队列上锁?
从上面的代码我们可以看出,是在当任务进入阻塞期间,在这个时间段需要给队列上锁,而且我们还发现不仅队列上锁了调度器也被挂起了。
- 第二个问题:为什么要在任务进入阻塞期间需要挂起调度器且队列上锁?
因为在将任务设置为阻塞的过程中(是这个过程中),系统不希望有其它任务和中断操作这个队列的 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表,而什么情况下会去操作这两个列表,只有其他更高优先级的任务或者中断来读写队列。
- 第三个问题:为什么在这个过程中不能有其他任务或者中断来操作这两个列表?
因为可能引起其它任务解除阻塞,这可能会发生优先级翻转。比如任务 A 的优先级低于当前任务,但是在当前任务进入阻塞的过程中,任务 A 却因为其它原因解除阻塞了,此时系统切换到任务A,而当前任务的手上的活还没完全干完,就被优先级反转了,这是要绝对禁止的。
- 最重要的一个问题:讲了这么多,给队列上锁有何作用?
简单来讲就是去避免中断在当前任务进入阻塞期间来操作这个队列的 xTasksWaitingToReceive 列表或者 xTasksWaitingToSend 列表。
因为挂起调度器只能禁止任务切换,也就是说可以避免其他任务来操作队列。但挂起调度器并不会禁止中断的发生,中断服务函数中仍然可以操作队列事件列表,可能会解除任务阻塞、就可能会进行任务切换,这是不允许的。所以给队列上锁就是防止中断来操作事件列。
这里注意一个问题我们要防止的是中断操作事件列表(xTasksWaitingToReceive ,xTasksWaitingToSend ),并不是说给队列上锁可以阻止中断的发生,其他在队列上锁期间中断服务函数仍然可以调用中断级的出队入队函数,来让消息入队或者出队,但是因为队列锁住则不能去操作xTasksWaitingToReceive ,xTasksWaitingToSend 这两个事件列表(防止让其他任务解除阻塞)。等到将中断级的通用入队函数的时候在详细讲解。
队列上锁如何起作用?
当中断服务程序操作队列并且导致阻塞的任务解除阻塞时(比如入队成功,如果有等待接收的任务则需要解除该任务的阻塞),会首先判断该队列是否上锁,如果没有上锁,则解除被阻塞的任务,还会根据需要设置任务切换的请求标志,如果队列已经上锁,则不会解除被阻塞的任务,而是将xRxLock或xTxLock加1,表示队列上锁期间出队或入队的数目,也表示有任务可以解除阻塞了,等待最后队列解锁再来处理这些需要解除阻塞的任务。
1.队列上锁
prvLockQueue :其实是一个宏,内容很简单。
先回顾一下cRxLock,cTxLock两个变量的含义:
cRxLock:队列上锁时,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED值为-1。
cTxLock::队列上锁时,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED值为-1。
prvLockQueue 给队列上锁这个宏很简单只需,将cRxLock,cTxLock赋值成queueLOCKED_UNMODIFIED(0),则就算给队列上锁了。
2.队列解锁
队列解锁依靠prvUnlockQueue()函数来完成。
prvUnlockQueue()函数源码分析:
static void prvUnlockQueue( Queue_t * const pxQueue ){ taskENTER_CRITICAL(); { int8_t cTxLock = pxQueue->cTxLock; while( cTxLock > queueLOCKED_UNMODIFIED ) { #if ( configUSE_QUEUE_SETS == 1 ) { } #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();}
队列解锁prvUnlockQueue()函数主要是判断在队列上锁期间是否有中断在读写队列,因为在队列上锁期间,中断来读写队列,只会拷贝消息(消息的出队或入队),并不会去操作xTasksWaitingToReceive ,xTasksWaitingToSend 这两个事件列表,只会记录(cRxLock或cTxLock加1),比如cTxLock+1表示队列上锁时,传输到队列(添加到队列)的入队项目数,则在队列解锁时则需要进行cTxLock次判断是否有任务在等待读取队列如果有则需要唤醒该任务。
其实队列锁,就相当于在队列上锁期间,保护xTasksWaitingToReceive ,xTasksWaitingToSend 这两个事件列表不被访问,在队列解锁之后补回来就行。
如果对队列锁还有点不清楚的,看后面的中断级入队通用函数的讲解就焕然大悟了。
(3).队列入队(中断级)
其中在中断中使用入队函数也有4个,xQueueSendFromISR() 、
xQueueSendToBackFromISR()、xQueueSendToFrontFromISR()、xQueueOverwriteFromISR(),与任务级一样他们也都是宏定义,真正执行的的xQueueGenericSendFromISR()函数,不过是入队方式的区别罢了。
xQueueGenericSendFromISR()函数原型:
函数参数:
xQueue:待写入的队列
pvItemToQueue:待写入的消息
pxHigherPriorityTaskWoken:是否需要任务切换标记,为pdTRUE表示需要任务切换,为pdFALSE表示不需要任务切换。
xCopyPosition:队列写入方式(尾部入队、头部入队、覆写入队)
函数返回值:
pdTRUE:入队成功
errQUEUE_FULL:队列满,入队失败
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 = xQueue; configASSERT( pxQueue ); configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) ); configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) ); portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) ) { const int8_t cTxLock = pxQueue->cTxLock; const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting; traceQUEUE_SEND_FROM_ISR( pxQueue ); ( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); if( cTxLock == queueUNLOCKED ) { #if ( configUSE_QUEUE_SETS == 1 ) { } #else { if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){ if( pxHigherPriorityTaskWoken != NULL ) { *pxHigherPriorityTaskWoken = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); }}else{ mtCOVERAGE_TEST_MARKER();} } else {mtCOVERAGE_TEST_MARKER(); } ( void ) uxPreviousMessagesWaiting; } #endif } else { configASSERT( cTxLock != queueINT8_MAX ); pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 ); } xReturn = pdPASS; } else { traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue ); xReturn = errQUEUE_FULL; } } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xReturn;}
xQueueGenericSendFromISR()函数代码分析:
中断级的入队函数基本与任务级差不多,大体流程不变,只是中断中不能阻塞,不能立马切换任务。
挑几个重要的地方讲解一下:
1.当允许入队的情况下,函数中会将消息一样的入队与任务中没有区别。
2.紧接着就会判断队列是否上锁,也就是之前任务级入队函数任务进入阻塞期间需要将队列上锁防止中断访问两个事件列表,如果队列上锁则只是增加cTxLock的值,等到队列解锁再来操作事件列表,如果队列没有上锁则与任务级入队函数一样的操作(判断是否有等待接收的任务,有则唤醒)。
3.当需要任务切换时,并不会立马切换任务,只是将pxHigherPriorityTaskWoken表示需要切换任务,等快退出中断服务函数时,再调用有关任务切换的函数。(详情用法后面任务入队与中断入队的区别再讲)
4.如果队列无空闲的写入位置,且不覆写,中断中绝不阻塞,即刻返回队列满错误。
(4).任务与中断中入队的区别
在FreeRTOS中,在任务与在中断中会分别实现一套API函数,中断中使用的函数的函数名含有后缀"FromISR"字样,表示"从 ISR 中调用该函数",这样做的目的是
1.中断是快进快出,执行的代码越少越好(不然会造成用户系统卡顿)。
2.在中断中是不能进入阻塞态,不然还怎么实现快进快出。
3.在中断中不能立马切换任务,因为任务切换是依靠PendSV中断来实现的,而PendSV中断是优先级最低的中断,因此在中断中只会标记需要任务切换,等到中断退出后才能进行真正的任务切换。
关于PendSV中断请参考:FreeRTOS-ARM架构深入理解
这里只是简单谈一下区别,后面有机会可以详细谈谈FreeRTOS的中断。
我们来对比任务级xQueueGenericSend()与中断级xQueueGenericSendFromISR()函数的区别有哪些。
1.首先参数不一样
2.在任务中调用xQueueGenericSend()当队列已满,则可以根据我们传入的xTicksToWait来决定任务阻塞等待的时间,而xQueueGenericSendFromISR()函数中若队列已满,则即刻返回入队失败错误。
3.当入队函数中解除阻塞的任务优先级大于当前任务优先级,则需要发起一次任务调度,xQueueGenericSend()函数中的处理是即刻调度,而xQueueGenericSendFromISR()函数中只是用pxHigherPriorityTaskWoken变量标记一下需要任务调度并不会立即进行任务切换。
为什么在中断中不能立即发生任务调度呢?
因为任务切换是依靠PendSV中断来实现的,而PendSV中断是优先级最低的中断,所以即使悬起了PendSV中断也不能打断当前中断而进行任务调度,因此在中断中只会标记需要任务切换。
那既然不能立马发生调度,那到底什么时候进行任务调度呢?
pxHigherPriorityTaskWoken参数使用例子如下图:
会先定义一个xHigherPriorityTaskWoken变量初始值为pdFALSE传变量地址到xQueueSendToBackFromISR函数中,若该函数中需要任务切换则会将xHigherPriorityTaskWoken变量的值赋值为pdTRUE ,然后在中断服务函数的末尾判断xHigherPriorityTaskWoken是否为pdTRUE ,若为pdTRUE 则发起一次任务调度(悬起PendSV中断)。
注意:就算是在中断服务函数的末尾来发起任务调度,也不会立即发送任务切换,原因就是发送任务调度的本质是悬起PendSV中断,则只要未退出该中断,则PendSV中断就发生不了(任务也无法切换)。
最后一个问题既然在中断内反正都不能发生调度,为啥非要到快退出中断的时候(中断服务函数的末尾)来判断是否要发生调度,为啥不直接在xQueueSendToBackFromISR()函数中直接调度?
原因就是为了节省时间,因为反正在中断中不能任务调度,何不到快要退出中断的时候再发起任务调度呢(因为中断服务函数中可能会多次调用xQueueSendToBackFromISR(),难道我们要多次去发起任务调度,结果是又不能真正实现任务切换,只会浪费时间罢了,所以FreeRTOS中才设计了一个xHigherPriorityTaskWoken的变量)
自此队列入队的函数全部讲完,其实到这队列的知识已经全部讲完,因为所谓的队列出队与入队是如出一辙,原理是一模一样,后面我直接贴源码了。
3.从队列读取消息(出队)
关于队列出队有四个函数如下图所示,与写队列不同,读队列只能从队列头部读取消息,且读取完消息可以选择删除消息,则可只读取消息而不删除,所谓删除消息将这个变量uxMessagesWaiting减1即可。
(1).队列出队 (任务级)
任务中出队API函数有两个xQueueReceive()与xQueuePeek() 它们的区别主要在于一个读取队列消息后会删除消息,而另外一个则只读队列不删除消息。
xQueueReceive()函数原型:
函数参数:
xQueue:待读取的队列
pvBuffer:存储读取到的消息的缓冲区
xTicksToWait:阻塞超时时间
函数返回值:
pdTRUE:读取成功
pdFALSE:读取失败
xQueueReceive()函数源码分析:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ){ BaseType_t xEntryTimeSet = pdFALSE; TimeOut_t xTimeOut; Queue_t * const pxQueue = 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 ) { prvCopyDataFromQueue( pxQueue, pvBuffer ); traceQUEUE_RECEIVE( pxQueue ); pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1; if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ) { queueYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } taskEXIT_CRITICAL(); return pdPASS; } else { if( xTicksToWait == ( TickType_t ) 0 ) { taskEXIT_CRITICAL(); traceQUEUE_RECEIVE_FAILED( pxQueue ); return errQUEUE_EMPTY; } else if( xEntryTimeSet == pdFALSE ) { vTaskInternalSetTimeOutState( &xTimeOut ); xEntryTimeSet = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } } taskEXIT_CRITICAL(); vTaskSuspendAll(); prvLockQueue( pxQueue ); if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) { if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) { traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue ); vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait ); prvUnlockQueue( pxQueue ); if( xTaskResumeAll() == pdFALSE ) { portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } else { prvUnlockQueue( pxQueue ); ( void ) xTaskResumeAll(); } } else { prvUnlockQueue( pxQueue ); ( void ) xTaskResumeAll(); if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) { traceQUEUE_RECEIVE_FAILED( pxQueue ); return errQUEUE_EMPTY; } else { mtCOVERAGE_TEST_MARKER(); } } } }
代码分析:
其实不用分析了,出队函数基本与入队函数是对称的,这里调用的几个重要函数在入队的时候已经详细讲解过,他们最大的区别就是一个是判断队列满一个是判断队列空,一个是操作xTasksWaitingToReceive列表一个是操作xTasksWaitingToSend列表。
这里唯一要讲的是消息如何出队(依靠pcReadFrom指针):
prvCopyDataFromQueue函数源码分析:
出队的方式就是头部出队
图像分析:
xQueuePeek() 函数原型与xQueueReceive()一致就不再赘述了:
xQueuePeek() 函数源码分析:
BaseType_t xQueuePeek( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ){ BaseType_t xEntryTimeSet = pdFALSE; TimeOut_t xTimeOut; int8_t * pcOriginalReadPosition; Queue_t * const pxQueue = 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.xQueue.pcReadFrom; prvCopyDataFromQueue( pxQueue, pvBuffer ); traceQUEUE_PEEK( pxQueue ); pxQueue->u.xQueue.pcReadFrom = pcOriginalReadPosition; if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ) { queueYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } taskEXIT_CRITICAL(); return pdPASS; } else { if( xTicksToWait == ( TickType_t ) 0 ) { taskEXIT_CRITICAL(); traceQUEUE_PEEK_FAILED( pxQueue ); return errQUEUE_EMPTY; } else if( xEntryTimeSet == pdFALSE ) { vTaskInternalSetTimeOutState( &xTimeOut ); xEntryTimeSet = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } } taskEXIT_CRITICAL(); vTaskSuspendAll(); prvLockQueue( pxQueue ); if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) { if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) { traceBLOCKING_ON_QUEUE_PEEK( pxQueue ); vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait ); prvUnlockQueue( pxQueue ); if( xTaskResumeAll() == pdFALSE ) { portYIELD_WITHIN_API(); } else { mtCOVERAGE_TEST_MARKER(); } } else { prvUnlockQueue( pxQueue ); ( void ) xTaskResumeAll(); } } else { prvUnlockQueue( pxQueue ); ( void ) xTaskResumeAll(); if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) { traceQUEUE_PEEK_FAILED( pxQueue ); return errQUEUE_EMPTY; } else { mtCOVERAGE_TEST_MARKER(); } } } }
代码分析:
xQueueReceive()与xQueuePeek()函数源码基本一致,这里主要分析他们的区别在哪。
1.xQueuePeek()只读取消息而不删除消息,而xQueueReceive()读完就删。
这里为什么要恢复pcReadFrom指针,因为在prvCopyDataFromQueue读取消息函数里面会移动pcReadFrom指针的位置,但我们只是读取消息并没有删除消息,我们需要恢复pcReadFrom原来的位置。
2.xQueuePeek()是操作的xTasksWaitingToReceive列表,而xQueueReceive()操作的是xTasksWaitingToSend列表。
其实仔细一想就能理解,xQueuePeek()读取完队列消息并没有删除消息,所以代表队列中有消息可以出队,当然是判断是否有等待队列消息的任务咯。
除了这两个区别其他没有啥区别。
(2).队列出队(中断级)
中断出队API函数有两个xQueueReceiveFromISR()与xQueuePeekFromISR() 它们的区别主要在于一个读取队列消息后会删除消息,而另外一个则只读队列不删除消息。
xQueueReceiveFromISR()函数原型:
函数参数:
xQueue:待读取的队列
pvBuffer:存储读取到的消息的缓冲区
pxHigherPriorityTaskWoken:需要任务切换标记
函数返回值:
pdTRUE:读取成功
pdFALSE:读取失败
xQueueReceiveFromISR()函数源码分析
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken ){ BaseType_t xReturn; UBaseType_t uxSavedInterruptStatus; Queue_t * const pxQueue = xQueue; configASSERT( pxQueue ); configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) ); portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting; if( uxMessagesWaiting > ( UBaseType_t ) 0 ) { const int8_t cRxLock = pxQueue->cRxLock; traceQUEUE_RECEIVE_FROM_ISR( pxQueue ); prvCopyDataFromQueue( pxQueue, pvBuffer ); pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1; if( cRxLock == queueUNLOCKED ) { if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ) { if( pxHigherPriorityTaskWoken != NULL ) {*pxHigherPriorityTaskWoken = pdTRUE; } else {mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { configASSERT( cRxLock != queueINT8_MAX ); pxQueue->cRxLock = ( int8_t ) ( cRxLock + 1 ); } xReturn = pdPASS; } else { xReturn = pdFAIL; traceQUEUE_RECEIVE_FROM_ISR_FAILED( pxQueue ); } } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xReturn;}
就不分析代码了,自己去看就OK
xQueuePeekFromISR函数源码分析:
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void * const pvBuffer ){ BaseType_t xReturn; UBaseType_t uxSavedInterruptStatus; int8_t * pcOriginalReadPosition; Queue_t * const pxQueue = xQueue; configASSERT( pxQueue ); configASSERT( !( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) ); configASSERT( pxQueue->uxItemSize != 0 ); portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { if( pxQueue->uxMessagesWaiting > ( UBaseType_t ) 0 ) { traceQUEUE_PEEK_FROM_ISR( pxQueue ); pcOriginalReadPosition = pxQueue->u.xQueue.pcReadFrom; prvCopyDataFromQueue( pxQueue, pvBuffer ); pxQueue->u.xQueue.pcReadFrom = pcOriginalReadPosition; xReturn = pdPASS; } else { xReturn = pdFAIL; traceQUEUE_PEEK_FROM_ISR_FAILED( pxQueue ); } } portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xReturn;}
三.总结
费了比较多的时间去写这篇文章,第一个队列确实非常重要,还是那句话想要学好FreeRTOS就必须去分析它的源码,源码都看懂了,原理真的是随便懂,相信学完队列这一章,FreeRTOS后面的学习会越来越轻松了,最后希望文章能够帮助大家,感谢大家支持一波,我才有更新的动力!!!
来源地址:https://blog.csdn.net/k666499436/article/details/128530538