队列基础:任务间通信的“快递通道”
大家好,我是你们的老朋友。今天咱们聊聊FreeRTOS里最常用的通信手段——队列。
说实话,我刚开始做嵌入式开发那会儿,对队列的理解就是“一个能放数据的数组”。后来踩了不少坑才明白,队列远不止这么简单。它其实是任务与任务之间、中断与任务之间的“快递通道”。
你想想看,一个任务采集了传感器数据,怎么告诉另一个任务去处理?直接访问全局变量?那得加锁、关中断,麻烦不说,还容易出bug。队列就是来解决这个问题的。
核心概念:队列是一种先进先出(FIFO)的数据结构。发送方把数据“扔”进队列,接收方从队列“取”走数据。整个过程是线程安全的,不需要你操心互斥问题。
队列的工作原理
队列内部是怎么工作的?我画个图你就明白了。
队列本质上是一个环形缓冲区。它有几个关键属性:
- 队列长度:最多能存多少个数据项
- 数据项大小:每个数据占多少字节
- 读写指针:标记当前读和写的位置
发送数据时,数据会被拷贝到队列中,而不是传递指针。这一点很重要——如果你发送的是结构体,整个结构体都会被复制进去。我个人习惯用较小的数据项,比如32位整数或者指针,这样拷贝开销小。
小技巧:如果数据量比较大,可以只发送指针。但要注意指针指向的内存必须一直有效,不能是局部变量。
队列的创建与发送
创建队列用 xQueueCreate() 函数。原型很简单:
QueueHandle_t xQueueCreate(
UBaseType_t uxQueueLength, // 队列长度
UBaseType_t uxItemSize // 每个数据项的大小(字节)
);
举个例子,我要创建一个能存10个int数据的队列:
QueueHandle_t xDataQueue;
void create_data_queue(void)
{
xDataQueue = xQueueCreate(10, sizeof(int));
if (xDataQueue == NULL) {
// 创建失败,内存不够
printf("队列创建失败!\n");
} else {
printf("队列创建成功!\n");
}
}
发送数据用 xQueueSend() 或者 xQueueSendToBack()。这两个函数其实是一样的,都是往队尾添加数据。
int send_data(int value)
{
BaseType_t ret = xQueueSend(xDataQueue, &value, portMAX_DELAY);
if (ret == pdPASS) {
return 0; // 发送成功
} else {
return -1; // 发送失败
}
}
第三个参数是超时时间。我用 portMAX_DELAY 表示一直等,直到队列有空位。如果不想等,可以传0。
注意:在中断服务函数里,不能调用 xQueueSend(),要用 xQueueSendFromISR()。这个我后面会详细讲。
队列接收与超时
接收数据用 xQueueReceive()。它会从队首取出一个数据,并把它拷贝到你提供的缓冲区里。
int receive_data(int *p_value)
{
BaseType_t ret = xQueueReceive(xDataQueue, p_value, pdMS_TO_TICKS(100));
if (ret == pdPASS) {
return 0; // 接收成功
} else {
return -1; // 接收超时或队列为空
}
}
这里我用了 pdMS_TO_TICKS(100),意思是等100毫秒。如果100毫秒内没收到数据,就返回失败。
为什么会超时?说白了就是队列里没数据。接收任务会被阻塞,直到有数据进来或者超时时间到。
我曾经遇到过一个坑:接收超时时间设得太短,结果数据还没到就超时了,导致数据丢失。后来我把超时时间改成了 portMAX_DELAY,配合任务优先级设计,问题就解决了。
| 函数 | 作用 | 适用场景 |
|---|---|---|
| xQueueSend() | 发送数据到队尾 | 任务中调用 |
| xQueueSendFromISR() | 中断中发送数据 | 中断服务函数 |
| xQueueReceive() | 从队首接收数据 | 任务中调用 |
| xQueuePeek() | 查看队首数据但不移除 | 需要预览数据时 |
实战:数据采集系统
好了,理论说完了,咱们来点实际的。我做一个简单的数据采集系统:一个任务采集模拟数据,另一个任务处理数据。
// 定义数据结构
typedef struct {
uint32_t timestamp;
int16_t temperature;
int16_t humidity;
} SensorData_t;
// 队列句柄
QueueHandle_t xSensorQueue;
// 采集任务
void vSensorTask(void *pvParameters)
{
SensorData_t data;
uint32_t tick = 0;
for (;;) {
// 模拟采集数据
data.timestamp = tick++;
data.temperature = 25 + (rand() % 10);
data.humidity = 60 + (rand() % 20);
// 发送到队列
if (xQueueSend(xSensorQueue, &data, 0) != pdPASS) {
// 队列满了,丢弃旧数据
printf("队列满,数据丢弃!\n");
}
vTaskDelay(pdMS_TO_TICKS(100)); // 100ms采集一次
}
}
// 处理任务
void vProcessTask(void *pvParameters)
{
SensorData_t data;
for (;;) {
// 等待数据,超时时间500ms
if (xQueueReceive(xSensorQueue, &data, pdMS_TO_TICKS(500)) == pdPASS) {
printf("时间:%u 温度:%d°C 湿度:%d%%\n",
data.timestamp, data.temperature, data.humidity);
} else {
printf("采集超时,检查传感器!\n");
}
}
}
// 主函数
void main(void)
{
// 创建队列,长度5,每个数据项大小是SensorData_t
xSensorQueue = xQueueCreate(5, sizeof(SensorData_t));
if (xSensorQueue != NULL) {
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 2, NULL);
xTaskCreate(vProcessTask, "Process", 256, NULL, 1, NULL);
vTaskStartScheduler();
}
}
这个例子有几个要点:
- 采集任务优先级高(2),处理任务优先级低(1)
- 采集任务用
vTaskDelay控制采集频率 - 队列长度设成5,如果处理不过来,旧数据会被丢弃
- 处理任务超时500ms,如果没收到数据就报错
我的经验:队列长度要根据数据产生速度和消费速度来定。如果生产者快、消费者慢,队列要设大一点,或者考虑丢数据策略。
嗯,这里要注意一点:队列里的数据是拷贝的,不是引用。所以 SensorData_t 结构体不要太大,否则拷贝开销会很高。我一般控制在几十个字节以内。
说实话,队列这个东西,用起来简单,但用好需要经验。我刚开始做项目时,队列长度设得太小,导致高频数据丢失;后来设得太大,又浪费内存。最终还是要根据实际数据流来调整。
好了,队列的基础知识就讲到这里。记住一句话:队列是任务间通信的“快递通道”,用好了,你的系统会非常健壮。
公众号:蓝海资料掘金营,微信deep3321