FreeRTOS 消息队列

@(嵌入式)

简述

FreeRTOS 任务间通信方式有

  • 消息通知 Notifications(V8.20版本开始支持)
  • 消息队列 Queues
  • 二进制信号量 Binary Semaphores
  • 计数信号量 Counting Semaphores
  • 互斥锁 Mutexes
  • 递归互斥锁 Recursive Mutexes

上面这几中方式中, 除了消息通知, 其他几种实现都是基于消息队列。消息队列作为主要的通信方式, 支持在任务间, 任务和中断间传递消息内容。 这一章介绍 FreeRtos 消息队列的基本使用, 重点分析其实现的方式。

分析的源码版本是 v9.0.0


Queue 使用

FreeRTOS 官方提供了比较详细的接口使用文档 ( 戳我 ), 因此这里不花费太多的篇幅重复。 只是简单地介绍下, 主要使用到的接口以及示例。

创建一个消息队列

使用消息队列前, 需要先创建队列, 并拿到返回的句柄用于操作队列。

// 定义队列句柄变量
QueueHandle_t xQueue;
// 申请队列
// 参数 1 : 队列深度
// 参数 2 : 队列项内容大小
xQueue = xQueueCreate( 10, sizeof( unsigned long ) );

把队列比喻为一个邮箱, 那么队列项目是每封邮件的的大小, 深度是邮箱最大可以存储多少封邮件。

发送消息 & 接受消息

队列的基本操作就是出队(接收消息)和入队(发送消息), 如上图所示, 有两个任务 A 和 B, A 发送消息给任务 B

void funOfTaskA()
{
    unsigned long pxMessage;
    // ...
    if( xQueue != 0 ) {
        // 发送消息
        // 参数 1 : 队列句柄
        // 参数 2 : 队列内容指针
        // 参数 3 : 允许阻塞时间
        xQueueSend( xQueue, ( void * ) &pxMessage, ( TickType_t ) 0 );
    }
}

void funOfTaskB()
{
    //... 
    // 接收消息
    // 参数 1 : 队列句柄
    // 参数 2 : 队列内容返回保存指针
    // 参数 3 : 允许阻塞时间
    if( xQueueReceive( xQueue, &( pxMessage ), ( TickType_t ) 10 ) )
        {
            // pcRxedMessage now points to the struct AMessage variable posted
            // by vATask.
        }
}

上面例子, 第一个函数发送消息到队列, 如果队列已经满了, 直接返回不阻塞。 第二过函数接收队列消息, 如果队列中没有消息, 会阻塞任务等待最长10个 Ticks。

FreeRTOS 的队列内容是内存拷贝, 我们将要发送的内容的地址传递给发送函数,该函数会将地址上的内容拷贝到自己的存储区域;而接收函数会将消息内容拷贝到我们传递给他的指针指向的内存区域。

如果消息内容太大, 队列需要提前占用的存储空间对应也会变大, 消息传递过程的内存拷贝也会导致效率下降。 对于这种情况, 可以通过传递指针而不是实际内容代替, 消息中是指向数据的指针, 接收任务接收消息后通过该指针读取到实际的内容。 如下例子所示

// 任务间传递的消息格式
 struct AMessage
 {
     char ucMessageID;
     char ucData[ 20 ];
 } xMessage;

 QueueHandle_t xQueue;

// 发送任务
 void vATask( void *pvParameters )
 {
    struct AMessage *pxMessage;
    // 队列的内容是 指向结构体的指针
    xQueue = xQueueCreate( 10, sizeof( struct AMessage * ) );
    if( xQueue == 0 )
    {
        // Failed to create the queue.
    }

        // ...
    pxMessage = & xMessage;
    // 发送消息 传递的消息内容指针的指针
    xQueueSend( xQueue, ( void * ) &pxMessage, ( TickType_t ) 0 );

    // ... Rest of task code.
 }
 
// 接收任务
void vADifferentTask( void *pvParameters )
{
    struct AMessage *pxRxedMessage;
    if( xQueue != 0 )
    {
        if( xQueueReceive( xQueue, &( pxRxedMessage ), ( TickType_t ) 10 ) )
            {
                // pcRxedMessage 指向 xMessage
            }
    }
    
    // ... Rest of task code.
}

简单的队列使用基本如上, 更加详细的操作, 请直接参考 官方文档

