FreeRTOS队列是实时嵌入式系统中任务间通信的核心机制。本文深入浅出地介绍了队列的工作原理、核心API函数以及多种实际应用场景。通过四个精心设计的实战示例,读者将掌握队列的基础数据收发、多源数据识别、大块数据传输以及邮箱机制的实现方法。
一、队列的作用与价值
在嵌入式实时系统(RTOS)中,应用程序通常由多个独立的任务构成,每个任务负责特定的功能模块。这些任务之间并非孤立存在,它们需要相互协作、交换信息,共同完成复杂的系统功能。例如,一个数据采集任务需要将传感器数据传递给处理任务,而处理任务又可能需要将结果发送给显示任务或通信任务。
如何实现任务间高效、可靠的数据交换,就成为RTOS设计的关键问题。FreeRTOS对此提供了一个名为 队列(Queue) 的机制——有时也称为消息队列(Message Queue)——来完成任务与任务、任务与中断之间的消息传递。
队列的核心价值可以概括为以下几点:
-
解耦任务:发送任务和接收任务不需要知道对方的存在,只需通过队列这个中介进行通信。
-
线程安全:队列操作在内核层面保证了多任务环境下的数据完整性,无需开发者额外使用锁机制。
-
阻塞机制:任务可以在队列空或满时进入阻塞状态,让出CPU给其他任务,提高系统效率。
-
灵活的数据传输:支持任意类型的数据传递,从简单的整数到复杂的结构体,甚至可以通过指针传输大块数据。
本文将系统性地介绍FreeRTOS队列的方方面面,从核心原理到API详解,再到四个实际应用示例,帮助您彻底掌握这一重要工具。
二、队列的核心特性
2.1 数据结构特性
队列本质上是一种数据结构,在FreeRTOS中采用**先进先出(FIFO,First In First Out)**的存储方式。新数据总是被写入队列的尾部,而读取数据时则从队列的头部取出。这种机制确保了数据处理的公平性——先到的数据先被处理。
从物理结构上看,队列具备以下特征:
-
固定长度:队列可以包含若干个数据项,这个数量被称为队列的“长度”(length),在创建队列时就需指定。
-
固定大小:每个数据项的大小(以字节为单位)在创建队列时确定,队列中所有数据项大小一致。
-
独立存储:队列拥有自己的存储空间,由FreeRTOS内核负责分配和管理,无需任务操心。
图1展示了队列的基本操作模型:
写队列(尾部) → [数据4] [数据3] [数据2] [数据1] → 读队列(头部)
(最新) (最旧)
在FreeRTOS源码中,队列的控制块(Queue Control Block)定义为Queue_t结构体,其中包含了关键的管理信息:
-
pcHead/pcTail:指向队列存储区的起始和结束位置
-
-
uxMessagesWaiting:当前队列中的消息个数
-
-
-
xTasksWaitingToSend和xTasksWaitingToReceive:阻塞在发送和接收操作上的任务列表
这些成员共同构成了队列的“大脑”,管理着数据的流入流出以及任务的阻塞与唤醒。
2.2 数据传输机制:拷贝 vs 引用
FreeRTOS队列在传输数据时,默认采用**拷贝(Copy)**的方式。这意味着当任务调用发送函数时,数据本身(而不是数据的引用)被复制到队列的存储区中;当接收任务读取数据时,再从队列复制到接收缓冲区。
这种设计带来了多方面的优势:
-
安全性高:即使发送任务在发送后立即修改或释放了原变量,队列中的数据依然完好无损。
-
无需额外缓冲区:发送任务可以使用局部变量直接发送,无需动态分配内存来保存数据。
-
任务解耦:发送任务和接收任务不需要约定由谁负责释放数据,内核接管了所有内存管理。
-
适用于内存保护系统:在有MPU(内存保护单元)的系统中,内核具有足够的权限将数据从一个受限区域复制到队列,再复制到另一个受限区域。
然而,当需要传输的数据量很大时(例如一个1KB的传感器数据包),每次都进行完整拷贝会影响系统效率。这时可以采用第二种方式:引用(Reference)。即队列中传输的是指向数据的指针,而不是数据本身。
// 引用方式示例:传输大块数据的地址
char *buffer = pcLargeDataBuffer;
xQueueSend(xQueue, &buffer, 0); // 只复制指针(4或8字节),而非整个buffer
使用引用方式时,需要特别注意内存的管理:
-
数据必须持久存在:通常应该是全局变量或静态分配的变量,不能是发送任务的局部变量(函数退出后会被回收)。
-
访问权限需明确:确保接收任务有权限访问该内存区域。
-
避免同时修改:发送后到接收前,应避免其他任务修改同一块内存。
2.3 阻塞机制
阻塞机制是FreeRTOS队列强大功能的精髓所在。它允许任务在无法立即完成队列操作时主动让出CPU,进入等待状态,从而避免忙等待浪费CPU资源。
2.3.1 读阻塞
当一个任务试图从一个空队列中读取数据时,可以选择:
-
立即返回:设置阻塞时间为0,函数立刻返回
errQUEUE_EMPTY,任务继续执行后续代码。
-
有限等待:指定一个阻塞超时时间(如100个系统节拍),任务进入阻塞状态。在此期间,如果队列中有了数据,任务被唤醒并读取数据;如果超时时间到仍然没有数据,任务自动转为就绪态,函数返回错误。
-
无限等待:设置阻塞时间为
portMAX_DELAY,任务将一直阻塞直到队列中有数据(前提是将INCLUDE_vTaskSuspend设置为1)。
2.3.2 写阻塞
类似地,当任务试图向一个满队列中写入数据时:
-
如果设置阻塞时间为0,队列满时立即返回
errQUEUE_FULL
-
如果设置阻塞时间大于0,任务进入阻塞状态,等待队列出现空闲空间
-
当其他任务从队列中读取数据后,等待的任务被唤醒并完成写入操作
-
如果超时时间到仍未等到空间,任务转为就绪态并返回错误
2.3.3 多任务阻塞的唤醒规则
当多个任务阻塞在同一个队列上时(无论是读阻塞还是写阻塞),队列可用时的唤醒顺序遵循明确的规则:
-
-
如果多个阻塞任务优先级相同,那么等待时间最久的任务(即最早进入阻塞态的任务)被唤醒
这种机制确保了系统的实时性和公平性,高优先级任务能够及时响应,同优先级任务也能按顺序获得服务。
重要提示:中断服务程序(ISR)中使用的队列API函数(以FromISR结尾)不支持阻塞操作。这是因为中断上下文不能进入阻塞状态——中断必须快速执行并返回。
三、队列核心API详解
FreeRTOS提供了丰富而清晰的队列操作函数。所有队列API的原型都定义在queue.h头文件中。按照功能,我们可以将这些API分为四大类:生命周期管理、数据写入、数据读取和辅助查询。
3.1 队列的生命周期管理
3.1.1 创建队列
FreeRTOS提供了两种创建队列的方式:动态内存分配和静态内存分配。
动态创建:xQueueCreate
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
-
uxQueueLength:队列长度,即最多能存放的数据项个数
-
uxItemSize:每个数据项的大小,以字节为单位
-
返回值:非NULL表示成功返回队列句柄;NULL表示内存不足创建失败
动态创建方式最为常用,队列所需的内存(包括控制块和存储区)由FreeRTOS内核从堆中自动分配。
静态创建:xQueueCreateStatic
QueueHandle_t xQueueCreateStatic(
UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer
);
-
pucQueueStorageBuffer:指向存储区的指针,大小至少为uxQueueLength * uxItemSize字节
-
pxQueueBuffer:指向StaticQueue_t结构体的指针,用于保存队列数据结构
-
返回值:成功返回队列句柄;失败返回NULL(通常因为
pxQueueBuffer为NULL)
静态创建适用于需要完全控制内存分配的场景,如安全关键系统或不允许使用动态内存的环境。
静态创建示例:
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof(uint32_t)
StaticQueue_t xStaticQueue; // 用于保存队列结构体
uint8_t ucQueueStorage[QUEUE_LENGTH * ITEM_SIZE]; // 用于保存队列数据
void vATask(void *pvParameters) {
QueueHandle_t xQueue;
xQueue = xQueueCreateStatic(QUEUE_LENGTH, ITEM_SIZE,
ucQueueStorage, &xStaticQueue);
// 现在可以使用xQueue了
}
3.1.2 复位队列
BaseType_t xQueueReset(QueueHandle_t xQueue);
复位函数将队列恢复到刚创建时的初始状态——清空所有数据。无论复位前队列中有多少数据,调用后队列变为空。该函数必定成功,返回值恒为pdPASS。
3.1.3 删除队列
void vQueueDelete(QueueHandle_t xQueue);
删除队列并释放其占用的内存。需要注意的是,此函数只能删除使用xQueueCreate动态创建的队列。静态创建的队列需要由开发者自行管理内存释放。删除队列前,应确保没有任何任务正在阻塞于该队列。
3.2 数据写入操作
FreeRTOS提供了多种写入队列的方式,满足不同场景的需求。
3.2.1 尾部写入(标准FIFO)
BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
xQueueSend等同于xQueueSendToBack,都是将数据写入队列尾部,遵循标准的FIFO顺序。
3.2.2 头部写入(LIFO紧急数据)
BaseType_t xQueueSendToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
此函数将数据写入队列头部,使得新数据成为下一个被读取的对象,实现后进先出(LIFO)的行为。适用于紧急消息的处理。
3.2.3 中断安全的写入版本
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);
这些是在中断服务程序中使用的版本,不可阻塞。pxHigherPriorityTaskWoken用于指示是否因为此次写入操作唤醒了一个优先级高于当前被中断任务的任务。如果该参数被设置为pdTRUE,则应在中断退出前执行上下文切换。
3.2.4 覆盖写入(用于队列长度为1)
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void *pvItemToQueue);
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);
这些函数用于队列长度为1的特殊情况。它们会无条件覆盖队列中的现有数据,无论队列当前是否满。由于覆盖操作总是成功,这些函数不会被阻塞。
写入函数的参数说明:
|
参数
|
说明
|
xQueue |
队列句柄,指定要操作的队列
|
pvItemToQueue |
指向待发送数据的指针,数据会被复制到队列中(复制大小为创建时指定的uxItemSize)
|
xTicksToWait |
阻塞超时时间(以Tick为单位)。0表示不阻塞;portMAX_DELAY表示无限等待
|
| 返回值 |
pdPASS
:成功写入;errQUEUE_FULL:队列满且超时时间内未能写入
|
3.3 数据读取操作
3.3.1 读取并移除数据
BaseType_t xQueueReceive(QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait);
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxTaskWoken);
xQueueReceive从队列头部读取一个数据项,并将其从队列中移除。读取的数据被复制到pvBuffer指向的缓冲区中(缓冲区大小至少为队列的uxItemSize)。
参数说明:
|
参数
|
说明
|
xQueue |
队列句柄
|
pvBuffer |
指向接收缓冲区的指针,数据将被复制到这里
|
xTicksToWait |
阻塞超时时间
|
| 返回值 |
pdPASS
:成功读取;errQUEUE_EMPTY:队列空且超时时间内无数据
|
3.3.2 读取并保留数据(窥探)
BaseType_t xQueuePeek(QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait);
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void *pvBuffer);
“窥探”(Peek)操作从队列头部复制出数据,但不移除该数据项。这意味着多次调用xQueuePeek会得到相同的数据,直到有任务读取并移除了该数据。
xQueuePeek在队列为空时会阻塞,而xQueuePeekFromISR(中断版本)不支持阻塞,队列为空时立即返回错误。
3.4 辅助查询函数
3.4.1 查询消息数量
UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue);
UBaseType_t uxQueueMessagesWaitingFromISR(const QueueHandle_t xQueue);
返回队列中当前可用的数据项个数。中断服务程序中应使用FromISR版本。
3.4.2 查询空闲空间
UBaseType_t uxQueueSpacesAvailable(const QueueHandle_t xQueue);
返回队列中剩余的空闲空间数量(即还可以容纳多少个数据项)。此函数可用于判断是否可以向队列写入数据而不阻塞。
四、典型场景示例
理论结合实践是最好的学习方式。本节通过四个逐步深入的示例,展示FreeRTOS队列在各种实际场景中的应用。所有示例均基于FreeRTOS内核,可在主流开发板上运行。
4.1 示例1:基础数据收发
场景描述:本示例创建一个队列,然后创建2个发送任务和1个接收任务。发送任务分别向队列写入100和200,接收任务读取并打印这些数值。通过这个基础示例,了解队列的创建、发送和接收流程。
代码实现:
/* FreeRTOS_08_queue - 基础队列收发示例 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>
/* 队列句柄 */
QueueHandle_t xQueue;
/* 发送任务函数 */
static void vSenderTask(void *pvParameters) {
int32_t lValueToSend = (int32_t)pvParameters;
BaseType_t xStatus;
for (;;) {
/* 向队列发送数据,不阻塞 */
xStatus = xQueueSendToBack(xQueue, &lValueToSend, 0);
if (xStatus != pdPASS) {
printf("Sender %d: Queue is full!\r\n", lValueToSend);
}
/* 延迟一段时间,避免独占CPU */
vTaskDelay(pdMS_TO_TICKS(500));
}
}
/* 接收任务函数 */
static void vReceiverTask(void *pvParameters) {
int32_t lReceivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
for (;;) {
/* 从队列接收数据,阻塞等待最多100ms */
xStatus = xQueueReceive(xQueue, &lReceivedValue, xTicksToWait);
if (xStatus == pdPASS) {
printf("Receiver: Received %d\r\n", lReceivedValue);
} else {
printf("Receiver: Timeout - no data\r\n");
}
}
}
int main(void) {
/* 创建队列:长度为5,数据大小为4字节(int32_t) */
xQueue = xQueueCreate(5, sizeof(int32_t));
if (xQueue != NULL) {
/* 创建两个发送任务,优先级为1 */
xTaskCreate(vSenderTask, "Sender1", 1000, (void*)100, 1, NULL);
xTaskCreate(vSenderTask, "Sender2", 1000, (void*)200, 1, NULL);
/* 创建一个接收任务,优先级为2(高于发送任务) */
xTaskCreate(vReceiverTask, "Receiver", 1000, NULL, 2, NULL);
/* 启动调度器 */
vTaskStartScheduler();
}
/* 如果运行到这里,说明内存不足 */
return 0;
}
运行结果分析:
程序运行后,接收任务优先级更高,因此一旦队列中有数据,接收任务会立即读取。发送任务以500ms的间隔发送数据,接收任务读取并打印。
这个基础示例展示了队列的最基本用法。值得注意的是,当接收任务优先级高于发送任务时,队列中的数据会被立即处理;反之,如果接收任务优先级较低,则可能处理不及时,导致队列堆积。
4.2 示例2:多源数据识别
场景描述:当多个任务通过同一个队列发送数据时,接收任务需要能够分辨数据的来源。本示例通过传输结构体(包含数据ID和值)来解决这个问题。
问题分析:简单的整数传输无法携带来源信息。解决方案是定义包含“数据源ID”的结构体,发送任务构造结构体后发送,接收任务根据ID区分来源。
代码实现:
/* FreeRTOS_09_queue_datasource - 多源数据识别示例 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>
/* 定义数据来源ID枚举 */
typedef enum {
eMotorSpeed, // 电机速度数据
eSpeedSetPoint // 速度设定点数据
} DataID_t;
/* 定义队列传输的数据格式 */
typedef struct {
DataID_t eDataID; // 数据来源ID
int32_t lDataValue; // 数据值
} MotorData_t;
/* 定义两个结构体实例 */
static const MotorData_t xStructures[2] = {
{ eMotorSpeed, 1000 }, // 电机速度数据
{ eSpeedSetPoint, 800 } // 速度设定点数据
};
QueueHandle_t xQueue;
/* 发送任务 - 根据传入参数发送不同结构体 */
static void vSenderTask(void *pvParameters) {
const MotorData_t *pxData = (const MotorData_t *)pvParameters;
BaseType_t xStatus;
for (;;) {
/* 发送结构体到队列 */
xStatus = xQueueSend(xQueue, pxData, 0);
if (xStatus != pdPASS) {
printf("Sender failed (ID: %d)\r\n", pxData->eDataID);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* 接收任务 - 根据ID识别数据来源 */
static void vReceiverTask(void *pvParameters) {
MotorData_t xReceivedData;
BaseType_t xStatus;
for (;;) {
xStatus = xQueueReceive(xQueue, &xReceivedData, portMAX_DELAY);
if (xStatus == pdPASS) {
/* 根据数据源ID进行不同处理 */
switch (xReceivedData.eDataID) {
case eMotorSpeed:
printf("Motor Speed: %d RPM\r\n", xReceivedData.lDataValue);
break;
case eSpeedSetPoint:
printf("Set Point: %d RPM\r\n", xReceivedData.lDataValue);
break;
default:
printf("Unknown data source\r\n");
break;
}
}
}
}
int main(void) {
/* 创建队列:传输结构体 */
xQueue = xQueueCreate(5, sizeof(MotorData_t));
if (xQueue != NULL) {
/* 创建两个发送任务,传入不同结构体的地址 */
xTaskCreate(vSenderTask, "MotorTask", 1000, (void*)&xStructures[0], 1, NULL);
xTaskCreate(vSenderTask, "SetTask", 1000, (void*)&xStructures[1], 1, NULL);
/* 创建接收任务 */
xTaskCreate(vReceiverTask, "ControlTask", 1000, NULL, 2, NULL);
vTaskStartScheduler();
}
return 0;
}
运行结果示例:
Motor Speed: 1000 RPM
Set Point: 800 RPM
Motor Speed: 1000 RPM
Set Point: 800 RPM
...
深入分析:任务优先级与阻塞唤醒
这个示例中,接收任务优先级(2)高于发送任务(1),因此数据总是立即被处理。但如果交换优先级——让接收任务优先级低于发送任务,情况会如何?
此时,发送任务会连续写入队列直到满,然后进入阻塞状态;接收任务以较低优先级运行,每次读取一个数据后,由于发送任务优先级更高且正在等待队列空间,系统会立即切换回发送任务。这种场景下,数据在发送任务和接收任务间“乒乓”传递,体现了FreeRTOS的优先级调度和阻塞唤醒机制。
4.3 示例3:大块数据传输
场景描述:当需要传输大块数据(如长字符串、传感器数据包、图像数据)时,每次都拷贝整个数据会影响系统效率。本示例演示如何通过传输指针来实现高效的大块数据传递。
设计要点:
-
-
-
代码实现:
/* FreeRTOS_10_queue_bigtransfer - 大块数据传输示例 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>
#include <string.h>
/* 全局数据缓冲区 - 保证数据持久存在 */
static char cBuffer[100];
QueueHandle_t xQueue;
/* 发送任务 */
static void vSenderTask(void *pvParameters) {
static int ulCount = 0;
char *pcBufferPtr;
BaseType_t xStatus;
for (;;) {
/* 在全局缓冲区中构造数据 */
snprintf(cBuffer, sizeof(cBuffer), "Message from sender #%d\r\n", ulCount++);
/* 获取缓冲区地址(指针) */
pcBufferPtr = cBuffer;
/* 发送指针到队列(注意:只复制指针,不复制整个缓冲区) */
xStatus = xQueueSend(xQueue, &pcBufferPtr, 0);
if (xStatus != pdPASS) {
printf("Failed to send pointer\r\n");
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
/* 接收任务 */
static void vReceiverTask(void *pvParameters) {
char *pcReceivedPtr;
BaseType_t xStatus;
for (;;) {
/* 接收指针 */
xStatus = xQueueReceive(xQueue, &pcReceivedPtr, portMAX_DELAY);
if (xStatus == pdPASS) {
/* 通过指针访问大块数据 */
printf("Received: %s", pcReceivedPtr);
}
}
}
int main(void) {
/* 创建队列:长度为3,数据大小为指针大小 */
xQueue = xQueueCreate(3, sizeof(char*));
if (xQueue != NULL) {
/* 创建任务:发送任务优先级1,接收任务优先级2 */
xTaskCreate(vSenderTask, "Sender", 1000, NULL, 1, NULL);
xTaskCreate(vReceiverTask, "Receiver", 1000, NULL, 2, NULL);
vTaskStartScheduler();
}
return 0;
}
注意事项:
使用指针传递大块数据时,有几个关键点必须牢记:
-
数据持久性:缓冲区必须是全局变量、静态变量或动态分配的内存(确保在接收前不会被释放)。绝不能使用发送任务的局部变量。
-
访问同步:发送任务在将指针放入队列后,应避免继续修改缓冲区内容,直到确认接收任务已经完成读取。可以通过额外的同步机制(如信号量)来协调。
-
多接收者问题:如果有多个任务可能接收指针,需要明确“所有权”规则——谁接收到指针,谁就负责使用并(如果需要)释放内存。
-
内存泄漏预防:如果使用动态分配的内存发送数据,接收任务必须在使用后释放内存,否则会造成内存泄漏。
4.4 示例4:邮箱(Mailbox)机制
场景描述:邮箱是一种特殊的队列(队列长度为1),具有“覆盖写”和“窥探读”的特性。它就像一个公共公告板——最新消息覆盖旧消息,任何人都可以查看而不移除消息。本示例演示邮箱的应用。
邮箱特性:
-
写操作:使用
xQueueOverwrite,总是成功(无论队列是否满)
-
读操作:使用
xQueuePeek,读取但不移除数据;第一次读取可能阻塞,之后直接返回最新数据
代码实现:
/* FreeRTOS_11_queue_mailbox - 邮箱(Mailbox)示例 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>
QueueHandle_t xMailbox;
/* 发送任务(更新邮箱) */
static void vSenderTask(void *pvParameters) {
uint32_t ulValueToSend = 0;
for (;;) {
/* 使用xQueueOverwrite覆盖写入邮箱(队列长度必须为1) */
xQueueOverwrite(xMailbox, &ulValueToSend);
printf("Mailbox updated to: %d\r\n", ulValueToSend);
ulValueToSend++;
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
/* 接收任务(读取邮箱) */
static void vReceiverTask(void *pvParameters) {
uint32_t ulReceivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS(1000);
for (;;) {
/* 使用xQueuePeek窥探邮箱(读取但不移除数据) */
xStatus = xQueuePeek(xMailbox, &ulReceivedValue, xTicksToWait);
if (xStatus == pdPASS) {
printf("Receiver peeked: %d\r\n", ulReceivedValue);
} else {
printf("Receiver: Mailbox empty (timeout)\r\n");
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
/* 第二个接收任务 */
static void vReceiver2Task(void *pvParameters) {
uint32_t ulReceivedValue;
BaseType_t xStatus;
for (;;) {
xStatus = xQueuePeek(xMailbox, &ulReceivedValue, portMAX_DELAY);
if (xStatus == pdPASS) {
printf("Receiver2 peeked: %d\r\n", ulReceivedValue);
}
vTaskDelay(pdMS_TO_TICKS(700));
}
}
int main(void) {
/* 创建队列长度为1 - 这就是邮箱 */
xMailbox = xQueueCreate(1, sizeof(uint32_t));
if (xMailbox != NULL) {
/* 发送任务优先级2,先执行 */
xTaskCreate(vSenderTask, "Sender", 1000, NULL, 2, NULL);
/* 接收任务优先级1 */
xTaskCreate(vReceiverTask, "Receiver1", 1000, NULL, 1, NULL);
xTaskCreate(vReceiver2Task, "Receiver2", 1000, NULL, 1, NULL);
vTaskStartScheduler();
}
return 0;
}
运行结果分析:
程序运行初期,发送任务先执行,将邮箱初始化为0。随后,两个接收任务不断“偷看”邮箱,反复读取到相同的值0,直到发送任务3秒后更新邮箱为1,接收任务开始读取新值。
这个示例展示了邮箱的两个核心特点:
-
-
多接收者共享:多个任务可以同时“查看”同一数据,互不干扰
邮箱机制适用于系统状态信息、配置参数等需要被多个任务共享的场景。
五、队列集
本文主要聚焦于单个队列的使用。但在复杂系统中,有时一个任务需要同时处理来自多个队列的数据。FreeRTOS提供了**队列集(Queue Set)**功能来解决这一问题。
队列集允许任务同时阻塞在多个队列和/或信号量上,类似于网络编程中的select()函数。使用流程为:
-
-
使用
xQueueAddToSet()将队列或信号量添加到集合中
-
使用
xQueueSelectFromSet()等待集合中任一成员就绪
然而,官方文档也指出,在许多情况下,通过设计一个单一队列并传输包含事件类型的结构体,可以更高效地实现相同的功能。这正是我们在示例2中所采用的方法。
FreeRTOS-Plus-TCP协议栈的实现就是一个很好的例子:它使用一个队列接收来自ARP定时器、以太网硬件、应用程序等各种来源的事件,事件类型字段告诉处理任务该事件的性质和关联数据。
六、总结
FreeRTOS队列是任务间通信的基石,理解并掌握其使用方法对于开发可靠的嵌入式系统至关重要。本文从队列的基本特性出发,详细介绍了其工作原理、API函数,并通过四个由浅入深的示例展示了队列在不同场景下的应用。
核心要点回顾:
-
-
阻塞机制使得任务可以在队列空/满时让出CPU,提高系统效率
-
多任务阻塞的唤醒规则:优先级最高优先,同优先级则等待最久者优先
-
传输大块数据时考虑使用指针,但需注意数据生命期管理
-
队列的应用场景非常广泛——从简单的数据传递,到复杂的多源事件处理,再到任务间的同步协作。掌握队列的使用,将使您的FreeRTOS应用设计更加游刃有余。