队列与通信¶
在多任务系统中,任务之间经常需要交换数据。FreeRTOS 提供了队列(Queue)作为主要的数据传递机制——它是线程安全的 FIFO 缓冲区,支持任务间和中断到任务的数据传输。
为什么需要队列?¶
直接用全局变量不行吗?¶
在裸机中我们习惯用全局变量传数据,但在 RTOS 多任务环境中这样做有严重问题:
// ⚠️ 危险:多任务共享全局变量
volatile float temperature = 0;
void Sensor_Task(void *argument)
{
for (;;)
{
temperature = read_sensor(); // 写入
osDelay(100);
}
}
void Display_Task(void *argument)
{
for (;;)
{
float t = temperature; // 读取 ← 可能读到写了一半的数据!
update_display(t);
osDelay(200);
}
}
全局变量的问题
- 数据撕裂:如果
temperature是float(4 字节),写入过程中可能被任务切换打断,另一个任务读到"半新半旧"的值 - 无同步机制:读任务不知道数据什么时候更新了
- 无缓冲:如果写入速度 > 读取速度,旧数据直接被覆盖
队列的优势¶
| 特性 | 全局变量 | 队列 |
|---|---|---|
| 线程安全 | ❌ | ✅ 自动加锁 |
| 数据缓冲 | ❌ 只存最新值 | ✅ FIFO,可缓冲多个 |
| 阻塞等待 | ❌ 需要轮询 | ✅ 没数据时任务自动休眠 |
| 超时机制 | ❌ | ✅ 可设置等待超时 |
| ISR 支持 | ⚠️ 需手动保护 | ✅ 有专用 FromISR API |
队列的基本原理¶
FIFO(先进先出)¶
队列像一个"管道",先放进去的数据先被取出:
graph LR
subgraph 队列(容量=5)
Q1["数据1<br>(最先放入)"]
Q2["数据2"]
Q3["数据3<br>(最后放入)"]
Q4["空"]
Q5["空"]
end
PUT["发送端<br>osMessageQueuePut()"] -->|"从队尾放入"| Q3
Q1 -->|"从队头取出"| GET["接收端<br>osMessageQueueGet()"]
值传递 vs 引用传递¶
FreeRTOS 队列默认是值传递(拷贝数据),不是传指针:
// 发送端:数据被拷贝到队列
float temp = 25.6f;
osMessageQueuePut(queueHandle, &temp, 0, osWaitForever);
// 发送后修改 temp 不影响队列中的数据
// 接收端:从队列拷贝出来
float received;
osMessageQueueGet(queueHandle, &received, NULL, osWaitForever);
传大结构体时的优化
对于大数据块,可以传指针而非值(但要确保指针指向的内存在接收端使用期间有效):
CubeMX 中创建队列¶
图形化配置¶
- Middleware → FREERTOS → Tasks and Queues 标签页
- 在 Queues 区域点击 Add
- 配置参数:
| 参数 | 说明 | 示例 |
|---|---|---|
| Queue Name | 队列名称 | SensorQueue |
| Queue Size | 队列容量(可存多少个元素) | 10 |
| Item Size | 每个元素的大小(字节) | sizeof(float) 即 4 |
生成的代码¶
/* 自动生成的队列定义 */
osMessageQueueId_t SensorQueueHandle;
const osMessageQueueAttr_t SensorQueue_attributes = {
.name = "SensorQueue"
};
void MX_FREERTOS_Init(void)
{
/* 创建队列:10个元素,每个4字节 */
SensorQueueHandle = osMessageQueueNew(10, sizeof(float), &SensorQueue_attributes);
}
队列 API 详解¶
发送数据¶
osStatus_t osMessageQueuePut(
osMessageQueueId_t mq_id, // 队列句柄
const void *msg_ptr, // 指向待发送数据的指针
uint8_t msg_prio, // 消息优先级(一般填 0)
uint32_t timeout // 超时时间(ms)
);
timeout 参数说明:
| 值 | 含义 |
|---|---|
0 |
不等待,队列满立即返回 osErrorResource |
osWaitForever |
一直等到队列有空位 |
100 |
等待最多 100ms |
返回值:
| 返回值 | 含义 |
|---|---|
osOK |
发送成功 |
osErrorResource |
队列已满(超时=0 时) |
osErrorTimeout |
等待超时 |
接收数据¶
osStatus_t osMessageQueueGet(
osMessageQueueId_t mq_id, // 队列句柄
void *msg_ptr, // 指向接收缓冲区的指针
uint8_t *msg_prio, // 接收消息优先级(可填 NULL)
uint32_t timeout // 超时时间(ms)
);
当队列为空时,调用 osMessageQueueGet() 的任务会进入阻塞态,让出 CPU。
其他常用 API¶
// 获取队列中当前有多少数据
uint32_t count = osMessageQueueGetCount(queueHandle);
// 获取队列的剩余空间
uint32_t space = osMessageQueueGetSpace(queueHandle);
// 清空队列(丢弃所有数据)
osMessageQueueReset(queueHandle);
实战案例¶
案例 1:传感器数据采集与处理¶
一个经典的生产者-消费者模式:
graph LR
S[传感器任务<br>生产者] -->|"采集数据<br>放入队列"| Q[(队列<br>容量10)]
Q -->|"取出数据<br>处理显示"| P[处理任务<br>消费者]
CubeMX 配置:
- 创建队列
SensorQueue,大小 10,元素 4 字节(float) - 创建任务
Sensor_Task(AboveNormal)和Process_Task(Normal)
/* 传感器采集任务(生产者) */
void Sensor_Task(void *argument)
{
float temperature;
uint32_t tick = osKernelGetTickCount();
for (;;)
{
/* 读取 ADC(模拟温度传感器) */
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 10);
uint32_t adc_val = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);
/* 转换为温度值 */
temperature = (float)adc_val * 3.3f / 4096.0f * 100.0f;
/* 发送到队列,等待最多 10ms */
osStatus_t status = osMessageQueuePut(SensorQueueHandle,
&temperature, 0, 10);
if (status != osOK)
{
// 队列满了,数据丢弃(或做其他处理)
}
tick += 100;
osDelayUntil(tick); // 100ms 精确周期采集
}
}
/* 数据处理任务(消费者) */
void Process_Task(void *argument)
{
float received_temp;
char msg[64];
for (;;)
{
/* 从队列取数据,无数据则阻塞等待 */
osStatus_t status = osMessageQueueGet(SensorQueueHandle,
&received_temp, NULL,
osWaitForever);
if (status == osOK)
{
/* 处理数据并通过串口输出 */
snprintf(msg, sizeof(msg), "Temp: %.1f C\r\n", received_temp);
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
}
}
}
案例 2:结构体传递¶
传递复杂数据时,使用结构体:
/* 定义消息结构体 */
typedef struct {
uint8_t sensor_id; // 传感器编号
float value; // 测量值
uint32_t timestamp; // 时间戳
} SensorMsg_t;
/* CubeMX 中创建队列:
Queue Size = 16
Item Size = sizeof(SensorMsg_t) = 12 字节 */
/* 生产者:多个传感器任务 */
void Sensor1_Task(void *argument)
{
SensorMsg_t msg;
for (;;)
{
msg.sensor_id = 1;
msg.value = read_temperature();
msg.timestamp = osKernelGetTickCount();
osMessageQueuePut(SensorQueueHandle, &msg, 0, 10);
osDelay(200);
}
}
void Sensor2_Task(void *argument)
{
SensorMsg_t msg;
for (;;)
{
msg.sensor_id = 2;
msg.value = read_humidity();
msg.timestamp = osKernelGetTickCount();
osMessageQueuePut(SensorQueueHandle, &msg, 0, 10);
osDelay(500);
}
}
/* 消费者:统一处理 */
void Process_Task(void *argument)
{
SensorMsg_t received;
for (;;)
{
if (osMessageQueueGet(SensorQueueHandle, &received, NULL,
osWaitForever) == osOK)
{
switch (received.sensor_id)
{
case 1:
printf("[%lu] Temp: %.1f\r\n",
received.timestamp, received.value);
break;
case 2:
printf("[%lu] Humi: %.1f%%\r\n",
received.timestamp, received.value);
break;
}
}
}
}
案例 3:串口接收数据转发¶
中断中收到数据 → 放入队列 → 任务中处理:
/* 串口接收中断回调(在 stm32f1xx_it.c 或 main.c 中) */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
/* 在中断中发送到队列(必须用 0 超时) */
osMessageQueuePut(UartRxQueueHandle, &rx_byte, 0, 0);
/* 重新开启接收 */
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
/* 串口处理任务 */
void UART_Process_Task(void *argument)
{
uint8_t byte;
uint8_t buffer[128];
uint16_t idx = 0;
for (;;)
{
/* 等待队列中的数据 */
if (osMessageQueueGet(UartRxQueueHandle, &byte, NULL,
osWaitForever) == osOK)
{
buffer[idx++] = byte;
/* 收到换行符,处理一帧完整数据 */
if (byte == '\n' || idx >= sizeof(buffer) - 1)
{
buffer[idx] = '\0';
process_command((char*)buffer);
idx = 0;
}
}
}
}
中断中使用队列
在中断回调中调用 osMessageQueuePut() 时,timeout 必须设为 0。在 CMSIS_V2 中,API 会自动检测是否在中断上下文中,内部调用对应的 FromISR 版本。
任务通知(Task Notification)¶
FreeRTOS 还提供了一种更轻量的通信方式——任务通知,适合简单的事件通知或传递一个 32 位值。
任务通知 vs 队列¶
| 特性 | 队列 | 任务通知 |
|---|---|---|
| 内存开销 | 需要额外内存 | 无(内嵌在 TCB 中) |
| 发送速度 | 较慢 | 快 45% |
| 多对一 | ✅ 多个发送方 | ✅ 多个发送方 |
| 一对多 | ✅ 多个接收方 | ❌ 只能通知特定任务 |
| 数据量 | 任意大小 | 仅 32 位值 |
| 缓冲 | ✅ 可缓冲多个 | ❌ 只存最新一个 |
使用方法¶
/* 任务 A:发送通知 */
void TaskA(void *argument)
{
for (;;)
{
// 发送通知(发送一个 32 位值给 TaskB)
osThreadFlagsSet(TaskBHandle, 0x01); // 设置标志位 bit0
osDelay(1000);
}
}
/* 任务 B:等待通知 */
void TaskB(void *argument)
{
for (;;)
{
// 等待标志位 bit0 被设置
uint32_t flags = osThreadFlagsWait(0x01, osFlagsWaitAny,
osWaitForever);
if (flags & 0x01)
{
// 收到通知,执行操作
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
}
什么时候用任务通知代替队列?
- 只需要通知"事件发生了",不需要传数据 → 用任务通知
- 只传一个简单的值(如错误码、状态) → 用任务通知
- 需要传复杂数据(结构体)或缓冲多条消息 → 用队列
事件组(Event Group)¶
事件组可以让一个任务等待多个条件同时满足:
/* 创建事件组 */
osEventFlagsId_t eventHandle;
eventHandle = osEventFlagsNew(NULL);
/* 定义事件标志位 */
#define EVT_SENSOR_READY (1 << 0)
#define EVT_WIFI_CONNECTED (1 << 1)
#define EVT_BUTTON_PRESSED (1 << 2)
/* 任务 A:等待传感器就绪 AND WiFi连接 */
void Main_Task(void *argument)
{
for (;;)
{
// 等待 bit0 和 bit1 同时被置位
uint32_t flags = osEventFlagsWait(eventHandle,
EVT_SENSOR_READY | EVT_WIFI_CONNECTED,
osFlagsWaitAll, // 等待全部
osWaitForever);
if (flags & (EVT_SENSOR_READY | EVT_WIFI_CONNECTED))
{
// 两个条件都满足了,开始上传数据
upload_data();
}
}
}
/* 传感器任务:传感器准备好后设置标志 */
void Sensor_Task(void *argument)
{
for (;;)
{
if (sensor_init_ok())
{
osEventFlagsSet(eventHandle, EVT_SENSOR_READY);
}
osDelay(100);
}
}
/* WiFi 任务:连接成功后设置标志 */
void WiFi_Task(void *argument)
{
for (;;)
{
if (wifi_connected())
{
osEventFlagsSet(eventHandle, EVT_WIFI_CONNECTED);
}
osDelay(1000);
}
}
常见问题¶
队列容量设多大?
一般原则:
- 生产速度 ≈ 消费速度:3~5 个就够
- 生产速度 > 消费速度(突发数据):根据最大突发量设置
- 串口接收:至少 64~128 个字节
- 队列越大占用内存越多,需要在缓冲能力和内存之间权衡
队列满了怎么办?
三种策略:
- 等待(
osWaitForever):发送方阻塞直到有空间 - 超时丢弃(设定 timeout):等超时后返回错误,放弃这次发送
- 覆盖最旧数据:使用
osMessageQueueReset()清空后重新发送(适合只关心最新数据的场景)