注意,在中断中使用 FreeRTOS 的接口, 需是结尾带有 FromISR的。


Queue 实现

按照上面举例的顺序, 从创建队列 -> 发送消息 -> 接收消息 依次展开分析 FreeRTOS 的队列源码实现。 这部分代码在源码目录下 queue.c 中。

数据结构

队列实现围绕其数据结构, 如下说明队列的数据结构, 其每个数据成员的作用。 姑且不管是否理解, 后续会一步一步介绍它的具体应用。

typedef struct QueueDefinition
{
    // 指向队列存储区域起始地址 -> 第一个队列项
    int8_t *pcHead;
    // 指向队列存储区域结束地址
    int8_t *pcTail;
    // 指向队列存储区域下一个空闲地址
    int8_t *pcWriteTo;
    
    // 不同情况下 一种有效
    union
    {
        // 作为队列时, 指向最后一个出队项
        int8_t *pcReadFrom;
        // 作为互斥变量, 记录 take 的次数, 递归计数
        UBaseType_t uxRecursiveCallCount;
    } u;

    // 管理因为等待入队而被阻塞的任务
    List_t xTasksWaitingToSend;
    // 管理因为等待消息而阻塞的任务
    List_t xTasksWaitingToReceive;
    
    // 当前队列消息数 
    volatile UBaseType_t uxMessagesWaiting;
    // 队列项最大数目
    UBaseType_t uxLength;   
    // 每个队列项大小          
    UBaseType_t uxItemSize;

    // 队列锁住的情况下, 不能修改事件链表 xTasksWaitingToSend 和 xTasksWaitingToReceive
    // 记录锁定期间 从列表收到的数目 (调用 接收函数次数) --> xTasksWaitingToSend 
    volatile int8_t cRxLock;        
    // 记录锁定期间 列表收到的数目 (调用了 发送函数次数) -->xTasksWaitingToReceive 
    volatile int8_t cTxLock;
    // 用于解锁列表后对应恢复任务个数

    #if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        // 队列静态还是动态申请的
        uint8_t ucStaticallyAllocated;
    #endif

    #if ( configUSE_QUEUE_SETS == 1 )
        // 队列管理
        struct QueueDefinition *pxQueueSetContainer;
    #endif

    // 调试
    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxQueueNumber;
        uint8_t ucQueueType;
    #endif

} xQUEUE;
typedef xQUEUE Queue_t;

队列创建

前面举例创建列表调用的 API xQueueCreate 实际是一个宏定义, 后面真正实现函数是

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, 
            const UBaseType_t uxItemSize, 
            const uint8_t ucQueueType )

相比该宏, 函数多了一个参数, 在开头提到过 FreeRTOS 的信号量,互斥锁也是基于队列实现的, 而这个函数的第三个参数的作用用于指定创建的对象类型, 这个类型变量主要用于调试的。

旧版本这个函数还会包括其他参数, 用于指定队列内存是堆还是静态,但是新版本 FreeRTOS 用不同函数实现不同内存申请方式, 对应的静态内存创建队列的接口是 :

QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength, 
                    const UBaseType_t uxItemSize, 
                    uint8_t *pucQueueStorage, 
                    StaticQueue_t *pxStaticQueue, 
                    const uint8_t ucQueueType )

多了两个参数用于传递静态内存的指针。

主要分析动态申请内存的函数, 静态函数的差别主要在于调用时传入队列的内存。创建队列的函数本身比较简单, 基本就是申请内存, 初始化内存, 返回指针。 看下源代码 :

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, 
    const UBaseType_t uxItemSize, const uint8_t ucQueueType )
{
    Queue_t *pxNewQueue;
    size_t xQueueSizeInBytes;
    uint8_t *pucQueueStorage;
    
    // 计算队列项存储区域需要的内存总大小
    // FreeRTOS 插入队列会把内容拷贝到自己的空间
    if( uxItemSize == ( UBaseType_t ) 0 )
    {
        xQueueSizeInBytes = ( size_t ) 0;
    }
    else
    {
        xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
    }
    
    // 申请内存 : 队列结构体 + 队列项存储区
    pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );

    if( pxNewQueue != NULL )
    {
        // 队列项存储区起始地址   
        pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );

        #if( configSUPPORT_STATIC_ALLOCATION == 1 )
        {
            // 标识队列内存是动态申请的, 删除队列时需要回收内存
            pxNewQueue->ucStaticallyAllocated = pdFALSE;
        }
        #endif /* configSUPPORT_STATIC_ALLOCATION */
        
        // 初始化队列结构成员
        prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );
    }
    // 返回句柄
    return pxNewQueue;
}

