FreeRTOS中的消息队列介绍&常见API函数
本文介绍了FreeRTOS消息队列的基本概念和使用方法。消息队列是一种遵循FIFO原则的异步通信机制,允许任务间安全传递固定大小的数据。文章详细讲解了创建队列(xQueueCreate)、发送数据(xQueueSend)、接收数据(xQueueReceive)等核心API的使用,并通过LED控制示例展示了生产者和消费者模式的实际应用。特别强调了队列长度设计、阻塞超时设置和数据完整性等关键注意事项,
目录
4. xQueueSendFromISR() / xQueueReceiveFromISR() - 中断安全版本

消息队列介绍
什么是消息队列?
简单来说,消息队列是一个可以存储有限数量、固定大小数据的缓冲区。它遵循先进先出(FIFO)的原则。任务可以将数据发送(写入)到队列中,其他任务可以从队列中接收(读取)数据。
消息队列的特点
-
异步通信: 任务不需要同时运行。发送任务可以在数据准备好时将其放入队列,接收任务可以在任何时候从队列中取出数据。
-
数据拷贝: 默认情况下,发送到队列中的数据是按值拷贝的。这意味着当数据被发送时,它会从发送任务的内存空间拷贝到队列的内存空间。当数据被接收时,它会从队列的内存空间拷贝到接收任务的内存空间。这种机制避免了指针传递可能带来的数据完整性问题(比如发送方在接收方读取前修改了数据)。
-
阻塞机制:
-
发送时阻塞: 如果队列已满,发送任务可以选择阻塞等待,直到队列中有空间可用。
-
接收时阻塞: 如果队列为空,接收任务可以选择阻塞等待,直到队列中有数据可用。
-
可以通过设置超时时间来限制阻塞的最长时间,防止任务无限期等待。
-
-
优先级处理:
-
正常优先级(尾部发送): 数据通常被添加到队列的尾部。
-
高优先级(头部发送): FreeRTOS 也支持将数据插入队列的头部(例如 xQueueSendToFront() ),这对于需要立即处理的紧急消息很有用。
-
-
ISR 安全性: FreeRTOS 提供了专门的FromISR版本 API,允许在中断服务程序中安全地向队列发送数据,但不能在中断中接收数据(因为中断通常不应该阻塞)。
消息队列的工作原理
- 创建队列: 使用 xQueueCreate() 函数创建队列。你需要指定队列能存储的最大项目数量和每个项目的大小(以字节为单位)。一旦创建,队列就会分配所需的内存。
- 发送数据: 当一个任务想向队列发送数据时,它会调用 xQueueSend()(或 xQueueSendToFront())。如果队列已满,并且指定了超时时间,任务会进入阻塞状态,等待队列中有空间。
- 接收数据: 当一个任务想从队列接收数据时,它会调用 xQueueReceive()。如果队列为空,并且指定了超时时间,任务会进入阻塞状态,等待队列中有数据。
- 任务调度: 当发送任务成功将数据放入队列,并且有任务正在等待从该队列接收数据时,等待中的接收任务(如果其优先级高于当前任务)可能会被解除阻塞并立即得到调度。同样,当接收任务成功取出数据,并且有任务正在等待向该队列发送数据时,等待中的发送任务也可能被解除阻塞。
消息队列中的常用函数
1. xQueueCreate() - 创建消息队列
这是使用消息队列的第一步,它负责分配队列所需的内存并初始化队列结构。
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
-
参数:
-
uxQueueLength:队列可以存储的最大项目(消息)数量。例如,如果你想存储 10 个整数,这里就填 10。 -
uxItemSize:队列中每个项目(消息)的大小,以字节为单位。例如,如果你要存储int类型,这里就填sizeof(int);如果你要存储一个结构体MyStruct_t,这里就填sizeof(MyStruct_t)。
-
-
返回值:
-
QueueHandle_t:如果队列创建成功,返回一个有效的队列句柄(QueueHandle_t类型)。这个句柄是后续所有队列操作的凭证。 -
NULL:如果因为内存不足或其他原因导致队列创建失败,则返回NULL。
-
-
使用场景:
-
通常在系统初始化阶段(例如
main()函数中,在调度器启动之前)调用一次,为任务间的通信准备好队列。
-
示例:
QueueHandle_t xMyQueue;
// 创建一个能存储5个整数的队列
xMyQueue = xQueueCreate( 5, sizeof( int ) );
if( xMyQueue == NULL )
{
// 队列创建失败,处理错误
printf("Error: Failed to create queue.\n");
}
2. xQueueSend() - 向队列发送数据
这个函数用于将数据发送(写入)到队列的尾部。
函数原型
BaseType_t xQueueSend( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
-
参数:
-
xQueue:要发送数据的队列的句柄,由xQueueCreate()返回。 -
pvItemToQueue:指向要发送数据的指针。请注意,数据是按值拷贝的,所以这里是数据的源地址。例如,如果你要发送一个整数my_int,这里就是&my_int。 -
xTicksToWait:如果队列已满,任务愿意等待的最长时间(以时钟节拍为单位)。-
0:表示不等待,如果队列满则立即返回失败。 -
portMAX_DELAY:表示无限等待,直到队列有空间可用。 -
其他正整数:表示等待指定的时钟节拍数。
-
-
-
返回值:
-
pdPASS:数据成功发送到队列。 -
errQUEUE_FULL:队列已满,并且等待时间已过(或设置为0不等待)。
-
-
使用场景:
-
生产者任务将处理好的数据发送给消费者任务。
-
事件触发任务将事件通知发送给事件处理任务。
-
int my_data = 123;
// 尝试发送数据,如果队列满则等待100ms
if( xQueueSend( xMyQueue, ( void * ) &my_data, pdMS_TO_TICKS( 100 ) ) != pdPASS )
{
printf("Failed to send data to queue.\n");
}
3. xQueueReceive() - 从队列接收数据
这个函数用于从队列的头部接收(读取)数据。
BaseType_t xQueueReceive( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait );
-
参数:
-
xQueue:要接收数据的队列的句柄。 -
pvBuffer:指向存储接收到数据的缓冲区的指针。数据会从队列拷贝到这个地址。例如,如果你要接收一个整数到received_int,这里就是&received_int。 -
xTicksToWait:如果队列为空,任务愿意等待的最长时间(以时钟节拍为单位)。-
0:表示不等待,如果队列空则立即返回失败。 -
portMAX_DELAY:表示无限等待,直到队列有数据可用。 -
其他正整数:表示等待指定的时钟节拍数。
-
-
-
返回值:
-
pdPASS:数据成功从队列接收。 -
errQUEUE_EMPTY:队列为空,并且等待时间已过(或设置为0不等待)。
-
-
使用场景:
-
消费者任务从队列中获取数据进行处理。
-
事件处理任务等待并响应事件。
-
示例:
int received_data;
// 尝试从队列接收数据,如果队列空则无限等待
if( xQueueReceive( xMyQueue, ( void * ) &received_data, portMAX_DELAY ) == pdPASS )
{
printf("Received data: %d\n", received_data);
}
4. xQueueSendFromISR() / xQueueReceiveFromISR() - 中断安全版本
这两个函数是专门为在**中断服务程序(ISR)**中安全地操作队列而设计的。它们不能导致中断阻塞。
xQueueSendFromISR() - 向队列发送数据(中断安全)
BaseType_t xQueueSendFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken
);
xQueueReceiveFromISR() - 从队列接收数据(中断安全)
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void * pvBuffer,
BaseType_t * pxHigherPriorityTaskWoken
);
这里不详细介绍这两个函数
5. vQueueDelete() - 删除消息队列
当队列不再需要时,应该调用此函数来释放其占用的内存。
void vQueueDelete( QueueHandle_t xQueue );
-
参数:
-
xQueue:要删除的队列的句柄。
-
-
返回值:
-
无返回值(
void)。
-
-
使用场景:
-
在系统关闭、模块卸载或动态创建/销毁队列的场景中,为了避免内存泄漏,需要显式删除不再使用的队列。
-
// 当不再需要 xMyQueue 时
vQueueDelete( xMyQueue );
xMyQueue = NULL; // 最佳实践:将句柄置为NULL,防止野指针
消息队列使用示例
示例功能介绍

