FreeRTOS 消息队列

2023-05-16

@(嵌入式)

    • 简述
    • Queue 使用
      • 创建一个消息队列
      • 发送消息 接受消息
    • Queue 实现
      • 数据结构
      • 队列创建
      • 发送消息
        • 任务中调用发送函数
        • 中断中调用发送函数
      • 接收消息
    • 参考

Freertos
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 ) );

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

发送消息 & 接受消息

queue

队列的基本操作就是出队(接收消息)和入队(发送消息), 如上图所示, 有两个任务 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

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;
        }
    }
}

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

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

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

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

queueSend

中断中调用发送函数

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

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 队列的介绍完毕。

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


参考

  • FreeRTOS Queue
  • FreeRTOS Queue API
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FreeRTOS 消息队列 的相关文章

  • 【FreeRTOS(三)】任务状态

    文章目录 任务状态 任务挂起 vTaskSuspend 取消任务挂起 vTaskResume 挂起任务调度器 vTaskSuspendAll 取消挂起任务调度器 xTaskResumeAll 代码示例 任务挂起 取消任务挂起 代码示例 挂起
  • 使用RabbitMQ实现延时队列

    之前公司是一个类电商公司 会有用户下单后未支付取消订单的场景 解决方案是使用RabbitMQ的死信队列来实现一个延时队列 下单时 将订单丢进消息队列 设置过期时间 订单失效时间 然后到时候检查订单状态 如果未支付则取消订单 1 什么是死信
  • 【RabbitMQ】Consumer之消费模式、消息确认与拒绝 - 基于AMQP 0-9-1

    这篇文章主要和大家分享RabbitMQ Consumer端的知识点 主要包括Consumer的消费模式 消息是如何确认以及如何拒绝的 当消息拒绝之后 如何让消息重新进入队列 推模式 RabbitMQ支持推和拉两种消费模式 推模式就是由Bro
  • 7 SpringBoot整合RocketMQ发送单向消息

    发送单向消息是指producer向 broker 发送消息 执行 API 时直接返回 不等待broker 服务器的结果 这种方式主要用在不特别关心发送结果的场景 举例 日志发送 RocketMQTemplate给我们提供了sendOneWa
  • RocketMQ第五篇 RocketMQ API基本使用

    目录 生产者Product 消费者Consumer 前面已经学习了Rocket的基本知识 以及搭建MQ单机版和集群环境 下面开始进行实际开发 根据前面下载的RocketMQ源码 开展讲解RocketMQ 的基本使用 生产者Product 在
  • stm32f103zet6移植标准库的sdio驱动

    sdio移植 st官网给的标准库有给一个用于st出的评估板的sdio外设实现 但一是文件结构有点复杂 二是相比于国内正点原子和野火的板子也有点不同 因此还是需要移植下才能使用 当然也可以直接使用正点原子或野火提供的实例 但为了熟悉下sdio
  • FreeRTOS之软件定时器

    FreeRTOS之软件定时器 声明 本人按照正点原子的FreeRTOS例程进行学习的 欢迎各位大佬指责和批评 谢谢 include sys h include delay h include usart h include led h in
  • FreeRTOS之事件

    FreeRTOS之事件 声明 本人按照正点原子的FreeRTOS例程进行学习的 欢迎各位大佬指责和批评 谢谢 一 事件定义 事件 事件集 与高数上的集合意义差不多 事件啊 其实是实现任务间通信的机制 主要用于实现多任务间的同步 但是事件类型
  • 基于STM32的FreeRTOS学习之中断测试实验(五)

    记录一下 方便以后翻阅 本章内容是接着上一章节进行的实际演练 1 实验目的 FreeRTOS可以屏蔽优先级低于configMAX SYSCALL INTERRUPT PRIORITY的中断 不会屏蔽高于其的中断 本次实验就是验证这个说法 本
  • FreeRTOS死机原因

    1 中断回调函数中没有使用中断级API xxFromISR 函数 xSemaphoreGiveFromISR uart busy HighterTask 正确 xSemaphoreGive uart busy 错误 2 比configMAX
  • [FreeRTOS入门学习笔记]定时器

    定时器的使用步骤 1 定义一个handle xTimerCreate创建 2 启动定时器 在Task1中调用 通过队列通知守护任务来执行定时器任务 要再config头文件中定义守护任务相关配置 虽然定时器是在task1中启动 但是定时器的任
  • FreeRTOS学习---“定时器”篇

    总目录 FreeRTOS学习 任务 篇 FreeRTOS学习 消息队列 篇 FreeRTOS学习 信号量 篇 FreeRTOS学习 事件组 篇 FreeRTOS学习 定时器 篇 FreeRTOS提供了一种软件定时器 用来快速实现一些周期性的
  • RabbitMQ(四):RabbitMQ高级特性

    消息队列在使用过程中 面临着很多实际问题需要思考 消息可靠性问题 如何确保发送的消息至少被消费 次 延迟消息问题 如何实现消息的延迟投递 消息堆积问题 如何解决数百万消息堆积 无法及时消费的问题 高可用问题 如何避免单点的MQ故障而导致的不
  • FreeRTOS之系统配置

    1 FreeRTOS的系统配置文件为FreeRTOSConfig h 在此配置文件中可以完成FreeRTOS的裁剪和配置 在官方的demo中 每个工程都有一个该文件 2 先说一下 INCLUDE 开始的宏 使用 INCLUDE 开头的宏用来
  • 如何更改 FreeRTOS 中任务的最大可用堆大小?

    我通过以下方式在任务中创建元素列表 l dllist pvPortMalloc sizeof dllist dlllist 有 32 字节大 我的嵌入式系统有 60kB SRAM 所以我希望系统可以轻松处理我的 200 个元素列表 我发现在
  • 高可用:如何实现消息队列的 HA?

    管理学上有一个木桶理论 一只水桶能装多少水取决于它最短的那块木板 这个理论推广到分布式系统的可用性上 就是系统整体的可用性取决于系统中最容易出现故障 或者性能最低的组件 系统中的各个组件都要进行高可用设计 防止单点故障 消息队列也不例外 本
  • C++ freeRTOS任务,非静态成员函数的无效使用

    哪里有问题 void MyClass task void pvParameter while 1 this gt update void MyClass startTask xTaskCreate this gt task Task 204
  • GNU Arm Cortex m4 上的 C++ 异常处理程序与 freertos

    2016 年 12 月更新现在还有一个关于此行为的最小示例 https community nxp com message 862676 https community nxp com message 862676 我正在使用带有 free
  • 小型 ARM 微控制器的 RTOS 内核之间的可量化差异 [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 有许多不同的 RTOS 可用于微控制器 我专门寻找支持 ARM Cortex M 处理器的 RTOS 另外 我对闭源解决方案不感兴趣 试图从网站
  • 有关 CMake 错误的问题:没有为目标提供源

    我正在尝试使用 cmake 和 eclipse 将 FreeRtos 添加到我的项目中 但出现错误 我运行的是 debian 10 我的 cmake 版本是 3 13 4 cmake 的文件可以在以下位置找到这个 git 仓库 https

随机推荐

  • STVP烧录程序

    STVP介绍 STVD xff1a ST Visual Develop xff0c 可视化开发工具STVP xff1a ST Visual Programmer xff0c 可视化编程工具 xff0c stvp主要打开一些固件 hex s1
  • 传感器标定系列——Kalibr工具

    文章目录 Kalibr介绍安装资源 Kalibr介绍 Kalibr is a toolbox that solves the following calibration problems 多相机标定 Multi Camera Calibra
  • TCP传输图片及多线程

    最终效果图 xff1a 注意 xff1a 图片发送大体流程同 Qt网络编程 TCP IP 一 只是在发送时这里采用 的 数据流QDataStream形式 因为使用摄像进行监控时也是对一帧一帧的图片进 行处理 xff0c 因此掌握了如何用Qt
  • GCC编译过程及基本命令总结

    一 GCC简介 GCC即GNU Compiler Collection xff0c 原本只是针对C语言的编译工具 xff0c 现在已经变成了一个工具集 xff0c 包含了C C 43 43 JAVA等语言的集合体 管理和维护 xff1a 由
  • 第一章 PX4-Pixhawk-程序编译过程解析

    第一章 PX4程序编译过程解析 PX4 是一款软硬件开源的项目 xff0c 目的在于学习和研究 其中也有比较好的编程习惯 xff0c 大家不妨可以学习一下国外牛人的编程习惯 这个项目是苏黎世联邦理工大学的一个实验室搞出来的 该方案是基于 N
  • 第二章 PX4-Pixhawk-RCS启动文件解析

    第二章 PX4 RCS 启动文件解析 RCS 的启动类似于 linux 的 shell 文件 xff0c 如果不知道 shell 文件是什么东西可以理解成是为程序的流程框 xff0c 它是告诉处理器应该怎么样去运行 xff0c 前一章介绍的
  • 第三章 PX4-Pixhawk-SPI底层驱动解析

    第三章 PX4 SPI 底层驱动解析 这一章节我们会对 PX4 的底层驱动进行解析 xff0c 我们这里主要解析的是 SPI 协议 xff0c 因为这个协议是所有传感器的一个协议 xff0c 至于 IIC 和串口就可以类似的读写一下 xff
  • 第四章 PX4-Pixhawk-MPU6000传感器驱动解析

    第四章MPU6000传感器驱动解析 Mpu6000 是一个 3 轴加速度和 3 轴陀螺仪传感器 xff0c 这一章节我们将对 MPU6000 这个传感器进行解析 xff0c 依照这个解析步骤同样可以对 L3GD20 xff08 3 轴陀螺仪
  • 第五章 PX4-Pixhawk-GPS解析

    第五章 PX4 GPS解析 在上一章节我们对传感器MPU6000做了一个解析 xff0c MPU6000所支持的协议是SPI 这一章节我们来解析GPS xff0c GPS使用的是串口通信 这里我们着重讲解UBLOX的解析过程 xff0c 并
  • 第六章 PX4-Pixhawk-Sensors解析

    xfeff xfeff 第六章 PX4 Sensors 解析 这一章节并不难 xff0c 也很容易理解 xff0c 但是这一章节有几个函数需要我们去理解一下 xff0c 所以这里我们这里写一章来说明一下 Sensors 是所有传感器进行数据
  • 第七章 PX4-Pixhawk-Mavlink解析

    xfeff xfeff 第七章 PX4 Mavlink 解析 首先我们是还是来说一说 mavlink 吧 Mavlink 协议是无人机的一种开源通信协议 可以理解就是按照一定的格式来发送数据 这一章节涉及到了消息的打包发送和接收解析 首先我
  • STM8S 低功耗模式

    功耗控制的概念调整 功耗控制之硬件调整 在硬件体系中可以有以下优化手段 xff1a 优化电路设计优化电源供电效率 xff08 减少用于发热的能源 xff09 调节单片机时钟频率 xff08 频率越高功耗越高 xff09 选择单片机或者外围的
  • 第八章 PX4-Pixhawk-SDlog解析

    xfeff xfeff 第八章 PX4 SDlog 解析 这一章节我们对 SD 存储卡来进行解析 SD 卡涉及到 log 日志 xff0c 在很多飞行中 log 文件非常重要的 xff0c 尤其是新手在炸机过程中有了这些数据我们基本上就能分
  • 第九章 PX4-pixhawk-姿态估计解析

    第九章 PX4 pixhawk 姿态估计解析 这一章节我们对姿态估计进行解析 xff0c 这一章节涉及到算法 xff0c 主要涉及到的还是DCM 方向余弦 算法 首先我们还从启动文件开始进行讲解 我们找到rc mc apps中 xff0c
  • 第十章 PX4-Pixhawk-姿态控制

    第十章 PX4 Pixhawk 姿态控制 这一章节我们对姿态控制进行解析 xff0c 姿态解算我们还是从启动文件开始 xff0c 找到姿态解算的启动文件rc mc app 这里面找到姿态解算的启动项为mc att control start
  • 第十一章 PX4-Pixhawk-LPE位置估计

    xfeff xfeff 位置估计的解析我们还是遵循源代码的规则 xff0c 至于组合惯导和扩展卡尔曼我们以后慢慢分析 xff0c 这里主要还是对源代码来进行解析 在前一章节我们找到SYS MC EST GROUP参数设置的是1 xff0c
  • Prometheus + Grafana 接入实践

    文章目录 Prometheus 系统监控数据模型jobs安装 Grafana 可视化应用监控接入exporter 接入应用中集成 prometheus client 告警 Alertmanager参考 Prometheus 系统监控 Pro
  • docker 使用实践

    文章目录 准备环境安装运行配置环境使用镜像加速器修改 docker 目录翻墙设置代理限制容器 log 大小 操作命令基本命令容器网络网络模式容器连接外部外部连接容器容器互联 数据管理数据卷数据卷容器 挂载本机目录 镜像构建使用 docker
  • FreeRTOS 软定时器实现

    64 嵌入式 简述使用定时器 配置定时器服务任务创建 启动 停止定时器修改定时器获取定时器状态 定时器实现 数据结构 定时器控制块定时器管理链表命令队列 定时器服务任务 回调定时器处理节拍计数器溢出命令处理 参考 FreeRtos 简述 考
  • FreeRTOS 消息队列

    64 嵌入式 简述Queue 使用 创建一个消息队列发送消息 接受消息 Queue 实现 数据结构队列创建发送消息 任务中调用发送函数中断中调用发送函数 接收消息 参考 FreeRtos 简述 FreeRTOS 任务间通信方式有 消息通知