4. 队列(Queue)通信:队列的创建、发送与接收
各位同学,今天我们来聊聊FreeRTOS里最常用的通信方式——队列。说实话,我做了这么多年嵌入式开发,队列这东西几乎每个项目都会用到。它就像任务之间的“快递通道”,一个任务往里扔数据,另一个任务取出来用。
我记得刚入行那会儿,有个同事用全局变量传数据,结果两个任务同时读写,程序跑着跑着就崩了。后来换成队列,问题立马解决。嗯,这就是队列的价值——安全、可靠地传递数据。
4.1 队列的核心概念
队列本质上是一个先进先出(FIFO)的数据缓冲区。你可以把它想象成一个管道,数据从一端进去,从另一端出来。每个队列都有固定的长度和每个数据项的大小。
我个人习惯把队列比作“邮箱”:
- 创建队列:相当于在系统里挂一个邮箱
- 发送数据:往邮箱里塞一封信
- 接收数据:从邮箱里取出一封信
- 阻塞等待:如果邮箱空了,你就等着有人投信
关键点:队列是“拷贝传递”的。发送方把数据拷贝进队列,接收方从队列拷贝出来。这意味着你传递的是数据的副本,而不是指针。这样做的好处是安全——发送方修改原数据不会影响接收方。
4.2 队列的创建
创建队列用 xQueueCreate() 函数。它的原型很简单:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
- uxQueueLength:队列能容纳的最大消息数量
- uxItemSize:每个消息的大小(字节)
- 返回值:队列句柄,创建失败返回 NULL
举个例子,创建一个能存5个整数的队列:
QueueHandle_t xQueue;
xQueue = xQueueCreate(5, sizeof(int32_t));
if (xQueue == NULL) {
// 创建失败,处理错误
printf("队列创建失败!\n");
}
我的经验:创建队列时,uxItemSize 不要设得太大。我在项目中见过有人把整个结构体(几百字节)直接塞进队列,结果内存消耗巨大。如果数据量大,建议传递指针,队列只存指针大小(4或8字节)。
4.3 队列的发送与接收
4.3.1 发送数据:xQueueSend()
发送数据用 xQueueSend()。它会把数据拷贝到队列尾部。函数原型:
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait );
- xQueue:队列句柄
- pvItemToQueue:指向要发送的数据的指针
- xTicksToWait:队列满时的等待时间(单位:系统时钟节拍)
- 返回值:成功返回 pdPASS,失败返回 errQUEUE_FULL
看个实际例子:
int32_t data = 100;
BaseType_t ret;
// 发送数据,如果队列满则等待100个节拍
ret = xQueueSend(xQueue, &data, pdMS_TO_TICKS(100));
if (ret == pdPASS) {
printf("数据发送成功\n");
} else {
printf("队列满了,发送超时\n");
}
4.3.2 接收数据:xQueueReceive()
接收数据用 xQueueReceive()。它会从队列头部取出数据并删除该数据项。函数原型:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait );
- xQueue:队列句柄
- pvBuffer:接收数据的缓冲区指针
- xTicksToWait:队列空时的等待时间
- 返回值:成功返回 pdPASS,失败返回 errQUEUE_EMPTY
接收示例:
int32_t received_data;
BaseType_t ret;
// 接收数据,如果队列空则一直等待
ret = xQueueReceive(xQueue, &received_data, portMAX_DELAY);
if (ret == pdPASS) {
printf("收到数据:%d\n", received_data);
}
注意:接收缓冲区的大小必须大于等于队列创建时指定的 uxItemSize。我曾经犯过这个错——缓冲区小了,结果数据被截断,排查了半天才发现是缓冲区大小不匹配。
4.4 队列阻塞时间设置
阻塞时间是队列通信的精髓。说白了,就是当队列满(发送)或空(接收)时,任务愿意等多久。
| 阻塞时间 | 含义 | 使用场景 |
|---|---|---|
| 0 | 不等待,立即返回 | 轮询检查,不想阻塞任务 |
| pdMS_TO_TICKS(ms) | 等待指定毫秒数 | 有超时要求的通信 |
| portMAX_DELAY | 无限等待,直到成功 | 必须等到数据的场景 |
我个人习惯这样用:
- 发送任务:一般用
0或短超时。如果队列满了,说明接收方处理不过来,这时候丢数据比阻塞任务更合理。 - 接收任务:常用
portMAX_DELAY。因为接收任务通常就是等着数据来了再干活,没必要空转浪费CPU。
避坑指南:我曾经在一个项目里把接收任务的阻塞时间设成了 0,结果任务一直在空循环检查队列,CPU占用率飙到90%以上。改成 portMAX_DELAY 后,CPU占用率直接降到5%。你想想看,这差距多大!
4.5 队列集(Queue Set)简介
当你有多个队列,想同时等待它们中的任何一个有数据时,怎么办?一个一个轮询?太浪费CPU了。这时候就需要队列集。
队列集就像一个“队列的集合”。你可以把多个队列注册到同一个队列集里。然后任务只需要等待这个队列集,一旦其中任何一个队列有数据,任务就会被唤醒。
创建队列集:
QueueSetHandle_t xQueueSet;
xQueueSet = xQueueCreateSet( QUEUE_SET_SIZE );
把队列加入队列集:
xQueueAddToSet( xQueue1, xQueueSet );
xQueueAddToSet( xQueue2, xQueueSet );
从队列集中获取有数据的队列:
QueueSetMemberHandle_t xActivatedQueue;
xActivatedQueue = xQueueSelectFromSet( xQueueSet, portMAX_DELAY );
// 然后从激活的队列中接收数据
if (xActivatedQueue == xQueue1) {
xQueueReceive(xQueue1, &data, 0);
} else if (xActivatedQueue == xQueue2) {
xQueueReceive(xQueue2, &data, 0);
}
我的建议:队列集适合管理多个数据源。比如一个任务要同时处理按键消息、传感器数据、网络包,用队列集就很优雅。但要注意,队列集有额外的内存开销,如果只有一两个队列,直接用单个队列就够了。
4.6 实战:按键消息通过队列驱动LED
好了,理论讲完了,咱们来做个实战。这个项目很简单:按一下按键,LED状态翻转。但关键是用队列来传递按键消息。
整体思路:
- 创建一个队列,用于传递按键事件
- 按键中断(或轮询)中发送消息到队列
- LED控制任务从队列接收消息,并翻转LED
先定义消息类型:
typedef enum {
KEY_PRESS_SHORT, // 短按
KEY_PRESS_LONG // 长按
} KeyEvent_t;
创建队列:
QueueHandle_t xKeyQueue;
xKeyQueue = xQueueCreate(10, sizeof(KeyEvent_t));
if (xKeyQueue == NULL) {
// 处理错误
}
按键任务(模拟按键检测):
void vKeyTask(void *pvParameters) {
KeyEvent_t keyEvent;
for (;;) {
// 检测按键(这里简化,实际用GPIO读取)
if (按键按下) {
keyEvent = KEY_PRESS_SHORT;
xQueueSend(xKeyQueue, &keyEvent, 0);
}
vTaskDelay(pdMS_TO_TICKS(20)); // 20ms扫描一次
}
}
LED控制任务:
void vLEDTask(void *pvParameters) {
KeyEvent_t receivedEvent;
BaseType_t ret;
for (;;) {
// 阻塞等待按键消息
ret = xQueueReceive(xKeyQueue, &receivedEvent, portMAX_DELAY);
if (ret == pdPASS) {
switch (receivedEvent) {
case KEY_PRESS_SHORT:
// 翻转LED
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
printf("短按:LED翻转\n");
break;
case KEY_PRESS_LONG:
// 长按可以执行其他操作
printf("长按:执行特殊功能\n");
break;
default:
break;
}
}
}
}
创建任务:
xTaskCreate(vKeyTask, "KeyTask", 128, NULL, 2, NULL);
xTaskCreate(vLEDTask, "LEDTask", 128, NULL, 1, NULL);
我的经验:这个架构的好处是解耦。按键检测和LED控制完全独立。如果你想增加“双击”功能,只需要在按键任务里增加判断,发送新的消息类型,LED任务那边加个case就行。互不影响。
4.7 本章小结
队列是FreeRTOS任务通信的基石。说白了,它解决了两个核心问题:
- 数据安全:队列内部有互斥保护,多任务读写不会冲突
- 任务同步:通过阻塞机制,让任务在数据到达时自动唤醒
我个人觉得,掌握队列的使用是入门FreeRTOS的关键一步。你想想看,有了队列,任务之间就能有序地传递信息,整个系统就像有了“神经系统”一样协调工作。
下一章我们会讲信号量和互斥量,它们和队列有很深的渊源。到时候你就知道,其实信号量本质上就是一个特殊的队列。