ARTICLE · 992014
FreeRTOS 消息队列源码剖析:为什么用了 xQueueSend 还是丢数据?
你的任务明明用了 xQueueSend,为什么偶尔还是丢数据?为什么中断里一发队列就断言死机?为什么发了结构体,接收方拿到的却不是你写的值?这些坑的答案,全都藏在 queue.c 的同一层设计里——队列锁。大多数人把队列当黑盒用:数据进、数据出,能跑就行。但队列一旦和中断、阻塞、超时搅在一起,黑盒就会在最不该出错的时刻出问题。这篇文章把 FreeRTOS V11.1.0 的 queue.c 拆开,看清楚快路径/慢路径怎么配合、锁计数怎么防止丢唤醒,最后用一段可跑的实验代码验证"传值不传引用"。读完你会发现:队列丢的不是数据,是你没看懂的那层设计。
内核版本:FreeRTOS V11.1.0(Cortex-M3)
1. 概述
1.1 消息队列是什么
FreeRTOS 消息队列是一个 FIFO 环形缓冲区,内核在 queue.c 中用 QueueDefinition(typedef 为 Queue_t)描述。它不是只存数据,而是数据存储区 + 两条等待链表 + 两个锁计数的组合体:
/* queue.c */typedef struct QueueDefinition{int8_t * pcHead; /* 存储区起始地址 */int8_t * pcWriteTo; /* 下一个写入位置 */union{QueuePointers_t xQueue; /* 队列专用: pcReadFrom / pcTail */SemaphoreData_t xSemaphore; /* 信号量专用 */} u;List_t xTasksWaitingToSend; /* 等空位的任务链表(按优先级排序) */List_t xTasksWaitingToReceive; /* 等数据的任务链表(按优先级排序) */volatile UBaseType_t uxMessagesWaiting;/* 当前消息条数 */UBaseType_t uxLength; /* 队列容量(条数) */UBaseType_t uxItemSize; /* 每条消息字节数 */volatile int8_t cRxLock; /* 锁定期间接收计数 */volatile int8_t cTxLock; /* 锁定期间发送计数 */} xQUEUE;typedef xQUEUE Queue_t;
核心特性:按值拷贝,不传引用。xQueueCreate(uxLength, uxItemSize) 中 uxItemSize 决定每条消息 memcpy 的字节数。发结构体指针也可以,但指针指向的内存生命周期要自己保证。
1.2 API 家族
所有发送接口都是 xQueueGenericSend 的宏封装:
/* queue.h */#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )#define xQueueSendToBack /* 同上, queueSEND_TO_BACK */#define xQueueSendToFront /* queueSEND_TO_FRONT */#define xQueueOverwrite /* queueOVERWRITE, 仅长度1 */
接收侧同理,xQueueReceive 直接是函数体(V11 不再走 GenericReceive)。
1.3 三个位置常量
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) /* 队尾入队(FIFO) */#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) /* 队头入队(插队) */#define queueOVERWRITE ( ( BaseType_t ) 2 ) /* 覆盖(仅长度1) */
2. 线程安全
2.1 结论先行
多任务 + 中断并发访问同一队列是安全的。 保护机制分三层,不是全程关中断——目标是把关中断时间压到最短。
2.2 三层保护机制
taskENTER_CRITICAL 临界区;慢路径:vTaskSuspendAll 挂调度器 | ||
prvLockQueue + ISR 递增锁计数 + prvUnlockQueue 补处理 | ||
taskENTER_CRITICAL_FROM_ISR |
2.3 锁计数常量与宏
/* queue.c */#define queueUNLOCKED ( ( int8_t ) -1 ) /* 未锁定 */#define queueLOCKED_UNMODIFIED ( ( int8_t ) 0 ) /* 已锁定, 未发生操作 */
prvLockQueue 把 cRxLock/cTxLock 从 queueUNLOCKED(-1) 置为 queueLOCKED_UNMODIFIED(0)。ISR 在锁定期间操作队列时,通过 prvIncrementQueueTxLock/prvIncrementQueueRxLock 把计数从 0 往上加,记录"锁定期间发生了几次操作"。
/* queue.c prvLockQueue 宏 */#define prvLockQueue( pxQueue ) \taskENTER_CRITICAL(); \{ \if( ( pxQueue )->cRxLock == queueUNLOCKED ) \( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED; \if( ( pxQueue )->cTxLock == queueUNLOCKED ) \( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED; \} \taskEXIT_CRITICAL()
2.4 为什么需要锁计数(精髓)
任务走慢路径(挂起调度器 + 锁队列 + 挂等待链表)时,中断仍可运行。慢路径期间队列非满即空——发送任务阻塞说明队列满,接收任务阻塞说明队列空。此时 ISR 对同方向的操作必然失败(满队列发送得 errQUEUE_FULL、空队列接收得失败,ISR 不阻塞直接返回),但反方向的操作可以成功:
队列满、发送任务在等空位 → ISR 取走一条数据,腾出空位; 队列空、接收任务在等数据 → ISR 送来一条数据,有了数据。
矛盾来了:ISR 取走数据后"本应"唤醒等空位的发送任务,送来数据后"本应"唤醒等数据的接收任务——但它不能碰事件链表,因为慢路径任务正在挂链表的半途中,ISR 动链表会破坏任务状态。
解法:ISR 把延迟处理的唤醒记录在锁计数上——取走数据递增 cRxLock、送来数据递增 cTxLock——等任务稍后执行 prvUnlockQueue 时统一补处理。两个方向:
xQueueReceiveFromISR | cRxLock++ | |||
xQueueSendFromISR | cTxLock++ |
两点澄清(容易误读成"锁计数 = 通行证"):
- 锁计数不是通行证,是延迟处理的唤醒记录。
发送任务醒来能入队,靠的是 ISR 取数据后 uxMessagesWaiting真实减一、队列真的有空位了;锁计数唯一的作用是保证唤醒不丢——等在链表上的任务不会因为 ISR 不敢动链表而永远睡死; - ISR 的数据操作不经过锁批准。
锁只保护事件链表这一个东西,环形缓冲区的读写和 uxMessagesWaiting增减在 FromISR 里照常执行(由taskENTER_CRITICAL_FROM_ISR保护)。锁计数的唯一读者是prvUnlockQueue,别处不看它。
例外:长度 1 的队列配 xQueueOverwriteFromISR时,满队列也能写入(覆盖语义),此时照常走锁计数分支。
锁计数全流程时序图(发送慢路径 + ISR 取走两条数据)
场景:长度 3 的队列已满,发送任务 T1、T2 阻塞等空位。T2 正在挂链表的半途中(队列已锁定),期间 ISR 从队列取走了 2 条数据——看这些延迟的唤醒怎么被补处理,两个任务怎么各自填入自己的数据:

