FreeRTOS消息队列
本文主要基于消息队列的源码进行分析,来对FreeRTOS的消息队列进一步学习。
消息队列非常重要,因为后面的各种信号量基本都是基于队列的,搞清楚消息队列的源码,也就搞清楚消息队列的原理。
参考链接:FreeRTOS-消息队列详解_freertos消息队列-CSDN博客
目录
- FreeRTOS消息队列
- 一、消息队列
- 1. 消息队列的概念
- 2. 队列结构体
- 二、消息队列API函数源码分析
- 1. 创建队列
- 2. 向队列发送消息(入队)
- 队列入队(任务级)
- 队列锁
- 队列入队(中断级)
- 任务与中断中入队的区别
- 3. 从队列中读取消息(出队)
- 队列出队(任务级)
- 队列出队(中断级)
一、消息队列
1. 消息队列的概念
消息队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息(而这个消息可以是任意类型的数据),任务能够从队列里面读取消息,也能够向队列发送消息。
基于队列,FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、递归互斥信号量,所以掌握队列就显得十分重要。
一个队列可以很多任务来写队列也可以很多任务来读队列。但是并不能两个队列同时来写或读队列。队列属于临界资源,是通过关中断的方式来保证队列同一时间只能一个任务进行读写。因为关中断:任务无法切换,且一些中断也无法来干扰。
- 一般情况下队列消息是先进先出方式排队(当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据),但同时 FreeRTOS的队列也支持将数据写入到队列的头部,并且还可以指定是否覆盖先前已经在队列头部的数据。(后面会详细讲解pcHead、pcTail、pcWriteTo、pcReadFrom的指向关系,明白了这些就明白了队列环形缓冲区)
- 队列传输数据时有两种方法:1. 直接拷贝数据 2.拷贝数据的地址,然后根据地址读取数据。 第二种方法适合传输大数据比如一个大数组, 或者一个结构体变量。
- 队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。
因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。 - 读写队列均支持阻塞机制
(以读队列为例:在任务从队列读取消息时,可以指定一个阻塞超时时间。如果队列不为空则会读取队列中第一个消息,如果队列为空,则看我们自己设置阻塞时间(1.阻塞时间为0,表示不等待,直接返回队列空错误;2.阻塞时间不为0,则等待给定的阻塞时间,如果这个时间内有消息则唤醒然后读取,没有消息则唤醒,返回队列空错误;3.阻塞时间为最大,则死等,一直阻塞直到队列中有消息) - 当在中断中读写队列时,如果队列空或满,不会进行阻塞,直接返回队列空或队列满错误,因为中断要的就是快进快出。
2. 队列结构体
typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
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; /*< 队列将保存的每个队列项的大小(单位为字节)。*/
volatile int8_t cRxLock; /*< 存储队列锁定时,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED。 */
volatile int8_t cTxLock; /*< 存储队列锁定时,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED。 */
#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*< 如果队列使用的内存是静态分配的,则设置为 pdTRUE,以确保不尝试释放内存。*/
#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;
typedef struct QueuePointers
{
int8_t *pcTail; //队列存储区域的结束地址,与pcHead一样一个指向开始地址一个指向结束地址
int8_t *pcReadFrom; // 最后一次读取队列的位置
} QueuePointers_t;
typedef struct SemaphoreData
{
TaskHandle_t xMutexHolder; // 互斥信号量的持有者
UBaseType_t uxRecursiveCallCount; //递归互斥信号量被递归获取计数器
} SemaphoreData_t;
- pcHead:指向队列消息存储区起始位置,即第一个消息空间。
- pcWriteTo :指向队列消息存储区下一个可用消息空间。(一般就是从pcWriteTo 位置入队消息即尾插)。
- 一个联合体变量: 当用于队列时使用xQueue结构体变量,当用于信号量 时使用xSemaphore结构体变量
xTasksWaitingToSend :发送消息阻塞列表,看英文意思也知道:等待发送的任务,也就是队列已满,任务想要发送消息到队列(入队),如果设定了阻塞时间,任务就会挂入该列表,表示任务已阻塞,任务会按照优先级进行排序(后面解除阻塞就是按照任务的优先级:当队列不为满了,xTasksWaitingToSend 列表中优先级高的就会先被唤醒)。 - xTasksWaitingToReceive:等待消息阻塞列表,看英文意思也知道:等待接收的任务,也就是队列已空,任务想要从队列中读取消息(出队),如果设定了阻塞时间,任务就会挂入该列表,表示任务已阻塞,任务会按照优先级进行排序(后面解除阻塞就是按照任务的优先级:当队列不为空了,xTasksWaitingToReceive列表中优先级高的就会先被唤醒)。
- uxMessagesWaiting:用于记录当前消息队列的消息个数,如果消息队列被用于信号量的时候,这个值就表示有效信号量个数。
- uxLength:表示队列的长度,表示一共能存放多少消息。
- uxItemSize:表示单个消息的大小(单位为字节)。
- cRxLock:队列上锁后,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED。
- cTxLock:队列上锁后,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED。
- ucQueueType:队列的类型,信号量都是基于队列的。
/* For internal use only. These definitions *must* match those in queue.c. */
#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 ) // 递归互斥信号量
二、消息队列API函数源码分析
1. 创建队列
主要讲解队列的动态创建,队列的创建xQueueCreate实际上调用的xQueueGenericCreate函数,xQueueGenericCreate函数相比起xQueueCreate函数,多了一项queueQUEUE_TYPE_BASE,就是用于区分不同的类型。因此xQueueGenericCreate是通用的队列创建函数。
队列的动态创建主要是进行:
- 队列结构体和缓冲区的内存分配
- 初始化缓冲区的参数
- 两条阻塞链表的初始化或者是旧队列的重置(将阻塞于旧队列的写任务恢复,因为旧队列此时被重置了)
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( 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;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
configASSERT( uxQueueLength > ( UBaseType_t ) 0 ); /* 检查队列长度是否大于0 */
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
/* 为队列申请内存空间:队列结构体+队列环形存储区域 */
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );
if( pxNewQueue != NULL ) /* 内存申请成功 */
{
/* 获取队列环形存储区域的起始地址(跳过队列结构体) */
pucQueueStorage = ( uint8_t * ) pxNewQueue;
pucQueueStorage += sizeof( Queue_t ); // pucQueueStorage为缓冲区的首地址
/* 此宏用于启用支持静态内存管理 */
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
/* 此宏用于启用支持静态内存管理,以防静态地理以后被删除 */
pxNewQueue->ucStaticallyAllocated = pdFALSE;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
/* 初始化队列 */
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
}
else
{
traceQUEUE_CREATE_FAILED( ucQueueType );
mtCOVERAGE_TEST_MARKER();
}
return pxNewQueue; /* 返回队列句柄(队列结构体地址) */
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
调用prvInitialiseNewQueue函数进行初始化队列:
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, uint8_t *pucQueueStorage, const uint8_t ucQueueType, Queue_t *pxNewQueue )
{
/* 防止编译器警告(可能用不到这个ucQueueType参数) */
( void ) ucQueueType;
if( uxItemSize == ( UBaseType_t ) 0 )
{
/* 如果队列项目大小为 0(类型为信号量),那么就不需要存储空间
让pcHead指向一个有效区域就行,这里指向队列结构体起始地址 */
pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
}
else
{
/* 将pcHead设置为队列环形存储区域的起始地址. */
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 */
/* 此宏用于使能使用队列集 */
#if ( configUSE_QUEUE_SETS == 1 )
{
/* 队列所在队列集设为空 */
pxNewQueue->pxQueueSetContainer = NULL;
}
#endif /* configUSE_QUEUE_SETS */
/* 用于调试 */
traceQUEUE_CREATE( pxNewQueue );
}
xQueueGenericReset重置队列函数:
主要初始化缓冲区的指针pcTail、pcWriteTo,判断是否是重置队列还是初始化新队列,如果是重置队列,那么要把旧队列中,因缓冲区满而等待写入的任务唤醒(唤醒写入阻塞队列的优先级最高的任务);如果是新队列,那么初始化两个阻塞链表(阻塞写入和阻塞读出队列)
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 ) &&
/* Check for multiplication overflow. */
( ( SIZE_MAX / pxQueue->uxLength ) >= pxQueue->uxItemSize ) )
{
taskENTER_CRITICAL();
/* pcTail:队列存储区域的结束地址 */
pxQueue->u.xQueue.pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize );
/* uxMessagesWaiting:队列中现有消息数量 */
pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;
/* pcWriteTo:下一个写入的位置 */
pxQueue->pcWriteTo = pxQueue->pcHead;
/* pcReadFrom:最后一次读取的位置 */
pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - 1U ) * pxQueue->uxItemSize );
/* 消息队列没有上锁,设置为 queueUNLOCKED=-1 */
pxQueue->cRxLock = queueUNLOCKED;
pxQueue->cTxLock = queueUNLOCKED;
/* xNewQueue为pdFALSE表示队列为旧队列 */
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();
}
else
{
xReturn = pdFAIL;
}
configASSERT( xReturn != pdFAIL );
return xReturn;
}
调用了xTaskRemoveFromEventList函数实现唤醒写入阻塞任务队列的最高优先级的任务:
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList )
{
TCB_t * pxUnblockedTCB;
BaseType_t xReturn;
/* 事件列表按优先级顺序排序,因此可以删除列表中的第一个,因为它已知优先级最高。
* 从延迟列表中删除TCB,并将其添加到就绪列表中
* 如果事件是针对已锁定的队列,则此函数将永远不会被调用-队列上的锁定计数将被修改。
* 这意味着此处保证对事件列表的独占访问权限。 */
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 )
{
/* 如果从事件列表中删除的任务的优先级高于调用任务,则返回 true
表明需要进行一次任务切换*/
xReturn = pdTRUE;
/* 标记收益处于挂起状态,
以防用户未将“xHigherPriorityTaskWoken”参数用于 ISR 安全 FreeRTOS 函数 */
xYieldPending = pdTRUE;
}
else
{
xReturn = pdFALSE;
}
return xReturn;
}
- 首先将任务从事件列表(xTasksWaitingToSend或xTasksWaitingToReceive)中移除,该任务是列表中优先级最高的。
- 将任务从事件列表移除后,此时任务并没有被真正唤醒,因为任务还被挂入了延时列表中
- 根据调度器是否被挂起,进行不同的操作:如果调度器没有被挂起,那么将任务放到就绪链表中;如果调度器挂起,就把任务放在等待放入就绪的xPendingReadyList链表中,等调度器恢复后,任务才会被放入就绪链表。
2. 向队列发送消息(入队)
FreeRTOS的队列入队提供很多接口函数,其实主要是xQueueGenericSend()与xQueueGenericSendFromISR()函数为实际执行函数,一个是在任务中调用的,一个是在中断中调用的,其中又因为入队的方式又分成几个函数(尾部入队,头部入队),最后还有一个特殊情况,覆写入队,这种方式下队列只能有一个消息,每次写入都会覆盖上一个消息的内容。
队列入队(任务级)
在任务中往队列写入消息的函数有函数 xQueueSend() 、 xQueueSendToBack() 、xQueueSendToFront()、xQueueOverwrite(),虽然有四个,但是这些函数实际上都是宏定义,四个函数最终调用的是xQueueGenericSend()函数,只不过传入的参数不一样即入队的方式会有所差异而已。
因此只需要分析xQueueGenericSend函数即可
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, // 要写入的队列
const void * const pvItemToQueue, // 要写入的消息
TickType_t xTicksToWait, // 阻塞超时时间
const BaseType_t xCopyPosition //要写入的位置)
/*
* 函数返回值:
* 1.pdTRUE:写入成功
* 2.errQUEUE_FULL:队列满,写入失败
*/
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 );
/* 检查参数的合法性:要写入数据地址不为NULL,消息的大小uxItemSize不为0 */
configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
/* 限制:当队列写入方式是覆写入队,队列的长度必须为1 */
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 /* configUSE_QUEUE_SETS */
{
/* 将消息拷贝到队列的环形存储区的指定位置(即消息入队) */
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 /* configUSE_QUEUE_SETS */
/* 开中断(退出临界区) */
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
{
/* Entry time was already set. */
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;
}
} /*lint -restore */
}
prvCopyDataToQueue本质上其实就是使用memcpy往队列缓冲区里面写数据,实际上操作的是那几个指针,pcWriteto、pcReadFrom指针,其实也就是用双指针去维护缓冲区,本质上是用指针模拟双端队列。
队列入队总结:
抛开一些细枝末节的东西,队列入队大致分为几个步骤:
1. 首先要入队判断是否可以入队(队列有空闲位置或者覆写入队方式时即可入队)
2. 若可以入队,则拷贝要入队的消息到指定的队列存储区的位置(入队方式不同,入队位置略有差异),然后判断是否有任务在等待读队列,如果有则立马唤醒它,如果唤醒的任务的优先级大于当前任务则立即切换任务,最后返回入队成功。
3. 若不能入队(队列已满(不为覆写入队)),则需要判断任务是否需要阻塞:
(1): 若设置的超时时间为0,则即刻返回队列已满错误(入队失败)。
(2): 若设置的超时时间不为0(但不为最大值),则经过时间补偿,判断是否真的需要阻塞,需要则将任务阻塞,若过了超时时间队列还未有空闲位置,则即可返回队列已满错误(入队失败)。
(3): 若设置的超时时间为portMAX_DELAY,则一直等待直到等到队列有空闲位置可以写入,即可唤醒。
在(2)中,任务阻塞其实被挂入了两条链表,一条是等待写入的链表xTasksWaitingToSend,一条是延时链表。
后面的出队其实原理是一模一样的,只不过一个判断队列是否为满,一个判断队列是否为满,一个判断是否唤醒在等到读队列的任务,一个是判断是否唤醒在等到写队列的任务
队列锁
以写队列为例,当前任务因队列满而进入阻塞,在进入阻塞期间,系统会关闭调度器和给队列上锁。关闭调度器是避免其他任务修改这个队列,队列锁是避免中断修改这个队列的两个链表(等待发送链表和等待接受链表),但允许中断给队列缓冲区读写数据。
为什么不一刀切,直接进入临界区,让任务和中断都不能访问队列呢?个人的理解还是提升效率,尽量减少临界区。
允许中断给队列缓冲区读写数据,但是不允许修改队列的两个链表,不允许修改链表的原因是:避免在中断中修改两个链表,修改链表中其他任务的状态,这有可能会导致优先级翻转。假设当前正在进入阻塞的任务(因队列满而阻塞,但还没阻塞,还没放入等待发送链表)优先级高于在等待发送链表中的其他任务,如果在中断中读队列,修改了等待发送链表,企图唤醒其他等待发送链表中的任务,那么这就相当于唤醒了低优先级的任务,本应该是唤醒当前正在阻塞的任务。
简单来讲就是去避免中断在当前任务进入阻塞期间来操作这个队列的 xTasksWaitingToReceive 列表或者 xTasksWaitingToSend 列表。
因为挂起调度器只能禁止任务切换,也就是说可以避免其他任务来操作队列。但挂起调度器并不会禁止中断的发生,中断服务函数中仍然可以操作队列事件列表,可能会解除任务阻塞、就可能会进行任务切换,这是不允许的。所以给队列上锁就是防止中断来操作事件链表。
- cRxLock:队列上锁时,从队列接收(从队列中删除)的出队项目数。 如果队列没有上锁,设置为queueUNLOCKED值为-1。
- cTxLock::队列上锁时,传输到队列(添加到队列)的入队项目数。 如果队列没有上锁,设置为queueUNLOCKED值为-1。
- prvLockQueue 给队列上锁这个宏很简单只需,将cRxLock,cTxLock赋值成queueLOCKED_UNMODIFIED(0),则就算给队列上锁了。
prvUnlockQueue( pxQueue );
prvLockQueue( pxQueue );
#define prvLockQueue( pxQueue ) \
taskENTER_CRITICAL(); \
{ \
if( ( pxQueue )->cRxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED; \
} \
if( ( pxQueue )->cTxLock == queueUNLOCKED ) \
{ \
( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED; \
} \
} \
taskEXIT_CRITICAL()
/*-----------------------------------------------------------*/
static void prvUnlockQueue( Queue_t * const pxQueue )
{
/* 必须在计划程序挂起的情况下调用此函数 */
/* 锁定计数包含在队列锁定时在队列中放置或删除(入队或出队)的消息的数量。
当队列被锁定时,可以添加或删除消息,但无法更新事件列表。 */
taskENTER_CRITICAL();
{
/* 获取队列的写入上锁计数器 */
int8_t cTxLock = pxQueue->cTxLock;
/* 判断队列在上锁期间是否被写入消息 */
while( cTxLock > queueLOCKED_UNMODIFIED )
{
/* Data was posted while the queue was locked. Are any tasks
* blocked waiting for data to become available? */
#if ( configUSE_QUEUE_SETS == 1 )
{
/* 关于队列集的代码省略 */
}
#else /* configUSE_QUEUE_SETS */
{
/* 从事件列表中删除的任务将添加到等待就绪列表中,
因为调度器被挂起了不能直接访问就绪列表 */
/* 判断队列的等待读取事件任务列表是否不为空 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
{
/* 将等待读取事件列表中的最高优先级的任务解除阻塞 */
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
{
/* 如果被解除阻塞的任务的优先级更高,因此记录需要任务切换 */
vTaskMissedYield();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
break;
}
}
#endif /* configUSE_QUEUE_SETS */
/* 处理完一个读取阻塞任务后,
更新队列写入上锁计数器,直到写入解锁为止 */
--cTxLock;
}
/* 设置队列写入解锁 */
pxQueue->cTxLock = queueUNLOCKED;
}
taskEXIT_CRITICAL();
/* 对 cRxLock 锁执行相同的操作 */
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 这两个事件列表不被访问,在队列解锁之后补回来就行。
队列入队(中断级)
其中在中断中使用入队函数也有4个,xQueueSendFromISR() 、
xQueueSendToBackFromISR()、xQueueSendToFrontFromISR()、xQueueOverwriteFromISR(),与任务级一样他们也都是宏定义,真正执行的的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 ) ) );
/* 这里限制了只有在队列长度为 1 时,才能使用覆写 */
configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );
/* 只有受 FreeRTOS 管理的中断才能调用该函数 */
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
/* 队列没有空闲位置时不会阻塞在这里也不会被阻塞
而且返回错误, 也不要直接唤醒在阻塞的等待接收的任务
而是返回一个标志来说明是否需要上下文切换 */
/* 屏蔽受 FreeRTOS 管理的中断,并保存,屏蔽前的状态,用于恢复 */
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 /* configUSE_QUEUE_SETS */
{
/* 队列有阻塞的读取任务 */
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 /* configUSE_QUEUE_SETS */
}
/* 队列写入已被上锁 */
else
{
/* 增加锁定计数,以便解锁队列的任务
知道数据是在锁定时发布的 */
configASSERT( cTxLock != queueINT8_MAX );
/* 上锁次数加 1 **/
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;
}
/*-----------------------------------------------------------*/
中断级的入队函数基本与任务级差不多,大体流程不变
如果队列满,那么直接返回队列满错误,不会发生阻塞。
如果队列不满,那么将数据写入队列缓冲区,如果队列上锁,那就修改上锁次数cTxLock,返回,在队列解锁函数中去处理;如果队列没有上锁,并且等待接受链表不空,那么修改等待接受链表头部任务的状态,然后将pxHigherPriorityTaskWoken变量置为pdTRUE,表示需要发起调度。
任务与中断中入队的区别
在FreeRTOS中,在任务与在中断中会分别实现一套API函数,中断中使用的函数的函数名含有后缀"FromISR"字样,表示"从 ISR 中调用该函数",这样做的目的是
1.中断是快进快出,执行的代码越少越好(不然会造成用户系统卡顿)。
2.在中断中是不能进入阻塞态,不然还怎么实现快进快出。
3.在中断中不能立马切换任务,因为任务切换是依靠PendSV中断来实现的,而PendSV中断是优先级最低的中断,因此在中断中只会标记需要任务切换,等到中断退出后才能进行真正的任务切换。
在任务中调用xQueueGenericSend()当队列已满,则可以根据我们传入的xTicksToWait来决定任务阻塞等待的时间,而xQueueGenericSendFromISR()函数中若队列已满,则即刻返回入队失败错误。
当入队函数中解除阻塞的任务优先级大于当前任务优先级,则需要发起一次任务调度,xQueueGenericSend()函数中的处理是即刻调度,而xQueueGenericSendFromISR()函数中只是用pxHigherPriorityTaskWoken变量标记一下需要任务调度并不会立即进行任务切换。
为什么在中断中不能立即发生任务调度呢?
因为任务切换是依靠PendSV中断来实现的,而PendSV中断是优先级最低的中断,所以即使悬起了PendSV中断也不能打断当前中断而进行任务调度,因此在中断中只会标记需要任务切换。
那既然不能立马发生调度,那到底什么时候进行任务调度呢?
pxHigherPriorityTaskWoken参数使用例子如下图:
会先定义一个xHigherPriorityTaskWoken变量初始值为pdFALSE传变量地址到xQueueSendToBackFromISR函数中,若该函数中需要任务切换则会将xHigherPriorityTaskWoken变量的值赋值为pdTRUE ,然后在中断服务函数的末尾判断xHigherPriorityTaskWoken是否为pdTRUE ,若为pdTRUE 则发起一次任务调度(悬起PendSV中断)。
注意:就算是在中断服务函数的末尾来发起任务调度,也不会立即发送任务切换,原因就是发送任务调度的本质是悬起PendSV中断,则只要未退出该中断,则PendSV中断就发生不了(任务也无法切换)。
最后一个问题既然在中断内反正都不能发生调度,为啥非要到快退出中断的时候(中断服务函数的末尾)来判断是否要发生调度,为啥不直接在xQueueSendToBackFromISR()函数中直接调度?
原因就是为了节省时间,因为反正在中断中不能任务调度,何不到快要退出中断的时候再发起任务调度呢(因为中断服务函数中可能会多次调用xQueueSendToBackFromISR(),难道我们要多次去发起任务调度,结果是又不能真正实现任务切换,只会浪费时间罢了,所以FreeRTOS中才设计了一个xHigherPriorityTaskWoken的变量)
3. 从队列中读取消息(出队)
所谓的队列出队与入队是如出一辙,原理是一模一样,基本是对称的思想,就不再详细讲解,只贴代码注释
队列出队(任务级)
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait )
{
BaseType_t xEntryTimeSet = pdFALSE;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = xQueue;
/* Check the pointer is not NULL. */
configASSERT( ( pxQueue ) );
/* 仅当数据大小为零时,接收数据的缓冲区才能为 NULL(因此不会将数据复制到缓冲区中)). */
configASSERT( !( ( ( pvBuffer ) == NULL ) && ( ( pxQueue )->uxItemSize != ( UBaseType_t ) 0U ) ) );
/* Cannot block if the scheduler is suspended. */
#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;
/* 读取完消息则队列有空闲可以入队,则判断是否有任务在等待发送消息给队列
如果有则解除xTasksWaitingToSend中优先级最高的任务的阻塞 */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
/* 解除xTasksWaitingToSend列表中优先级最高的任务的阻塞 */
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 )
{
/* 当我们设置的超时时间xTicksToWait为0
则任务不需要阻塞,立即返回队列为空错误*/
taskEXIT_CRITICAL();
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
else if( xEntryTimeSet == pdFALSE )
{
/* 队列空,并指定了阻塞时间(则任务需要阻塞),
所以需要记录下此时系统节拍计数器的值和溢出次数
用于下面对阻塞时间进行补偿 */
vTaskInternalSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
/* Entry time was already set. */
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();
}
}
} /*lint -restore */
}
/*-----------------------------------------------------------*/
队列出队(中断级)
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 ) ) );
/* 只有受 FreeRTOS 管理的中断才能调用该函数 */
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
/* 屏蔽受 FreeRTOS 管理的中断,并保存,屏蔽前的状态,用于恢复 */
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 );
/* 队列消息数减1 */
pxQueue->uxMessagesWaiting = uxMessagesWaiting - ( UBaseType_t ) 1;
/* 如果队列被锁定,则不会修改事件列表。而是更新锁定计数,
以便解锁队列的任务知道 ISR 在队列锁定时已删除数据。 */
/* 判断队列的读取是否上锁 */
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;
}
/*-----------------------------------------------------------*/