乐于分享
好东西不私藏

FreeRTOS 消息队列:从源码撕开内核通信的秘密

FreeRTOS 消息队列:从源码撕开内核通信的秘密

1. 消息队列API

FreeRTOS 的消息队列是任务间通信(IPC)的核心机制,它不仅仅是简单的数据传递通道,更是实现信号量、互斥量等同步原语的基础,消息队列本文分为两篇,本篇是下篇重点介绍消息队列的内部机制,在阅读本文前建议先看上篇文章搞懂消息队列,你的 FreeRTOS 才算入门,由浅入深进行阅读

FreeRTOS 队列的设计遵循以下核心原则:

  • 按值复制:数据在入队时被完整复制到队列内部缓冲区,而非传递指针。这避免了悬空指针和数据竞争问题。
  • FIFO 顺序:默认先进先出,同时支持优先级消息(发送到队首)。
  • 阻塞与超时:支持任务在队列满/空时阻塞等待,带超时机制。
  • ISR 安全:提供独立的 ‎FromISR API,可在中断中安全操作。

1.1 API 层次结构

FreeRTOS 队列 API 采用分层设计:

  • 应用层:用户直接调用的宏(‎xQueueCreate, ‎xQueueSend, ‎xQueueReceive
  • 通用层:实际实现逻辑的函数(‎xQueueGenericSend, ‎xQueueGenericCreate
  • 辅助层:内部数据操作函数(‎prvCopyDataToQueue, ‎prvCopyDataFromQueue
  • 任务层:任务阻塞/唤醒管理(‎vTaskPlaceOnEventList, ‎xTaskRemoveFromEventList
  • 移植层:临界区和上下文切换(‎taskENTER_CRITICAL, ‎portYIELD

2. 核心数据结构 Queue_t

队列的核心是 Queue_t 结构体(在 queue.c 中定义为 xQUEUE),它包含了队列运行所需的所有状态信息。

2.1 结构体定义

typedef struct QueueDefinition{        int8_t *pcHead;         // 指向队列存储区起始位置        int8_t *pcWriteTo;      // 指向下一次写入的位置        union        {                QueuePointers_t xQueue;     // 队列模式使用的指针                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;

2.2 关键字段解析

指针字段(队列模式)

字段
说明
pcHead
存储区起始地址,分配时指向 ‎Queue_t 结构体之后的内存
pcWriteTo
下一个写入位置,入队时前进,到达尾部回绕到 ‎pcHead
u.xQueue.pcTail
存储区结束地址(‎pcHead + uxLength * uxItemSize
u.xQueue.pcReadFrom
上次读取位置,出队时前进,到达尾部回绕到 ‎pcHead

计数字段

字段
说明
uxMessagesWaiting
当前消息数量,入队+1,出队-1
uxLength
队列容量,创建时指定
uxItemSize
每条消息大小(字节),信号量为 0

等待列表

字段
说明
xTasksWaitingToSend
队列满时,发送任务阻塞在此列表
xTasksWaitingToReceive
队列空时,接收任务阻塞在此列表

两个列表都按任务优先级排序,确保高优先级任务优先被唤醒。

锁计数器

字段
说明
cRxLock
接收锁,‎-1 表示解锁,‎>=0 表示锁定期间接收的消息数
cTxLock
发送锁,‎-1 表示解锁,‎>=0 表示锁定期间发送的消息数

2.3 联合体设计

Queue_t 使用联合体来区分队列和信号量两种模式:

union{        QueuePointers_t xQueue;      // 队列模式:存储读写指针        SemaphoreData_t xSemaphore;  // 信号量模式:存储互斥量持有者} u;
  • 队列模式:使用 ‎pcTail 和 ‎pcReadFrom 实现环形缓冲区
  • 信号量模式:使用 ‎xMutexHolder 记录互斥量持有者任务句柄

这种设计使得同一个结构体可以同时支持队列和信号量,减少了代码重复。


3. 环形缓冲区原理

FreeRTOS 队列使用环形缓冲区(Circular Buffer)存储消息数据,这是一种高效的数据结构,支持 O(1) 时间复杂度的入队和出队操作。

3.1 内存布局

队列的存储区是一块连续的内存,逻辑上视为环形

  • pcHead:存储区起始,固定不变
  • pcTail:存储区结束,‎pcHead + uxLength * uxItemSize
  • pcWriteTo:下一个写入位置,入队后前进
  • pcReadFrom:上次读取位置,出队后前进

3.2 入队操作(写入)

prvCopyDataToQueue() 函数负责将数据复制到队列:

static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,                                                                              const void *pvItemToQueue,                                                                              const BaseType_t xPosition ){    if( xPosition == queueSEND_TO_BACK )        {                // 复制数据到 pcWriteTo 位置                memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue,                                 ( size_t ) pxQueue->uxItemSize );                // pcWriteTo 前进                pxQueue->pcWriteTo += pxQueue->uxItemSize;                // 到达尾部则回绕到头部                if( pxQueue->pcWriteTo >= pxQueue->u.xQueue.pcTail )                {                        pxQueue->pcWriteTo = pxQueue->pcHead;                }        }        else // queueSEND_TO_FRONT        {                // 复制数据到 pcReadFrom 位置                memcpy( ( void * ) pxQueue->u.xQueue.pcReadFrom, pvItemToQueue,                                 ( size_t ) pxQueue->uxItemSize );                // pcReadFrom 后退(注意方向相反)                pxQueue->u.xQueue.pcReadFrom -= pxQueue->uxItemSize;                // 到达头部则回绕到尾部                if( pxQueue->u.xQueue.pcReadFrom < pxQueue->pcHead )                {                        pxQueue->u.xQueue.pcReadFrom =                                 ( pxQueue->u.xQueue.pcTail - pxQueue->uxItemSize );                }        }        // 消息计数增加        pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1;        return xReturn;}

关键点

  • 发送到队尾:从 ‎pcWriteTo 写入,指针前进
  • 发送到队首:从 ‎pcReadFrom 写入,指针后退
  • 指针到达边界时自动回绕,实现环形效果

3.3 出队操作(读取)

prvCopyDataFromQueue() 函数负责从队列读取数据:

staticvoidprvCopyDataFromQueue( Queue_t * const pxQueue,                                                                      void * const pvBuffer ){        if( pxQueue->uxItemSize != 0 )        {                // pcReadFrom 前进                pxQueue->u.xQueue.pcReadFrom += pxQueue->uxItemSize;                // 到达尾部则回绕到头部                if( pxQueue->u.xQueue.pcReadFrom >= pxQueue->u.xQueue.pcTail )                {                        pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead;                }                // 从 pcReadFrom 位置复制数据                memcpy( ( void * ) pvBuffer,                                 ( void * ) pxQueue->u.xQueue.pcReadFrom,                                 ( size_t ) pxQueue->uxItemSize );      }}

重要细节pcReadFrom 是先前进再读取,这意味着它指向的是上次读取的位置,而非下一次读取的位置。

3.4 队列重置

xQueueGenericReset() 函数将队列恢复到初始状态:

BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue ){        pxQueue->u.xQueue.pcTail = pxQueue->pcHead +                 ( pxQueue->uxLength * pxQueue->uxItemSize );        pxQueue->uxMessagesWaiting = 0;        pxQueue->pcWriteTo = pxQueue->pcHead;        pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead +                 ( ( pxQueue->uxLength - 1 ) * pxQueue->uxItemSize );        pxQueue->cRxLock = queueUNLOCKED;        pxQueue->cTxLock = queueUNLOCKED;        // ...}

注意 pcReadFrom 初始化为最后一个槽位,因为它是"先前进再读取"的设计。


4. 队列创建过程

4.1 动态创建

xQueueGenericCreate() 函数负责动态分配内存并初始化队列:

QueueHandle_t xQueueGenericCreateconst UBaseType_t uxQueueLength,                                                                        const UBaseType_t uxItemSize,                                                                        const uint8_t ucQueueType ){        Queue_t *pxNewQueue;        size_t xQueueSizeInBytes;        uint8_t *pucQueueStorage;        // 计算存储区大小        xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );        // 一次性分配 Queue_t 结构体 + 存储区        pxNewQueue = ( Queue_t * ) pvPortMalloc(                 sizeof( Queue_t ) + xQueueSizeInBytes );        if( pxNewQueue != NULL )        {                // 存储区紧随 Queue_t 之后                pucQueueStorage = ( uint8_t * ) pxNewQueue;                pucQueueStorage += sizeof( Queue_t );                // 初始化队列                prvInitialiseNewQueue( uxQueueLength, uxItemSize,                                            pucQueueStorage, ucQueueType, pxNewQueue );        }        return pxNewQueue;}

内存分配策略

这种"一次分配"策略减少了内存碎片和分配开销。

4.2 静态创建

xQueueGenericCreateStatic() 使用用户提供的内存:

QueueHandle_t xQueueGenericCreateStatic(    const UBaseType_t uxQueueLength,        const UBaseType_t uxItemSize,         uint8_t *pucQueueStorage,        StaticQueue_t *pxStaticQueue,         const uint8_t ucQueueType ){        Queue_t *pxNewQueue;    // 使用用户提供的结构体        pxNewQueue = ( Queue_t * ) pxStaticQueue;        if( pxNewQueue != NULL )        {                // 标记为静态分配(删除时不会 free)                pxNewQueue->ucStaticallyAllocated = pdTRUE;                prvInitialiseNewQueue( uxQueueLength, uxItemSize,                                            pucQueueStorage, ucQueueType, pxNewQueue );        }        return pxNewQueue;}

4.3 初始化过程

prvInitialiseNewQueue() 完成队列的初始化:

staticvoidprvInitialiseNewQueueconst UBaseType_t uxQueueLength,                                                                       const UBaseType_t uxItemSize,                                                                       uint8_t *pucQueueStorage,                                                                       const uint8_t ucQueueType,                                                                       Queue_t *pxNewQueue ){        // 信号量模式下,pcHead 不能为 NULL(NULL 表示互斥量)        if( uxItemSize == 0 )        {                pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;        }        else        {                pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;        }        pxNewQueue->uxLength = uxQueueLength;        pxNewQueue->uxItemSize = uxItemSize;        // 调用 Reset 完成剩余初始化        xQueueGenericReset( pxNewQueue, pdTRUE );}

5. 消息发送机制

xQueueGenericSend() 是所有发送操作的核心函数,它处理了队列满时的阻塞和超时逻辑。

5.1 函数签名

BaseType_t xQueueGenericSend(         QueueHandle_t xQueue,        const void * const pvItemToQueue,        TickType_t xTicksToWait,        const BaseType_t xCopyPosition )

参数说明:

  • xQueue:队列句柄
  • pvItemToQueue:待发送数据的指针
  • xTicksToWait:队列满时的等待超时(tick 数)
  • xCopyPosition:发送位置(‎queueSEND_TO_BACK、‎queueSEND_TO_FRONT、‎queueOVERWRITE

5.2 核心逻辑

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 )                                {                                        // 被唤醒的任务优先级更高,需要上下文切换                                        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 ); // 锁定队列        // 检查是否超时        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )        {                // 未超时,检查队列是否仍然满                if( prvIsQueueFull( pxQueue ) != pdFALSE )                {                        // 仍然满,阻塞等待                        vTaskPlaceOnEventList(                                 &( pxQueue->xTasksWaitingToSend ), xTicksToWait );                        prvUnlockQueue( pxQueue );                        if( xTaskResumeAll() == pdFALSE )                        {                                portYIELD_WITHIN_API();                        }                }                else                {                        // 队列有空间了,重试                        prvUnlockQueue( pxQueue );                        ( void ) xTaskResumeAll();                }        }        else        {                // 超时,返回失败                prvUnlockQueue( pxQueue );                ( void ) xTaskResumeAll();                return errQUEUE_FULL;        }}

5.3 关键设计点

  1. 双重检查模式

    • 第一次检查在临界区内(快速路径)
    • 第二次检查在锁定队列后(慢速路径,处理竞态条件)
  2. 调度器挂起与队列锁定

    • vTaskSuspendAll() 防止任务切换
    • prvLockQueue() 防止 ISR 修改事件列表
    • 两者配合确保操作的原子性
  3. 超时处理

    • 使用 ‎xTimeOut 结构体记录进入时间
    • 每次循环检查是否超时
    • 超时后返回 ‎errQUEUE_FULL

5.4 ISR 版本

xQueueGenericSendFromISR() 是中断安全版本:

BaseType_t xQueueGenericSendFromISR(    QueueHandle_t xQueue,        const void * const pvItemToQueue,        BaseType_t * const pxHigherPriorityTaskWoken,        const BaseType_t xCopyPosition ){        uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();        {                if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||                         ( xCopyPosition == queueOVERWRITE ) )                {                        // 复制数据                        ( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );                        // 如果队列未锁定,直接唤醒任务                        if( cTxLock == queueUNLOCKED )                        {                                iflistLIST_IS_EMPTY(&( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )                                {                                        ifxTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )                                        {                                                // 需要上下文切换                                                *pxHigherPriorityTaskWoken = pdTRUE;                                        }                                }                        }                        else                        {                                // 队列已锁定,增加锁计数                                pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );                        }                        xReturn = pdPASS;                }                else                {                        xReturn = errQUEUE_FULL;                }        }       portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );        return xReturn;}

ISR 版本的特点

  • 不阻塞(无 ‎xTicksToWait 参数)
  • 使用 ‎portSET_INTERRUPT_MASK_FROM_ISR() 替代临界区
  • 通过 ‎pxHigherPriorityTaskWoken 返回是否需要上下文切换

6. 消息接收机制

xQueueReceive() 是接收操作的核心函数,其逻辑与发送类似但方向相反。

6.1 核心逻辑

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;                                // 唤醒等待发送的任务                                iflistLIST_IS_EMPTY(&( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )                                {                                        ifxTaskRemoveFromEventList(&( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )                                        {                                                queueYIELD_IF_USING_PREEMPTION();                                        }                                }                                taskEXIT_CRITICAL();                                return pdPASS;                        }                        else  // 队列空                        {                                if( xTicksToWait == 0 )                                {                                        taskEXIT_CRITICAL();                                        return errQUEUE_EMPTY;                                }                                // ... 超时处理逻辑(与发送类似)                        }                }                taskEXIT_CRITICAL();                // 阻塞等待...        }}

6.2 Peek 操作

xQueuePeek() 读取但不移除数据:

BaseType_t xQueuePeek( QueueHandle_t xQueue,                       void * const pvBuffer,                                                TickType_t xTicksToWait ){        // 保存原始读取位置        pcOriginalReadPosition = pxQueue->u.xQueue.pcReadFrom;        prvCopyDataFromQueue( pxQueue, pvBuffer );        // 恢复读取位置(数据不移除)        pxQueue->u.xQueue.pcReadFrom = pcOriginalReadPosition;        // 注意:仍然唤醒其他等待任务        // 因为数据仍在队列中,其他任务也可以读取}

7. 队列锁与 ISR 安全

7.1 队列锁定机制

队列锁定用于在批量操作期间提高效率:

// 锁定队列#define prvLockQueue( pxQueue )                                 taskENTER_CRITICAL();                                       {                                                                   if( ( pxQueue )->cRxLock == queueUNLOCKED )                     {                                                                       ( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED;                }                                                               if( ( pxQueue )->cTxLock == queueUNLOCKED )                     {                                                                       ( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED;                }                                                       }                                                           taskEXIT_CRITICAL()

7.2 锁计数器的作用

当队列被锁定时,ISR 不能直接修改事件列表,而是增加锁计数器:

// ISR 中的发送操作if( cTxLock == queueUNLOCKED ){        // 队列未锁定,直接唤醒任务        xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) );}else{        // 队列已锁定,只增加计数        pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );}

7.3 解锁时处理

prvUnlockQueue() 在解锁时处理积压的操作:

static void prvUnlockQueue( Queue_t * const pxQueue ){        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();        // 类似处理 cRxLock...}

设计优势

  • 减少了 ISR 中的操作时间
  • 批量唤醒任务,提高效率
  • 保证了事件列表的一致性

8. 阻塞与超时机制

8.1 阻塞流程

当队列满/空时,任务进入阻塞状态:

// 1. 记录进入时间vTaskInternalSetTimeOutState( &xTimeOut );xEntryTimeSet = pdTRUE;// 2. 挂起调度器vTaskSuspendAll();// 3. 锁定队列prvLockQueue( pxQueue );// 4. 检查超时ifxTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ){        // 未超时        ifprvIsQueueFull( pxQueue ) != pdFALSE )        {                // 仍然满,加入等待列表                vTaskPlaceOnEventList(&( pxQueue->xTasksWaitingToSend ), xTicksToWait );        }}// 5. 解锁队列prvUnlockQueue( pxQueue );// 6. 恢复调度器ifxTaskResumeAll() == pdFALSE ){        portYIELD_WITHIN_API();  // 需要上下文切换}

8.2 超时检测

xTaskCheckForTimeOut() 检查是否已超时:

BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut, TickType_t * const pxTicksToWait ){        // 计算已过去的时间        // 如果超时,返回 pdTRUE        // 否则,更新剩余等待时间,返回 pdFALSE}

8.3 唤醒机制

当队列状态改变时,等待的任务被唤醒:

// 从等待列表移除任务BaseType_t xTaskRemoveFromEventList(const List_t * const pxEventList ){        // 移除优先级最高的任务        // 如果该任务优先级高于当前任务,返回 pdTRUE        // 调用者应触发上下文切换}

9. 总结

9.1 核心设计要点

  1. 环形缓冲区:高效的数据存储,O(1) 入队出队
  2. 按值复制:避免指针问题,提高安全性
  3. 优先级等待列表:保证高优先级任务优先响应
  4. 队列锁机制:平衡 ISR 响应速度和操作原子性
  5. 结构复用:队列、信号量、互斥量共用同一结构

9.2 性能考量

  • 内存分配:一次分配减少碎片
  • 临界区最小化:只在必要时使用临界区
  • 批量操作:队列锁减少 ISR 开销
  • 优先级排序:确保实时性

9.3 使用建议

  1. 队列深度:根据生产者/消费者速率差设置
  2. 消息大小:尽量小,大数据传递指针
  3. 超时设置:避免无限阻塞导致死锁
  4. ISR 操作:始终使用 ‎FromISR 版本

9.4 源码导航

功能
函数
队列创建
xQueueGenericCreate
队列重置
xQueueGenericReset
发送操作
xQueueGenericSend
接收操作
xQueueReceive
数据入队
prvCopyDataToQueue
数据出队
prvCopyDataFromQueue
队列锁定
prvLockQueue
队列解锁
prvUnlockQueue

历史系列文章

告别硬件开发板,手把手教你零成本学习 FreeRTOS

FreeRTOS 链表:从数据结构到调度内核的基石

FreeRTOS 从创建第一个任务到任务启动 — 完整流程深度解析

搞懂消息队列,你的 FreeRTOS 才算入门

文章内容较长,一次阅读记不住,点赞、收藏、关注不迷路,感谢!