时序图的三个要点:
- 数据不回流
——ISR 取走的 A、B 已进 ISR 侧缓冲区交给消费者;醒来的 T1、T2 填入的是各自的数据 X、Y。空位是"槽位",不是数据本身; - 数量对应
——取走 2 条 = 2 个空位 = cRxLock=2= 补唤醒 2 个任务。若链表上只有 1 个发送任务,prvUnlockQueue第 2 轮发现链表空则提前结束,剩下 1 个空位留给后续快路径; - 锁计数不是通行证
——T1、T2 醒来后能入队,靠的是快路径判满时 uxMessagesWaiting < uxLength成立(空位事实);cRxLock只保证唤醒不丢。
反向场景(队列空 + 接收任务阻塞 + xQueueSendFromISR 送来数据,记 cTxLock 补唤醒等接收的任务)完全对称,更完整的逐步分解见 4.7 节。
3. 应用场景
xQueueOverwrite,生产者永不阻塞 | |
| 不该用队列 |
4. 源码刨析
4.1 数据拷贝:prvCopyDataToQueue / prvCopyDataFromQueue
发送时把数据 memcpy 到环形缓冲区,接收时 memcpy 出来。这就是"按值拷贝"的证据。
/* queue.c prvCopyDataToQueue */static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,const void * pvItemToQueue,const BaseType_t xPosition ){BaseType_t xReturn = pdFALSE;UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;if( pxQueue->uxItemSize == ( UBaseType_t ) 0 ){/* 信号量/互斥量: 不拷贝数据, 仅处理优先级继承 */}else if( xPosition == queueSEND_TO_BACK ){/* 队尾入队: 拷到 pcWriteTo, 指针前移, 到尾则回绕 */( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue,( size_t ) pxQueue->uxItemSize );pxQueue->pcWriteTo += pxQueue->uxItemSize;if( pxQueue->pcWriteTo >= pxQueue->u.xQueue.pcTail )pxQueue->pcWriteTo = pxQueue->pcHead;}else /* queueSEND_TO_FRONT 或 queueOVERWRITE */{/* 队头入队: 写到 pcReadFrom 之前的位置 */( void ) memcpy( ( void * ) pxQueue->u.xQueue.pcReadFrom, pvItemToQueue,( size_t ) pxQueue->uxItemSize );pxQueue->u.xQueue.pcReadFrom -= pxQueue->uxItemSize;if( pxQueue->u.xQueue.pcReadFrom < pxQueue->pcHead )pxQueue->u.xQueue.pcReadFrom = ( pxQueue->u.xQueue.pcTail - pxQueue->uxItemSize );if( xPosition == queueOVERWRITE ){if( uxMessagesWaiting > 0 )--uxMessagesWaiting; /* 覆盖不增加条数 */}}pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;return xReturn;}
/* queue.c prvCopyDataFromQueue */staticvoidprvCopyDataFromQueue( Queue_t * const pxQueue, void * const pvBuffer ){if( pxQueue->uxItemSize != 0 ){pxQueue->u.xQueue.pcReadFrom += pxQueue->uxItemSize;if( pxQueue->u.xQueue.pcReadFrom >= pxQueue->u.xQueue.pcTail )pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead;( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.xQueue.pcReadFrom,( size_t ) pxQueue->uxItemSize );}}
注意 prvCopyDataFromQueue是先移动 pcReadFrom 再 memcpy——因为 pcReadFrom 初始指向"最后一条已读消息的位置",前移后才指向"下一条要读的消息"。
4.2 任务发送:xQueueGenericSend
这是最核心的函数。分为快路径(有空位,临界区内搞定)和慢路径(满了,挂调度器+锁队列+挂等待链表)。
/* queue.c xQueueGenericSend (精简, 去掉 queue sets 分支) */BaseType_t xQueueGenericSend( QueueHandle_t xQueue,const void * const pvItemToQueue,TickType_t xTicksToWait,const BaseType_t xCopyPosition ){BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;TimeOut_t xTimeOut;Queue_t * const pxQueue = xQueue;configASSERT( pxQueue );configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );for( ; ; ){taskENTER_CRITICAL();{/* ===== 快路径: 队列有空位 ===== */if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) ){xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );/* 唤醒等数据的任务 */if( listLIST_IS_EMPTY( &pxQueue->xTasksWaitingToReceive ) == pdFALSE ){if( xTaskRemoveFromEventList( &pxQueue->xTasksWaitingToReceive ) != pdFALSE ){/* 唤醒了更高优先级任务, 在临界区内 yield 是安全的 */queueYIELD_IF_USING_PREEMPTION();}}taskEXIT_CRITICAL();return pdPASS; /* 快路径结束 */}else{/* ===== 队列满了 ===== */if( xTicksToWait == 0 ){taskEXIT_CRITICAL();return errQUEUE_FULL; /* 不等待 */}else if( xEntryTimeSet == pdFALSE ){vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;}}}taskEXIT_CRITICAL();/* ===== 慢路径: 挂调度器 + 锁队列 + 挂等待链表 ===== */vTaskSuspendAll(); /* 任务不切换, 中断仍开 */prvLockQueue( pxQueue ); /* cRxLock/cTxLock = 0 */if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){if( prvIsQueueFull( pxQueue ) != pdFALSE ){/* 队列仍满: 挂到 xTasksWaitingToSend */vTaskPlaceOnEventList( &pxQueue->xTasksWaitingToSend, xTicksToWait );prvUnlockQueue( pxQueue ); /* 解锁 + 补处理 ISR 期间的唤醒 */if( xTaskResumeAll() == pdFALSE )taskYIELD_WITHIN_API(); /* 真正切出去 */}else{/* 期间有人取走数据, 重试 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();}}else{/* 超时 */prvUnlockQueue( pxQueue );( void ) xTaskResumeAll();return errQUEUE_FULL;}} /* for(;;) 循环: 被唤醒后回到开头重新走快路径 */}
关键点:
for(;;)循环:被唤醒后不直接返回,而是回到开头重新检查队列状态(醒来时队列可能又变了); 快路径临界区只包住"判满 + 拷贝 + 唤醒"几行,微秒级; 慢路径用 vTaskSuspendAll(不是关中断),中断吞吐不受影响;先锁队列再挂链表:防止"挂上链表"和"真正切换"之间被 ISR 抢走唤醒机会。

