FreeRTOS 消息队列机制解析与 STM32 实战指南
嵌入式系统中的任务通信机制
在基于实时操作系统(RTOS)的嵌入式开发中,多任务协同工作是核心需求。任务之间、任务与中断服务程序之间需要可靠的数据交换与同步机制。FreeRTOS 内核提供了一套基于队列(Queue)的通信方案,它是系统内部所有通信原语的基础。
一、队列基本原理
队列本质上是一块共享的内存区域,用于存储特定类型的数据项。它可以被多个任务或中断同时访问,但内核保证了访问的原子性与安全性。
队列具有深度(Length)和项大小(Item Size)两个关键属性。深度决定了队列能缓存多少条消息,项大小定义了每条消息占用的字节数。数据通常遵循先进先出(FIFO)原则,即后写入的数据排在队尾,先写入的数据从队头读出。当然,开发者也可以配置为后进先出(LIFO)模式。
为了区分数据来源,通常建议将数据源标识与数据内容封装在结构体中一同发送,这样接收端可以根据标识符决定如何处理数据。
二、核心 API 函数解析
1. 队列的创建
队列必须在使用前初始化。FreeRTOS 支持动态内存分配和静态内存分配两种方式。动态创建由内核管理内存,静态创建则由开发者提供内存缓冲区,适合对内存确定性要求高的场景。
// 动态创建队列
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
// 静态创建队列
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
StaticQueue_t *pxQueueBuffer);
示例:创建一个能容纳 8 个 uint32_t 类型数据的队列。
QueueHandle_t g_msgQueue;
g_msgQueue = xQueueCreate(8, sizeof(uint32_t));
2. 数据发送
向队列写入数据被称为"发送"。根据写入位置不同,可分为发送到队尾(FIFO)或队头(LIFO)。此外,针对中断上下文,提供了后缀为 FromISR 的安全版本。
// 发送到队尾(阻塞模式)
BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
// 发送到队头(实现 LIFO)
BaseType_t xQueueSendToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
// 中断安全版本
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken);
对于长度仅为 1 的队列,若队列已满,可使用覆盖发送函数,新数据将直接替换旧数据。
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void *pvItemToQueue);
3. 数据接收
从队列读取数据被称为"接收"。读取操作可以选择是否将数据项从队列中移除。
// 读取并移除数据
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
// 仅读取不移除( Peek 操作)
BaseType_t xQueuePeek(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
// 中断安全接收
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue, void *pvBuffer, BaseType_t *pxHigherPriorityTaskWoken);
4. 状态查询与管理
开发者可以查询队列的剩余空间或待处理消息数量,也可执行删除或复位操作。
UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue); // 剩余空间
UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue); // 待处理消息数
void vQueueDelete(QueueHandle_t pxQueueToDelete); // 删除队列
BaseType_t xQueueReset(QueueHandle_t xQueue); // 复位队列
5. 阻塞机制
当任务向满队列发送数据,或从空队列接收数据时,若设置了等待时间,任务将进入阻塞状态。以下情况可解除阻塞:
- 等待超时时间到达。
- 队列状态发生变化(如有了新空间或新数据)。
若多个任务因同一队列阻塞,优先级最高的任务将优先被唤醒;若优先级相同,则等待时间最长的任务优先。
三、STM32 实战案例
1. 实验设计
本案例旨在验证任务间及中断与任务间的通信能力。
- 创建一个深度为 20 的全局队列 g_msgQueue。
- 创建生产者任务 vProducerTask:扫描三个按键,按下不同按键发送不同数值。
- 创建消费者任务 vConsumerTask:从队列读取数据并通过串口打印。
- 配置 RTC 唤醒中断:每秒向队列发送特定中断标识数据。
2. 硬件抽象层配置
使用 STM32CubeMX 工具进行初始化。首先配置 GPIO 引脚用于按键输入(KEY0, KEY1, KEY2),配置 USART1 用于日志输出。接着启用 RTC 模块并配置为周期性唤醒模式。
在 FreeRTOS 中间件配置中,添加两个任务并创建一个队列。注意 RTC 中断优先级需配置在 FreeRTOS 最大可调用中断优先级范围内(通常为 5-15 之间),此处设为 7。
3. 代码实现
在生成的工程中,需重定向 printf 函数至串口。并在 freertos.c 中包含必要头文件。
#include "stdio.h"
#include "queue.h"
实现 RTC 回调函数,在中断中发送数据。注意使用中断安全 API 并检查是否需要上下文切换。
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
uint32_t interruptData = 99;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 中断上下文发送数据
xQueueSendToBackFromISR(g_msgQueue, &interruptData, &xHigherPriorityTaskWoken);
// 触发上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
生产者任务逻辑:检测按键状态,映射不同数值发送。
void vProducerTask(void *argument)
{
uint32_t msgData = 0;
for(;;)
{
msgData = 0;
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) == GPIO_PIN_RESET)
msgData = 30;
else if(HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) == GPIO_PIN_RESET)
msgData = 20;
else if(HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin) == GPIO_PIN_RESET)
msgData = 10;
if(msgData != 0)
{
// 发送数据,阻塞等待 50ms
if(xQueueSendToBack(g_msgQueue, &msgData, pdMS_TO_TICKS(50)) == errQUEUE_FULL)
{
xQueueReset(g_msgQueue);
}
osDelay(300); // 按键消抖
}
else
{
osDelay(10);
}
}
}
消费者任务逻辑:循环读取队列数据并打印状态。
void vConsumerTask(void *argument)
{
UBaseType_t waitingMsg = 0;
UBaseType_t spaceLeft = 0;
uint32_t recvData = 0;
for(;;)
{
waitingMsg = uxQueueMessagesWaiting(g_msgQueue);
spaceLeft = uxQueueSpacesAvailable(g_msgQueue);
if(xQueueReceive(g_msgQueue, &recvData, pdMS_TO_TICKS(50)) == pdPASS)
{
printf("Waiting: %d, Space: %d, Data: %d\r\n",
(int)waitingMsg, (int)spaceLeft, (int)recvData);
}
osDelay(100);
}
}
4. 验证结果
编译并下载程序至开发板。打开串口调试工具,观察到每秒自动输出一条数据为 99 的消息,代表 RTC 中断触发。当按下 KEY0 时,输出数据 10;按下 KEY1 时,输出数据 20;按下 KEY2 时,输出数据 30。串口终端实时显示队列当前的等待消息数与剩余空间,验证了队列通信机制正常工作。