目录
在 FreeRTOS 快速入门(四)之队列 一文中,我简单地叙述了 FreeRTOS 中队列的工作机制和基本使用。这一节我将依据 FreeRTOS V10.4.3 的源码深入地去探究队列是如何实现的。学好队列对我们后续学习信号量等知识的时候有很大的帮助。
一、队列
1、队列结构体
队列结构体定义在目录 queue.c 下:
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; // 在队列被锁定时,存储从队列中接收(移除)的项数。当队列未被锁定时,设置为 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; // 用于存储队列的编号,帮助在调试或跟踪时识别和管理不同的队列实例
uint8_t ucQueueType; // 用于存储队列的类型,帮助区分不同类型的队列(例如,普通队列、互斥锁队列等)
#endif
} xQUEUE;
typedef xQUEUE Queue_t;
注意其中的 QueuePointers 类型,定义如下:
typedef struct QueuePointers
{
int8_t * pcTail; /* 指向队列存储区域末尾的字节。分配的字节数比存储队列项所需的字节数多一个 */
int8_t * pcReadFrom; /* 指向上次读取队列项的位置 */
} QueuePointers_t;

有关链表 List 的内容可以参考:FreeRTOS 列表 List 源码解析。
各个成员变量的含义已经在注释中给出,下面不再赘述。
并且在 queue.h 下为其创建了新的别名供外部用户使用,也是我们所熟知的:
struct QueueDefinition; /* Using old naming convention so as not to break kernel aware debuggers. */
typedef struct QueueDefinition * QueueHandle_t;
这种在头文件中声明,而在源文件中实现定义的方式,属于 C 语言中的不完整类型。
C 语言的不完整类型和前置声明
C语言中使用不完全类型(Incomplete Types)来保护结构体的方式,主要涉及到在声明结构体时不提供完整的定义,仅在需要时(如在其源文件中)才给出完整的定义。这种方式的的优点和缺点:
- 优点:
- 封装性增强:使用不完全类型可以在一定程度上隐藏结构体的内部细节,防止外部代码直接访问结构体的成员,从而提高代码的封装性和安全性。
- 模块间解耦:通过不完全类型声明,可以在多个模块之间传递结构体的指针,而无需暴露结构体的完整定义。这有助于减少模块间的耦合度,使得系统更加灵活和易于维护。
- 缺点:
- 使用限制:不完全类型有一些使用上的限制,比如不能直接使用
sizeof运算符来获取不完全类型的大小(因为编译器不知道其完整定义)。这可能导致在需要知道结构体大小的情况下无法使用不完全类型。 - 容易出错:如果在使用不完全类型时没有正确地提供其完整定义,或者在多个地方提供了不一致的定义,都可能导致编译错误或运行时错误。
- 使用限制:不完全类型有一些使用上的限制,比如不能直接使用
通过这种方式可以很好地实现封装抽象,因为队列的具体定义对用户来说就是透明的了,不能直接的访问结构成员,只能提供相应的接口来供访问,这样做的好处显而易见,可以防止用户随意破坏模块内部的抽象数据类型。
此外,不完整类型很好地解决了头文件循环包含的问题。见下:
// circle.h
#include "point.h"
struct circle {
struct coordinate center;
};
// point.h
#include "circle.h"
struct coordinate {
struct circle cir;
};
如果编译这个程序,你会发现因为头文件循环包含而发生编译错误。
这个时候就可以使用前置声明轻松的解决这个问题,但是必须要使用指向不完整类型的指针了。
// circle.h
struct coordinate;
struct circle {
struct coordinate *center;
};
// point.h
struct circle;
struct coordinate {
struct circle *cir;
};
这样我们连头文件都不用包含,还可以缩短编译的时间。
2、队列类型
在文件 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 ) /* 递归互斥信号量 */
queueQUEUE_TYPE_BASE 即基本的消息队列,另外,信号量机制也是通过队列实现的,因此当用于互斥信号量,二值信号量等时,会标记对于的队列类型。
二、队列相关操作
1、初始化

