4. 队列(Queue)深度解析:队列工作原理、创建与发送/接收、队列在传感器数据采集中的应用
4.1 队列到底是什么?—— 我眼中的“数据管道”
队列,说白了就是FreeRTOS里的一条数据管道。
任务A往里面扔数据,任务B从里面取数据。就这么简单。
但我刚接触FreeRTOS那会儿,总觉得队列是个很玄乎的东西。后来我做了几个项目才明白,它其实就是一块内存,加上一套读写规则。嗯,这块内存被设计成了“先进先出”的结构——你先放进去的数据,别人先取走。
为什么会这样设计?你想想看,如果数据乱序了,那传感器采集的温度值可能就和时间戳对不上了。这在实时控制里可是大忌。
核心要点:队列的本质是“生产者-消费者”模型。一个任务负责生产数据,另一个任务负责消费数据。两者通过队列解耦,互不阻塞。
4.2 队列的工作原理——我画了一张图给你看
我个人习惯用一张图来理解队列。下面这张SVG图,是我在项目里经常用来给团队新人讲解的。
这张图里,任务A往队列里写数据,任务B从队列里读数据。队列内部维护了一个环形缓冲区,有头指针和尾指针。写入时数据放到尾指针位置,读取时从头指针位置取。
我记得有一次调试一个传感器采集程序,发现数据总是错位。查了半天,原来是队列深度设置太小,数据被覆盖了。从那以后,我每次创建队列都会仔细算一下深度。
4.3 创建队列——xQueueCreate() 的用法
创建队列的API就一个:xQueueCreate()。但参数的含义,我建议你认真理解。
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
| 参数 | 含义 | 我的建议 |
|---|---|---|
| uxQueueLength | 队列能存储的最大数据项个数 | 根据数据产生速率和消费速率计算,留20%余量 |
| uxItemSize | 每个数据项的大小(字节) | 尽量用结构体,别用指针。我踩过坑 |
| 返回值 | 队列句柄,NULL表示创建失败 | 一定要检查返回值! |
我的经验:创建队列时,uxItemSize 最好用 sizeof(你的数据结构)。我曾经图省事,直接传了个指针大小,结果数据全乱了。队列内部是值拷贝,不是引用拷贝。你传指针,它拷贝的是指针值,不是指针指向的数据。
4.4 发送与接收——xQueueSend() 和 xQueueReceive()
发送和接收是队列的核心操作。我习惯把它们叫做“扔数据”和“拿数据”。
4.4.1 发送数据
BaseType_t xQueueSend( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );
这个函数会把 pvItemToQueue 指向的数据拷贝到队列里。注意,是拷贝,不是引用。
第三个参数 xTicksToWait 是等待时间。如果队列满了,任务会阻塞在这里等待。我一般设成 portMAX_DELAY,除非有特殊要求。
避坑指南:我曾经在中断服务函数里调用 xQueueSend(),结果系统直接崩溃。后来查资料才知道,中断里要用 xQueueSendFromISR()。记住:带 FromISR 后缀的才是中断安全版本。
4.4.2 接收数据
BaseType_t xQueueReceive( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait );
接收时,数据从队列头部取出,拷贝到 pvBuffer 指向的缓冲区。取完后,队列里的数据就被删除了。
如果队列为空,任务会阻塞等待。我通常设一个超时时间,比如 100ms,这样即使没有数据,任务也能定期醒来做其他事情。
4.5 实战:传感器数据采集中的队列应用
好了,理论说完了。咱们来点实际的。
我之前做过一个温湿度采集项目,传感器每100ms产生一次数据。主控需要把这些数据汇总后通过串口发送出去。
如果不用队列,你可能会这么写:
// 糟糕的设计:全局变量 + 标志位
float temperature;
float humidity;
uint8_t dataReady = 0;
void Sensor_Task(void *pvParameters) {
while(1) {
temperature = readTemperature();
humidity = readHumidity();
dataReady = 1;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void UART_Task(void *pvParameters) {
while(1) {
if(dataReady) {
sendToUART(temperature, humidity);
dataReady = 0;
}
}
}
这种写法有什么问题?我告诉你,问题大了去了。
- 如果UART任务还没读完,传感器任务又写了新数据,数据就丢了
- 两个任务同时访问 dataReady 标志,存在竞态条件
- 扩展性差,加一个传感器就得加一堆全局变量
用队列改造后,代码清爽多了:
// 定义数据结构
typedef struct {
float temperature;
float humidity;
uint32_t timestamp;
} SensorData_t;
// 队列句柄
QueueHandle_t xSensorQueue;
void Sensor_Task(void *pvParameters) {
SensorData_t data;
while(1) {
data.temperature = readTemperature();
data.humidity = readHumidity();
data.timestamp = xTaskGetTickCount();
// 发送到队列
if(xQueueSend(xSensorQueue, &data, portMAX_DELAY) != pdPASS) {
// 发送失败处理
// 我一般在这里加个日志
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void UART_Task(void *pvParameters) {
SensorData_t receivedData;
while(1) {
// 等待数据,超时设为500ms
if(xQueueReceive(xSensorQueue, &receivedData, pdMS_TO_TICKS(500)) == pdPASS) {
// 格式化输出
printf("Temp: %.2f, Hum: %.2f, Time: %lu\n",
receivedData.temperature,
receivedData.humidity,
receivedData.timestamp);
} else {
// 超时处理
// 我习惯在这里喂一下看门狗
}
}
}
void main(void) {
// 创建队列:深度10,每个元素大小 sizeof(SensorData_t)
xSensorQueue = xQueueCreate(10, sizeof(SensorData_t));
if(xSensorQueue == NULL) {
// 创建失败,我一般直接死循环
while(1);
}
// 创建任务
xTaskCreate(Sensor_Task, "Sensor", 256, NULL, 2, NULL);
xTaskCreate(UART_Task, "UART", 256, NULL, 1, NULL);
vTaskStartScheduler();
}
这个设计的优点:
- 传感器任务只管采集,UART任务只管发送,互不干扰
- 队列自带互斥保护,不用加锁
- 队列深度10,即使UART偶尔卡顿,也能缓存10组数据
- 扩展方便:再加一个气压传感器,只需定义新结构体,创建新队列
4.6 队列的进阶用法——我常用的几个技巧
4.6.1 队列超时处理
我建议你永远不要用 portMAX_DELAY 做接收超时。为什么?因为如果传感器坏了,你的UART任务会永远阻塞在那里。设一个合理的超时时间,比如500ms,超时后做一次系统健康检查。
4.6.2 队列复位
有时候系统需要复位队列,清空所有数据。可以用 xQueueReset()。我在系统初始化或者模式切换时经常用到。
// 清空队列,丢弃所有未处理的数据
xQueueReset(xSensorQueue);
4.6.3 查询队列状态
调试时,我喜欢用 uxQueueMessagesWaiting() 查看队列里有多少数据。这能帮我判断数据生产速度和消费速度是否匹配。
UBaseType_t count = uxQueueMessagesWaiting(xSensorQueue);
if(count > 8) {
// 队列快满了,传感器太快或者UART太慢
// 我一般在这里调整任务优先级
}
4.7 队列的注意事项——我踩过的坑
我曾经踩过的坑:
- 队列深度设太小:有一次传感器采集频率是1kHz,队列深度只设了5。结果UART任务稍微卡一下,数据就丢了。后来我改成深度50,问题解决。
- 在中断里用普通API:这个前面说过,要用 FromISR 版本。
- 传递指针而不是数据:如果传递指针,要确保指针指向的内存一直有效。我建议直接传结构体,省心。
- 忘记检查返回值:创建队列、发送、接收都要检查返回值。我见过太多人忽略这个,结果系统跑飞了都不知道原因。
4.8 小结
队列是FreeRTOS里最常用的通信机制,没有之一。它简单、可靠、高效。
我个人觉得,掌握队列的关键就三点:
- 理解它的“先进先出”和“值拷贝”特性
- 记住中断里要用 FromISR 版本
- 合理设置队列深度和超时时间
好了,队列的内容就讲到这里。你可以在自己的项目里试试,把全局变量+标志位的通信方式改成队列。相信我,改完之后你会觉得世界清净了很多。
公众号:蓝海资料掘金营,微信deep3321