队列机制详解:队列的创建、发送、接收与删除

队列这东西,说白了就是任务间传递数据的“管道”。

我刚开始用RTOS那会儿,总觉得队列不就是个数组嘛,有啥好讲的?后来踩了坑才明白——队列远不止“放数据、取数据”这么简单。它背后牵涉到任务调度、临界区保护、内存管理一堆事儿。

今天咱们就把队列的四个核心操作掰开揉碎讲清楚:创建、发送、接收、删除。

1. 队列的创建——你得先挖个坑

创建队列,就像在系统里挖一个蓄水池。你得告诉RTOS:这个池子多大?能存多少条消息?每条消息多长?

以FreeRTOS为例,创建队列的API长这样:

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
  • uxQueueLength:队列能容纳的消息数量。比如设成5,最多存5条。
  • uxItemSize:每条消息的字节数。注意,这里是拷贝数据,不是传指针。

我习惯把队列长度设成2的幂次,比如4、8、16。为什么?因为有些RTOS内部会用位运算做取模,效率高那么一丢丢。当然,这不是必须的,算是个小癖好。

我的经验:队列创建时,RTOS会从堆里分配内存。如果你用静态创建(xQueueCreateStatic),就得自己准备内存块。我建议新手先用动态创建,省心。等项目跑起来了,再根据实际情况优化。

创建成功后,返回一个队列句柄。这个句柄就是你的“通行证”,后面所有操作都靠它。

举个例子:

QueueHandle_t xMsgQueue;
xMsgQueue = xQueueCreate( 10, sizeof( uint32_t ) );

if( xMsgQueue == NULL )
{
    // 创建失败,多半是内存不够
    // 我遇到过这种情况:堆设小了,队列一多就崩
}

2. 队列的发送——把数据扔进去

发送数据,就是把消息塞进队列。RTOS会帮你把数据拷贝到队列内部缓冲区。

发送API有两种:

  • xQueueSend():往队列尾部发送。最常用。
  • xQueueSendToFront():插队,塞到队列头部。优先级高的消息可以用这个。

原型长这样:

BaseType_t xQueueSend( QueueHandle_t xQueue, 
                       const void * pvItemToQueue, 
                       TickType_t xTicksToWait );

第三个参数是等待时间。如果队列满了,任务可以选择等一会儿,或者直接返回。

重点:发送的是数据的拷贝,不是指针。你传一个结构体进去,RTOS会完整复制一份。所以消息体别太大,否则拷贝开销会让你肉疼。

我见过有人把1KB的传感器数据直接往队列里塞,结果CPU一半时间都在拷贝数据。后来改成传指针,问题就解决了。

发送的返回值:

返回值 含义
pdPASS 发送成功
errQUEUE_FULL 队列满了,发送失败

代码示例:

uint32_t ulData = 100;
BaseType_t xResult;

xResult = xQueueSend( xMsgQueue, &ulData, 0 );

if( xResult != pdPASS )
{
    // 队列满了,数据丢了
    // 我曾经在这里吃过亏:中断里发数据,忘了检查返回值
}

3. 队列的接收——把数据掏出来

接收数据,就是从队列里取出一条消息。如果队列是空的,任务可以选择阻塞等待。

接收API:

BaseType_t xQueueReceive( QueueHandle_t xQueue, 
                          void * pvBuffer, 
                          TickType_t xTicksToWait );
  • pvBuffer:接收缓冲区,用来存放取出的数据。
  • xTicksToWait:最大等待时间。设成portMAX_DELAY就是死等。

接收成功后,数据从队列里移除。这一点和邮箱不同——邮箱是覆盖式存储,队列是先进先出。

注意:接收缓冲区的大小必须≥创建队列时指定的消息大小。否则数据会溢出,把栈给踩了。我调试过一个bug,就是接收缓冲区小了2个字节,结果偶尔死机,查了两天才找到原因。

接收示例:

uint32_t ulReceivedData;

if( xQueueReceive( xMsgQueue, &ulReceivedData, pdMS_TO_TICKS( 100 ) ) == pdPASS )
{
    // 成功收到数据
    printf("收到数据: %lu\n", ulReceivedData);
}
else
{
    // 等了100ms还没数据,超时了
}

还有个变种叫xQueuePeek(),它只“看一眼”队列头部的数据,但不移除。我偶尔用它做调试,或者做“预览”功能。

4. 队列的删除——清理战场

队列用完了,得删掉。不然内存就泄漏了。

删除API很简单:

void vQueueDelete( QueueHandle_t xQueue );

调用后,RTOS会释放队列占用的内存。注意:

  • 删除前,确保没有任务在等待这个队列。否则那些任务会一直阻塞,变成“僵尸任务”。
  • 删除后,队列句柄就失效了。再使用就是野指针操作。
我的习惯:删除队列后,把句柄手动置为NULL。这样万一不小心用了,至少能通过断言捕获到。

代码:

vQueueDelete( xMsgQueue );
xMsgQueue = NULL;  // 防止误用

5. 避坑指南——我踩过的雷

队列用起来简单,但坑也不少。分享几个我亲身经历过的:

  • 中断里发数据,忘了检查返回值。队列满了怎么办?数据丢了,系统行为就不可控了。后来我改成在中断里用xQueueSendFromISR(),并且检查返回值,满了就丢弃旧数据。
  • 队列长度设得太小。高负载时,发送方频繁失败。我一般根据数据产生速率和消费速率,留出20%~30%的余量。
  • 消息体太大。拷贝开销高,还占内存。建议消息体控制在几十字节以内。大的数据块,传指针就好。
  • 多个任务同时操作同一个队列。RTOS内部会保护,但如果你在中断和任务间共享队列,记得用中断安全版本API。

6. 小结

队列是RTOS里最常用的通信手段。创建时想好长度和消息大小,发送时检查返回值,接收时注意缓冲区大小,删除时清理干净。

嗯,掌握了这四个操作,队列这块基本就稳了。下一章咱们聊聊邮箱——它和队列有啥区别?什么时候该用邮箱而不是队列?到时候见。