1.1 静态创建队列
前文中提到过,队列静态分配内存使用的是 xQueueCreateStatic() 函数,它其实是一个宏函数(在 queue.h 下):
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
#define xQueueCreateStatic( uxQueueLength, uxItemSize, pucQueueStorage, pxQueueBuffer ) xQueueGenericCreateStatic( ( uxQueueLength ), ( uxItemSize ), ( pucQueueStorage ), ( pxQueueBuffer ), ( queueQUEUE_TYPE_BASE ) )
#endif /* configSUPPORT_STATIC_ALLOCATION */
它实际上是调用了 xQueueGenericCreateStatic 函数来实现了静态初始化队列的功能。
其定义在 queue.c 下:
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength, // 队列长度
const UBaseType_t uxItemSize, // 每个数据的大小
uint8_t * pucQueueStorage, // 数据存储块
StaticQueue_t * pxStaticQueue, // 保存队列的数据结构
const uint8_t ucQueueType ) // 队列的类型(用途)
{
Queue_t * pxNewQueue;
configASSERT( uxQueueLength > ( UBaseType_t ) 0 );
/* 必须提供 StaticQueue_t 结构和队列存储区域 */
configASSERT( pxStaticQueue != NULL );
/* 如果项目大小不为 0,则应提供队列存储区域,如果项目大小为 0,则不应提供队列存储区域 */
configASSERT( !( ( pucQueueStorage != NULL ) && ( uxItemSize == 0 ) ) );
configASSERT( !( ( pucQueueStorage == NULL ) && ( uxItemSize != 0 ) ) );
#if ( configASSERT_DEFINED == 1 )
{
/* 检查用于声明 StaticQueue_t 或 StaticSemaphore_t 类型变量的结构
* 的大小是否与实际队列和信号结构的大小相等。 */
volatile size_t xSize = sizeof( StaticQueue_t );
configASSERT( xSize == sizeof( Queue_t ) );
( void ) xSize; /* 使编译器忽略这个警告 */
}
#endif /* configASSERT_DEFINED */
/* 将一个静态分配的队列结构体指针转换为动态分配的队列结构体指针,以便后续的队列初始化和操作 */
pxNewQueue = ( Queue_t * ) pxStaticQueue;
if( pxNewQueue != NULL )
{
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
{
/* 该队列是静态分配的,所以要设置标志位以防队列后来被删除 */
pxNewQueue->ucStaticallyAllocated = pdTRUE;
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
// 队列创建后的初始化,下面会提到
prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
}
else
{
traceQUEUE_CREATE_FAILED( ucQueueType );
mtCOVERAGE_TEST_MARKER();
}
return pxNewQueue;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
程序大体比较简单,容易理解,注释也已经说明了各部分代码的作用。不过有几个细节需要注意一下:
中间有一段这样的代码:
( void ) xSize;
这段的作用如下:由于 xSize 变量在后续代码中没有被使用,编译器可能会发出未使用变量的警告。通过将 xSize 强制转换为 void,可以明确地告诉编译器忽略这个警告。这个技巧很常用,用于处理临时或未来可能使用的变量,但在当前代码中确实不需要它们的情况。下面举一个简单的例子:
void exampleFunction(int condition) {
size_t xSize = 100;
if (condition) {
// 使用 xSize 进行某些操作
printf("Size: %zu\n", xSize);
} else {
// 不使用 xSize,但为了避免编译器警告
( void ) xSize;
}
}
如果是在函数调用前面加
( void )表示显式指明,程序不处理函数返回值。
最后出现的 traceQUEUE_CREATE_FAILED( ucQueueType ) 是用来检查宏是否定义:
// FreeRTOS.h
#ifndef traceQUEUE_CREATE
#define traceQUEUE_CREATE( pxNewQueue )
#endif
这段代码的主要功能是确保 traceQUEUE_CREATE_FAILED 宏在没有被定义的情况下被定义为一个空操作。这样可以避免在后续代码中使用未定义的宏,从而防止编译错误。
- 如果
traceQUEUE_CREATE_FAILED宏已经被定义,那么这段代码不会做任何事情,因为#ifndef条件不成立。 - 如果
traceQUEUE_CREATE_FAILED宏没有被定义,那么这段代码会定义它为一个空宏,即不执行任何操作。
这种做法常见于库的实现中,用于确保某些宏在用户代码中没有被重复定义,从而避免潜在的冲突和错误。这个技巧在 FreeRTOS 中使用地非常多。
而 mtCOVERAGE_TEST_MARKER() 则是定义了一个空函数,这种做法通常用于代码覆盖率测试,在需要插入标记以确保代码路径被测试到的地方使用。
// FreeRTOS.h
#ifndef mtCOVERAGE_TEST_MARKER
#define mtCOVERAGE_TEST_MARKER()
#endif
1.2 动态创建队列
同理,动态创建队列的宏定义如下:
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endif
现在来看 xQueueGenericCreate 的实现:
#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; // 指向队列存储区域的指针
// 确保队列长度大于0
configASSERT( uxQueueLength > ( UBaseType_t ) 0 );
/* 计算队列存储区域大小
* 如果队列用来表示信号量,则 uxItemSize = 0 */
xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
/* 检查乘法溢出 */
configASSERT( ( uxItemSize == 0 ) || ( uxQueueLength == ( xQueueSizeInBytes / uxItemSize ) ) );
/* 检查加法溢出 */
configASSERT( ( sizeof( Queue_t ) + xQueueSizeInBytes ) > xQueueSizeInBytes );
/* 动态分配内存,大小为队列结构大小加上队列存储区域大小 */
pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes ); /*lint !e9087 !e9079 see comment above. */
// 检查内存分配是否成功
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

最低0.47元/天 解锁文章
8046

被折叠的 条评论
为什么被折叠?



