队列基础:任务间通信的“快递通道”

大家好,我是你们的老朋友。今天咱们聊聊FreeRTOS里最常用的通信手段——队列。

说实话,我刚开始做嵌入式开发那会儿,对队列的理解就是“一个能放数据的数组”。后来踩了不少坑才明白,队列远不止这么简单。它其实是任务与任务之间、中断与任务之间的“快递通道”。

你想想看,一个任务采集了传感器数据,怎么告诉另一个任务去处理?直接访问全局变量?那得加锁、关中断,麻烦不说,还容易出bug。队列就是来解决这个问题的。

核心概念:队列是一种先进先出(FIFO)的数据结构。发送方把数据“扔”进队列,接收方从队列“取”走数据。整个过程是线程安全的,不需要你操心互斥问题。

队列的工作原理

队列内部是怎么工作的?我画个图你就明白了。

队列工作原理示意图 队列(Queue) 数据项1 数据项2 数据项3 队列长度 = 4,每个数据项大小 = sizeof(Data_t) 发送方 xQueueSend() 接收方 xQueueReceive() 数据从队尾入队,从队首出队(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