队列 | 高效的任务间数据传输¶
参考出处
本文档参考了DURUOFU的ESP32-教程。
Tip
需要用到队列的地方要引用头文件 #include "freertos/queue.h"
。
1.队列的创建与传参¶
队列是任务间通信的主要形式。它们可以用于在**任务之间** 以及**中断和任务之间**发送消息。在大多数情况下,队列用作线程安全的 FIFO(先进先出)缓冲区, 新数据被发送到队列的后面,但也可以发送到前面。
1.1 API 说明¶
队列操作主要涉及以下几个API:
函数名 | 功能 | 备注 |
---|---|---|
xQueueCreate | 创建一个队列 | 创建指定长度和大小的队列 |
xQueueSend | 向队列中发送数据 | 如果队列已满,任务可选择阻塞或立即返回 |
xQueueReceive | 从队列中接收数据 | 如果队列为空,任务可选择阻塞或立即返回 |
xQueueSendToFront | 将数据发送到队列的队头位置 | 与 xQueueSend 类似,但优先级更高 |
xQueueSendToBack | 将数据发送到队列的队尾位置 | 默认行为,等效于 xQueueSend |
uxQueueMessagesWaiting | 查询队列中当前等待的消息数量 | 返回队列中尚未读取的消息数 |
xQueueCreate:创建队列¶
原型:
参数说明: - uxQueueLength:队列的长度(可以容纳的元素数量)。 - uxItemSize:队列中每个元素的大小(以字节为单位)。
返回值: 成功时返回队列句柄;失败时返回 NULL。
示例:
QueueHandle_t xQueue;
xQueue = xQueueCreate(10, sizeof(int)); // 创建一个可以存储 10 个整数的队列
if (xQueue == NULL) {
// 队列创建失败,处理错误
}
xQueueSend:向队列发送数据¶
原型:
参数说明:
- xQueue:队列的句柄。
- pvItemToQueue:指向要发送到队列的数据的指针。
- xTicksToWait:当队列已满时,任务等待的时间(以 tick 为单位)。设置为 0 表示不等待。
返回值: - pdPASS:数据成功发送到队列。 - errQUEUE_FULL:队列已满,数据发送失败。
示例:
xQueueReceive:从队列接收数据¶
原型:
参数说明: - xQueue:队列的句柄。 - pvBuffer:指向接收数据的缓冲区的指针。 - xTicksToWait:当队列为空时,任务等待的时间(以 tick 为单位)。设置为 0 表示不等待。
返回值: - pdPASS:数据成功接收。 - pdFALSE:队列为空,接收失败。
示例:
int receivedData;
if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdPASS) {
// 成功接收数据,进行处理
}
1.2 队列传参示例:¶
(1)队列传参-常量¶
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "main";
void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 0;
for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0)!= pdPASS) {
ESP_LOGI(TAG, "数据发送失败");
}
else
{
ESP_LOGI(TAG, "数据发送成功");
i++;
}
if(i == 10)
{
i = 0;
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
for (;;)
{
int receivedData;
if (xQueueReceive(xQueue, &receivedData, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据接收失败");
}
else
{
ESP_LOGI(TAG, "数据接收成功,数据为:%d", receivedData);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void app_main(void)
{
TaskHandle_t taskHandle_1 = NULL;
TaskHandle_t taskHandle_2 = NULL;
QueueHandle_t xQueue;
// 创建队列
xQueue = xQueueCreate(10, sizeof(int));
if (xQueue != NULL)
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_1);
// 接收数据任务
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_2);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}
(2)队列传参-结构体¶
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "main";
// 定义结构体
typedef struct
{
int id;
int data[3];
} MyStruct;
void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
MyStruct shendData = {1, {1, 2, 3}};
for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &shendData, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据发送失败");
}
else
{
ESP_LOGI(TAG, "数据发送成功");
shendData.id++;
}
if (shendData.id == 10)
{
shendData.id = 0;
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
for (;;)
{
MyStruct receivedData;
if (xQueueReceive(xQueue, &receivedData, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据接收失败");
}
else
{
ESP_LOGI(TAG, "数据接收成功,数据为:%d-[%d,%d,%d]", receivedData.id, receivedData.data[0], receivedData.data[1], receivedData.data[2]);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void app_main(void)
{
TaskHandle_t taskHandle_1 = NULL;
TaskHandle_t taskHandle_2 = NULL;
QueueHandle_t xQueue;
// 创建队列
xQueue = xQueueCreate(10, sizeof(MyStruct));
if (xQueue != NULL)
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_1);
// 接收数据任务
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_2);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}
(3)队列传参-指针¶
// 队列传参_指针: 一般用于传递占用内存较大的数据. 传递指针, 可以避免拷贝数据, 提高效率.
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "main";
void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 0;
for (;;)
{
char *pCharSend = (char *)malloc(50); // 申请内存
snprintf(pCharSend, 50, "Hello World! - %d", i);
i++;
// 发送数据到队列
if (xQueueSend(xQueue, &pCharSend, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据发送失败");
}
else
{
ESP_LOGI(TAG, "数据发送成功");
}
if (i == 10)
{
i = 0;
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
char *pCharReceived = NULL; // 接收数据
for (;;)
{
if (xQueueReceive(xQueue, &pCharReceived, 0) != pdPASS)
{
ESP_LOGI(TAG, "数据接收失败");
}
else
{
ESP_LOGI(TAG, "数据接收成功,数据为:%s", pCharReceived);
free(pCharReceived); // 释放内存
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void app_main(void)
{
TaskHandle_t taskHandle_1 = NULL;
TaskHandle_t taskHandle_2 = NULL;
QueueHandle_t xQueue;
// 创建队列
xQueue = xQueueCreate(10, sizeof(char *));
if (xQueue != NULL)
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_1);
// 接收数据任务
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue, 12, &taskHandle_2);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}
2.队列多进单出模型¶
Info
实际上是多个任务向一个队列发送数据,一个任务从队列中接收数据。这种模型常用于数据监听。
示例代码:
// 队列多进单出: 任务1和任务2发送数据到队列,任务3接收数据 任务3的优先级高于任务1和任务2,已达到数据监听的目的
// 参考:https://www.bilibili.com/video/BV1R44y177VS/?spm_id_from=333.788.top_right_bar_window_history.content.click&vd_source=ef5a0ab0106372751602034cdd9ab98e
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "main";
void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 111;
for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0) != pdPASS)
{
ESP_LOGI(TAG, "任务1数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务1数据发送成功");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 222;
for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0) != pdPASS)
{
ESP_LOGI(TAG, "任务2数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务2数据发送成功");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_3(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
for (;;)
{
int receivedData;
// 使用portMAX_DELAY阻塞等待数据
if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY) != pdPASS)
{
ESP_LOGI(TAG, "任务3数据接收失败");
}
else
{
ESP_LOGI(TAG, "任务3数据接收成功,数据为:%d", receivedData);
}
}
vTaskDelete(NULL);
}
void app_main(void)
{
QueueHandle_t xQueue;
// 创建队列
xQueue = xQueueCreate(10, sizeof(int));
if (xQueue != NULL)
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue, 1, NULL);
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue, 1, NULL);
// 接收数据任务
xTaskCreate(Task_3, "Task_1", 1024 * 4, (void *)xQueue, 2, NULL);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}
3.队列集合¶
Note
FreeRTOS 队列集合(Queue Sets)是一种机制,用于同时监听多个队列和信号量,以实现任务间的高效通信。
3.1 API说明:¶
函数名 | 功能 | 备注 |
---|---|---|
xQueueCreateSet | 创建一个队列集合 | 队列集合必须与队列或信号量配合使用 |
xQueueAddToSet | 将队列或信号量添加到队列集合 | 被添加的队列或信号量必须为空 |
xQueueRemoveFromSet | 从队列集合中移除队列或信号量 | |
xQueueSelectFromSet | 从队列集合中选择一个有数据可用的队列或信号量 | 返回非空的队列或信号量 |
3.2 示例代码:¶
// 队列集合:
// https : // www.bilibili.com/video/BV1zq4y1m7UK?spm_id_from=333.788.videopod.sections&vd_source=ef5a0ab0106372751602034cdd9ab98e
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "main";
void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 111;
for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0) != pdPASS)
{
ESP_LOGI(TAG, "任务1数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务1数据发送成功");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t xQueue = (QueueHandle_t)pvParameters;
int i = 222;
for (;;)
{
// 发送数据到队列
if (xQueueSend(xQueue, &i, 0) != pdPASS)
{
ESP_LOGI(TAG, "任务2数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务2数据发送成功");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_3(void *pvParameters)
{
// 取得队列集合句柄
QueueSetHandle_t xQueueSet = (QueueSetHandle_t)pvParameters;
// 取得有数据队列句柄
QueueSetMemberHandle_t QueueData;
for (;;)
{
QueueData = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
if (QueueData != NULL)
{
int i;
if (xQueueReceive(QueueData, &i, portMAX_DELAY) != pdPASS)
{
ESP_LOGI(TAG, "任务3数据接收失败");
}
else
{
ESP_LOGI(TAG, "任务3数据接收成功,数据为:%d", i);
}
}
}
vTaskDelete(NULL);
}
void app_main(void)
{
QueueHandle_t xQueue_1;
QueueHandle_t xQueue_2;
// 创建队列
xQueue_1 = xQueueCreate(10, sizeof(int));
xQueue_2 = xQueueCreate(10, sizeof(int));
// 创建队列集合
QueueSetHandle_t xQueueSet;
xQueueSet = xQueueCreateSet(20);
// 将队列添加到队列集合
xQueueAddToSet(xQueue_1, xQueueSet);
xQueueAddToSet(xQueue_2, xQueueSet);
if ((xQueue_1 != NULL )&& (xQueue_2 != NULL) && (xQueueSet != NULL))
{
ESP_LOGI(TAG, "队列创建成功");
// 发送数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)xQueue_1, 1, NULL);
xTaskCreate(Task_2, "Task_1", 1024 * 4, (void *)xQueue_2, 1, NULL);
// 接收数据任务
xTaskCreate(Task_3, "Task_1", 1024 * 4, (void *)xQueueSet, 2, NULL);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}
4.队列邮箱¶
FreeRTOS的邮箱概念跟别的RTOS不一样,它是一个队列,队列长度只有1. 写邮箱:新数据覆盖旧数据,读邮箱:读数据时,数据不会被移除;这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。
// 队列邮箱: FreeRTOS的邮箱概念跟别的RTOS不一样,它是一个队列,队列长度只有1.
// 写邮箱:新数据覆盖旧数据,读邮箱:读数据时,数据不会被移除;
// 这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。
// https : // www.bilibili.com/video/BV1zq4y1m7UK?spm_id_from=333.788.videopod.sections&vd_source=ef5a0ab0106372751602034cdd9ab98e
#include <stdio.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
static const char *TAG = "main";
// 写数据队列
void Task_1(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t Mailbox = (QueueHandle_t)pvParameters;
int i = 0;
for (;;)
{
// 发送数据到队列
if (xQueueOverwrite(Mailbox, &i) != pdPASS)
{
ESP_LOGI(TAG, "任务1数据发送失败");
}
else
{
ESP_LOGI(TAG, "任务1数据发送成功");
}
i++;
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
// 读数据队列
void Task_2(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t Mailbox = (QueueHandle_t)pvParameters;
int i = 0;
for (;;)
{
// 读取数据
if (xQueuePeek(Mailbox, &i, portMAX_DELAY) == pdPASS)
{
ESP_LOGI(TAG, "任务2数据读取成功,数据为:%d", i);
}
else
{
ESP_LOGI(TAG, "任务2数据读取失败");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_3(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t Mailbox = (QueueHandle_t)pvParameters;
int i = 0;
for (;;)
{
// 读取数据
if (xQueuePeek(Mailbox, &i, portMAX_DELAY) == pdPASS)
{
ESP_LOGI(TAG, "任务3数据读取成功,数据为:%d", i);
}
else
{
ESP_LOGI(TAG, "任务3数据读取失败");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void Task_4(void *pvParameters)
{
// 取得队列句柄
QueueHandle_t Mailbox = (QueueHandle_t)pvParameters;
int i = 0;
for (;;)
{
// 读取数据
if (xQueuePeek(Mailbox, &i, portMAX_DELAY) == pdPASS)
{
ESP_LOGI(TAG, "任务4数据读取成功,数据为:%d", i);
}
else
{
ESP_LOGI(TAG, "任务4数据读取失败");
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
vTaskDelete(NULL);
}
void app_main(void)
{
QueueHandle_t Mailbox; // 创建邮箱
// 创建队列(注意:队列长度只有1)
Mailbox = xQueueCreate(1, sizeof(int));
if ((Mailbox != NULL) )
{
ESP_LOGI(TAG, "队列创建成功");
// 写数据任务
xTaskCreate(Task_1, "Task_1", 1024 * 4, (void *)Mailbox, 2, NULL);
xTaskCreate(Task_2, "Task_2", 1024 * 4, (void *)Mailbox, 1, NULL);
xTaskCreate(Task_3, "Task_3", 1024 * 4, (void *)Mailbox, 1, NULL);
xTaskCreate(Task_4, "Task_4", 1024 * 4, (void *)Mailbox, 1, NULL);
}
else
{
ESP_LOGI(TAG, "队列创建失败");
}
}