08. FreeRTOS的队列
一、FreeRTOS的队列
队列是一种任务到任务、任务到中断、中断到任务数据交流的一种机制。在队列中可以存储数量有限、大小固定的多个数据,队列中的每一个数据叫做队列项目,队列能够存储队列项目的最大数量称为队列的长度,在创建队列的时候,就需要指定所创建队列的长度及队列项目的大小。因为队列是用来在任务与任务或任务于中断之间传递消息的一种机制,因此队列也叫做 消息队列。
任务与其它任务或任务与中断间的通讯一般可以通过全局变量或消息队列来完成。
如果是使用全局变量的话,那么这个全局变量将被作为任务与任务之间或任务与中断之间的共享资源,因此开发者在设计时还需要考虑该共享资源的互斥访问问题,并且当全局变量被一个任务或中断访问更新后,通讯中全局变量的接收任务无法知道该全局变量是否已经被更新,从而无法实时地获取该全局变量的最新数据。
全局变量的弊端:数据无保护,导致数据不安全,当多个任务同时对该变量操作时,数据易受损。
正是由于使用全局变量进行任务与任务或任务与中断之间的通讯存在诸多的弊端,因此诞生了消息队列这一机制。消息队列中包含了消息,这些消息可以通过消息队列进行传输,也可以以直接送到指定的任务中,因为每个任务都可以有独自的内嵌消息队列。通过消息队列传输的消息,可以发送给多个任务。
队列通常采用 FIFO(先进先出)的存储缓冲机制,当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据。但同时 FreeRTOS 的队列也支持将数据写入到队列的头部,并且还可以指定是否覆盖先前已经在队列头部的数据。
FreeRTOS 中队列采用 实际值传递,即将数据拷贝到队列中进行传递, FreeRTOS 采用拷贝数据传递,也可以传递指针,所以在传递较大的数据的时候采用指针传递。
在任务从队列中读取消息或任务往队列中写入消息时,可以指定一个阻塞超时时间。假设此时队列已经为空无法读取消息或者队列已满无法写入消息时:
- 若阻塞时间为 0:直接返回不会等待。
- 若阻塞时间为 0~ port_MAX_DELAY:等待设定的阻塞时间,若在该时间内还无法出队或者入队,超时后直接返回不再等待。
- 若阻塞时间为 port_MAX_DELAY:死等,一直等到可以出队或者入队为止。
在任务从队列读取消息时,可以指定一个阻塞超时时间。如果任务在读取队列时,队列为空,这时任务将被根据指定的阻塞超时时间添加到 阻塞态任务列表 中进行阻塞,以等待队列中有可用的消息。当有其他任务或中断将消息写入队列中,因等待队列而阻塞任务将会被添加到 就绪态任务列表 中,并读取队列中可用的消息。如果任务因等待队列而阻塞的时间超过指定的阻塞超时时间,那么任务也将自动被转移到 就绪态任务列表 中,但不再读取队列中的数据。
队列为空,此时读取不了数据:
- 将该任务的状态列表项挂载在 pxDelayedTaskList(阻塞列表)。
- 将该任务的事件列表项挂载在 xTasksWaitingToReceive(等待读取列表)。
当多个任务从一个 “空队列” 读取消息时,这些任务都会进入阻塞状态,也就是说有多个任务在等待同一个队列的消息。那当队列中有消息时,优先级最高的任务会进入就绪态。如果任务优先级都一样,那等待时间最久的任务会进入就绪态。
与队列读取一样,在任务往队列写入消息时,也可以指定一个阻塞超时时间。如果任务在写入队列时,队列已经满了,这时任务将被根据指定的阻塞超时时间添加到 阻塞态任务列表 中进行阻塞,以等待队列有空闲的位置可以写入消息。指定的阻塞超时时间为任务阻塞的最大时间,如果在阻塞超时时间到达之前,队列有空闲的位置,那么队列写入阻塞任务将会解除阻塞,并往队列中写入消息,如果达到指定的阻塞超时时间,队列依旧没有空闲的位置写入消息,那么队列写入阻塞任务将会自动转移到 就绪态任务列表 中,但不会往队列中写入消息。
队列满了,此时写不进去数据:
- 将该任务的状态列表项挂载在 pxDelayedTaskList(阻塞列表)。
- 将该任务的事件列表项挂载在 xTasksWaitingToSend(等待发送里里列表)。
当多个任务写入消息给一个 “满队列” 时,这些任务都会进入阻塞状态,也就是说有多个任务在等待同一个队列的空间。那当队列中有空间时,优先级最高的任务会进入就绪态。如果任务优先级都一样,那等待时间最久的任务会进入就绪态。
队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。
二、队列的基本操作
2.1、队列结构体
typedef struct QueueDefinition
{
int8_t * pcHead; // 存储区域的起始地址
int8_t * pcWriteTo; // 存储区域的起始地址
// 信号量是由队列实现的,此结构体能用于队列和信号量,
union
{
QueuePointers_t xQueue; // 用于队列时,使用联合体中的 xQueue,
SemaphoreData_t xSemaphore; // 当用于信号量时,使用联合体中的xSemaphore
} u;
List_t xTasksWaitingToSend; // 写入阻塞任务列表
List_t xTasksWaitingToReceive; // 读取阻塞任务列表
volatile UBaseType_t uxMessagesWaiting; // 非空闲项目的数量
UBaseType_t uxLength; // 队列的长度
UBaseType_t uxItemSize; // 队列项目的大小
// 锁用于在任务因队列操作被阻塞前,防止中断或其他任务操作队列。上锁期间,队列可以写入和读取消息,但不会操作队列阻塞任务列表。
// 当有消息写入时,cTxLock 加 1,当有消息被读取时,cRxLock 加 1,
// 在解锁时,会统一处理队列的阻塞任务列表
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; // 仅用于调试
// 队列的类型: 0: 队列或队列集; 1: 互斥信号量; 2: 计数型信号量; 3: 二值信号量; 4: 可递归信号量
uint8_t ucQueueType;
#endif
} xQUEUE;
typedef xQUEUE Queue_t;
在队列的结构体中,就包含了一个联合体 u,当队列结构体用作队列时,使用联合体 u 中的 xQueue,其数据类型为 QueuePointers_t,在 queue.c 文件中有定义,具体的代码如下所示:
typedef struct QueuePointers
{
int8_t * pcTail; // 存储区域的结束地址
int8_t * pcReadFrom; // 最后一次读取队列的位置
} QueuePointers_t;
而当队列结构体用于互斥信号量和递归互斥信号量时,则是使用联合体 u 中的 xSemaphore,其数据类型为 SemaphoreData_t,在 queue.c 文件中有定义,具体的代码如下所示:
typedef struct SemaphoreData
{
TaskHandle_t xMutexHolder; // 互斥信号量的持有者
UBaseType_t uxRecursiveCallCount; // 递归互斥信号量被递归获取计数器
} SemaphoreData_t;
2.2、创建队列
2.2.1、动态方式创建队列
xQueueCreate()
函数用于使用动态方式创建队列,队列所需的内存空间由 FreeRTOS 从 FreeRTOS 管理的堆中分配。函数 xQueueCreate()
实际上是一个宏定义,在 queue.h 文件中有定义,具体的代码如下所示:
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
形参 uxQueueLength
是 队列长度,形参 uxItemSize
是 队列项目的大小。函数 xQueueCreate()
的返回 NULL 表示 队列创建失败,返回其它值表示 队列创建成功,返回队列的起始地址。
函数 xQueueCreate()
实际上是调用了函数 xQueueGenericCreate()
,函数 xQueueGenericCreate()
用于使用动态方式创建指定类型的队列,前面说 FreeRTOS 基于队列实现了多种功能,每一种功能对应一种队列类型,队列类型的 queue.h 文件中有定义,具体的代码如下所示:
#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 ) // 递归互斥信号量
函数 xQueueGenericCreate()
在 queue.c 文件中有定义,具体的代码如下所示:
// 返回NULL表示队列创建失败,返回其它值表示队列创建成功,返回队列的起始地址
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 );
// 检查参数设置,队列长度大于0才有意义
if( ( uxQueueLength > ( UBaseType_t ) 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;
}
从上面的代码可以看出,函数 xQueueGenericCreate()
主要负责为队列申请内存,然后调用函数 prvInitialiseNewQueue()
对队列进行初始化,函数 prvInitialiseNewQueue()
在 queue.c 文件中有定义,具体的代码如下所示:
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 /* configUSE_TRACE_FACILITY */
#if ( configUSE_QUEUE_SETS == 1 ) // 此宏用于使能使用队列集
{
pxNewQueue->pxQueueSetContainer = NULL; // 队列所在队列集设为空
}
#endif /* configUSE_QUEUE_SETS */
traceQUEUE_CREATE( pxNewQueue ); // 用于调试
}
从上面的代码可以看出,函数 prvInitialiseNewQueue()
主要用于初始化队列结构体中的成员变量,其中还会调用函数 xQueueGenericReset()
对队列进行重置,函数 xQueueGenericReset()
在queue.c 文件中有定义,具体的代码如下所示:
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 ) );
vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
}
}
// 退出临界区
taskEXIT_CRITICAL();
}
else
{
xReturn = pdFAIL;
}
configASSERT( xReturn != pdFAIL );
traceRETURN_xQueueGenericReset( xReturn );
return xReturn;
}
从上面的函数可以看出,函数 xQueueGenericReset()
复位队列的操作也是复位队列的结构体中的成员变量。
2.2.2、静态方式创建队列
xQueueCreateStatic()
函数用于使用静态方式创建队列,队列所需的内存空间需要由用户手动分配并提供。函数 xQueueCreateStatic()
实际上是一个宏定义,在 queue.h 文件中有定义,具体的代码如下所示:
#define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxQueueBuffer ) xQueueGenericCreateStatic( ( uxQueueLength ), ( uxItemSize ), ( pucQueueStorage ), ( pxQueueBuffer ), ( queueQUEUE_TYPE_BASE ) )
形参 uxQueueLength
是 队列长度。形参 uxItemSize
是 队列项目的大小。形参 pucQueueStorage
是 队列存储区域的起始地址。形参 pxQueueBuffer
是 静态队列结构体。返回 非 NULL 值 表示 队列创建成功,返回队列的起始地址。
函数 xQueueCreateStatic()
的形参 pxQueueBuffer 的数据类型为 StaticQueue_t*,结构体 StaticQueue_t本质上与队列结构体 Queue_t 是一样的,区别在于 Queue_t 是在 queue.c 文件中定义的,属于 FreeRTOS 的内部结构体,对于数据隐藏策略而言,用户在应用程序开发时,是无法访问 FreeRTOS 内部使用的结构体的,但是使用静态方式创建队列时,需要根据队列结构体的大小来分配内存,因此用户需要在不访问队列结构体的前提下,确定队列结构体的大小,因此 FreeRTOS 在 FreeRTOS.h 文件中提供了 StaticQueue_t 结构体,具体的代码如下所示:
typedef struct xSTATIC_QUEUE
{
// pvDummy1[0] --> int8_t * pcHead
// pvDummy1[1] --> int8_t * pcWriteTo
// pvDummy1[2] --> int8_tpcTail(用于队列时)或 TaskHandle_txMutexHolder(用于互斥信号量时)
void * pvDummy1[ 3 ];
union
{
// int8_t * pcReadFrom(用于队列时)
void * pvDummy2;
// UBaseType_tuxRecursiveCallCount(用户互斥信号量时)
UBaseType_t uxDummy2;
} u;
// xDummy3[0] --> List_t xTasksWaitingToSend
// xDummy3[1] --> List_t xTasksWaitingToReceive
StaticList_t xDummy3[ 2 ];
// uxDummy4[0] --> volatile UBaseType_t uxMessagesWaiting
// uxDummy4[1] --> UBaseType_t uxLength
// uxDummy4[2] --> UBaseType_t uxItemSize
UBaseType_t uxDummy4[ 3 ];
// ucDummy5[0] --> volatile int8_t cRxLock
// ucDummy5[1] --> volatile int8_t cTxLock
uint8_t ucDummy5[ 2 ];
#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
// uint8_t ucStaticallyAllocated
uint8_t ucDummy6;
#endif
#if ( configUSE_QUEUE_SETS == 1 )
// struct QueueDefinition * pxQueueSetContainer
void * pvDummy7;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
// UBaseType_t uxQueueNumber
UBaseType_t uxDummy8;
// uint8_t ucQueueType
uint8_t ucDummy9;
#endif
} StaticQueue_t;
typedef StaticQueue_t StaticSemaphore_t;
从上面的代码中可以看出,结构体 StaticQueue_t 与结构体 Queue_t 是一一对应的,但是结构体 StaticQueue_t 的成员变量的命名方式都以 ucDummy 开头,这也表明用户应该使用这个结构体来确定队列结构体的大小,而不应该直接访问 StaticQueue_t 中的成员变量。
函数 xQueueCreateStatic()
实际上是调用了函数 xQueueGenericCreateStatic()
,函数 xQueueGenericCreateStatic()
用于使用静态方式创建指定类型的队列。
// 返回非NULL值表示队列创建成功,返回队列的起始地址
QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength, // 队列长度
const UBaseType_t uxItemSize, // 队列项目的大小
uint8_t * pucQueueStorage, // 队列存储区域的起始地址
StaticQueue_t * pxStaticQueue, // 队列结构体的起始地址
const uint8_t ucQueueType ) // 队列类型
{
Queue_t * pxNewQueue = NULL;
traceENTER_xQueueGenericCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxStaticQueue, ucQueueType );
configASSERT( pxStaticQueue );
// 队列长度大于0才有意义,不许提供队列控制块所需内存
// 如果提供了存储区域所需内存,那项目大小就不能为0
// 如果项目大小不为,那就必须提供存储区域所需内存
if( ( uxQueueLength > ( UBaseType_t ) 0 ) &&
( pxStaticQueue != NULL ) &&
( !( ( pucQueueStorage != NULL ) && ( uxItemSize == 0U ) ) ) &&
( !( ( pucQueueStorage == NULL ) && ( uxItemSize != 0U ) ) ) )
{
#if ( configASSERT_DEFINED == 1 ) // 用于调试
{
volatile size_t xSize = sizeof( StaticQueue_t );
configASSERT( xSize == sizeof( Queue_t ) );
( void ) xSize;
}
#endif /* configASSERT_DEFINED */
// 已结构体 Queue_t 的方式获取 pxStaticQueue,
// 结构体 Queue_t 中的成员变量与结构体 StaticQueue_t 中的成员变量在内存上是一一对应的
pxNewQueue = ( Queue_t * ) pxStaticQueue;
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) // 此宏用于启用动态方式管理内存
{
pxNewQueue->ucStaticallyAllocated = pdTRUE; // 标记队列是静态方式创建的
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
// 初始化队列结构体中的成员变量
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
}
else
{
configASSERT( pxNewQueue );
mtCOVERAGE_TEST_MARKER();
}
traceRETURN_xQueueGenericCreateStatic( pxNewQueue );
return pxNewQueue;
}
从上面的代码中可以看出,因为用户已经提供了队列与其内存区域所需的内存,因此函数 xQueueGenericCreateStatic()
只需调用函数 prvInitialiseNewQueue()
初始化队列结构体中的成员变量即可。
2.3、往队列写入消息
2.3.1、在任务中往队列写入消息的函数
在任务中往队列写入消息的函数有函数 xQueueSend()
、xQueueSendToBack()
、xQueueSendToFront()
、xQueueOverwrite()
,这 4 个函数实际上都是宏定义,在 queue.h 文件中有定义,具体的代码如下所示:
// 往队列的尾部写入消息
#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 )
// 覆写队列消息(只用于队列长度为 1 的情况)
#define xQueueOverwrite( xQueue, pvItemToQueue ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
从上面的代码中可以看到,函数 xQueueSend()
、函数 xQueueSendToBack()
、函数 xQueueSendToFront()
和函数 xQueueOverwrite()
实际上都是调用了函数 xQueueGenericSend()
,只是指定了不同的写入位置,队列一共有 3 种写入位置,在 queue.h 文件中有定义,具体的代码如下所示:
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) // 写入队列尾部
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) // 写入队列头部
#define queueOVERWRITE ( ( BaseType_t ) 2 ) //覆写队列
覆写方式写入队列,只有在队列的队列长度为 1 时,才能够使用。
函数 xQueueGenericSend()
用于在任务中往队列的指定位置写入消息。其在 queue.c 文件中有定义,具体的代码如下所示:
// 返回值:pdTRUE: 队列写入成功; 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;
traceENTER_xQueueGenericSend( xQueue, pvItemToQueue, xTicksToWait, xCopyPosition );
configASSERT( pxQueue );
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 ) // 此宏用于使能启用队列集
{
// 获取队列中非空闲项目的数量
const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting;
// 待写入消息按指定写入方式复制到队列
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
if( pxQueue->pxQueueSetContainer != NULL ) // 断队列是否在队列集中
{
// 写入位置为覆写,且队列非空闲项目数量不为0
if( ( xCopyPosition == queueOVERWRITE ) && ( uxPreviousMessagesWaiting != ( UBaseType_t ) 0 ) )
{
/* Do not notify the queue set as an existing item
* was overwritten in the queue so the number of items
* in the queue has not changed. */
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 );
// 队列有阻塞的读取任务
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 ) // 此时不能写入消息,因此要将任务阻塞
{
taskEXIT_CRITICAL(); // 如果不选则阻塞等待,退出临界区
// 用于调试
traceQUEUE_SEND_FAILED( pxQueue );
traceRETURN_xQueueGenericSend( errQUEUE_FULL );
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 ) // 恢复任务调度器
{
taskYIELD_WITHIN_API(); // 根据需要进行任务切换
}
}
else
{
prvUnlockQueue( pxQueue ); // 队列解锁
( void ) xTaskResumeAll(); // 恢复任务调度器
}
}
else // 阻塞时间补偿后,已不需要阻塞
{
prvUnlockQueue( pxQueue ); // 解锁队列
( void ) xTaskResumeAll(); // 恢复任务调度器
// 用于调试
traceQUEUE_SEND_FAILED( pxQueue );
traceRETURN_xQueueGenericSend( errQUEUE_FULL );
return errQUEUE_FULL; // 返回队列满错误
}
}
}
2.3.2、在中断中往队列写入消息的函数
在任务中往队列写入消息的函数有 xQueueSendFromISR()
、xQueueSendToBackFromISR()
、xQueueSendToFrontFromISR()
、xQueueOverwriteFromISR()
,这 4个函数实际上都是宏定义,在 queue.h 文件中有定义,具体的代码如下所示:
#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 )
从上面的代码中可以看到,函数 xQueueSendFromISR()
、函数 xQueueSendToBackFromISR()
、函数 xQueueSendToFrontFromISR()
和函数 xQueueOverwriteFromISR()
实际上都是调用了函数 xQueueGenericSendFromISR()
,只是指定了不同的写入位置。
函数 xQueueGenericSendFromISR()
用于在中断中往队列的指定位置写入消息。其在 queue.c 文件中有定义,具体的代码如下所示:
// 返回值: pdTRUE: 队列写入成功; errQUEUE_FULL: 队列写入失败
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;
traceENTER_xQueueGenericSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken, xCopyPosition );
configASSERT( pxQueue );
configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
// 这里限制了只有在队列长度为1时,才能使用覆写
configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );
portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); // 有受FreeRTOS 管理的中断才能调用该函数
uxSavedInterruptStatus = ( UBaseType_t ) taskENTER_CRITICAL_FROM_ISR(); // 屏蔽受FreeRTOS 管理的中断,并保存,屏蔽前的状态,用于恢复
{
// 有空闲的写入位置,或为覆写
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 ) // 此宏用于使能队列集
{
if( pxQueue->pxQueueSetContainer != NULL )
{
// 写入位置为覆写,且队列非空闲项目数量不为〇
if( ( xCopyPosition == queueOVERWRITE ) && ( uxPreviousMessagesWaiting != ( UBaseType_t ) 0 ) )
{
mtCOVERAGE_TEST_MARKER();
}
// 通知队列集
else if( prvNotifyQueueSetContainer( pxQueue ) != pdFALSE )
{
if( pxHigherPriorityTaskWoken != NULL ) // 判断是否接收需要任务切换标记
{
*pxHigherPriorityTaskWoken = pdTRUE; // 标记要进行任务切换
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
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();
}
}
}
#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 // 队列写入已被上锁
{
prvIncrementQueueTxLock( pxQueue, cTxLock ); // 上锁次数加 1
}
xReturn = pdPASS;
}
else // 无空闲的写入位置,且不覆写
{
traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue ); // 用于调试
xReturn = errQUEUE_FULL;
}
}
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus ); // 恢复屏蔽中断前的中断状态
traceRETURN_xQueueGenericSendFromISR( xReturn );
return xReturn;
}
2.4、从队列读取消息
2.4.1、在任务中从队列读取消息
// 返回值: pdTRUE: 读取成功; pdFALSE: 读取失败
BaseType_t xQueueReceive( QueueHandle_t xQueue, // 待读取的队列
void * const pvBuffer, // 信息读取缓冲区
TickType_t xTicksToWait ); // 阻塞超时时间
此函数用于在任务中,从队列中读取消息,并且消息读取成功后,会将消息从队列中移除。消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。
// 返回值: pdTRUE: 读取成功; pdFALSE: 读取失败
BaseType_t xQueuePeek( QueueHandle_t xQueue, // 待读取的队列
void * const pvBuffer, // 信息读取缓冲区
TickType_t xTicksToWait ); // 阻塞超时时间
此函数用于在任务中,从队列中读取消息,但与函数 xQueueReceive()
不同,此函数在成功读取消息后,并不会移除已读取的消息,这意味着,下次读取队列时,还能够读取到相同的内容。消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。
2.4.2、在中断中队列读取消息
// 返回值: pdTRUE: 读取成功; pdFALSE: 读取失败
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, // 待读取的队列
void * const pvBuffer, // 信息读取缓冲区
BaseType_t * const pxHigherPriorityTaskWoken ); // 需要任务切换标记
此函数用于在中断中,从队列中读取消息,并且消息读取成功后,会将消息从队列中移除。消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。
// 返回值: pdTRUE: 读取成功; pdFALSE: 读取失败
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, // 待读取的队列
void * const pvBuffer ); // 信息读取缓冲区
此函数用于在中断中,从队列中读取消息,但与函数 xQueueReceiveFromISR()
不同,此函数在成功读取消息后,并不会移除已读取的消息,这意味着,下次读取队列时,还能够读取到相同的内容。消息的读取是通过拷贝的形式传递的,具体拷贝数据的大小,为队列项目的大小。
三、实验例程
main() 函数:
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
UART_Init(&g_usart1_handle, USART1, 115200);
Key_Init();
freertos_demo();
return 0;
}
FreeRTOS 的入口函数:
QueueHandle_t queue1;
QueueHandle_t queue2;
/**
* @brief FreeRTOS的入口函数
*
*/
void freertos_demo(void)
{
queue1 = xQueueCreate(2, sizeof(uint8_t));
if (queue1 != NULL)
{
printf("队列queue1创建成功\r\n");
}
else
{
printf("队列queue1创建失败\r\n");
}
queue2 = xQueueCreate(2, sizeof(char *));
if (queue2 != NULL)
{
printf("队列queue2创建成功\r\n");
}
else
{
printf("队列queue2创建失败\r\n");
}
xTaskCreate((TaskFunction_t ) start_task, // 任务函数
(char * ) "start_task", // 任务名
(configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE, // 任务栈大小
(void * ) NULL, // 入口参数
(UBaseType_t ) START_TASK_PRIORITY, // 任务优先级
(TaskHandle_t * ) start_task_handle); // 任务句柄
vTaskStartScheduler(); // 开启任务调度器
}
start_task 任务配置:
/**
* START_TASK 任务配置
* 包括: 任务优先级 任务栈大小 任务句柄 任务函数
*/
#define START_TASK_PRIORITY 1
#define START_TASK_STACK_SIZE 128
TaskHandle_t start_task_handle;
void start_task(void *pvParameters );
/**
* @brief 开始任务的任务函数
*
* @param pvParameters 任务函数的入口参数
*/
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); // 进入临界区,关闭中断
xTaskCreate((TaskFunction_t ) task1, // 任务函数
(char * ) "task1", // 任务名
(configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE, // 任务栈大小
(void * ) NULL, // 入口参数
(UBaseType_t ) TASK1_PRIORITY, // 任务优先级
(TaskHandle_t * ) &task1_handle); // 任务句柄
xTaskCreate((TaskFunction_t ) task2, // 任务函数
(char * ) "task2", // 任务名
(configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE, // 任务栈大小
(void * ) NULL, // 入口参数
(UBaseType_t ) TASK2_PRIORITY, // 任务优先级
(TaskHandle_t * ) &task2_handle); // 任务句柄
xTaskCreate((TaskFunction_t ) task3, // 任务函数
(char * ) "task3", // 任务名
(configSTACK_DEPTH_TYPE) TASK3_STACK_SIZE, // 任务栈大小
(void * ) NULL, // 入口参数
(UBaseType_t ) TASK3_PRIORITY, // 任务优先级
(TaskHandle_t * ) &task3_handle); // 任务句柄
vTaskDelete(NULL); // 删除任务自身
taskEXIT_CRITICAL(); // 退出临界区,重新开启中断
}
task1 任务配置:
/**
* TASK1 任务配置
* 包括: 任务优先级 任务栈大小 任务句柄 任务函数
*/
#define TASK1_PRIORITY 2
#define TASK1_STACK_SIZE 128
TaskHandle_t task1_handle;
void task1(void *pvParameters);
/**
* @brief 任务1的任务函数
*
* @param pvParameters 任务函数的入口参数
*/
void task1(void *pvParameters)
{
uint8_t key = 0;
BaseType_t result = 0;
char *data1 = "你好啊,我是小樱,请多多关照!";
char *data2 = "隐藏在黑暗里的钥匙啊,和你定下约定的小樱命令你:封印解除!";
while (1)
{
key = Key_Scan(0);
switch (key)
{
case WKUP_PRESS:
result = xQueueSend(queue1, &key, portMAX_DELAY);
if (result != pdTRUE)
{
printf("队列queue1发送失败\r\n");
}
break;
case KEY1_PRESS:
result = xQueueSend(queue2, &data1, portMAX_DELAY);
if (result != pdTRUE)
{
printf("队列queue2发送失败\r\n");
}
break;
case KEY2_PRESS:
result = xQueueSend(queue1, &key, portMAX_DELAY);
if (result != pdTRUE)
{
printf("队列queue1发送失败\r\n");
}
break;
case KEY3_PRESS:
result = xQueueSend(queue2, &data2, portMAX_DELAY);
if (result != pdTRUE)
{
printf("队列queue2发送失败\r\n");
}
break;
default:
break;
}
vTaskDelay(10);
}
}
task2 任务配置:
/**
* TASK2 任务配置
* 包括: 任务优先级 任务栈大小 任务句柄 任务控制块 任务栈 任务函数
*/
#define TASK2_PRIORITY 2
#define TASK2_STACK_SIZE 128
TaskHandle_t task2_handle;
void task2(void *pvParameters);
/**
* @brief 任务2的任务函数
*
* @param pvParameters 任务函数的入口参数
*/
void task2(void *pvParameters )
{
uint8_t key = 0;
BaseType_t result = 0;
while (1)
{
result = xQueueReceive(queue1, &key, portMAX_DELAY);
if (result != pdTRUE)
{
printf("queue1队列读取失败\r\n");
}
else
{
printf("queue1队列读取成功,数据: %d\r\n", key);
}
}
}
task3 任务配置:
/**
* TASK3 任务配置
* 包括: 任务优先级 任务栈大小 任务句柄 任务函数
*/
#define TASK3_PRIORITY 4
#define TASK3_STACK_SIZE 128
TaskHandle_t task3_handle;
void task3(void *pvParameters);
/**
* @brief 任务3的任务函数
*
* @param pvParameters 任务函数的入口参数
*/
void task3(void *pvParameters )
{
char *buffer;
BaseType_t result = 0;
while (1)
{
result = xQueueReceive(queue2, &buffer, portMAX_DELAY);
if (result != pdTRUE)
{
printf("queue2队列读取失败\r\n");
}
else
{
printf("queue2队列读取成功,数据: %s\r\n", buffer);
}
}
}