4.3 ISR 发送:xQueueGenericSendFromISR
ISR 不能阻塞,所以没有慢路径,满了直接返回 errQUEUE_FULL。核心差异:锁定期间不碰链表,只递增 cTxLock。
/* queue.c xQueueGenericSendFromISR (精简) */BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue,const void * const pvItemToQueue,BaseType_t * const pxHigherPriorityTaskWoken,const BaseType_t xCopyPosition ){BaseType_t xReturn;UBaseType_t uxSavedInterruptStatus;Queue_t * const pxQueue = xQueue;portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); /* 校验中断优先级 */uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();{if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) ){const int8_t cTxLock = pxQueue->cTxLock;( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );if( cTxLock == queueUNLOCKED ) /* 队列未锁定 */{if( listLIST_IS_EMPTY( &pxQueue->xTasksWaitingToReceive ) == pdFALSE ){if( xTaskRemoveFromEventList( &pxQueue->xTasksWaitingToReceive ) != pdFALSE ){if( pxHigherPriorityTaskWoken != NULL )*pxHigherPriorityTaskWoken = pdTRUE; /* 通知需切上下文 */}}}else /* 队列已锁定: 只递增计数, 不碰链表 */{prvIncrementQueueTxLock( pxQueue, cTxLock );}xReturn = pdPASS;}else{xReturn = errQUEUE_FULL; /* ISR 不阻塞 */}}taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );return xReturn;}
关键:pxHigherPriorityTaskWoken 只是置位标记,真正的上下文切换由 ISR 退出时的 portYIELD_FROM_ISR() 完成。
4.4 任务接收:xQueueReceive
与发送对称,快路径判空→拷贝→唤醒等空位的任务;慢路径挂 xTasksWaitingToReceive。
/* queue.c xQueueReceive (与 xQueueGenericSend 镜像对称) */BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ){...for( ; ; ){taskENTER_CRITICAL();{const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;if( uxMessagesWaiting > 0 ) /* ===== 快路径: 有数据 ===== */{prvCopyDataFromQueue( pxQueue, pvBuffer );pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;... /* 唤醒 xTasksWaitingToSend 的任务, 镜像发送侧 */taskEXIT_CRITICAL();return pdPASS;}else /* ===== 队列空 ===== */{... /* 不等待则 return errQUEUE_EMPTY; 否则记录超时起点 */}}taskEXIT_CRITICAL();/* ===== 慢路径: 挂到 xTasksWaitingToReceive ===== */vTaskSuspendAll();prvLockQueue( pxQueue );if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){if( prvIsQueueEmpty( pxQueue ) != pdFALSE ){vTaskPlaceOnEventList( &pxQueue->xTasksWaitingToReceive, xTicksToWait );prvUnlockQueue( pxQueue );if( xTaskResumeAll() == pdFALSE )taskYIELD_WITHIN_API();}else{... /* 期间有人送来数据: 解锁后重试 */}}else{/* 超时: 解锁返回 errQUEUE_EMPTY; 但若超时瞬间有数据, 回到 for 循环重读 */...}}}

