参考教程:【正点原子】手把手教你学FreeRTOS实时系统_哔哩哔哩_bilibili
一、队列介绍
1、概述
(1)队列是任务到任务、任务到中断、中断到任务数据交流的一种机制,它不同于全局变量。假设有一个全局变量a,现有两个任务都在写这个变量a,如下所示,变量自增分为三个步骤,如果在任务1读数据以后、修改数据以前发生任务切换,这将导致任务2和任务1读取相同的数据,并且基于相同的数据做相同的修改,这显然是有问题的,而使用队列可以避免这种问题(指访问冲突)。
(2)FreeRTOS基于队列实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、递归互斥信号量,并且读写队列做好了保护(主要依靠临界区),防止多任务同时访问冲突,用户只需要直接调用API函数即可。
(3)在队列中可以存储数量有限、大小固定的数据,队列中的每一个数据叫做"队列项目",队列能够存储"队列项目"的最大数量称为队列的长度,在创建队列时,用户要指定队列长度以及队列项目的大小。
(4)FreeRTOS队列的特点:
①数据入队出队方式:队列通常采用"先进先出"(FIFO)的数据存储缓冲机制,即先入队的数据会先从队列中被读取;FreeRTOS中也可以配置为"后进先出"(LIFO)方式。
②数据传递方式:FreeRTOS中队列可采用实际值传递,即将数据拷贝到队列中进行传递, 也可以采用指针传递,在传递较大的数据时,将数据的地址拷贝到队列中进行传递。
③队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息。
④当任务向一个队列发送消息时,可以指定一个阻塞时间。
假设此时队列已满无法入队(入队阻塞):
[1]若阻塞时间为0,其它任务打算让数据入队时直接无法入队,并返回errQUEUE_FULL。
[2]若阻塞时间为0~port_MAX_DELAY,其它任务打算让数据入队时会等待设定的阻塞时间(期间任务将会进入阻塞态),若在该时间内还无空位可入队,超时后直接返回errQUEUE_FULL,不再等待。
[3]若阻塞时间为port_MAX_DELAY,其它任务打算让数据入队时会死等(期间任务将会进入阻塞态),一直等到可以入队为止。
假设此时队列已空无法出队(出队阻塞):
[1]若阻塞时间为0,其它任务打算让数据出队时直接无法出队,并返回pdFALSE。
[2]若阻塞时间为0~port_MAX_DELAY,其它任务打算让数据出队时会等待设定的阻塞时间(期间任务将会进入阻塞态),若在该时间内还无数据可出队,超时后直接返回pdFALSE,不再等待。
[3]若阻塞时间为port_MAX_DELAY,其它任务打算让数据出队时会死等(期间任务将会进入阻塞态),一直等到可以出队为止。
当多个任务写入消息给一个"满队列"时,这些任务都会进入阻塞状态,也就是说有多个任务在等待同一个队列的空间,当队列中有空间时,优先级最高的任务会最先进入就绪态并获得队列"使用权",如果诸任务的优先级相同,那等待时间最久的任务会进入就绪态。
(5)队列操作基本过程举例:
2、队列的数据结构
(1)队列的结构体:
cpp
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; //写入上锁计数器
/* 其他的一些条件编译 */
} xQUEUE;
(2)上面的结构体可用于队列,也可用于互斥信号量和递归互斥信号量。
①当用于队列使用时:
cpp
typedef struct QueuePointers
{
int8_t * pcTail; /* 存储区的结束地址 */
int8_t * pcReadFrom; /* 最后一个读取队列的地址 */
}QueuePointers_t;
②当用于互斥信号量和递归互斥信号量时:
cpp
typedef struct SemaphoreData
{
TaskHandle_t xMutexHolder; /* 互斥信号量持有者 */
UBaseType_t uxRecursiveCallCount; /* 递归互斥信号量的获取计数器 */
}SemaphoreData_t;
(3)队列结构体整体示意图:
3、队列相关API函数介绍
(1)FreeRTOS基于队列实现了多种功能,每一种功能对应一种队列类型,队列类型在 queue.h文件中有定义:
cpp
#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 ) //递归互斥信号量
(2)创建队列相关API函数介绍:
|----------------------|----------|
| 函数 | 描述 |
| xQueueCreate() | 动态方式创建队列 |
| xQueueCreateStatic() | 静态方式创建队列 |
①动态和静态创建队列之间的区别:动态创建队列所需的内存空间由FreeRTOS 从FreeRTOS管理的堆中分配,而静态创建需要用户自行分配内存。
②动态方式创建队列函数xQueueCreate的宏函数接口:
cpp
#define xQueueCreate (uxQueueLength, uxItemSize) \
xQueueGenericCreate ((uxQueueLength),(uxItemSize),(queueQUEUE_TYPE_BASE ))
[1]函数参数:
|---------------|---------|
| 形参 | 描述 |
| uxQueueLength | 队列长度 |
| uxItemSize | 队列项目的大小 |
[2]返回值:
|------|---------------|
| 返回值 | 描述 |
| NULL | 队列创建失败 |
| 其它值 | 队列创建成功,返回队列句柄 |
(3)往队列写入消息相关API函数介绍:
|----------------------------|--------------------------|
| 函数 | 描述 |
| xQueueSend() | 往队列的尾部写入消息 |
| xQueueSendToBack() | 同 xQueueSend() |
| xQueueSendToFront() | 往队列的头部写入消息 |
| xQueueOverwrite() | 覆写队列消息(只用于队列长度为1的情况) |
| xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
| xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
| xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
| xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为1的情况) |
①上述8个函数可分为任务级与中断级。
②队列一共有3个写入位置,定义如下:
cpp
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) /* 写入队列尾部 */
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) /* 写入队列头部 */
#define queueOVERWRITE ( ( BaseType_t ) 2 ) /* 覆写队列*/
//覆写方式写入队列,只有在队列的队列长度为1时才能够使用
③任务级写入函数的宏函数接口(中断级暂时不做介绍):
cpp
#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)
这几个写入函数调用的是同一个函数xQueueGenericSend,只是指定了不同的写入位置。
④xQueueGenericSend函数的入口参数及返回值解析:
cpp
BaseType_t xQueueGenericSend
(
QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition
)
[1]函数参数:
|---------------|----------|
| 形参 | 描述 |
| xQueue | 待写入的队列 |
| pvItemToQueue | 待写入消息的地址 |
| xTicksToWait | 阻塞超时时间 |
| xCopyPosition | 写入的位置 |
[2]返回值:
|---------------|--------|
| 返回值 | 描述 |
| pdTRUE | 队列写入成功 |
| errQUEUE_FULL | 队列写入失败 |
(4)从队列读取消息相关API函数介绍:
|------------------------|----------------------------------|
| 函数 | 描述 |
| xQueueReceive() | 从队列头部读取消息,消息读取成功后,会将消息从队列中移除 |
| xQueuePeek() | 从队列头部读取消息 |
| xQueueReceiveFromISR() | 在中断中从队列头部读取消息,消息读取成功后,会将消息从队列中移除 |
| xQueuePeekFromISR() | 在中断中从队列头部读取消息 |
①上述4个函数可分为任务级与中断级,这里暂时仅介绍任务级的函数。
②xQueueReceive函数的入口参数及返回值解析:
cpp
BaseType_t xQueueReceive
(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
)
[1]函数参数:
|--------------|------------|
| 形参 | 描述 |
| xQueue | 待读取的队列 |
| pvBuffer | 信息读取缓冲区的地址 |
| xTicksToWait | 阻塞超时时间 |
[2]返回值:
|---------|------|
| 返回值 | 描述 |
| pdTRUE | 读取成功 |
| pdFALSE | 读取失败 |
③xQueuePeek函数的入口参数及返回值解析:
cpp
BaseType_t xQueuePeek
(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
)
[1]函数参数:
|--------------|------------|
| 形参 | 描述 |
| xQueue | 待读取的队列 |
| pvBuffer | 信息读取缓冲区的地址 |
| xTicksToWait | 阻塞超时时间 |
[2]返回值:
|---------|------|
| 返回值 | 描述 |
| pdTRUE | 读取成功 |
| pdFALSE | 读取失败 |
二、常用队列相关API函数的源码剖析
1、xQueueCreate函数
(1)如上所述,该函数用于创建队列(动态方法)。
(2)xQueueCreate函数的底层实际上是xQueueGenericCreate函数,相比之下xQueueGenericCreate函数多出一个参数------queueQUEUE_TYPE_BASE,它表示创建的队列为普通队列(而非队列集、互斥信号量等)。
cpp
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xQueueCreate(uxQueueLength, uxItemSize) xQueueGenericCreate((uxQueueLength), (uxItemSize), (queueQUEUE_TYPE_BASE))
#endif
(3)xQueueGenericCreate函数的实现:
cpp
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;
traceENTER_xQueueGenericCreate( uxQueueLength, uxItemSize, ucQueueType );
if( ( uxQueueLength > ( UBaseType_t ) 0 ) && //判断队列长度是否合法,即大于0
( ( SIZE_MAX / uxQueueLength ) >= uxItemSize ) &&
( ( UBaseType_t ) ( SIZE_MAX - sizeof( Queue_t ) ) >= ( uxQueueLength * uxItemSize ) ) ) //判断是否会出现溢出的情况
{
xQueueSizeInBytes = (size_t) ( (size_t)uxQueueLength * (size_t) 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 /* configSUPPORT_STATIC_ALLOCATION */
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue ); //初始化队列结构体成员变量
}
else
{
traceQUEUE_CREATE_FAILED( ucQueueType );
mtCOVERAGE_TEST_MARKER();
}
}
else
{
configASSERT( pxNewQueue );
mtCOVERAGE_TEST_MARKER();
}
traceRETURN_xQueueGenericCreate( pxNewQueue );
return pxNewQueue; //返回队列结构体首地址
}
(4)xQueueGenericCreate函数中调用到的函数剖析:
①prvInitialiseNewQueue函数的实现:
cpp
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 ); //传入pdTRUE是为了初始化接收列表WaitingToReceive和发送列表xTasksWaitingToSend
#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函数的实现:
cpp
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
BaseType_t xReturn = pdPASS;
Queue_t * const pxQueue = xQueue;
traceENTER_xQueueGenericReset( xQueue, xNewQueue );
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 ) );
//初始化接收列表WaitingToReceive
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
//初始化发送列表xTasksWaitingToSend
}
}
taskEXIT_CRITICAL(); //开中断(退出临界区)
}
else
{
xReturn = pdFAIL;
}
configASSERT( xReturn != pdFAIL );
traceRETURN_xQueueGenericReset( xReturn );
return xReturn;
}
2、xQueueSend函数
(1)如上所述,该函数用于往队列中写数据。
(2)xQueueSend函数的底层实际上是xQueueGenericSend函数,相比之下xQueueGenericSend函数多出一个参数------queueSEND_TO_BACK,它表示从队列尾部写入数据。
cpp
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend((xQueue), (pvItemToQueue), (xTicksToWait), queueSEND_TO_BACK)
(3)xQueueGenericSend函数的实现:
cpp
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;
traceENTER_xQueueGenericSend( xQueue, pvItemToQueue, xTicksToWait, xCopyPosition );
configASSERT( pxQueue );
configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) ); //如果是覆写方式,需要队列长度为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 ) //使用队列集,编译如下代码块
{
const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting;
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
if( pxQueue->pxQueueSetContainer != NULL )
{
if( ( xCopyPosition == queueOVERWRITE ) && ( uxPreviousMessagesWaiting != ( UBaseType_t ) 0 ) )
{
mtCOVERAGE_TEST_MARKER();
}
else if( prvNotifyQueueSetContainer( pxQueue ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
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();
}
}
}
#else /* configUSE_QUEUE_SETS */ //使用队列方式,编译如下代码块
{
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); //复制数据pvItemToQueue到队列pxQueue的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(); //开中断(退出临界区)
traceRETURN_xQueueGenericSend( pdPASS );
return pdPASS;
}
else
{
//如果队列已满
if( xTicksToWait == ( TickType_t ) 0 ) //如果设定的阻塞时间为0,则不需要阻塞,直接返回写错误errQUEUE_FULL
{
taskEXIT_CRITICAL(); //开中断(退出临界区)
traceQUEUE_SEND_FAILED( pxQueue );
traceRETURN_xQueueGenericSend( errQUEUE_FULL );
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 ) //如需执行任务切换,执行即可
{
taskYIELD_WITHIN_API();
}
}
else
{
prvUnlockQueue( pxQueue ); //队列解锁
( void ) xTaskResumeAll(); //恢复任务调度器
}
}
else
{
prvUnlockQueue( pxQueue ); //队列解锁
( void ) xTaskResumeAll(); //恢复任务调度器
traceQUEUE_SEND_FAILED( pxQueue );
traceRETURN_xQueueGenericSend( errQUEUE_FULL );
return errQUEUE_FULL; //返回队列满的错误信息
}
}
}
(4)xQueueSend函数中调用到的函数剖析:
①prvCopyDataToQueue函数的实现:
cpp
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) //队列项数据为零代表队列是信号量
{
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{
xReturn = xTaskPriorityDisinherit(pxQueue->u.xSemaphore.xMutexHolder);
pxQueue->u.xSemaphore.xMutexHolder = NULL;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_MUTEXES */
}
else if( xPosition == queueSEND_TO_BACK ) //从队列尾部写入数据
{
//将数据pvItemToQueue拷贝至队列pxQueue下一个写入数据的位置pcWriteTo(即尾部)
( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
//修改下一次写入数据的位置
pxQueue->pcWriteTo += pxQueue->uxItemSize;
if( pxQueue->pcWriteTo >= pxQueue->u.xQueue.pcTail ) //当下一次写入数据的位置到达存储区末尾
{
pxQueue->pcWriteTo = pxQueue->pcHead; //下一次写入数据的位置回到队列项存储区头部
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else //从队列头部写入数据或覆写队列
{
/* 将数据pvItemToQueue拷贝至队列pxQueue上一次读取数据的位置pcReadFrom(即头部) */
( void ) memcpy( ( void * ) pxQueue->u.xQueue.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
pxQueue->u.xQueue.pcReadFrom -= pxQueue->uxItemSize;
/* 修改上一次读取数据的位置 */
if( pxQueue->u.xQueue.pcReadFrom < pxQueue->pcHead ) //当上一次读取数据的位置到达存储区头部
{
pxQueue->u.xQueue.pcReadFrom = ( pxQueue->u.xQueue.pcTail - pxQueue->uxItemSize ); //当上一次读取数据的位置回到队列项存储区末尾
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xPosition == queueOVERWRITE ) //如果是覆写方式
{
if( uxMessagesWaiting > ( UBaseType_t ) 0 ) //如果队列中已有数据
{
--uxMessagesWaiting; //return之前会加一个空闲数据计数,由于覆盖了原本的数据,为保证空闲数据总数不变,这里先扣一个
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
pxQueue->uxMessagesWaiting = ( UBaseType_t ) ( uxMessagesWaiting + ( UBaseType_t ) 1 ); //空闲数据数+1
return xReturn;
}
②xTaskRemoveFromEventList函数的实现:
cpp
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList )
{
TCB_t * pxUnblockedTCB;
BaseType_t xReturn;
traceENTER_xTaskRemoveFromEventList( pxEventList );
pxUnblockedTCB = listGET_OWNER_OF_HEAD_ENTRY(pxEventList); //获取等待接收列表的任务控制块
configASSERT(pxUnblockedTCB);
listREMOVE_ITEM(&(pxUnblockedTCB->xEventListItem)); //移除控制块的事件列表项
if( uxSchedulerSuspended == ( UBaseType_t ) 0U ) //判断调度器是否被挂起
{
listREMOVE_ITEM( &( pxUnblockedTCB->xStateListItem ) ); //如未挂起,移除状态列表项,即从阻塞列表中将任务移除
prvAddTaskToReadyList( pxUnblockedTCB ); //将任务添加进就序列表中,即唤醒任务
#if ( configUSE_TICKLESS_IDLE != 0 )
{
prvResetNextTaskUnblockTime();
}
#endif
}
else
{
/* 否则只能先将任务移动至等待就绪列表 */
listINSERT_END( &( xPendingReadyList ), &( pxUnblockedTCB->xEventListItem ) );
}
#if ( configNUMBER_OF_CORES == 1 )
{
if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority )
{
xReturn = pdTRUE;
xYieldPendings[ 0 ] = pdTRUE;
}
else
{
xReturn = pdFALSE;
}
}
#else /* #if ( configNUMBER_OF_CORES == 1 ) */
{
xReturn = pdFALSE;
#if ( configUSE_PREEMPTION == 1 )
{
prvYieldForTask( pxUnblockedTCB );
if( xYieldPendings[ portGET_CORE_ID() ] != pdFALSE )
{
xReturn = pdTRUE;
}
}
#endif /* #if ( configUSE_PREEMPTION == 1 ) */
}
#endif /* #if ( configNUMBER_OF_CORES == 1 ) */
traceRETURN_xTaskRemoveFromEventList( xReturn );
return xReturn;
}
③xTaskCheckForTimeOut函数的实现:
cpp
BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut,
TickType_t * const pxTicksToWait )
{
BaseType_t xReturn;
traceENTER_xTaskCheckForTimeOut( pxTimeOut, pxTicksToWait );
configASSERT( pxTimeOut );
configASSERT( pxTicksToWait );
taskENTER_CRITICAL();
{
//当前系统节拍计数值
const TickType_t xConstTickCount = xTickCount;
//当前系统节拍计数值 - 之前vTaskInternalSetTimeOutState获取的系统节拍计数值
const TickType_t xElapsedTime =xConstTickCount -pxTimeOut->xTimeOnEntering;
#if ( INCLUDE_xTaskAbortDelay == 1 )
if( pxCurrentTCB->ucDelayAborted != ( uint8_t ) pdFALSE )
{
pxCurrentTCB->ucDelayAborted = ( uint8_t ) pdFALSE;
xReturn = pdTRUE;
}
else
#endif
#if ( INCLUDE_vTaskSuspend == 1 ) //如果使能了任务挂起,编译以下代码块
if( *pxTicksToWait == portMAX_DELAY ) //若阻塞时间设为最大值
{
xReturn = pdFALSE; //需要继续阻塞,返回pdFALSE
}
else
#endif
if( ( xNumOfOverflows != pxTimeOut->xOverflowCount ) && ( xConstTickCount >= pxTimeOut->xTimeOnEntering ) )
{
//判断当前系统节拍溢出次数与之前vTaskInternalSetTimeOutState获取的是否相同
//同时判断当前系统节拍计数值是否大于等于之前vTaskInternalSetTimeOutState获取的系统节拍计数值
//由此判断两个时刻的时间差值(判断它们之间相差的时间是不是大于等于2^32个系统节拍)
xReturn = pdTRUE; //没有剩余的块时间,并且发生了超时,返回pdTRUE
*pxTicksToWait = ( TickType_t ) 0; //阻塞时间更改为0
}
else if( xElapsedTime < *pxTicksToWait )
{
//原定的阻塞时间还没到,因为已经过去的时间还小于原定的阻塞时间
*pxTicksToWait -= xElapsedTime; //原定的阻塞时间减去已经过去的时间,剩下的时间就是需要继续阻塞的时间
vTaskInternalSetTimeOutState( pxTimeOut );
xReturn = pdFALSE; //需要继续阻塞,返回pdFALSE
}
else
{
//原定的阻塞时间已到
*pxTicksToWait = ( TickType_t ) 0;
xReturn = pdTRUE; //不需要继续阻塞,返回pdTRUE
}
}
taskEXIT_CRITICAL(); //开中断(退出临界区)
traceRETURN_xTaskCheckForTimeOut( xReturn ); //返回是否需要继续阻塞的结果
return xReturn;
}
④vTaskPlaceOnEventList函数的实现:
cpp
void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait )
{
traceENTER_vTaskPlaceOnEventList( pxEventList, xTicksToWait );
configASSERT( pxEventList );
vListInsert( pxEventList, &( pxCurrentTCB->xEventListItem ) ); //将事件列表项挂到等待发送列表
prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE ); //将任务挂到阻塞列表中
traceRETURN_vTaskPlaceOnEventList();
}
3、xQueueReceive函数
(1)如上所述,该函数用于从队列中读数据。
(2)xQueueReceive函数的实现:
cpp
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait )
{
BaseType_t xEntryTimeSet = pdFALSE;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = xQueue;
traceENTER_xQueueReceive( xQueue, pvBuffer, xTicksToWait );
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 ); //将队列中的一个数据拷贝至pvBuffer中
traceQUEUE_RECEIVE( pxQueue );
pxQueue->uxMessagesWaiting = ( UBaseType_t ) ( uxMessagesWaiting - ( UBaseType_t ) 1 ); //非空闲队列项数目-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(); //开中断(退出临界区)
traceRETURN_xQueueReceive( pdPASS );
return pdPASS;
}
else
{
if( xTicksToWait == ( TickType_t ) 0 )
{
//如果预期的阻塞时间为0,则不等待,直接返回读错误信息errQUEUE_EMPTY
taskEXIT_CRITICAL(); //开中断
traceQUEUE_RECEIVE_FAILED( pxQueue );
traceRETURN_xQueueReceive( errQUEUE_EMPTY );
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 ) //如需执行任务切换,执行即可
{
taskYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
prvUnlockQueue( pxQueue ); //队列解锁
( void ) xTaskResumeAll(); //恢复任务调度器
}
}
else
{
prvUnlockQueue( pxQueue ); //队列解锁
( void ) xTaskResumeAll(); //恢复任务调度器
if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
{
traceQUEUE_RECEIVE_FAILED( pxQueue );
traceRETURN_xQueueReceive( errQUEUE_EMPTY );
return errQUEUE_EMPTY; //返回队列空的错误信息
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
}
(3)xQueueReceive函数中调用到的prvCopyDataFromQueue函数剖析:
cpp
static void prvCopyDataFromQueue( Queue_t * const pxQueue, void * const pvBuffer )
{
if( pxQueue->uxItemSize != ( UBaseType_t ) 0 ) //队列项不为0,则不是信号量队列
{
pxQueue->u.xQueue.pcReadFrom += pxQueue->uxItemSize; //移动上一次读数据指针指向的位置到这次需要读数据的位置
if( pxQueue->u.xQueue.pcReadFrom >= pxQueue->u.xQueue.pcTail )
{
pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead; //如果读数据指针移至队列项存储区尾部,则跳回头部
}
else
{
mtCOVERAGE_TEST_MARKER();
}
( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.xQueue.pcReadFrom, ( size_t ) pxQueue->uxItemSize ); //将数据拷贝到pvBuffer中
}
}
三、队列操作实验
1、原理图与实验目标
(1)原理图(未画出OLED屏,接法与stm32教程中的一致):
(2)实验目标:
①设计4个任务------start_task、task1、task2、task3:
[1]start_task:用于创建其它三个任务,然后删除自身。
[2]task1:当按键key0按下,将当前按键次数拷贝到队列key_queue中;当按键key1按下,将传输大容量数据,拷贝大容量数据的地址到队列big_date_queue中。(入队)
[3]task2:读取队列key_queue中的消息,在OLED屏上打印出按键次数。(出队)
[4]task3:从队列big_date_queue读取大容量数据地址,通过地址访问大容量数据,并通过OLED显示。(出队)
②预期实验现象:
[1]程序下载到板子上后,暂时没有任何现象。
[2]按下相关按键,OLED屏会有相应变化。
2、实验步骤
(1)将"任务创建和删除的动态方法实验"的工程文件夹复制一份,在拷贝版中进行实验。
(2)在FreeRTOS_experiment.c文件中添加头文件queue.h,并定义一个整型数组(全局变量,作为大容量数据)以及两个队列句柄(分别为记录按键次数的队列和大容量数据队列)。
cpp
#include "queue.h"
int buffer[5] = {13, 32, 26, 114, 51};
QueueHandle_t key_queue; //记录按键次数的队列句柄
QueueHandle_t big_data_queue; //大容量数据队列句柄
(3)在FreeRTOS_Test函数中需要创建记录按键次数的队列和大容量数据队列,与它们的句柄一一对应。
cpp
void FreeRTOS_Test(void)
{
key_queue = xQueueCreate(2, sizeof(uint8_t)); //创建队列key_queue(如果返回值为NULL说明创建失败,可以进行后处理),容量(长度)为2
big_data_queue = xQueueCreate(2, sizeof(int*)); //创建队列big_data_queue(如果返回值为NULL说明创建失败,可以进行后处理),容量(长度)为2
xTaskCreate((TaskFunction_t)start_task, //指向任务函数的指针
"start_task", //任务名字
START_TASK_STACK_SIZE, //任务堆栈大小,单位为字
NULL, //传递给任务函数的参数
START_TASK_PRIO, //任务优先级
(TaskHandle_t *) &start_task_handler //任务句柄
);
vTaskStartScheduler();
}
(4)更改task1、task2和task3函数的实现。
cpp
void task1(void)
{
uint8_t key = 0;
unsigned int num = 0; //记录按键1按下次数
int* buf = buffer; //xQueueSend中不能直接传&buffer
while(1)
{
key = Key_GetNum(); //读取按键键值
if(key == 1)
{
num++; //修改计数值
BaseType_t error_1; //如果出现写失败可做相应后处理
error_1 = xQueueSend(key_queue, &num, portMAX_DELAY); //写不进去就死等
}
else if(key == 2)
{
buffer[1]++; //修改大容量数据本身的值
BaseType_t error_2; //如果出现写失败可做相应后处理
error_2 = xQueueSend(big_data_queue, &buf, portMAX_DELAY); //写不进去就死等
}
vTaskDelay(10); //延时(自我阻塞)10ms
}
}
void task2(void)
{
int buffer1 = 0;
while(1)
{
BaseType_t error_3; //如果出现读失败可做相应后处理
error_3 = xQueueReceive(key_queue, &buffer1, portMAX_DELAY); //读不出来就死等
OLED_ShowNum(1, 1, buffer1, 5);
vTaskDelay(10); //延时(自我阻塞)10ms
}
}
void task3(void)
{
int* buffer2 = NULL;
while(1)
{
BaseType_t error_4; //如果出现读失败可做相应后处理
error_4 = xQueueReceive(big_data_queue, &buffer2, portMAX_DELAY); //读不出来就死等
OLED_ShowNum(2, 1, buffer[1], 5); //显示大容量数据本身的一部分(用于对比)
OLED_ShowNum(3, 1, buffer2[1], 5); //显示通过地址访问的大容量数据的一部分
vTaskDelay(10); //延时(自我阻塞)10ms
}
}
(5)程序完善好后点击"编译",然后将程序下载到开发板上,根据程序注释进行调试。
3、程序执行流程
(1)main函数全流程:
①初始化OLED模块、按键模块、LED模块。
②调用FreeRTOS_Test函数。
(2)测试函数全流程:
①创建记录按键次数的队列和大容量数据队列(下图未示出)。
②创建任务start_task。
③开启任务调度器。
(3)多任务调度执行阶段(发生在开启任务调度器以后):
①在前面的实验中已对任务调度方面做了多次详细解释,从本实验开始简单的任务调度将不再详解。
②按下按键1,task1函数中记录的按键1按下次数自增,并将按键次数写进队列key_queue中;紧接着,原本task2函数中的xQueueReceive函数因为key_queue为空读不到数据而进入无限阻塞,现在key_queue有数据,xQueueReceive函数则将key_queue中的数据,也就是新的按键1按下次数取出,并显示在OLED屏上;以此往复,每次按下按键1,task1就会往key_queue中写一个数据,而task2会将它马上取出。
③按下按键2,task1函数先修改大容量数据本身的值,然后将大容量数据的地址写进队列big_data_queue中;紧接着,原本task3函数中的xQueueReceive函数因为big_data_queue为空读不到数据而进入无限阻塞,现在big_data_queue有数据,xQueueReceive函数则将big_data_queue中的数据,也就是大容量数据的地址取出,并通过这个地址访问大容量数据,显示一部分在OLED屏上;以此往复,每次按下按键2,task1就会往big_data_queue中写一个数据的地址,而task3会将它马上取出,然后可以通过地址访问数据。