队列 Queue
队列是 FreeRTOS 任务间传递数据的基石。理解它的拷贝语义、阻塞超时、队列集与邮箱模式,以及如何在中断里安全收发。
队列是 FreeRTOS 任务间传递数据的基石。理解它的拷贝语义、阻塞超时、队列集与邮箱模式,以及如何在中断里安全收发。
xTicksToWait。发送时队列满、接收时队列空,调用任务会阻塞至多这么多 tick。取 0 表示不等待立即返回,取 portMAX_DELAY 表示无限等待(需 INCLUDE_vTaskSuspend=1)。这是队列实现任务同步的关键。xQueueSend(等价 xQueueSendToBack)把数据加到队尾;xQueueSendToFront 加到队头(可做"插队"高优先级消息);xQueueReceive 取出并移除队头数据;xQueuePeek 只查看队头不移除。返回 pdTRUE 成功、errQUEUE_FULL/pdFALSE 超时。FromISR 后缀的 API(xQueueSendFromISR 等)。它们不会阻塞(ISR 里不能阻塞),并带 pxHigherPriorityTaskWoken 出参,用于在中断退出时决定是否 portYIELD_FROM_ISR 触发切换。普通版和 FromISR 版绝不能混用。QueueSetHandle_t 把它们组合起来,任务阻塞在 xQueueSelectFromSet 上,任一成员就绪即返回该成员句柄。需 configUSE_QUEUE_SETS=1。它避免了轮询多个队列的低效,但用法较繁琐,很多场景可用任务通知或事件组替代。xQueueOverwrite 无条件覆盖唯一的槽位,用 xQueuePeek 读取但不移除。这样队列永远保存"最新一份数据",多个读者都能读到最新值,非常适合广播传感器最新读数、系统状态等。#include "FreeRTOS.h" #include "task.h" #include "queue.h" /* 定义要传输的消息类型 */ typedef struct { uint8_t sensor_id; float value; uint32_t timestamp; } SensorMsg_t; QueueHandle_t g_sensorQueue = NULL; /* 生产者:每 200ms 采集并入队 */ void ProducerTask(void *arg) { SensorMsg_t msg; for (;;) { msg.sensor_id = 1; msg.value = read_adc_temp(); msg.timestamp = xTaskGetTickCount(); /* 队满则等待最多 10ms;成功返回 pdTRUE */ if (xQueueSend(g_sensorQueue, &msg, pdMS_TO_TICKS(10)) != pdPASS) { printf("queue full, sample dropped\r\n"); } vTaskDelay(pdMS_TO_TICKS(200)); } } /* 消费者:阻塞等待数据,来一条处理一条 */ void ConsumerTask(void *arg) { SensorMsg_t rx; for (;;) { /* 队空则永久阻塞;有数据即取出 */ if (xQueueReceive(g_sensorQueue, &rx, portMAX_DELAY) == pdPASS) { printf("id=%u v=%.2f t=%lu\r\n", rx.sensor_id, rx.value, rx.timestamp); } } } void setup_queue_demo(void) { /* 长度 5,每条 sizeof(SensorMsg_t) 字节 */ g_sensorQueue = xQueueCreate(5, sizeof(SensorMsg_t)); configASSERT(g_sensorQueue != NULL); /* 堆不足则创建失败 */ xTaskCreate(ProducerTask, "PROD", 256, NULL, 2, NULL); xTaskCreate(ConsumerTask, "CONS", 256, NULL, 2, NULL); }
若消息很大(比如一整帧 512 字节的传感器数据),按值拷贝两次(入队+出队)会很浪费。此时创建 xQueueCreate(len, sizeof(void*)),发送方把指向数据的指针入队,接收方取出指针后处理。但要小心生命周期:数据必须在被消费前一直有效,通常来自内存池或 pvPortMalloc,消费完再释放。
/* 定时器中断:把采样值送入队列 */ void TIM2_IRQHandler(void) { BaseType_t woken = pdFALSE; if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); SensorMsg_t m = { .sensor_id = 2, .value = read_adc_fast(), .timestamp = xTaskGetTickCountFromISR() }; /* ISR 版本:非阻塞,woken 表示是否唤醒了更高优任务 */ xQueueSendFromISR(g_sensorQueue, &m, &woken); } portYIELD_FROM_ISR(woken); /* 必要时立即切换 */ }
中断上下文不能阻塞。在 ISR 里调用普通 xQueueSend(它可能阻塞)会破坏内核状态、直接崩溃或触发断言。ISR 中只能用 *FromISR 系列 API,且它们没有超时参数(因为不阻塞)。同理,取时间戳也要用 xTaskGetTickCountFromISR()。
/* 长度为 1 的队列当邮箱 */ QueueHandle_t g_statusMailbox; void setup_mailbox(void) { g_statusMailbox = xQueueCreate(1, sizeof(uint32_t)); } /* 写:无条件覆盖,永远存最新状态 */ void update_status(uint32_t s) { xQueueOverwrite(g_statusMailbox, &s); } /* 读:peek 不移除,多个读者都能读到 */ uint32_t read_status(void) { uint32_t s = 0; xQueuePeek(g_statusMailbox, &s, 0); /* 不阻塞 */ return s; }
/* 需 configUSE_QUEUE_SETS = 1 */ QueueSetHandle_t set; QueueHandle_t qCmd; /* 命令队列 */ SemaphoreHandle_t semTick; /* 定时信号量 */ void setup_set(void) { qCmd = xQueueCreate(4, sizeof(uint8_t)); semTick = xSemaphoreCreateBinary(); /* 集容量 = 所有成员长度之和 */ set = xQueueCreateSet(4 + 1); xQueueAddToSet(qCmd, set); xQueueAddToSet(semTick, set); } void DispatchTask(void *arg) { for (;;) { /* 阻塞直到任一成员就绪,返回就绪的那个句柄 */ QueueSetMemberHandle_t who = xQueueSelectFromSet(set, portMAX_DELAY); if (who == qCmd) { uint8_t cmd; xQueueReceive(qCmd, &cmd, 0); handle_cmd(cmd); } else if (who == semTick) { xSemaphoreTake(semTick, 0); handle_tick(); } } }
| 需求 | 推荐机制 | 理由 |
|---|---|---|
| 缓冲多条消息、多生产者/消费者 | 队列 | 唯一支持深度缓冲和多端 |
| ISR → 单任务的简单唤醒 | 任务通知 | 更快、更省 RAM |
| 广播"最新值"给多个读者 | 邮箱(Overwrite+Peek) | 不消耗、永远最新 |
| 等待多个队列/信号量之一 | 队列集 / 事件组 | 避免轮询 |
| 传大结构体 | 队列存指针 | 避免双重拷贝 |
队列是 FreeRTOS 传递数据的基石,一个定长 FIFO 环形缓冲区,创建时定长度与元素大小。它按值拷贝:发送和接收各拷贝一次,天然线程安全,传大数据时改为入队指针以省开销。xQueueCreate 创建,xQueueSend/xQueueSendToFront 发送、xQueueReceive 取出、xQueuePeek 只看不取;超时参数 0=不等、portMAX_DELAY=永久等。中断里只能用 *FromISR 版本并配 portYIELD_FROM_ISR(woken),绝不能用会阻塞的普通版。长度 1 的队列配 xQueueOverwrite+xQueuePeek 构成邮箱,永远保存最新值供多读者读取。队列集(configUSE_QUEUE_SETS)让任务用 xQueueSelectFromSet 同时等待多个队列/信号量。选型上:多消息缓冲用队列、简单唤醒用任务通知、广播最新值用邮箱、多源等待用队列集或事件组。