4.5 ISR 接收:xQueueReceiveFromISR
与 SendFromISR 对称,锁定期间递增 cRxLock.
/* queue.c xQueueReceiveFromISR (与 SendFromISR 对称) */BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer,BaseType_t * const pxHigherPriorityTaskWoken ){...uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();{const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;if( uxMessagesWaiting > 0 ){const int8_t cRxLock = pxQueue->cRxLock;prvCopyDataFromQueue( pxQueue, pvBuffer ); /* 数据出队 */pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;if( cRxLock == queueUNLOCKED )... /* 未锁定: 唤醒 xTasksWaitingToSend 的任务(镜像 SendFromISR) */elseprvIncrementQueueRxLock( pxQueue, cRxLock ); /* 锁定: 只递增 cRxLock, 不碰链表 */xReturn = pdPASS;}else{xReturn = pdFAIL; /* ISR 不阻塞, 空就失败 */}}taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );return xReturn;}
4.6 解锁补处理:prvUnlockQueue
这是"锁计数"机制的落地处。必须在调度器挂起状态下调用。它遍历 cTxLock/cRxLock 计数,把锁定期间 ISR 产生的唤醒补上——把任务从事件链表移到 pending ready 链表,调度器恢复时统一生效。
/* queue.c prvUnlockQueue (精简) */static void prvUnlockQueue( Queue_t * const pxQueue ){/* THIS FUNCTION MUST BE CALLED WITH THE SCHEDULER SUSPENDED. *//* ----- 处理 Tx 锁: 锁定期间有数据入队, 补唤醒等接收的任务 ----- */taskENTER_CRITICAL();{int8_t cTxLock = pxQueue->cTxLock;while( cTxLock > queueLOCKED_UNMODIFIED ) /* 计数几次补几次 */{if( listLIST_IS_EMPTY( &pxQueue->xTasksWaitingToReceive ) == pdFALSE ){if( xTaskRemoveFromEventList( &pxQueue->xTasksWaitingToReceive ) != pdFALSE )vTaskMissedYield(); /* 调度器挂着, 先标记需要调度 */}elsebreak; /* 没人等了 */--cTxLock;}pxQueue->cTxLock = queueUNLOCKED;}taskEXIT_CRITICAL();/* ----- 处理 Rx 锁: 锁定期间有数据出队, 补唤醒等发送的任务 ----- *//* 与上面 Tx 循环完全对称: 遍历 cRxLock 次, 唤醒 xTasksWaitingToSend 的任务, *//* 链表空则 break, 最后 cRxLock = queueUNLOCKED */...}
4.7 锁计数全流程时序图(ISR 与慢路径并发的精髓)
场景:队列空,接收任务 RTask 进入慢路径(正在挂等待链表的半途中),此刻串口中断触发,ISR 要把收到的字节送进队列——数据能进队列,但 RTask 的唤醒被延迟处理了。看锁计数怎么把这次唤醒补上:

