目录

消息队列介绍

什么是消息队列?

消息队列的特点

消息队列的工作原理

消息队列中的常用函数

1. xQueueCreate() - 创建消息队列

2. xQueueSend() - 向队列发送数据

3. xQueueReceive() - 从队列接收数据

4. xQueueSendFromISR() / xQueueReceiveFromISR() - 中断安全版本

5. vQueueDelete() - 删除消息队列

消息队列使用示例

示例功能介绍

代码实现:

代码解释:

消息队列使用注意:

1. 队列长度和项目大小的设计

2. 阻塞行为与超时设置

3. 数据完整性与竞态条件


消息队列介绍

什么是消息队列?

简单来说,消息队列是一个可以存储有限数量、固定大小数据的缓冲区。它遵循先进先出(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);
	}
}

代码解释:

这段代码的核心功能是:

  1. UARTTask 任务:通过串口接收两个字节的命令。第一个字节代表要控制的 LED 编号('1'到'4'),第二个字节代表 LED 的状态('0'为关,'1'为开)。

  2. LEDTask 任务:从一个 FreeRTOS 消息队列中接收这些 LED 控制信息,并根据信息控制对应的 GPIO 引脚,从而改变 LED 的亮灭。

  3. 消息队列 ledQueue:作为 UARTTaskLEDTask

这段代码通过 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 *)),你需要额外注意:
     
  • 数据生命周期: 确保指针指向的数据在消费者接收并处理之前不会被发送者修改、释放或失效。这通常意味着数据必须是全局的、静态的,或通过动态分配并在消费者处理后由消费者释放。
     
  • 互斥访问: 如果多个任务可能同时修改指针指向的数据,仍需要额外的同步机制(如互斥量)来保护该共享数据。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