队列管理
队列是 FreeRTOS 提供的一种先进先出(FIFO)机制,用于在任务之间或任务与中断之间安全地传递数据。 它支持阻塞操作,允许发送方等待空间、接收方等待数据,并提供专用的中断安全函数。 常用于实现任务间通信或生产者-消费者模型。
本文档仅涵盖队列管理中部分常用 API,更多内容请参考 FreeRTOS 官方文档。
备注
生产者-消费者模型是一种常见的并发编程模式,其中生产者负责生成数据并放入缓冲区,消费者从缓冲区中取出数据进行处理。 两者通过共享的缓冲区进行解耦,互不直接依赖,适合处理数据采集与处理解耦的场景。
在 ESP-IDF 中调用队列相关 API 时,需要在文件中包含以下头文件:
#include "freertos/queue.h"
动态创建队列
API 原型
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
UBaseType_t uxItemSize );
xQueueCreate()
用于创建一个用于任务间通信的队列,分配内存并初始化队列结构,使任务能够安全地发送和接收数据。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
队列一次可存储的最大项目数。 |
决定队列容量,限制一次能存多少个数据。 |
|
存储队列中每个项目所需的大小(以字节为单位)。 |
队列以复制方式存储每个项目,因此该参数值是每个入队项目将复制的字节数。队列中的每个项目必须具有相同的大小。 |
返回值 |
说明 |
---|---|
返回所创建队列的句柄 |
队列创建成功,返回有效句柄用于后续操作。 |
|
内存分配失败,队列创建失败。 |
示例 1:动态创建队列
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5 // 定义队列长度
#define ITEM_SIZE 4 // 定义项目大小(4 字节每项)
void app_main(void)
{
// 创建队列
queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);
if (queue == NULL) {
printf("Create FALSE\n");
}
else {
printf("Create SUCCESS\n");
}
}
以上代码执行结果如下:
Create SUCCESS
该示例展示了使用 xQueueCreate()
创建一个长度为 5、每项大小为 4 字节的队列,通过判断返回句柄是否为 NULL
来确认队列是否成功创建。
调用该 API 时需要注意:
合理设置队列长度和元素大小,注意所有元素大小必须一致且通过复制存储。
必须检查返回值防止内存不足导致创建失败,建议在系统初始化阶段创建队列以确保任务运行时队列已准备好,同时保证系统有足够堆空间。
队列本身线程安全,可安全用于任务间通信。
删除队列
API 原型
void vQueueDelete( QueueHandle_t xQueue );
vQueueDelete()
用于删除一个已经创建的队列,释放该队列占用的内存资源。调用后,该队列句柄将失效,不能再使用。
备注
该 API 仅适用于动态创建的队列。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
需要删除的队列的句柄 |
例如由 |
删除队列后,队列句柄本身的值不会自动变成 NULL
或其他特殊值,它仍然保持原来的数值。
但该句柄已失效,不能再用于任何队列操作,否则会导致未定义行为。
因此,调用 vQueueDelete()
后,建议手动将句柄置为 NULL
,防止误用。
发送数据
API 原型
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
xQueueSend()
用于向队列末尾发送数据,如果队列已满,可以选择阻塞等待直到有空间或超时,确保数据安全传递给接收方。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
数据传输的目标队列的句柄 |
例如由 |
|
指向待发送数据的指针 |
数据将直接复制到已创建的队列中,确保数据大小不超过队列元素大小。 |
|
最长等待时间 |
当队列已满时,调用者阻塞等待的最大时间,以滴答周期(tick)为单位定义;为 0 时立即返回。 |
备注
当任务尝试操作队列(如发送或接收数据)时,如果条件不满足(队列满或空),任务会暂停执行,进入等待状态,直到满足条件或超时。
在队列中,这种机制保证任务不会忙等待或浪费 CPU 资源,而是有效等待队列变为可用,提升系统效率和响应能力。
返回值 |
说明 |
---|---|
|
发送成功,数据已加入队列。 |
|
发送失败,队列已满且未等待或等待超时。 |
示例 2:发送数据至队列
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5 // 定义队列长度
#define ITEM_SIZE 4 // 定义项目大小(4 字节每项)
void app_main(void)
{
// 创建队列
queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);
int num = 1; // 待发送数据
BaseType_t xReturn; // 用于接收返回值
if (queue == NULL) {
printf("Create FALSE\n");
}
else {
printf("Create SUCCESS\n");
}
while (1) {
xReturn = xQueueSend(queue, (void *)&num, 10);
// 通过返回值判断数据是否发送成功
if (xReturn == pdTRUE) {
printf("%d: Item Send SUCCESS\n", num);
}
else {
printf("Item Send FALSE\n");
}
num++;
}
}
以上代码执行结果如下:
Create SUCCESS
1: Item Send SUCCESS
2: Item Send SUCCESS
3: Item Send SUCCESS
4: Item Send SUCCESS
5: Item Send SUCCESS
Item Send FALSE
Item Send FALSE
该示例主要展示了 xQueueSend()
的基本用法。
通过创建固定长度的队列并持续发送整数,演示如何使用该函数将数据写入队列,并通过返回值判断发送是否成功。
由于示例中未设置任务接收数据,队列空间不会释放,随着连续发送,队列逐渐填满。
当队列满后,再调用 xQueueSend()
会阻塞等待最多 10 个 tick(由参数指定)。
若超时仍无空位,函数返回 errQUEUE_FULL
,表示发送失败。
此外,传入的第二个参数必须是数据地址,通常需强制转换为 void *
。
队列通过复制数据副本实现传输,而非传递指针,保证了数据安全和一致性。
接收数据
API 原型
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
xQueueReceive()
用于从队列头部接收数据,如果队列为空,可以选择阻塞等待直到有数据或超时,确保安全获取队列中的数据。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
接收数据的目标队列的句柄 |
例如由 |
|
指向用于存储接收数据的缓冲区的指针 |
数据会复制到该缓冲区,确保该缓冲区大小不小于队列元素大小。 |
|
最长等待时间 |
当队列为空时,调用者阻塞等待的最大时间,以滴答周期(tick)为单位定义;为 0 时立即返回。 |
返回值 |
说明 |
---|---|
|
接收成功,数据已复制到指定的缓存区。 |
|
接收失败,队列为空且未等待或等待超时。 |
示例 3:发送接收数据
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5 // 定义队列长度
#define ITEM_SIZE 4 // 定义项目大小(4 字节每项)
static void vSendTask( void *pvParameters )
{
int num = 1; // 待发送数据
BaseType_t xReturn; // 用于接收返回值
while (1) {
xReturn = xQueueSend(queue, (void *)&num, 10);
// 通过返回值判断数据是否发送成功
if (xReturn == pdTRUE) {
printf("Item Send: %d \n", num);
num++;
}
else {
printf("Item Send FALSE\n");
}
vTaskDelay(1);
}
}
static void vReceiveTask( void *pvParameters )
{
int receiver = 0; // 用于接收数据
BaseType_t xReturn; // 用于接收返回值
while (1) {
xReturn = xQueueReceive(queue, (void *)&receiver, 10);
// 通过返回值判断数据是否接收成功
if (xReturn == pdTRUE) {
printf("Item Receive: %d \n", receiver);
}
else {
printf("Item Receive FALSE\n");
}
vTaskDelay(1);
}
}
void app_main(void)
{
// 创建队列
queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);
if (queue != NULL) {
xTaskCreatePinnedToCore(vSendTask, "SendTask", 20480, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(vReceiveTask, "ReceiveTask", 4096, NULL, 2, NULL, 0);
}
}
以上代码执行结果如下:
Item Send: 1
Item Receive: 1
Item Send: 2
Item Receive: 2
Item Send: 3
Item Receive: 3
Item Send: 4
Item Receive: 4
Item Send: 5
Item Receive: 5
Item Send: 6
Item Receive: 6
该示例展示了使用 xQueueSend()
和 xQueueReceive()
在两个任务之间进行数据传输的基本过程。
发送任务每次将整数写入队列,接收任务从队列中读取并打印数据,两个任务以相同频率运行,并通过返回值判断操作是否成功。
从执行结果可以看出数据被成功写入并接收,说明队列创建和基本通信功能正常。 由于两个任务运行频率一致、调度节奏同步且优先级相同,队列始终保持在可用状态,未出现满队列或空队列的情况,因此没有发生发送失败或接收失败。
在 ESP-IDF 环境中,若两个任务涉及同一个队列进行数据读写,建议将它们绑定在同一个 CPU 核上,以减少跨核调度带来的不确定性和性能损耗,确保通信行为更稳定可靠。
写入数据
API 原型
BaseType_t xQueueOverwrite( QueueHandle_t xQueue,
const void * pvItemToQueue );
xQueueOverwrite()
用于将数据写入队列中,如果队列已满,会覆盖掉旧数据,仅适用于队列长度为 1 的情况,常用于始终保持最新数据的场景。
备注
在队列长度大于 1 的情况下,不应使用 xQueueOverwrite()
,应使用标准的 xQueueSend()
。
避免直接覆盖头部数据,跳过正常的先进先出规则,导致破坏队列结构。
这种做法可能导致数据一致性问题,尤其是在多个任务同时访问同一队列时,容易引发逻辑错误或数据混乱。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
数据写入的目标队列的句柄 |
例如由 |
|
指向待写入数据的指针 |
数据将直接复制到已创建的队列中,确保数据大小不超过队列元素大小。 |
返回值 |
说明 |
---|---|
|
写入成功,数据已加入队列。 |
示例 4:最新数据覆盖
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 1 // 定义队列长度
#define ITEM_SIZE 4 // 定义项目大小(4 字节每项)
static void vSendTask( void *pvParameters )
{
int num = 1; // 待发送数据
BaseType_t xReturn; // 用于接收返回值
while (1) {
xReturn = xQueueOverwrite(queue, (void *)&num);
// 通过返回值判断数据是否发送成功
if (xReturn == pdTRUE) {
printf("Item Send: %d \n", num);
num++;
}
vTaskDelay(1);
}
}
static void vReceiveTask( void *pvParameters )
{
int receiver = 0; // 用于接收数据
BaseType_t xReturn; // 用于接收返回值
while (1) {
xReturn = xQueueReceive(queue, (void *)&receiver, 10);
// 通过返回值判断数据是否接收成功
if (xReturn == pdTRUE) {
printf("Item Receive: %d \n", receiver);
}
else {
printf("Item Receive FALSE\n");
}
vTaskDelay(5);
}
}
void app_main(void)
{
// 创建队列
queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);
if (queue != NULL) {
xTaskCreatePinnedToCore(vSendTask, "SendTask", 20480, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(vReceiveTask, "ReceiveTask", 4096, NULL, 2, NULL, 0);
}
}
以上代码执行结果如下:
Item Send: 1
Item Receive: 1
Item Send: 2
Item Send: 3
Item Send: 4
Item Send: 5
Item Receive: 5
Item Send: 6
Item Send: 7
Item Send: 8
Item Receive: 8
该示例展示了在队列长度为 1 的情况下,使用 xQueueOverwrite()
实现数据发送的行为特性。
发送任务持续将整数写入队列,接收任务周期性从队列中读取数据。
由于 xQueueOverwrite()
会在队列已满时覆盖旧数据,因此发送任务在未等待接收的情况下可以连续写入,新的数据总会替换旧数据保留在队列中。
接收任务每 5 个 tick 读取一次队列,因此只能读取到队列中最后一次被覆盖后仍未被再次覆盖的数据。
从执行结果可见,接收任务仅成功接收到部分数据,中间的大多数值由于在接收前已被覆盖,未能被获取。
这清晰体现了 xQueueOverwrite()
的覆盖特性,适用于仅关心最新数据的场景,例如传感器读数或状态刷新。
查看数据
API 原型
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
xQueuePeek()
用于查看队列头部的数据,但不会将数据从队列中移除,适合在不改变队列内容的情况下读取当前队首元素。
传入参数 |
参数功能 |
参数说明 |
---|---|---|
|
需要查看的队列的句柄 |
例如由 |
|
指向用于存储接收数据的缓冲区的指针 |
数据会复制到该缓冲区,确保该缓冲区大小不小于队列元素大小。 |
|
最长等待时间 |
当队列为空时,调用者阻塞等待的最大时间,以滴答周期(tick)为单位定义;为 0 时立即返回。 |
返回值 |
说明 |
---|---|
|
接收成功,数据已复制到指定的缓存区。 |
|
接收失败,队列为空且未等待或等待超时。 |
示例 5:查看数据
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
// 定义 QueueHandle_t 类型变量,用于存储队列创建成功后返回的句柄
QueueHandle_t queue = NULL;
#define QUEUE_LENGTH 5 // 定义队列长度
#define ITEM_SIZE 4 // 定义项目大小(4 字节每项)
static void vSendTask( void *pvParameters )
{
int num = 1; // 待发送数据
BaseType_t xReturn; // 用于接收返回值
while (1) {
xReturn = xQueueSend(queue, (void *)&num, 10);
// 通过返回值判断数据是否发送成功
if (xReturn == pdTRUE) {
printf("Item Send: %d \n", num);
num++;
}
vTaskDelay(1);
}
}
static void vReceiveTask( void *pvParameters )
{
int receiver = 0; // 用于接收数据
BaseType_t xReturn; // 用于接收返回值
while (1) {
xReturn = xQueuePeek(queue, (void *)&receiver, 10);
// 通过返回值判断数据是否接收成功
if (xReturn == pdTRUE) {
printf("Item Peek: %d \n", receiver);
}
else {
printf("Item Peek FALSE\n");
}
vTaskDelay(1);
}
}
void app_main(void)
{
// 创建队列
queue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);
if (queue != NULL) {
xTaskCreatePinnedToCore(vSendTask, "SendTask", 20480, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(vReceiveTask, "ReceiveTask", 4096, NULL, 2, NULL, 0);
}
}
以上代码执行结果如下:
Item Send: 1
Item Peek: 1
Item Send: 2
Item Peek: 1
Item Send: 3
Item Peek: 1
Item Send: 4
Item Peek: 1
Item Send: 5
该示例演示了使用 xQueuePeek()
查看队列头部数据的功能。
发送任务持续向队列写入递增的整数,而接收任务使用 xQueuePeek()
读取队列首个元素,但不将其移除。
执行结果显示,尽管发送任务不断写入新数据,接收任务读取的始终是队列中的第一个数据项(即第一次写入的值),说明 xQueuePeek()
仅用于查看数据而不会改变队列内容,适合在不影响队列状态的情况下获取当前队首信息。