两个关键问题:
- ISR 的数据发送到哪里了? 发送进队列的环形缓冲区(prvCopyDataToQueue memcpy 入队),数据本身成功入队,只是唤醒动作被延迟到解锁后补处理;- RTask 在哪里被唤醒? 不在 ISR 里(队列锁定,ISR 不能碰链表),而在 RTask 自己稍后执行的 prvUnlockQueue 中——遍历 cTxLock 计数,补做 xTaskRemoveFromEventList,把 RTask 从事件链表摘到 pending ready 链表,xTaskResumeAll 生效。
这就是慢路径"挂上链表"与"真正切出"之间缝隙的填补方式:ISR 把"有数据来了"记在 cTxLock上,prvUnlockQueue统一补发,唤醒一个都不会丢。反向场景(队列满 + 发送任务阻塞 +xQueueReceiveFromISR取走数据)完全对称:记cRxLock,补唤醒等发送的任务。
5. 场景示例
5.1 场景 A:中断 → 任务的数据搬运(最典型)
ISR 只入队,处理逻辑放任务。portMAX_DELAY 无限等待,任务平时 0% CPU。
/* main 里创建: 10 条 × 1 字节 */QueueHandle_t xUartRxQueue = xQueueCreate(10, 1);/* USART1 ISR(中断优先级数值须 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY) */void USART1_IRQHandler(void){BaseType_t xWoken = pdFALSE;uint8_t ch = USART_ReceiveData(USART1);xQueueSendFromISR(xUartRxQueue, &ch, &xWoken);portYIELD_FROM_ISR(xWoken); /* 唤醒了更高级任务就立刻切 */}/* 接收任务 */void vUartParseTask(void *arg){uint8_t ch;for (;;){if (xQueueReceive(xUartRxQueue, &ch, portMAX_DELAY) == pdPASS){/* 协议解析…… */}}}
5.2 场景 B:传感器任务 → 控制任务(解耦 + 削峰)
队列是速率平滑器。控制任务偶发卡顿也不丢数据。
typedef struct { uint16_t adc[4]; uint32_t tick; } SensorMsg_t;QueueHandle_t xSensorQ = xQueueCreate(8, sizeof(SensorMsg_t));voidvSampleTask(void *arg){SensorMsg_t msg;TickType_t xLastWake = xTaskGetTickCount();for (;;){fill_sensor(&msg);xQueueSend(xSensorQ, &msg, 0); /* 满? 丢新并计数 */vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(10));}}
5.3 场景 C:"最新值"语义——xQueueOverwrite
长度 1 + overwrite,生产者永远不阻塞、永远不丢"最新"。
xVoltageQ = xQueueCreate(1, sizeof(uint16_t));xQueueOverwrite(xVoltageQ, &voltage); /* 满→覆盖旧的 */xQueuePeek(xVoltageQ, &voltage, portMAX_DELAY); /* 取但不取走 */
5.4 完整实验:FIFO + 满阻塞 + 按值拷贝
下面用一段可跑的实验验证两个关键结论。实验分两阶段:
- 阶段一:int 队列(长度 3),生产者突发连发 5 个 → 前三个成功,第四五个满阻塞超时丢弃(errQUEUE_FULL),消费者按 FIFO 顺序收到;- 阶段二:结构体队列,发送方入队后立刻篡改局部变量,接收方拿到的仍是入队时的快照——证明队列传值不传引用。
核心片段 1:创建队列,故意让消费者晚启动
static QueueHandle_t xIntQueue; /* 长度 3 的 int 队列 */voidqueue_demo(void){/* 参数是"条数 × 每条字节数", 从 FreeRTOS 堆分配 */xIntQueue = xQueueCreate(3, sizeof(int));/* 生产者优先级 +2, 消费者 +1: 消费者头 1 秒故意不收, 队列必然被灌满 */xTaskCreate(q_producer_task, "q_prod", 256, NULL, tskIDLE_PRIORITY + 2, NULL);xTaskCreate(q_consumer_task, "q_cons", 256, NULL, tskIDLE_PRIORITY + 1, NULL);}
核心片段 2:生产者突发连发,制造满阻塞
staticvoidq_producer_task(void *pvParameters){int iValue;BaseType_t xStatus;/* ---- 突发段: 100ms 一个连发 5 个, 消费者还在睡, 队列(长度3)必满 ---- */for (iValue = 1; iValue <= 5; iValue++){vTaskDelay(pdMS_TO_TICKS(100));/* 队列满则阻塞最多 200ms, 超时返回 errQUEUE_FULL(该数据丢弃=丢新策略) */xStatus = xQueueSendToBack(xIntQueue, &iValue, pdMS_TO_TICKS(200));if (xStatus == pdPASS)printf("[生产者] 突发值=%d 入队成功\r\n", iValue);elseprintf("[生产者] 突发值=%d 队列满! 超时返回errQUEUE_FULL(数据被丢弃)\r\n", iValue);}/* 之后稳态每秒发 1 个(值 6~15), 最后发哨兵 -1, vTaskDelete(NULL) 自删 */}
预期输出:值 1、2、3 入队成功;值 4、5 阻塞 200ms 后失败丢弃。
核心片段 3:消费者阻塞接收,FIFO 顺序出队
staticvoidq_consumer_task(void *pvParameters){int iValue;vTaskDelay(pdMS_TO_TICKS(1000)); /* 故意晚启动, 制造满队列条件 */for (;;){/* 阻塞最多 2 秒等数据; 超时返回 errQUEUE_EMPTY */if (xQueueReceive(xIntQueue, &iValue, pdMS_TO_TICKS(2000)) != pdPASS)continue;if (iValue == -1)break; /* 哨兵: 阶段一结束, 切换到阶段二 */printf("[消费者] 收到值=%d <- 按FIFO顺序出队\r\n", iValue);}}
预期输出:收到值的顺序严格是 1, 2, 3, 6, 7, ... —— 塞进队的三条先出,被丢的 4、5 永远不会出现。
核心片段 4:发送后篡改变量,证明传的是拷贝
typedef struct{uint8_t id; /* 1=传感器A, 2=传感器B, 0=结束哨兵 */int16_t temp;} TempData_t;staticvoidq_struct_sender_task(void *pvParameters){TempData_t xMsg;uint8_t i;for (i = 0; i < 10; i++){vTaskDelay(pdMS_TO_TICKS(1000));xMsg.id = (uint8_t)((i % 2 == 0) ? 1 : 2);xMsg.temp = (int16_t)(25 + i / 2);xQueueSendToBack(xStructQueue, &xMsg, pdMS_TO_TICKS(500));printf("[发送方] 发送{id=%d, temp=%d}后立刻篡改本地变量{id=9, temp=-99}\r\n",(int)xMsg.id, (int)xMsg.temp);/* 篡改的只是发送方自己的局部变量: 队列在入队那一刻已经 memcpy 走了快照,接收方拿到的是入队时的值; 反之若队列存的是指针(引用), 接收方就会读到 {9, -99} */xMsg.id = 9;xMsg.temp = -99;}}
预期输出:接收方打印的永远是 {id, temp} 入队时的值,发送方随后的篡改影响不到——这就是第 1 节说的"按值拷贝"。
演示时序图

6. 注意事项(坑列表)
- 传值不传引用
:发指针可以,但指向的内存必须存活到接收方用完; - ISR 必须用 FromISR 版本
,且中断优先级数值须 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY; - FromISR 唤醒后要
portYIELD_FROM_ISR(woken),否则推迟到下个 tick; - 大数据走临界区
:拷贝在临界区内,大消息拉长关中断时间,考虑传指针或拆小; xQueueOverwrite仅限长度 1;- 别在中断里用
xQueuePeek,用 xQueuePeekFromISR; - 句柄先建后用
: xQueueCreate失败返回 NULL; - 速率失衡队列必然溢出
,满后策略(丢新/丢旧/记错误)要想清楚; - 删队列不负责善后
:别删还有任务在等的队列; - 创建参数是"条数×每条字节数"
:RAM = 结构体开销 + uxLength×uxItemSize。
— 写在最后 —
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连支持,这是我持续更新的最大动力。
关注我,更多硬核技术剖析持续更新中,下一期再见。