FreeRTOS 学习笔记(二):队列
任务之间怎么传数据?最简单的办法是全局变量。但全局变量没有”阻塞等待”能力——消费者不知道数据什么时候准备好,只能轮询。
队列解决了这个问题:生产者往里面放,消费者从里面取。如果队列空了,消费者可以选择阻塞等待。
它本质上是个先进先出的缓冲区,但多了任务间同步的能力。
创建队列:
1 | QueueHandle_t xQueueCreate( |
注意:uxItemSize 是每条消息的大小,不是总大小。队列的实际内存 = uxQueueLength * uxItemSize,这块内存由 FreeRTOS 从堆上分配。
如果不想用堆,可以用 xQueueCreateStatic(),自己提供 uint8_t 缓冲区。
发送:
1 | BaseType_t xQueueSend( |
数据是拷贝进去的,不是传指针——队列把你传入的 pvItemToQueue 所指的内存内容 memcpy 到内部缓冲区。所以 pvItemToQueue 可以指向局部变量,不用担心作用域问题。
除了 xQueueSend,还有几个变体:
1 | xQueueSendToBack() // 跟 xQueueSend 一样,放队尾 |
接收:
1 | BaseType_t xQueueReceive( |
读完后数据从队列里移除。如果想”只看不拿走”,用 xQueuePeek():
1 | xQueuePeek(queue, &buf, timeout); // 看一眼,数据还在队列里 |
一个发一个收的例子:
1 | // 生产者任务:每 200ms 产生一个传感器数据 |
队列里有 10 个坑位,生产速度 200ms,消费能力足够快就不会满。
队列满了怎么办?
取决于业务:
- 如果旧数据没意义(传感器读数),用
xQueueOverwrite(),只保留最新的 - 如果数据不能丢(日志、命令),增加队列长度或者提高消费者优先级
- 如果偶尔丢几帧可以接受,用
xQueueSend()+ 超时 0,满了直接返回errQUEUE_FULL,跳过这次发送
ISR 里发队列的坑。
在中断服务函数里不能调 xQueueSend——因为它可能阻塞,而 ISR 里不能阻塞。必须用 FromISR 版本:
1 | BaseType_t xQueueSendFromISR( |
pxHigherPriorityTaskWoken 是个标志位——如果发送后唤醒了一个更高优先级的任务,它会被设为 pdTRUE。然后你在 ISR 末尾调用 portYIELD_FROM_ISR() 触发一次上下文切换:
1 | // UART 接收中断 |
忘了检查这个标志位,结果是:消息发出去了,但消费者要等到下一个 tick 才会被调度——延迟一个 tick(1ms~10ms),对高实时场景可能刚好超时。
队列 vs 全局数组的实测对比。
在 STM32F407(168MHz)上做了个简单对比,单字节消息、发 10000 次:
1 | 全局数组(轮询): 平均 0.3μs/次,无阻塞能力,消费者忙等 |
队列慢了一个数量级,但这几微秒换来了阻塞等待能力和任务解耦——值不值取决于场景。传感器数据轮询够用了,网络协议栈就必须上队列。
另外队列创建时的 uxItemSize 越小越好。大结构体优先传指针:
1 | // ❌ 大结构体拷贝 |
传指针的话要自己管理 net_packet_t 的生命周期——被消费之前不能释放。通常用内存池配合队列,消费者取走指针、用完归还。
阻塞超时的小细节。
portMAX_DELAY 的意思是”等到天荒地老”。但如果用了 vTaskSuspendAll() 关了调度器,即使队列里有数据也不会被唤醒——调度器关了,任务切换不生效。
所以调试时如果发现任务卡在某个 portMAX_DELAY 上永远不动了,先检查是不是哪里关了调度器忘了开。