队列创建函数申请了队列及其存储队列项所需要的内存块, 而后对其进行初始化,

队列内存结构如下 :

queue.png

Queue 内存的分配情况如上图所示, 在该数据结构中, 其中有两个链表变量 xTasksWaitingToReceivexTasksWaitingToSend, 当某个任务调用队列 API 接收函数准备接收消息时, 队列刚好没有内容, 如果设置了阻塞时间, 则该任务会被插入到 xTasksWaitingToReceive 链表中, 等待新消息; 对应的, 发送消息的任务发送消息时碰上队列满了, 也会被插入到 xTasksWaitingToSend 链表中, 等待其他任务读取消息后空出空间。

参数初始化函数如下所示,

static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, 
    const UBaseType_t uxItemSize, 
    uint8_t *pucQueueStorage, 
    const uint8_t ucQueueType,
     Queue_t *pxNewQueue )
{
    // 避免编译器警告
    ( void ) ucQueueType;

    if( uxItemSize == ( UBaseType_t ) 0 )
    {
        // 没有存储区域的队列, 该指针指向队列自己
        // 不设置为 null, 因为互斥锁类型才设置为 null
        pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
    }
    else
    {
        // 指向队列项存储区域起始地址
        pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;
    }

    // 队列深度
    pxNewQueue->uxLength = uxQueueLength;
    // 每个队列项大小
    pxNewQueue->uxItemSize = uxItemSize;
    // 初始化其他参数
    ( void ) xQueueGenericReset( pxNewQueue, pdTRUE );

    // 调试调试
    #if ( configUSE_TRACE_FACILITY == 1 )
    {
        pxNewQueue->ucQueueType = ucQueueType;
    }
    #endif /* configUSE_TRACE_FACILITY */

    // 类型为集合的队列用到
    #if( configUSE_QUEUE_SETS == 1 )
    {
        pxNewQueue->pxQueueSetContainer = NULL;
    }
    #endif /* configUSE_QUEUE_SETS */

    traceQUEUE_CREATE( pxNewQueue );
}

发送消息

同创建队列的函数一样, 一般我们调用的接收 API 实际上也是一个宏, 对某些参数做了特定设置, 后面继续介绍到的发送 API 也是如此。 发送消息函数最终实现基本是以下两个函数 :

  • xQueueGenericSend 在普通任务函数中调用的接口
  • xQueueGenericSendFromISR 在中断中调用的接口。 因为 FreeRTOS 是一个实时操作系统, 为了保证中断发生时的实时响应, 做了优先级设置。 在中断中直接调用普通的系统接口函数可能导致阻塞其他中断, 为了避免这种情况, 提供了特定接口, 中断中调用系统的接口(FromISR后缀),会短期修改该中断的优先级,避免影响其他中断, 保证实时性。 同时, 在中断中调用的接口, 不会阻塞挂起。

任务中调用发送函数