关于这里的消息队列中的消息类型为结构体类型,
消息类型格式:LED灯编号(1~4)+开(1)关(0)
比如发送11,表示LED1亮起,发送30,表示LED3熄灭
注意这里发送的都是字符,并不是int类型,11表示两个字符 '1'
代码实现:
#include "queue.h"//使用消息队列的时候需要加这个头文件
typedef struct LEDInfo
{
uint8_t which; // LED 编号,例如 '1', '2', '3', '4'
uint8_t state; // LED 状态,例如 '0' (关), '1' (开)
}LED_Info_t;
xQueueHandle ledQueue; 队列句柄,声明为全局变量,因为多个任务都会用到
省略了关于任务的初始化结构体部分代码
void MX_FREERTOS_Init(void) {
ledQueue = xQueueCreate(1, sizeof(LED_Info_t));
/* Create the thread(s) */
/* creation of defaultTask */
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
/* USER CODE END RTOS_THREADS */
ledTaskHandle = osThreadNew(LEDTask, NULL, &ledTask_attributes);
UARTTaskHandle = osThreadNew(UARTTask, "helloworld", &UARTTask_attributes);
}
void LEDTask(void *arg)
{
LED_Info_t ledInfo;
uint8_t state;
while (1)
{
xQueueReceive(ledQueue, &ledInfo, portMAX_DELAY);
if (ledInfo.state == '0')
state = GPIO_PIN_RESET;
else if (ledInfo.state == '1')
state = GPIO_PIN_SET;
switch(ledInfo.which)
{
case '1':
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, (GPIO_PinState)state);
break;
case '2':
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, (GPIO_PinState)state);
break;
case '3':
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, (GPIO_PinState)state);
break;
case '4':
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, (GPIO_PinState)state);
break;
}
}
}
void UARTTask(void *arg)
{
uint8_t data[2] = {0};
LED_Info_t ledInfo;
while (1)
{
HAL_UART_Receive(&huart1, data, 2, HAL_MAX_DELAY);
if (data[0] >= '1' && data[0] <= '4' &&
data[1] >= '0' && data[1] <= '1')
{
ledInfo.which = data[0];
ledInfo.state = data[1];
}
xQueueSend(ledQueue, &ledInfo, portMAX_DELAY);
}
}
代码解释:
这段代码的核心功能是:
-
UARTTask 任务:通过串口接收两个字节的命令。第一个字节代表要控制的 LED 编号('1'到'4'),第二个字节代表 LED 的状态('0'为关,'1'为开)。
-
LEDTask 任务:从一个 FreeRTOS 消息队列中接收这些 LED 控制信息,并根据信息控制对应的 GPIO 引脚,从而改变 LED 的亮灭。
-
消息队列
ledQueue:作为 UARTTask 和 LEDTask
这段代码通过 FreeRTOS 的消息队列,建立了一个清晰且解耦的 生产者-消费者 模型:
-
UARTTask 是生产者,负责从外部(串口)获取命令。
-
LEDTask 是消费者,负责执行这些命令(控制 LED)。
-
ledQueue是两者之间传递命令的管道,确保了通信的线程安全和任务的同步。
这种设计使得代码结构清晰,易于理解和维护。当有串口命令时,UARTTask 被唤醒处理;当有 LED 命令到达队列时,LEDTask 被唤醒执行。两者之间的阻塞机制确保了它们能协同工作,避免了忙等待,提高了系统效率。
消息队列使用注意:
1. 队列长度和项目大小的设计
- 队列长度过小: 如果队列太短,生产者任务可能会频繁因队列满而阻塞,影响数据吞吐量。
- 队列长度过大: 占用过多 RAM,在内存受限的嵌入式系统中可能导致内存不足。
- 长度为 1: 这种队列常用于同步事件或传递最新状态,因为每次发送都会覆盖或等待上次的消费。它有效地将生产者和消费者解耦,但同时又确保了严格的“一对一”消息处理(即在处理完当前消息前,不能接收新消息)。
2. 阻塞行为与超时设置
portMAX_DELAY:
- 使用 portMAX_DELAY 可以让任务无限期阻塞,直到队列操作成功。这对于消费者任务(等待数据)或关键生产者任务(必须发送数据)来说很常见,因为它避免了忙等待,节省了 CPU 周期。
- 风险: 如果生产者停止发送或消费者停止接收,使用 portMAX_DELAY 的任务可能会永远阻塞。在某些场景下,这可能是死锁或系统停滞的原因。
非零超时值:
- 通过设置一个具体的超时时间(例如 pdMS_TO_TICKS(100)),任务只会在队列满/空时等待指定的时间。
- 优点: 避免任务无限期阻塞,可以在超时后执行替代操作(如错误处理、记录日志、重试)。
- 缺点: 需要额外的逻辑来处理超时失败的情况。
3. 数据完整性与竞态条件
- 数据拷贝: 队列通过拷贝数据来传递,这天然地解决了多任务访问同一数据时的竞态条件问题。一旦数据被发送到队列,发送者就可以自由地修改原始数据,而不会影响队列中的副本。
- 发送指针的注意事项: 如果你选择发送指向数据的指针而不是数据本身(通过将 uxItemSize 设置为 sizeof(void *)),你需要额外注意:
- 数据生命周期: 确保指针指向的数据在消费者接收并处理之前不会被发送者修改、释放或失效。这通常意味着数据必须是全局的、静态的,或通过动态分配并在消费者处理后由消费者释放。
- 互斥访问: 如果多个任务可能同时修改指针指向的数据,仍需要额外的同步机制(如互斥量)来保护该共享数据。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)