FreeRTOS 队列管理 完整内容梳理
一、章节介绍与范围
1. 核心定位
队列是 FreeRTOS 核心通信机制,支持任务与任务、任务与中断、中断与任务之间的数据交互,本章仅讲解任务到任务的通信,任务与中断的通信在后续章节说明。
2. 学习目标
- 掌握队列的创建方式
- 理解队列的数据管理逻辑
- 学会向队列发送数据、从队列接收数据
- 理解队列阻塞的含义,以及多队列阻塞机制
- 掌握队列数据覆盖、队列清空操作
- 了解队列读写对任务优先级的影响
二、队列的核心特征
1. 数据存储规则
- 基础属性:队列可存放有限数量、固定大小的数据单元;队列最大容纳单元数称为长度,单个数据单元大小、队列长度均在创建时指定。
- 读写规则:默认遵循 FIFO(先入先出),数据写入队列尾部(tail),从队列头部(head)读出;也支持向队列头部写入数据、覆盖头部已有数据。
- 运行示例(Task A 发数据、Task B 收数据):
- 队列创建后默认空;Task A 写入 10,队列仅存 10;
- Task A 再写入 20,队列头部为 10、尾部为 20;
- Task B 读取数据,优先取出头部的 10,队列剩余 20。
2. 两种实现方式
FreeRTOS 采用数据复制方式实现队列,同时兼容指针传递,两种方式对比如下:
表格
| 实现方式 | 原理 | FreeRTOS 选用原因 |
|---|---|---|
| 复制队列(FreeRTOS 默认) | 逐字节将数据拷贝至队列存储空间 | 1. 可直接发送栈变量,函数退出后数据依然有效;2. 无需提前分配缓冲区;3. 发送任务可立即复用原变量;4. 收发任务完全解耦;5. 适配内存保护系统,可跨内存边界传数据 |
| 引用队列 | 队列仅存储数据指针,不存储数据本身 | 适用于大数据传输,减少拷贝开销;需保证指针指向内存有效、读写互斥 |
补充:FreeRTOS 若需使用引用模式,只需将数据指针作为队列单元存入即可。
3. 多任务访问特性
- 队列是独立内核对象,所有知晓队列句柄的任务 / 中断均可访问。
- 支持多任务写入同一个队列(常用场景)、多任务从同一个队列读取(较少用)。
4. 阻塞机制(核心)
(1)队列读取阻塞
任务读取空队列时,可指定阻塞时长:
- 队列无数据:任务进入阻塞态,等待数据入队;
- 数据入队:阻塞任务转为就绪态;超时无数据,任务也自动转为就绪态;
- 多任务阻塞读:数据到来时,最高优先级任务优先解除阻塞;优先级相同时,等待时间最长的任务优先。
(2)队列写入阻塞
任务向满队列写数据时,可指定阻塞时长:
- 队列已满:任务进入阻塞态,等待队列腾出空间;
- 队列有空位:阻塞任务转为就绪态;超时无空间,任务也自动转为就绪态;
- 多任务阻塞写:队列腾出空间时,最高优先级任务优先解除阻塞;优先级相同时,等待时间最长的任务优先。
(3)阻塞多个队列
支持将多个队列编入队列集合,任务可阻塞等待集合内任意队列有数据,无需轮询所有队列。
三、队列基础 API 函数(任务上下文专用,中断有独立版本)
1. 队列创建函数
(1)xQueueCreate()(动态分配内存)
- 函数原型:
c
运行
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize); - 参数说明:
uxQueueLength:队列最大可容纳的数据单元个数;uxItemSize:单个数据单元的字节大小。
- 返回值:返回队列句柄(
QueueHandle_t类型);内存不足时返回NULL。 - 补充:队列内存从 FreeRTOS 堆中分配。
(2)xQueueCreateStatic()(静态分配内存,V9.0.0 新增)
编译阶段预先分配内存,适合对堆内存管控严格的场景。
(3)辅助函数 xQueueReset()
将队列重置为空状态,恢复初始状态。
2. 队列数据发送函数
注意:禁止在中断中调用,中断需使用
xxxFromISR后缀的中断安全版本。
xQueueSendToBack()/xQueueSend():两者完全等价,向队列尾部写入数据(标准 FIFO 用法)。xQueueSendToFront():向队列头部写入数据,优先级高于原有数据。
- 函数原型(以
xQueueSendToBack为例):c
运行
BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait ); - 参数说明:
xQueue:目标队列句柄;pvItemToQueue:待发送数据的指针,内核会拷贝对应字节数据;xTicksToWait:队列满时的阻塞时长(系统节拍数);0表示不阻塞,portMAX_DELAY表示永久阻塞(需开启任务挂起功能)。
- 返回值:
pdPASS写入成功;errQUEUE_FULL队列满、阻塞超时写入失败。
3. 队列数据接收函数 xQueueReceive()
注意:禁止在中断中调用,中断使用
xQueueReceiveFromISR()。
- 函数原型:
c
运行
BaseType_t xQueueReceive( QueueHandle_t xQueue, void *const pvBuffer, TickType_t xTicksToWait ); - 功能:从队列头部读取数据,读取后数据会从队列中删除。
- 参数说明:
xQueue:目标队列句柄;pvBuffer:接收数据的缓冲区指针,大小需匹配队列单元尺寸;xTicksToWait:队列为空时的阻塞时长,规则同发送函数。
- 返回值:
pdPASS读取成功;errQUEUE_EMPTY队列为空、阻塞超时读取失败。
4. 队列消息计数 uxQueueMessagesWaiting()
注意:禁止在中断中调用,中断使用
uxQueueMessagesWaitingFromISR()。
- 函数原型:
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue ); - 功能:查询队列当前已存储的数据单元数量,返回
0表示队列为空。
四、基础使用示例(任务间通信)
示例 1:整型数据收发(高优先级接收任务)
场景说明
创建 2 个发送任务(优先级 1)、1 个接收任务(优先级 2);接收任务优先级更高,数据一入队就会被立即读取,队列基本不会缓存数据。
- main 函数:创建队列、任务,启动调度器
c
运行
QueueHandle_t xQueue; int main( void ) { // 创建队列:最大存5个int32_t数据 xQueue = xQueueCreate( 5, sizeof( int32_t) ); if( xQueue != NULL ) { // 创建两个发送任务实例,分别发送100、200 xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL ); xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL ); // 创建接收任务(高优先级) xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL ); vTaskStartScheduler(); // 启动调度器 } for( ;; ); } - 发送任务
vSenderTask:循环向队列发数据,不阻塞c
运行
static void vSenderTask( void *pvParameters ) { int32_t lValueToSend = ( int32_t ) pvParameters; BaseType_t xStatus; for( ;; ) { xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 ); if( xStatus != pdPASS ) { vPrintString( "Could not send to the queue.\r\n" ); } } } - 接收任务
vReceiverTask:阻塞 100ms 等待数据,读取并打印c
运行
static void vReceiverTask( void *pvParameters ) { int32_t lReceivedValue; BaseType_t xStatus; const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 ); for( ;; ) { if( uxQueueMessagesWaiting( xQueue ) != 0 ) { vPrintString( "Queue should have been empty!\r\n" ); } xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait ); if( xStatus == pdPASS ) { vPrintStringAndNumber( "Received = ", lReceivedValue ); } else { vPrintString( "Could not receive from the queue.\r\n" ); } } }
示例 2:结构体数据收发(高优先级发送任务)
场景说明
发送任务优先级(2)高于接收任务(1),队列会被写满,发送任务阻塞等待队列释放空间;队列传输结构体,同时携带数据源标识。
- 定义结构体与枚举
c
运行
typedef enum { eSender1, eSender2 } DataSource_t; typedef struct { uint8_t ucValue; DataSource_t eDataSource; } Data_t; static const Data_t xStructsToSend[ 2 ] = {{ 100, eSender1 },{ 200, eSender2 }}; - main 函数:创建可存 3 个结构体的队列
c
运行
int main( void ) { xQueue = xQueueCreate( 3, sizeof( Data_t) ); if( xQueue != NULL ) { xTaskCreate( vSenderTask, "Sender1", 1000, &( xStructsToSend[ 0 ] ), 2, NULL); xTaskCreate( vSenderTask, "Sender2", 1000, &( xStructsToSend[ 1 ] ), 2, NULL); xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL ); vTaskStartScheduler(); } for( ;; ); } - 发送任务:队列满时阻塞 100ms
c
运行
static void vSenderTask( void *pvParameters ) { BaseType_t xStatus; const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 ); for( ;; ) { xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait ); if( xStatus != pdPASS ) { vPrintString( "Could not send to the queue.\r\n" ); } } } - 接收任务:仅队列满时运行,读取并区分数据源
c
运行
static void vReceiverTask( void *pvParameters ) { Data_t xReceivedStructure; BaseType_t xStatus; for( ;; ) { if( uxQueueMessagesWaiting( xQueue ) != 3 ) { vPrintString( "Queue should have been full!\r\n" ); } xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 ); if( xStatus == pdPASS ) { if( xReceivedStructure.eDataSource== eSender1 ) vPrintStringAndNumber( "From Sender 1 = ", xReceivedStructure.ucValue ); else vPrintStringAndNumber( "From Sender 2 = ", xReceivedStructure.ucValue ); } else { vPrintString( "Could not receive from the queue.\r\n" ); } } }
示例 3:队列传递指针(大数据优化方案)
针对大数据场景,队列存储数据缓冲区指针,减少数据拷贝,核心代码:
- 创建指针队列
c
运行
QueueHandle_t xPointerQueue; xPointerQueue = xQueueCreate( 5, sizeof( char * ) ); - 发送任务:申请缓冲区、写入数据、发送指针
c
运行
void vStringSendingTask( void *pvParameters ) { char *pcStringToSend; const size_t xMaxStringLength = 50; BaseType_t xStringNumber = 0; for( ;; ) { pcStringToSend = ( char * ) prvGetBuffer( xMaxStringLength ); // 获取缓冲区 snprintf( pcStringToSend, xMaxStringLength, "String number %d\r\n", xStringNumber ); xStringNumber++; xQueueSend( xPointerQueue, &pcStringToSend, portMAX_DELAY ); // 发送指针 } } - 接收任务:读取指针、处理数据、释放缓冲区
c
运行
void vStringReceivingTask( void *pvParameters ) { char *pcReceivedString; for( ;; ) { xQueueReceive( xPointerQueue, &pcReceivedString, portMAX_DELAY ); vPrintString( pcReceivedString ); prvReleaseBuffer( pcReceivedString ); // 释放缓冲区 } }
注意事项:保证指针指向内存有效、明确内存归属,禁止传递任务栈指针。
拓展:通用事件队列(结构体 + 指针组合)
参考 FreeRTOS+TCP 设计,用结构体封装事件类型 + 通用数据指针,实现单队列传输多类型数据:
- 事件结构体定义
c
运行
typedef enum { eNetworkDownEvent, eNetworkRxEvent, eTCPAcceptEvent } eIPEvent_t; typedef struct IP_TASK_COMMANDS { eIPEvent_t eEventType; // 事件类型 void *pvData; // 通用数据指针 } IPStackEvent_t; - 发送不同事件(伪代码):网络接收事件、连接事件、网络断开事件
- 接收任务:通过
eEventType区分事件,解析pvData处理对应数据。
五、队列集(Queue Set)
1. 功能与适用场景
- 作用:将多个队列 / 信号量加入集合,任务可阻塞等待集合内任意成员有数据,替代轮询,提升效率。
- 局限性:相比单结构体队列,代码繁琐、效率偏低,仅在第三方组件、多独立队列等约束场景使用。
- 前置配置:
FreeRTOSConfig.h中开启configUSE_QUEUE_SETS = 1。
2. 队列集核心 API
xQueueCreateSet():创建队列集- 原型:
QueueSetHandle_t xQueueCreateSet(const UBaseType_t uxEventQueueLength); - 参数:
uxEventQueueLength= 集合内所有队列 / 信号量最大容量之和; - 返回值:队列集句柄,内存不足返回
NULL。
- 原型:
xQueueAddToSet():将队列 / 信号量添加到队列集- 原型:
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet ); - 规则:队列 / 二进制信号量必须为空、计数信号量计数值为 0 时才能添加;一个成员仅能归属一个队列集。
- 原型:
xQueueSelectFromSet():从队列集获取有数据的成员句柄- 原型:
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, const TickType_t xTicksToWait ); - 规则:返回有数据的队列 / 信号量句柄;超时返回
NULL;获取句柄后,需单独读取原队列 / 信号量。
- 原型:
3. 队列集使用示例
两个独立队列加入队列集,接收任务阻塞监听队列集:
- main 函数:创建队列、队列集、任务
c
运行
static QueueHandle_t xQueue1 = NULL, xQueue2 = NULL; static QueueSetHandle_t xQueueSet = NULL; int main( void ) { xQueue1 = xQueueCreate( 1, sizeof( char * ) ); xQueue2 = xQueueCreate( 1, sizeof( char * ) ); xQueueSet = xQueueCreateSet( 2 ); // 总容量 2 xQueueAddToSet( xQueue1, xQueueSet ); xQueueAddToSet( xQueue2, xQueueSet ); xTaskCreate( vSenderTask1, "Sender1", 1000, NULL, 1, NULL ); xTaskCreate( vSenderTask2, "Sender2", 1000, NULL, 1, NULL ); xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL ); vTaskStartScheduler(); for( ;; ); return 0; } - 两个发送任务:定时向不同队列发数据
- 接收任务:从队列集获取句柄,再读取对应队列
c
运行
void vReceiverTask( void *pvParameters ) { QueueHandle_t xQueueThatContainsData; char *pcReceivedString; for( ;; ) { xQueueThatContainsData = ( QueueHandle_t ) xQueueSelectFromSet( xQueueSet, portMAX_DELAY ); xQueueReceive( xQueueThatContainsData, &pcReceivedString, 0 ); vPrintString( pcReceivedString ); } } - 进阶用法:队列集同时包含队列 + 二进制信号量,通过句柄类型区分处理逻辑。
六、队列实现邮箱(Mailbox)
1. 定义
FreeRTOS 中邮箱 = 长度为 1 的队列,使用逻辑和普通队列不同:
- 队列:数据流转,读取后数据删除;
- 邮箱:数据常驻,读取后数据保留,支持覆盖更新。
2. 专属 API 函数
(1)xQueueOverwrite()(覆盖写入,邮箱专用)
- 原型:
BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void * pvItemToQueue ); - 规则:仅支持长度为 1 的队列;无论队列是否已满,直接覆盖原有数据;无阻塞,永远返回
pdPASS;中断使用xQueueOverwriteFromISR()。
(2)xQueuePeek()(偷看数据,不删除)
- 原型:
BaseType_t xQueuePeek( QueueHandle_t xQueue, void *const pvBuffer, TickType_t xTicksToWait ); - 功能:读取队列头部数据,数据不从队列删除;参数、返回值规则同
xQueueReceive();中断使用xQueuePeekFromISR()。
3. 邮箱完整示例
- 定义数据结构体(带时间戳)、创建邮箱
c
运行
typedef struct xExampleStructure { TickType_t xTimeStamp; uint32_t ulValue; } Example_t; QueueHandle_t xMailbox; void vAFunction( void ) { xMailbox = xQueueCreate( 1, sizeof( Example_t ) ); // 长度为1的队列 = 邮箱 } - 更新邮箱(覆盖写入)
c
运行
void vUpdateMailbox( uint32_t ulNewValue ) { Example_t xData; xData.ulValue = ulNewValue; xData.xTimeStamp = xTaskGetTickCount(); // 记录时间戳 xQueueOverwrite( xMailbox, &xData ); } - 读取邮箱(数据保留,判断数据是否更新)
c
运行
BaseType_t vReadMailbox( Example_t *pxData ) { TickType_t xPreviousTimeStamp = pxData->xTimeStamp; xQueuePeek( xMailbox, pxData, portMAX_DELAY ); // 读取不删除 return ( pxData->xTimeStamp > xPreviousTimeStamp ) ? pdTRUE : pdFALSE; }
七、补充规则总结
- 区分任务版 API 与 中断版 API:常规队列函数禁止在中断中调用,中断必须使用
xxxFromISR后缀函数。 - 阻塞时间单位:系统节拍,可通过
pdMS_TO_TICKS()将毫秒转为节拍数。 - 优先级影响:队列阻塞任务的唤醒顺序,严格遵循优先级 > 等待时长规则。
- 内存安全:引用队列 / 指针队列,必须保证内存生命周期、读写互斥。
更多推荐


所有评论(0)