跳转至

队列与通信

在多任务系统中,任务之间经常需要交换数据。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);
    }
}

全局变量的问题

  • 数据撕裂:如果 temperaturefloat(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);

传大结构体时的优化

对于大数据块,可以传指针而非值(但要确保指针指向的内存在接收端使用期间有效):

// 队列元素类型设为指针大小
uint8_t *buffer_ptr = malloc_buffer();
osMessageQueuePut(queueHandle, &buffer_ptr, 0, osWaitForever);

CubeMX 中创建队列

图形化配置

  1. Middleware → FREERTOS → Tasks and Queues 标签页
  2. 在 Queues 区域点击 Add
  3. 配置参数:
参数 说明 示例
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 配置:

  1. 创建队列 SensorQueue,大小 10,元素 4 字节(float)
  2. 创建任务 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 个字节
  • 队列越大占用内存越多,需要在缓冲能力和内存之间权衡

队列满了怎么办?

三种策略:

  1. 等待osWaitForever):发送方阻塞直到有空间
  2. 超时丢弃(设定 timeout):等超时后返回错误,放弃这次发送
  3. 覆盖最旧数据:使用 osMessageQueueReset() 清空后重新发送(适合只关心最新数据的场景)

能不能一个队列多种数据类型共用?

可以,用联合体或结构体包装:

typedef struct {
    uint8_t type;  // 消息类型标识
    union {
        float temp;
        uint32_t count;
        uint8_t data[8];
    } payload;
} GenericMsg_t;