夜雨聆风学习资料网

ARTICLE · 992014

FreeRTOS 消息队列源码剖析:为什么用了 xQueueSend 还是丢数据?

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 三层保护机制

并发方
保护机制
关中断时长
任务 vs 任务
快路径:taskENTER_CRITICAL 临界区;慢路径:vTaskSuspendAll 挂调度器
微秒级(判满/拷贝/唤醒)
任务 vs 中断
队列锁 prvLockQueue + ISR 递增锁计数 + prvUnlockQueue 补处理
同上
中断 vs 中断
taskENTER_CRITICAL_FROM_ISR
(保存/恢复 BASEPRI)
同上

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 时统一补处理。两个方向:

慢路径方向
队列状态
期间能成功的 ISR 操作
锁计数递增
补唤醒谁
发送任务阻塞(等空位)
xQueueReceiveFromISR
 取走数据腾出空位
cRxLock++
等发送的任务
接收任务阻塞(等数据)
xQueueSendFromISR
 送来数据
cTxLock++
等接收的任务

两点澄清(容易误读成"锁计数 = 通行证"):

  1. 锁计数不是通行证,是延迟处理的唤醒记录。
     发送任务醒来能入队,靠的是 ISR 取数据后 uxMessagesWaiting 真实减一、队列真的有空位了;锁计数唯一的作用是保证唤醒不丢——等在链表上的任务不会因为 ISR 不敢动链表而永远睡死;
  2. ISR 的数据操作不经过锁批准。
     锁只保护事件链表这一个东西,环形缓冲区的读写和 uxMessagesWaiting 增减在 FromISR 里照常执行(由 taskENTER_CRITICAL_FROM_ISR 保护)。锁计数的唯一读者是 prvUnlockQueue,别处不看它。
例外:长度 1 的队列配 xQueueOverwriteFromISR 时,满队列也能写入(覆盖语义),此时照常走锁计数分支。

锁计数全流程时序图(发送慢路径 + ISR 取走两条数据)

场景:长度 3 的队列已满,发送任务 T1、T2 阻塞等空位。T2 正在挂链表的半途中(队列已锁定),期间 ISR 从队列取走了 2 条数据——看这些延迟的唤醒怎么被补处理,两个任务怎么各自填入自己的数据:

时序图的三个要点:

  1. 数据不回流
    ——ISR 取走的 A、B 已进 ISR 侧缓冲区交给消费者;醒来的 T1、T2 填入的是各自的数据 X、Y。空位是"槽位",不是数据本身;
  2. 数量对应
    ——取走 2 条 = 2 个空位 = cRxLock=2 = 补唤醒 2 个任务。若链表上只有 1 个发送任务,prvUnlockQueue 第 2 轮发现链表空则提前结束,剩下 1 个空位留给后续快路径;
  3. 锁计数不是通行证
    ——T1、T2 醒来后能入队,靠的是快路径判满时 uxMessagesWaiting < uxLength 成立(空位事实);cRxLock 只保证唤醒不丢。

反向场景(队列空 + 接收任务阻塞 + xQueueSendFromISR 送来数据,记 cTxLock 补唤醒等接收的任务)完全对称,更完整的逐步分解见 4.7 节。

3. 应用场景

场景
说明
中断 → 任务数据搬运
ISR 只入队,处理逻辑放任务,ISR 越短越好
任务间解耦 + 削峰
生产者突发数据由队列缓冲,消费端偶发卡顿不丢数据
"最新值"语义
长度 1 + xQueueOverwrite,生产者永不阻塞
命令分发
命令入队,多个执行任务各自接收(一对多)
不该用队列
仅"事件发生了吗"用任务通知;互斥访问用互斥量(队列无优先级继承);大数据流用 StreamBuffer

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 );                /* 唤醒等数据的任务 */                iflistLIST_IS_EMPTY( &pxQueue->xTasksWaitingToReceive ) == pdFALSE )                {                    ifxTaskRemoveFromEventList( &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 */        ifxTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )        {            ifprvIsQueueFull( pxQueue ) != pdFALSE )            {                /* 队列仍满: 挂到 xTasksWaitingToSend */                vTaskPlaceOnEventList( &pxQueue->xTasksWaitingToSend, xTicksToWait );                prvUnlockQueue( pxQueue );   /* 解锁 + 补处理 ISR 期间的唤醒 */                ifxTaskResumeAll() == 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 )   /* 队列未锁定 */            {                iflistLIST_IS_EMPTY( &pxQueue->xTasksWaitingToReceive ) == pdFALSE )                {                    ifxTaskRemoveFromEventList( &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 );        ifxTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )        {            ifprvIsQueueEmpty( pxQueue ) != pdFALSE )            {                vTaskPlaceOnEventList( &pxQueue->xTasksWaitingToReceive, xTicksToWait );                prvUnlockQueue( pxQueue );                ifxTaskResumeAll() == 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) */            else                prvIncrementQueueRxLock( 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 )  /* 计数几次补几次 */        {            iflistLIST_IS_EMPTY( &pxQueue->xTasksWaitingToReceive ) == pdFALSE )            {                ifxTaskRemoveFromEventList( &pxQueue->xTasksWaitingToReceive ) != pdFALSE )                    vTaskMissedYield();   /* 调度器挂着, 先标记需要调度 */            }            else                break;   /* 没人等了 */            --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(101);/* 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(8sizeof(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(1sizeof(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(3sizeof(int));    /* 生产者优先级 +2, 消费者 +1: 消费者头 1 秒故意不收, 队列必然被灌满 */    xTaskCreate(q_producer_task, "q_prod"256NULL, tskIDLE_PRIORITY + 2NULL);    xTaskCreate(q_consumer_task, "q_cons"256NULL, tskIDLE_PRIORITY + 1NULL);}

核心片段 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);        else            printf("[生产者] 突发值=%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. 注意事项(坑列表)

  1. 传值不传引用
    :发指针可以,但指向的内存必须存活到接收方用完;
  2. ISR 必须用 FromISR 版本
    ,且中断优先级数值须 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY
  3. FromISR 唤醒后要 portYIELD_FROM_ISR(woken)
    ,否则推迟到下个 tick;
  4. 大数据走临界区
    :拷贝在临界区内,大消息拉长关中断时间,考虑传指针或拆小;
  5. xQueueOverwrite 仅限长度 1;
  6. 别在中断里用 xQueuePeek
    ,用 xQueuePeekFromISR
  7. 句柄先建后用
    xQueueCreate 失败返回 NULL;
  8. 速率失衡队列必然溢出
    ,满后策略(丢新/丢旧/记错误)要想清楚;
  9. 删队列不负责善后
    :别删还有任务在等的队列;
  10. 创建参数是"条数×每条字节数"
    :RAM = 结构体开销 + uxLength×uxItemSize。

— 写在最后 —

如果这篇文章对你有帮助,欢迎点赞、在看、转发三连支持,这是我持续更新的最大动力。

关注我,更多硬核技术剖析持续更新中,下一期再见。

相关学习资料

返回首页浏览学习资料