以下主要分析普通任务下调用队列发送函数 xQueueGenericSend, 对源码的简化处理, 去除了集合部分的代码, 做了一些注释说明 :

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 = ( Queue_t * ) xQueue;

    // 各种 assert check

    // 为了提高效率 这个函数放松编码规则
    for( ;; )
    {
        taskENTER_CRITICAL();
        {
            // 判断队列未满 | 如果本次操作是覆盖插入, 则无所谓队列是否满
            if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
            {
                // 将新内容插入到队列存储
                // 对于互斥锁,相当一个放锁操作,释放时发现有优先级继承(说明有更高优先级任务等待锁), 所以返回需要任务切换
                xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

                #if ( configUSE_QUEUE_SETS == 1 )
                // 集合部分代码, 略  
                #else /* configUSE_QUEUE_SETS */
                {
                    // 查看之前是否有任务由于等待消息而挂起
                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
                    {
                        // 从事件链表恢复一个等待接收消息的任务
                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
                        {
                            // 解除阻塞的任务优先级比当前运行任务高
                            // 所以需要触发任务切换
                            // !此处允许在 cirtical 中切换 内核会处理
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                    }
                    else if( xYieldRequired != pdFALSE )
                    {
                        // 特殊情况 !
                        // 互斥锁释放时记录有优先级继承
                        // 有更高优先级任务等待拿锁, 需要进行任务切换
                        queueYIELD_IF_USING_PREEMPTION();
                    }
                }
                #endif /* configUSE_QUEUE_SETS */

                // 一般情况下成功插入队列并发返回
                taskEXIT_CRITICAL();
                return pdPASS;
            }
            else
            {
                // 队列当前是满的 新消息不能入队
                // 判断允许阻塞的时间
                if( xTicksToWait == ( TickType_t ) 0 )
                {
                    // 不阻塞等待, 入队失败马上返回
                    taskEXIT_CRITICAL();
                    return errQUEUE_FULL;
                }
                else if( xEntryTimeSet == pdFALSE )
                {
                    // 设置了阻塞等待,队列满 
                    // 初始化超时结构体
                    vTaskSetTimeOutState( &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;
        }
    }
}

方便理解, 参考如下流程图。

queueSend.png

函数首先判断了队列是否有空间存储新的数据, 如果队列已经满, 则会判断调用函数是否设置了阻塞等待时间,没有设置阻塞时间函数会直接返回队列满, 而如果设置超时, 并且时间没有到期,则会将当前任务插入到队列的等待链表, 并且触发任务切换, 释放 CPU 的使用, 等到队列有空间或者超时溢出再切换回来。

对于正常情况下, 数据可以插入队列, 调用拷贝函数将新数据保存到队列的队列项存储区域, 更新队列相关指针和参数, 对于拷贝函数, 在队列作为互斥锁时, 发送消息实际上就是释放锁, 而互斥锁为了避免任务优先级反转, 如果拿锁的任务优先级低于等待锁的任务, 拿锁任务优先级会段时间提高(优先级继承), 当释放锁的时候, 发现有优先级继承,说明有一个更高优先级的任务在等待当前任务放锁, 所以这时候需要进行任务切换。 处理优先级继承问题,在函数 prvCopyDataToQueue处理。

拷贝新数据后, 对应需要检查队列当前等待接收链表 xTasksWaitingToReceive是否有任务等待, 将最高优先级任务解除阻塞到就绪, 并判断新就绪任务优先级是否高于当前任务, 是的话, 触发任务切换。

中断中调用发送函数

相比在任务中调用的发送函数,在中断中调用的函数会更加简单一些, 没有任务阻塞操作。

FromISR 后缀的函数, 函数开头会先修改调用中断的优先级, 避免影响其他中断的响应, 保证系统的实时性。

函数 xQueueGenericSend中插入数据后, 会检查等待接收链表是否有任务等待,如果有会恢复就绪。如果恢复的任务优先级比当前任务高, 则会触发任务切换;但是在中断中调用的这个函数的做法是返回一个参数标志是否需要触发任务切换,并不在中断中切换任务。

在任务中调用的函数中有锁定和解锁队列的操作, 锁定队列的时候, 队列的事件链表不能被修改。 而下面这个函数中,被中断调用, 当遇到队列被锁定的时候, 将新数据插入到队列后, 并不会直接恢复因为等待接收的任务, 而是累加了计数, 当队列解锁的时候, 会根据这个计数, 对应恢复几个任务。

遇到队列满的情况, 函数会直接返回, 而不是阻塞等待, 因为在中断中阻塞是不允许的!!!

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 = ( Queue_t * ) xQueue;

    // 中断中调用系统函数, 调整中断优先级
    // 避免影响其他中断实时响应
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
        // 判断队列是否有空间插入新内容
        if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
        {
            const int8_t cTxLock = pxQueue->cTxLock;

            // 中断中不能使用互斥锁, 所以拷贝函数只是拷贝数据,
            // 没有任务优先级继承需要考虑
            ( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

            // 判断队列是否被锁定
            if( cTxLock == queueUNLOCKED )
            {
            #if ( configUSE_QUEUE_SETS == 1 )
                // 集合相关代码
            #else /* configUSE_QUEUE_SETS */
                {
                    // 将最高优先级的等待任务恢复到就绪链表
                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
                    {
                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
                        {
                            // 如果有高优先级的任务被恢复
                            // 此处不直接触发任务切换, 而是返回一个标记
                            if( pxHigherPriorityTaskWoken != NULL )
                            {
                                *pxHigherPriorityTaskWoken = pdTRUE;
                            }
                        }
                    }
                }
            #endif /* configUSE_QUEUE_SETS */
            }
            else
            {
                // 队列被锁定, 不能修改事件链表
                // 增加计数, 记录需要接触几个任务到就绪
                // 在解锁队列的时候会根据这个计数恢复任务
                pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );
            }
            xReturn = pdPASS;
        }
        else
        {
            // 队列满 直接返回 不阻塞
            xReturn = errQUEUE_FULL;
        }
    }

    // 恢复中断的优先级
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

    return xReturn;
}

接收消息

接收 API 和发送 API 差不多, 也是实现了几个宏, 但是实际实现的函数是xQueueGenericReceive``和 ``xQueueGenericReceiveFromISR这两个。 有了上面发送函数的介绍, 接收函数基本思路差不多, 所以, 以下, 主要简单地分析下任务中调用的发送函数。 源代码注释如下 :

BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, 
    void * const pvBuffer, 
    TickType_t xTicksToWait, 
    const BaseType_t xJustPeeking )
{
    BaseType_t xEntryTimeSet = pdFALSE;
    TimeOut_t xTimeOut;
    int8_t *pcOriginalReadPosition;
    Queue_t * const pxQueue = ( Queue_t * ) xQueue;

    for( ;; )
    {
        taskENTER_CRITICAL();
        {
            const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;

            // 判断消息队列中是否有消息待读
            if( uxMessagesWaiting > ( UBaseType_t ) 0 )
            {
                // 保留出队位置
                pcOriginalReadPosition = pxQueue->u.pcReadFrom;
                // 把数据拷贝到传递进来的指针内存区域
                prvCopyDataFromQueue( pxQueue, pvBuffer );

                // 判断是否只是查看一下数据 (不把数据出队)
                if( xJustPeeking == pdFALSE )
                {
                    // 接收了数据, 未读数据减1
                    pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1;

                    #if ( configUSE_MUTEXES == 1 )
                    {
                        // 对于类型是互斥锁
                        if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                        {
                            /* Record the information required to implement
                               priority inheritance should it become necessary. */
                            pxQueue->pxMutexHolder = ( int8_t * ) pvTaskIncrementMutexHeldCount();
                        }
                    }
                    #endif /* configUSE_MUTEXES */
                    // 判断是否有任务等待入队 如果有, 恢复最高优先级任务就绪
                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
                    {
                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
                        {
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                    }
                }
                else
                {

                    // 只是看一下数据(peek), 数据不出队, 恢复读指针
                    pxQueue->u.pcReadFrom = pcOriginalReadPosition;

                    // 判断是否有任务由于等待消息而被阻塞
                    // 恢复最高优先级任务
                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
                    {
                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
                        {
                            // 高优先级任务切换
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                    }
                }

                taskEXIT_CRITICAL();
                return pdPASS;
            }
            else
            {
                // 当前队列没有待读消息
                if( xTicksToWait == ( TickType_t ) 0 )
                {
                    // 没有设置阻塞, 没有消息,直接返回
                    taskEXIT_CRITICAL();
                    return errQUEUE_EMPTY;
                }
                else if( xEntryTimeSet == pdFALSE )
                {
                    // 设置延时结构
                    vTaskSetTimeOutState( &xTimeOut );
                    xEntryTimeSet = pdTRUE;
                }
            }
        }
        taskEXIT_CRITICAL();

        // 挂起任务调度
        vTaskSuspendAll();
        // 锁定队列
        prvLockQueue( pxQueue );

        // 判断是否等待超时
        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
        {
            // 判断是否有可读消息
            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
            {

                #if ( configUSE_MUTEXES == 1 )
                {
                    // 互斥锁处理
                    if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
                    {
                        taskENTER_CRITICAL();
                        {
                            // 优先级继承
                            // 当前任务等锁被阻塞,优先级低的任务拿锁了
                            // 提高该低优先级任务优先级
                            vTaskPriorityInherit( ( void * ) pxQueue->pxMutexHolder );
                        }
                        taskEXIT_CRITICAL();
                    }
                }
                #endif
                // 将任务插入队列的等待链表
                vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
                // 解锁队列 并切换任务
                prvUnlockQueue( pxQueue );
                if( xTaskResumeAll() == pdFALSE )
                {
                    portYIELD_WITHIN_API();
                }
            }
            else
            {
                // 有消息读, 重新尝试
                prvUnlockQueue( pxQueue );
                ( void ) xTaskResumeAll();
            }
        }
        else
        {
            prvUnlockQueue( pxQueue );
            ( void ) xTaskResumeAll();

            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
            {
                // 超时等待, 没有接收到消息
                // 返回错误
                return errQUEUE_EMPTY;
            }
        }
    }
}

任务调用接收函数收取队列消息, 函数首先判断当前队列是否有未读消息, 如果没有, 则会判断参数 xTicksToWait, 决定直接返回函数还是阻塞等待。

如果队列中有消息未读, 首先会把待读的消息复制到传进来的指针所指内, 然后判断函数参数 xJustPeeking == pdFALSE的时候, 符合的话, 说明这个函数读取了数据, 需要把被读取的数据做出队处理, 如果不是, 则只是查看(peek),只是返回数据,但是不会把数据清除。

对于正常读取数据的操作, 清除数据后队列会空出空位, 所以查看等待链表中是否有任务等发送数据而被挂起, 有的话恢复一个任务就绪, 并根据优先级判断是否需要出触发 PendSV 执行任务切换。 对于互斥锁, 接收消息函数对应的是请求锁, 所以会增加拿锁次数的记录。

对于只是查看数据的, 由于没有清除数据, 所以没有空间新空出,不需要检查发送等待链表, 但是会检查接收等待链表, 如果有任务挂起会切换其到就绪并判断是否需要切换。

到此, 对 FreeRTOS 队列的介绍完毕。

后续会专门一章分析下其信号量和互斥锁 基于队列的实现。


参考

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我的博客

CI基础知识笔记

1.知识点 $config[‘url_suffix’] = ”;//url后缀 $config[‘enable_query_strings’] = FALS...

2853
来自专栏智能合约

Phalcon入门教程之Volt模板引擎

1503
来自专栏java一日一条

ava多线程:volatile变量、happens-before关系及内存一致性

请参考来自 Jean-philippe Bempel 的评论。他提到了一个真实因 JVM 优化导致死锁的例子。我尽可能多地写博客的原因之一是一旦自己理解错了,可...

782
来自专栏好好学java的技术栈

各大公司Java后端开发面试题总结

Java虚拟机规范中将Java运行时数据分为六种。 1.程序计数器:是一个数据结构,用于保存当前正常执行的程序的内存地址。Java虚拟机的多线程就是通过线程轮流...

6145
来自专栏IMWeb前端团队

nodejs中错误捕获的一些最佳实践

本文作者:IMWeb yisbug 原文出处:IMWeb社区 未经同意,禁止转载 本文内容大部分来自 https://www.joyent.com/...

2226
来自专栏一名合格java开发的自我修养

由提交storm项目jar包引发对jar的原理的探索

序:在开发storm项目时,提交项目jar包当把依赖的第三方jar包都打进去提交storm集群启动时报了发现多个同名的文件错误由此开始了一段对jar包的深刻理解...

1191
来自专栏york技术分享

sed 使用教程 - 通读篇(30分钟入门系列)

和上篇 awk 分享一样,作为通读性的分享,不想引入太过复杂的东西,依然从日常工作中碰到的 80% 的需求出发,重点阐述最重点的部门,工作原理等,普及一些对se...

50122
来自专栏腾讯IVWEB团队的专栏

nodejs 中错误捕获的一些最佳实践

本文为翻译文章,原文比较长,感觉也有点啰嗦,所以根据个人理解猜测梳理出本文。

6460
来自专栏编程

autoload自动加载机制使用

在PHP开发过程中,如果希望从外部引入一个class,通常会使用include和require方法,去把定义这个class的文件包含进来,但是这样可能会使得在...

22210
来自专栏逸鹏说道

Python3 与 C# 并发编程之~ 进程实战篇

之前说过 Queue:在 Process之间使用没问题,用到 Pool,就使用 Manager().xxx, Value和 Array,就不太一样了:

1754

扫码关注云+社区

领取腾讯云代金券