FreeRTOS学习-队列管理

1. 简介

在FreeRTOS中,提供了多种任务间通讯的机制,包括消息队列、信号量和互斥锁、事件组、任务通知,他们的总体特征如下图所示:

FreeRTOS学习-队列管理_第1张图片

从图中可以看出,消息队列、信号量和互斥锁、事件组都是间接的任务间通信机制,而任务通知则是直接的通信方式,因此,任务通信的性能比其他几种间接的通信方式要高。

严格来说,互斥锁并不是一种任务间通信机制,将他们放在一块是因为互斥锁是基于信号量实现的,便于理解。在FreeRTOS中,信号量又是基于消息队列实现的,因此,先介绍消息队列,然后再介绍信号量和互斥锁。最后介绍事件组和任务通知。另外,由于FreeRTOSv10的实现更加易于理解并且具有更好的可维护性,因此这里将基于FreeRTOSv10的实现进行介绍。

消息队列(Queue)提供了任务与任务之间、任务与中断处理函数之间的交互机制。它是FreeRTOS中的基础功能,不能被裁剪。

2. 队列的特性

在FreeRTOS中,队列提供了任务间、任务和中断处理函数间的通信机制。

2.1.1. 数据存储方式

在FreeRTOS中,队列的长度指的是队列可以保存队列项(或称消息)的最大数量。

而每个队列项大小是在队列创建时设置的,且队列中所有队列项的大小一致

队列的实质就是一个FIFO,队列项从尾部写入,并从头部移除。另外,FreeRTOS的队列还支持从头部插入,也可以覆盖头部的数据。

需要注意的是,FreeRTOS在存储队列项时,采用的是副本方式而非引用,即原始数据入队需要一次拷贝动作。这么做的好处是:

  • 栈上的数据可以直接被入队;
  • 消息生产者可以继续使用已经发送的原始数据;
  • 这种方式依然可以当做引用方式来使用,即传入原始数据的指针即可;
  • 由FreeRTOS来管理消息队列的内存,对用户透明;
  • 由于某些支持内存保护的系统,使用引用的方式可能会导致两个任务之间无法正常的传递数据。

2.1.2. 被多个任务访问

一个队列可以被任意个任务和中断处理函数访问。

通常而言,一个队列有多个生产者,但只有一个消费者。

2.1.3. 阻塞读(Blocking on queue reads)

FreeRTOS的队列支持任务以阻塞的方式等待队列中的消息。并且支持多个任务同时等待同一个队列的消息,其阻塞和唤醒的方式如下:

  • 阻塞:当队列中的数据为空时,所有请求消息出队的任务会被阻塞,即变为Blocked状态。它们会从Ready任务队列迁移到Event任务队列。如果调用者还设置了等待时限,那任务还会被放入Delayed任务队列。
  • 唤醒:当数据就绪时,如果有多个任务在等待,那么只有最高优先级的任务会被唤醒,重新加入到Ready任务队列中。当优先级相同时,则等待时间最长的任务会被唤醒。如果设置了等待时限,等待超时也会将任务唤醒。

2.1.4. 阻塞写(Blocking on queue write)

与阻塞读类似,若队列已满,任何向该队列发送消息的任务都会被阻塞。

2.1.5. 多队列阻塞(Blocking on multiple queues)

有的时候,一个任务可能需要同时监控多个消息队列,那么可以将队列组织成队列组(Queue Set)。

与自行用多个队列来实现不同,它并不需要用户编写多个队列的轮询机制,因为这个队列组是由FreeRTOS来管理的。

3. 创建队列

动态创建队列的函数原型:需要设置configSUPPORT_DYNAMIC_ALLOCATION = 1

#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )

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

该API用于创建一个新的队列实例,并返回该队列的句柄。在FreeRTOS的实现中,每当创建队列时,需要为该队列创建两块内存:一块用于存储队列的元信息;另一块用于存储队列接受的实际消息。

其中,uxQueueLength表示该队列的长度;uxItemSize表示队列项的大小。

另外,FreeRTOS还支持静态创建队列的方式,这里就不多做介绍了。

可以看到,队列的创建实际上调用的是一个通用的创建函数xQueueGenericCreate()。该函数通过参数ucQueueType来判断需要创建的队列是何种类型,这些类型包括:

  • 普通队列;
  • 队列组;
  • 互斥锁;
  • 计数信号量;
  • 二值信号量;
  • 递归互斥锁。

这些会在后续陆续介绍。

下面给出该函数的实现活动图:

FreeRTOS学习-队列管理_第2张图片

  1. 申请队列所需内存:

    {
        参数检查:Assert `uxQueueLength > 0`。
    
        如果消息长度为0(`uxItemSize == 0`),则设置消息存储区域Bytes为0(xQueueSizeInBytes = 0)。
        否则,设置消息存储区域Bytes为队列长度*消息长度(`xQueueSizeInBytes = uxQueueLength * uxItemSize`)。
    
        申请队列所需的内存,长度为队列元信息结构体长度和消息存储区域长度的总和(`pvNewQueue = pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes )`)。
    
        如果申请成功(`pxNewQueue != NULL`),则跳转到第二步;
        否则直接返回`pxNewQueue`。
    }
    
  2. 初始化队列信息(static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, uint8_t *pucQueueStorage, const uint8_t ucQueueType, Queue_t *pxNewQueue )):如以下活动图所示

    FreeRTOS学习-队列管理_第3张图片
    复位队列请参看复位队列。

  3. 返回队列的句柄。

4. 队列控制

为了更好的说明队列控制API,我们假设抓取了某个队列在某个时刻的快照,它的存储方式如下图所示:

FreeRTOS学习-队列管理_第4张图片
所有队列控制都是基于这个视图进行的。

4.1. 入队操作

FreeRTOS提供了多种消息入队操作,它们的函数原型如下:

#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTickToWait ), queueSEND_TO_BACK )
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueu ), ( xTicksToWait ), queueSEND_TO_BACK )
#define xQueueOverwrite( xQueue, pvItemToQueue ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )

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

可以看到,FreeRTOS的队列可以支持头部、尾部入队,还能支持头部消息的重写。

他们的实现都是通过xQueueGenericSend()完成的,由参数来决定具体的行为。那我们便先来看看这个接口。它的主要职责是将一个消息放入队列中,如前文所述,这是通过内存拷贝来完成的,而不是引用。

pvItemToQueue便是需要入队的消息的指针,它的值会被拷贝到队列的消息存储区域。

前面提到,队列是可以实现阻塞写入的,而xTicksToWait便是阻塞的时间。特别地,如果xTicksToWait设置为0,且队列已满,则直接返回。

xCopyPosition表明了消息被插入的位置。可以用来实现消息优先级

如果在预设时间xTicksToWait内完成了入队,则返回pdTRUE,否则返回errQUEUE_FULL

在介绍具体实现之前,需要明确的是,FreeRTOS的互斥锁,信号量都是通过队列来实现的,而互斥锁的解锁以及信号量的Give都是通过这个消息入队函数来完成的。而这里只关注消息队列实现的细节,与信号量和互斥锁相关的细节会在其他相应的小节中介绍。因为入队操作涉及到任务状态切换等行为,需要先理解几个关键的概念和行为:

关键概念:

  • 队列的计数锁(读计数锁cRxLock和写计数锁cTxLock):这是为了保证消息队列中的事件任务队列不会因为并行访问而导致其一致性被破坏。并且这些锁是可以计数的,它表示在加锁状态下,入队和出队的消息数量。例如读计数锁的数值表示:在读加锁状态下,出队消息的数量;而写计数锁的数值则表示:在写加锁状态下,入队消息的数量。在加锁的情况下,消息可以入队或出队,但是事件任务队列不可以被更新。加锁和解锁的实现在关键行为中介绍。
  • Timeout信息:记录了系统当前Tick溢出的次数,以及当前的系统Tick。这个信息用于判断等待队列消息的任务是否发生了超时。它通过vTaskSetTimeOutState()vTaskInternalSetTimeOutState()获取当前系统Tick信息作为准备进入Delayed状态的时刻,在xTaskCheckForTimeOut()中用于检查是否超时。

关键行为:

  • 将任务加入到事件任务队列(void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait )):这实际上是任务管理的接口,但只能被FreeRTOS API的实现调用,这在任务管理章节中介绍过。该函数只能在中断屏蔽、或调度器被挂起且队列加锁的情况下调用。因为xEventListItemxItemValue默认值是portMAX_PRIORITIES减任务的优先级,因此队列中的任务按照任务优先级排序。

    {
        参数检查:Assert `pxEventList != NULL`。
    
        将任务加入到事件任务队列(`vListInsert( pxEventList, &( pxCurrentTCB->xEventListItem )`)。
    
        并且将任务加入Delayed任务队列(`prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE )`)。
    }
    
  • 将任务从事件任务队列中唤醒(BaseType_t xTaskRemoveFromEventList( const List * const pxEventList ) PRIVILEGED_FUNCTION):这也是任务管理的接口,但它只能被FreeRTOS API的实现调用。该接口假设事件任务队列中的任务是按照优先级来排序的。如果有更高优先级的任务被它唤醒,则返回pdTRUE

    {
        从队首获取一个任务(`pxUnblockedTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxEventList )`)。
        将该任务从事件任务队列中删除(`uxListRemove( &( pxUnblockedTCB->xEventListItem ) )`)。
    
        如果调度器未挂起(`uxSchedulerSuspended == pdFALSE`):
        {
            将任务从当前的状态(Blocked)任务队列中删除(`uxListRemove( &( pxUnblockedTCB->xStateListItem ) )`)。
            将其添加到Ready任务队列中(`prvAddTaskToReadyList( pxUnblockedTCB )`)。
        }
        否则,将任务加入到Pended任务队列中。
    
        如果唤醒的任务优先级比当前运行的优先级高(`pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority`),设置调度标志`xYieldPending = pdTRUE`并设置返回值为`pdTRUE`(`xReturn = pdTRUE`);否则设置返回值为`pdFALSE`。
    
        (仅开启USE_TICKLESS_IDLE)正常来说,系统下一次唤醒任务时刻`xNextTaskUnblockTime`会在更新系统tick时自动更新;但为了Tickless idle能够更快的进入低功耗模式,所以在这里就更新了系统唤醒任务的时刻(`prvResetNextTaskUnblockTime()`)。
    
        返回`xReturn`。
    }
    
  • 检查超时状态(BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut, TickType_t * const pxTicksToWait )):检查任务是否超时或被中止等待,如果是则返回pdTRUE,否则返回pdFALSE

    {
        参数检查:Assert `pxTimeOut != NULL`,`pxTicksToWait > 0`
    
        进入临界区(`taskENTER_CRITICAL()`)。
    
        获取系统Tick(`xConstTickCount = xTickCount`)。
        获取自上一次时间以来的时间流逝(`xElapsedTime = xConstTickCount - pxTimeOut->xTimeOnEntering`)。
    
        (仅开启xTaskAbortDelay)如果用户中止了延迟(`pxCurrentTCB->ucDelayAborted != pdFALSE`):
        {
            清除延迟中止标志(`pxCurrentTCB->ucDelayAborted = pdFALSE`)。
            返回值为超时(`xReturn = pdTRUE`)。
        }
    
        (仅开启vTaskSuspend)如果等待时限为无限时长(`*pxTicksToWait == portMAX_DELAY`),则应该返回`pdFALSE`(`xReturn = pdFALSE`)。
    
        如果发生了Tick溢出且当前系统Tick大于Timeout的tick(`( xNumOfOverflows != pxTimeOut->xOverflowCount ) && ( xConstTickCount >= pxTimeOut->xTImeOnEntering )`),此时已经溢出`xTickToWait`所能表示的数量了,因此肯定已经超时,设置`xReturn = pdTRUE`。
        否则,如果`xElapsedTime < *pxTicksToWait`:(如果`xConstTickCount < pxTimeOut->xTimeOnEntering`,那么`xElapsedTime`必定会大于`*pxTicksToWait`,所以不会选择这条分支)
        {
            对于这种情况,一定是没有发生Tick溢出(否则该条件不会成立)。
            此时没发生溢出,调整等待时间`*pxTicksToWait -= xElapsedTime`。
            重新获取Timeout信息(`vTaskInternalSetTimeOutState( pxTimeOut )`*)
    
            返回未超时(`xReturn = pdFALSE`)。
        }
        否则,都是属于超时的情况:
        {
            设置等待时间为0(`*pxTicksToWait = 0`)。
            返回超时(`xReturn = pdTRUE`)。
        }
    
        退出临界区(`taskEXIT_CRITICAL()`)。
    
        返回`xReturn`。
    }
    
  • 将消息拷贝到队列中(static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue, const void *pvItemToQueue, const BaseType_t xPosition )):该函数只能在临界区中被调用。对于普通队列,这是一个将消息拷贝到队列的操作;对于互斥锁而言,这是一个释放锁的操作。返回值表示是否需要发起一次调度。活动图如下:

    FreeRTOS学习-队列管理_第5张图片

  • 队列消息空、满状态判断(static BaseType_t prvIsQueueEmpty( const Queue_t *pxQueue )static BaseType_t prvIsQueueFull( const Queue_t * pxQueue )):

    队列空:
    {
        进入临界区(`taskENTER_CRITICAL()`)。
        如果消息未空,即`pxQueue->uxMessagesWaiting == 0`,设置`xReturn = pdTRUE`。
        否则设置`xReturn = pdFALSE`。
    
        退出临界区(`taskEXIT_CRITICAL()`)。
    
        返回`xReturn`。
    }
    
    队列满:
    {
        进入临界区(`taskENTER_CRITICAL()`)。
        如果消息满,即`pxQueue->uxMessageWaiting == pxQueue->uxLength`,设置`xReturn = pdTRUE`。
        否则设置`xReturn = pdFALSE`。
    
        退出临界区(`taskEXIT_CRITICAL()`)。
    }
    
  • 队列加锁(#define prvLockQueue( pxQueue ))和解锁(static void prvUnlockQueue( Queue_t * const pxQueue )):prvLockQueue()宏会进入临界区完成队列的读/写加锁;重点看看解锁操作:解锁时,调度器必须被挂起。

    {1. 处理队列的写计数锁)
        进入临界区(`taskENTER_CRITICAL()`)。
        获取队列的写计数锁(`cTxLock = pxQueue->cTxLock`)。
        循环处理队列写计数锁加锁时入队的消息(`while ( cTxLock > queueLOCKED_UNMODIFIED )`):
        {
            (仅开启USE_QUEUE_SETS):
            {
                如果队列在队列组中(`pxQueue->pxQueueSetContainer != NULL`):
                {
                    将消息通知到队列组,如果发现高优先级任务被唤醒(`prvNotifyQueueSetContainer( pxQueue, queueSEND_TO_BACK ) != pdFALSE`),则设置调度标志(`vTaskMissedYield()`),使得在下一次调度时进行任务切换。
                }
                否则,即队列不在任何队列组中:
                {
                    如果该队列的阻塞读队列不为空(`listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE`):
                    {
                        将任务从事件任务队列中删除,如果有高优先任务被唤醒(`xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE`),则设置调度标志(`vTaskMissedYield()`),使得在下一次调度时进行任务切换。
                    }
                    否则,即等待队列为空,则跳出循环。
                }
            }
    
            队列写计数锁减一(`--cTxLock`)。
        }
    
        释放队列写计数锁(`pxQueue->cTxLock = queueUNLOCKED`)。
    
        退出临界区(`taskEXIT_CRITICAL()`)。
    
        (2. 处理队列的读计数锁)
        进入临界区(`taskENTER_CRITICAL()`)。
        获取队列的读计数锁(`cRxLock = pxQueue->cRxLock`)。
    
        循环处理队列读计数锁加锁时出队的消息(`while ( cRxLock > queueLOCKED_UNMODIFIED )`):
        {
            如果队列的阻塞写队列不为空(`listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE`):
            {
                将任务从事件任务队列中删除,如果有更高优先级的任务被唤醒(`xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE`),则设置调度标志(`vTaskMiessdYield()`)。
    
                队列读计数锁减一(`--cRxLock`)。
            }
            否则,即阻塞写队列为空,则跳出循环。
        }
    
        释放队列读计数锁(`pxQueue->cRxLock = queueUNLOCKED`)。
    
        退出临界区(`taskEXIT_CRITICAL()`)。
    }
    

在了解了以上的关键概念和关键行为之后,就可以来看看他的具体实现了。其实现活动图如下:

FreeRTOS学习-队列管理_第6张图片

可以看到,这个函数的实现是目前看到的最复杂的函数实现了(互斥锁的实现会更复杂一些),这是因为消息入队涉及到了临界区的操作,调度器的挂起和恢复,队列的加锁和解锁,任务队列的操作等。

需要注意的是,这个接口只能在任务上下文中调用,如果在中断上下文中,则需要调用ISR的版本:

#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, phHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )
#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
#define xQueueSendFromISR( xQueue, pvItemToQueue, phHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
#define xQueueOverwriteFromISR( xQueue, pvItemToQueue), pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueOVERWRITE )

BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, const void * const pvItemToQueue, BaseType_t * const pxHigherPriorityTaskWoken, const BaseType_t xCopyPosition ) PRIVILEGED_FUNCTION;

中断版本通常更简洁,并输出pxHigherPriorityTaskWoken用于反馈任务唤醒信息,详情可参看在ISR中使用FreeRTOS API。

来看看中断版本的实现:与任务上下文的版本不同的是,它不会被阻塞,且不会在内部触发调度。另外,该实现仅用于普通队列的消息入队,而信号量和互斥锁使用的是xQueueGiveFromISR(),详情在归还信号量小节中介绍。

{
    参数检查:Assert `pxQueue != NULL`,`!( ( pvItemToQueue == NULL) && ( pxQueue->uxItemSize != 0 ) )`,`!( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) )`

    检查优先级是否满足需求(`portASSERT_IF_INTERRUPT_PRIORITY_INVALID()`)。

    进入临界区并保存当前中断优先级屏蔽值(`uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR()`)。

    如果队列没满(`pxQueue->uxMessagesWaiting < pxQueue->uxLength`)或入队方式为重写(`xCopyPosition == queueOVERWRITE`):
    {
        获取队列计数写锁(`cTxLock = pxQueue->cTxLock`)。

        (由于信号量和互斥锁的解锁不会调用该接口,所以这里只考虑队列的情况)将消息入队(`prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition )`)。

        如果队列没有加计数写锁(`cTxLock == queueUNLOCKED`):
        {
            (仅开启USE_QUEUE_SETS):
            {
                如果队列在某个队列组内(`pxQUeue->pxQueueSetContainer != NULL`):
                {
                    将消息给到对应的队列组,如果有高优先级的任务被唤醒(`prvNotifyQueueSetContainer( pxQueue, xCopyPosition ) != pdFALSE`),则设置`*pxHigherPriorityTaskWoken = pdTRUE`。
                }
                否则:
                {
                    如果有任务在等待该队列的消息(`listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE`):
                    {
                        将任务从事件任务队列中移除,如果唤醒了更高优先级的任务(`xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE`),设置`*pxHigherPriorityTaskWoken = pdTRUE`。
                    }
                }
            }
        }
        否则,即队列被加了计数写锁:
        {
            将计数写锁増1(`pxQueue->cTxLock = cTxLock + 1`)。
        }

        设置返回值`xReturn = pdPASS`。
    }
    否则,队列已满:
    {
        设置返回值`xReturn = errQUEUE_FULL`。
    }

    退出临界区(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)。

    返回`xReturn`。
}

4.2. 出队操作

在FreeRTOSv9时,出队相关操作的函数原型:

#define xQueuePeek( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdTRUE )
#define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdFALSE )

BaseType_t xQueueGenericReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait, const BaseType_t xJustPeek ) PRIVILEGED_FUNCTION;

与入队不同的是,消息永远只能从队列的首部出队。出队的接口方式与入队类似,都是通过宏定义来决定出队的方式,最终调用xQueueGenericReceive()接口实现。下面便介绍一下该接口。

该接口的主要职责是从队列中获取一个消息。这个获取行为也需要一次内存拷贝

pvBuffer是指向接受消息的缓存的指针,接收缓存的内存由调用者管理。

如前面提到,FreeRTOS的队列还支持阻塞读,而xTicksToWait便是本次读取动作的等待时限。特别地,如果xTicksToWait为0,且队列为空,则直接返回。

xJustPeek如果为pdTRUE,表示本次读取操作仅仅是消息拷贝,并不会导致消息从队列中出队;否则需要将消息从队列中移除。

如果在预设的时限内成功获取到消息,则返回pdTRUE,否则返回pdFALSE

FreeRTOSv10后,xQueueGenericReceive()已经被拆分为两个单独的接口,函数原型如下:

BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
BaseType_t xQueuePeek( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;

在介绍具体的实现之前,需要了解一下关键的行为:

  • 将队列中的数据出队(static void prvCopyDataFromQueue( Queue_t * const pxQueue, void * const pvBuffer )):先移动队首位置,再将数据拷贝出来。因此,复位队列时,队首的位置指向最后一个消息。

    {
        如果消息长度不为0(`pxQUeue->uxItemSize != 0`):
        {
            移动队首到下一个消息位置(`pxQueue->u.pcReadFrom += pxQueue->uxItemSize`)。
    
            如果队首已经溢出(`pxQueue->u.pcReadFrom >= pxQueue->pcTail`),则重置到队首(`pxQueue->u.pcReadFrom = pxQueue->pcHead`)。
    
            拷贝数据(`memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.pcReadFrom, ( size_t ) pxQueue->uxItemSize )`)。
        }
    }
    

下面看看xQueueReceive()函数的具体实现:用活动图表示如下

FreeRTOS学习-队列管理_第7张图片
需要注意的是,该接口也只能在任务上下文中调用,如果在中断上下文中,则需要调用ISR的版本:

BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue, void * const pvBuffer ) PRIVILEGED_FUNCTION;

BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken ) PRIVILEGED_FUNCTION;

下面看看xQueuePeekFromISR()具体的实现:

{
    初始化`pxQueue = ( Queue_t * ) xQueue`。

    参数校验:Assert `pxQueue != NULL`,`!( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != 0 ) )`,`pxQueue->uxItemSize != 0`(因为不能Peek信号量和互斥锁)

    检查中断优先级适合符合要求(`portASSERT_IF_INTERRUPT_PRIORITY_INVALID()`)。

    进入临界区,并保存原有的中断屏蔽优先级(`uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR()`)。
    {
        如果队列中的消息不为空(`pxQueue->uxMessagesWaiting > 0`):
        {
            记录当前的队首位置(`pcOriginalReadPosition = pxQueue->u.pcReadFrom`)。
            将消息拷出队列(`prvCopyDataFromQueue( pxQueue, pvBuffer )`)。
            恢复队首位置(`pxQueue->u.pcReadFrom = pcOriginalReadPosition`)。

            设置返回值`xReturn = pdPASS`。
        }
        否则,即消息队列为空:设置返回值`xReturn = pdFAIL`。
    }
    退出临界区,恢复原因的中断屏蔽优先级(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)。

    返回`xReturn`。
}

可以看到,这个实现是任务上下文中的简化版本,并且只考虑了Peek的情况。

下面看看xQueueReceiveFromISR()的实现:

{
    初始化`pxQueue = ( Queue_t * ) xQueue`。

    参数校验:Assert `pxQueue != NULL`,`!( ( pvBuffer == NULL ) && ( pxQueue->uxItemSize != 0 ) )`。

    检查中断优先级是否符合要求(`portASSERT_IF_INTERRUPT_PRIORITY_INVALID()`)。

    进入临界区,并保存中断优先级屏蔽状态(`portSET_INTERRUPT_MASK_FROM_ISR()`)。
    {
        获取队列的消息数量(`uxMessagesWaiting = pxQueue->uxMessagesWaiting`)。

        如果队列的存在消息(`uxMessagesWaiting > 0`):
        {
            获取队列的计数读锁(`cRxLock = pxQueue->cRxLock`)。

            将数据从队列中拷出(`prvCopyDataFromQueue( pxQueue, pvBuffer )`)。
            将队列的消息计数减一(`pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1`)。

            如果队列处于未加锁状态(`cRxLock == queueUNLOCKed`);
            {
                如果有任务正在等待入队消息(`listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE`):
                {
                    将当前任务从事件任务队列中唤醒,如果唤醒了高优先级的任务(`xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE`),则设置高优先级任务唤醒标志`*pxHigherPriorityTaskWoken = pdTRUE`。
                }
            }
            否则,即队列已加锁:
            {
                读计数锁加一(`pxQueue->cRxLock = cRxLock + 1`)。
            }

            设置返回`xReturn = pdPASS`。
        }
        否则,即队列无消息:
        {
            设置返回`xReturn = pdFAIL`。
        }
    }

    退出临界区,并恢复中断优先级屏蔽状态(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)。

    返回`xReturn`。
}

可以看到,中断实现比任务上下文的实现更加简洁,因为不需要考虑阻塞的状态。

4.3. 查询队列状态

FreeRTOS的队列支持查询队列中消息的数量,以及剩余空间,函数原型如下:

UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;

UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;

其中,uxQueueMessagesWaiting()将返回队列中等待获取的消息数量;uxQueueSpacesAvailable()则返回队列中剩余的空闲槽数量。在具体实现时,这两个函数需进入临界区读取任务的消息计数进而计算出结果。另外,还提供了ISR的版本:

BaseType_t xQueueIsQueueEmptyFromISR( const QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;
BaseType_t xQueueIsQueueFullFromISR( const QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;
UBaseType_t uxQueueMessagesWaitingFromISR( const QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;

这三个接口只能在ISR或者临界区使用。

4.4. 复位队列

FreeRTOS支持队列的一键复位,函数原型:

#define xQueueReset( xQueue ) xQueueGenericReset( xQueue, pdFALSE )

BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue ) PRIVILEGED_FUNCTION;

所谓复位,指的是将队列恢复到创建时的初始状态。

xQueueReset()实际上通过xQueueGenericReset()实现。

其中xNewQueue指示该队列是否是一个新创建的队列。该函数永远返回pdPASS

下面来看看其实现:用活动图表示如下所示

FreeRTOS学习-队列管理_第8张图片

4.5. 调试相关

FreeRTOS提供了内核调试相关的接口,例如队列名注册和注销,获取队列名等。函数原型:需要设置configQUEUE_REGISTRY_SIZE > 0

void vQueueAddToRegistry( QueueHandle_t xQueue, const char *pcName ) PRIVILEGED_FUNCTION;
void vQueueUnregisterQueue( QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;

const char *pcQueueGetName( QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;

将队列加入到注册表可以让内核调试工具感知到队列(或信号量,互斥锁)的存在,并且可以为队列赋予名字pcName。需要注意的是,注册表只记录了名字字符串的指针,因此需要注意传入的参数不能是栈上的变量。

将队列注册到队列注册表后,就可以通过pcQueueGetName()获取该队列的名字,否则会返回NULL

队列注册表的实现很简单,只是单纯的数组xQueueRegistry的操作。

5. 删除队列

FreeRTOS支持动态删除队列,函数原型如下:

void vQueueDelete( QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;

在删除队列时,FreeRTOS会将该队列所使用的两块内存全部释放,即元信息和队列的数据。

下面来看看具体的实现:

{
    初始化`pxQueue = ( Queue_t * ) xQueue`。

    参数检查:Assert `pxQueue != NULL`

    (仅开启configQUEUE_REGISTRY_SIZE > 0)将队列从注册表中注销(`vQueueUnregisterQueue( pxQueue )`)。

    如果队列是动态申请的(`pxQueue->ucStaticallyAllocated == pdFALSE`),释放内存(`vPortFree( pxQueue )`)。
}

6. 队列的应用

对于消息的内存占用较大的情况,推荐使用引用的方式,即通过传递消息的指针来进行通信。

对于消息类型不固定的情况,需要结合结构体与引用的方式,由使用者自身来确定如何解析数据。

7. 队列组

如前文所说,FreeRTOS的队列支持队列组的功能,从而使得任务对于这个队列组进行阻塞读/写操作。本小节则介绍这个队列组的使用。

队列组的使用需要注意一下几点问题:

  • 除非是非常有必要,很少会使用到这个功能;
  • 对于在组内使用了互斥锁的情况,并不会导致优先级的继承;
  • 队列组的长度为所有队列的长度之和,因此不适用于最大值很大的计数信号量;
  • 除非是通过xQueueSelectFromSet()获取的句柄,不应该单独对某个队列进行读取操作。

7.1. 创建队列组

函数原型:

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength ) PRIVILEGED_FUNCTION;

在创建了队列组后,便可以往该组里添加队列(或信号量、互斥锁)。

其中,uxEventQueueLength指的是队列组中所有队列的长度的总和,其中二值信号量和互斥锁的长度为1,计数信号量的长度为最大计数值。

下面来看看其具体的实现:

{
    创建Set类型的队列(`pxQueue = xQueueGenericCreate( uxEventQueueLength, sizeof( Queue_t * ), queueQUEUE_TYPE_SET )`。

    返回`pxQueue`。
}

可以看到,它的实现只是简单地调用了创建队列的方法,只是类型为queueQUEUE_TYPE_SET。另外,与创建队列一样,队列组还支持静态的创建,这里就不介绍了。

7.2. 添加/删除队列组成员

添加到队列组的函数原型:

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet ) PRIVILEGED_FUNCTION;

该函数将队列(或信号量,互斥锁)xQueueOrSemaphore加入到指定的队列xQueueSet中。如果成功则返回pdPASS;如果由于队列已经在其他队列组中,则返回pdFAIL

下面来看看其具体实现:

{
    进入临界区(`taskENTER_CRITICAL()`)。

    如果该队列已经在其他队列组中(`( ( Queue_t * ) xQueueOrSemaphore )->pxQueueSetContainer != NULL`),则设置返回值`xReturn = pdFAIL`。
    否则,如果队列中已经存在消息(`( ( Queue_t * ) xQueueOrSemaphore )->uxMessagesWaiting != 0`),则设置返回值`xReturn = pdFAIL`。
    否则,表明队列可以加入到队列组,将其加入到队列组中(`( ( Queue_t * ) xQueueOrSemaphore )->pxQueueSetContainer = xQueueSet`),并设置返回值`xReturn = pdPASS。

    退出临界区(`taskEXIT_CRITICAL()`)。

    返回`xReturn`。
}

将队列从队列组中删除的函数原型:

BaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet) PRIVILEGED_FUNCTION;

该函数从队列组xQueueSet中删除队列(或信号量、互斥锁)xQueueOrSemaphore。需要注意的是,只有当被删除的队列(或信号量、互斥锁)为空时,才能将其从队列组中删除。如果删除成功,则返回pdPASS;如果队列不在队列组中,或不为空,则返回pdFAIL

下面来看看具体的实现:

{
    初始化`pxQueueOrSemaphore = ( Queue_t * ) xQueueOrSemaphore`。

    如果队列不在该队列组中(`pxQueueOrSemaphore->pxQueueSetContainer != xQueueSet`),则设置返回值`xReturn = pdFAIL`。
    否则,如果该队列中的还有消息(`pxQueueOrSemaphore->uxMessagesWaiting != 0`),则设置返回值`xReturn = pdFAIL`。
    否则,可以将其从队列组中剔除:
    {
        进入临界区(`taskENTER_CRITICAL()`)。
        将队列从队列组中移除(`pxQueueOrSemaphore->pxQueueSetContainer = NULL`)。
        退出临界区(`taskEXIT_CRITICAL()`)。
        设置返回值`xReturn = pdPASS`。
    }

    返回`xReturn`。
}

7.3. 从队列组中获取消息

函数原型:

QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, const TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;

该函数中队列组中的所有成员中选择一个消息非空的成员,并返回其句柄给调用者。该函数也支持阻塞的等待,时限为xTicksToWait。如果在预设的时间内没有消息抵达,则返回NULL

在获取到了成员后,便可以对其进行进一步消息获取操作。

下面来看看其具体的实现:

{
    通过调用`xQueueGenericReceive( ( QueueHandle_t ) xQueueSet, &xReturn, xTicksToWait, pdFALSE )`实现。
    返回`xReturn`。
}

可以看到,队列组的消息实际上是队列组中的队列句柄。

这里还需要详细介绍一下在xQueueGenericReceive()的实现中,将入队消息通知到队列组的行为(static BaseType_t prvNotifyQueueSetContainer( const Queue_t * const pxQueue, const BaseType_t xCopyPostition )):该函数必须在临界区内调用。返回指表示是否唤醒了高优先级的任务。

FreeRTOS学习-队列管理_第9张图片

该函数的ISR版本:

QueueSetMemberHandle_t xQueueSelectFromSetFromISR( QueueSetHandle_t xQueueSet ) PRIVILEGED_FUNCTION;

其实现也很简单:

{
    通过调用`xQueueReceiveFromISR( ( QueueHandle_t ) xQueueSet, &xReturn, NULL )`实现。

    返回`xReturn`。
}

8. 队列的实现细节

8.1. 队列的接口

8.1.1. 队列的数据结构接口

8.1.1.1. 队列句柄

队列句柄是指向一个队列的指针,在创建队列时返回的类型,是所有队列操作的API所需的参数。

typedef void * QueueHandle_t;
8.1.1.2. 队列组(Queue Set)句柄

队列组句柄是指向一个队列组的指针,在创建队列组返回的类型,是所有队列组操作的API所需的参数。

typedef void *QueueSetHandle_t;
8.1.1.3. 队列组成员(Queue Set Member)句柄

由于队列组中可以包含队列和信号量,该类型代表两者中的任意一个。

typedef void * QueueSetMemberHandle_t;

8.2. 队列的内涵

8.2.1. 队列依赖的头文件

C标准库依赖头文件:

#include 
#include 

FreeRTOS依赖头文件:

#define MPU_WRAPPERS_INCLUDED_FROM_API_FILE

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

#if ( configUSE_CO_ROUTINES == 1 )
    #include "croutine.h"
#endif

#undef MPU_WRAPPERS_INCLUDED_FROM_API_FILE

8.2.2. 队列的私有宏

8.2.2.1. 队列的私有常量

表示队列的访问方式:

#define queueSEND_TO_BACK       ( ( BaseType_t ) 0 )
#define queueSEND_TO_FRONT      ( ( BaseType_t ) 1 )
#define queueOVERWRITE          ( ( BaseType_t ) 2 )

表示队列的类型:

#define queueQUEUE_TYPE_BASE                ( ( uint8_t ) 0U )
#define queueQUEUE_TYPE_SET                 ( ( uitt8_t ) 0U )
#define queueQUEUE_TYPE_MUTEX               ( ( uint8_t ) 1U )
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE  ( ( uint8_t ) 2U )
#define queueQUEUE_TYPE_BINARY_SEMAPHORE    ( ( uint8_t ) 3U )
#define queueQUEUE_TYPE_RECURSIVE_MUTEX     ( ( uint8_t ) 4U )

可以看出队列可以被实现为6种类型。

队列结构体的cRxLock和cTxLock成员的可取值的定义:

#define queueUNLOCKED               ( ( uint8_t ) -1 )
#define queueLOCKED_UNMODIFIED      ( ( uint8_t ) 0 )

信号量和互斥锁的消息长度:

#define queueSEMAPHORE_QUEUE_ITEM_LENGTH    ( ( UBaseType_t ) 0 )
#define queueMUTEX_GIVE_BLOCK_ITEM          ( ( TickType_t ) 0U )

因为信号量和互斥锁并不真的存储消息,因此他们的消息长度都为0。

8.2.2.2. 队列的私有宏函数

调度相关的宏函数:

#if ( configUSE_PREEMPTION == 0 )
    #define queueYIELD_IF_USING_PREEMTION()
#else
    #define queueYIELD_IF_USING_PREEMTION() portYIELD_WITHIN_API()
#endif

8.2.3. 队列的私有数据结构

8.2.3.1. 队列的元信息结构体

队列的元信息是队列实现的最重要的数据结构,其定义如下:

typedef struct QueueDefinition
{
    int8_t *pcHead;                 /* 指向队列消息存储区域的起始字节地址 */
    int8_t *pcTail;                 /* 指向队列消息存储区域的末尾字节地址 */
    int8_t *pcWriteTo;              /* 指向消息队列存储区域的下一个空闲的地址,即当前队尾 */

    union
    {
        int8_t *pcReadFrom;         /* 仅队列,指向最近一次读取所指向的消息存储区域的地址,即当前队首 */
        UBaseType_t uxRecursiveCallCount; /* 仅互斥锁,记录递归互斥锁的递归深度 */
    } u;

    List_t xTasksWaitingToSend;     /* 等待该队列的阻塞写的任务队列,根据任务优先级来排序 */
    List_t xTasksWaitingToReceive;  /* 等待该队列的阻塞读的任务队列,根据任务优先级来排序 */

    volatile UBaseType_t uxMessagesWaiting; /* 队列中当前的消息数 */
    UBaseType_t uxLength;           /* 队列长度 */
    UBaseType_t uxItemSize;         /* 消息长度 */

    volatile int8_t cRxLock;        /* 存储了当队列处于Locked状态时,出队的消息数量。如果队列未被锁时,设置为queueUNLOCKED */
    volatile int8_t cTxLock;        /* 存储了当队列处于Locked状态时,入队的消息数量。如果队列未被锁时,设置为queueUNLOCKED */

    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated;  /* 如果设置为pdTRUE,表示队列使用的内存是静态申请,不需要FreeRTOS来删除 */
    #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;

在队列的实现中,普通队列、信号量和互斥锁都是使用该数据结构存储元信息。我们来仔细看看队列数据结构中的一些成员。

对于pcHeadpcTail成员,根据队列的实现类型不同,主要有以下几种情况:

  • 如果Queue_t用于表示基本的队列,那么其pcHeadpcTail都作为指针使用,指向队列的消息。
  • 如果Queue_t用于表示互斥锁,那么pcHeadpcTail不会作为消息指针使用,为了更好的可读性,所以将pcHead通过宏定义重命名为uxQueueType。同时设置pcHead = queueQUEUE_IS_MUTEX,因此可通过uxQueueType来判断该队列是不是被实现为了互斥锁。而pcTail则用于表示互斥锁的所有者,将其重命名为pxMutexHolder

这里不使用联合体的原因是这将会破坏命名规范,宏定义如下:

#define pxMutexHolder           pcTail
#define uxQueueType             pcHead

#define queueQUEUE_IS_MUTEX     NULL

同样地,在pcReadFromuxRecursiveCallCount之间也有类似的关系。

xTasksWaitingToSendxTasksWaitingToReceive是非常重要的成员。它们与TCB_t中的xEventListItem互相关联,因为支持阻塞的读/写,通常还会与Delayed任务队列一同操作。

cRxLockcTxLock是队列的出队和入队计数锁。用于防止队列被并行访问而破坏上述两个队列的一致性,同时它们还记录加锁时队列的入队和出队消息的次数。

如果队列在某个队列组中,pxQueueSetContainer记录了该队列的所属队列组。

uxQueueNumberucQueueType用于第三方的Profiling工具。

8.2.3.2. 队列的注册表

队列注册表为内核调试工具定位某个队列的提供了方法。这是可选的功能。

定义如下:需要设置configQUEUE_REGISTRY_SIZE > 0

typedef struct QUEUE_REGISTRY_ITEM
{
    const char *pcQueueName;
    QueueHandle_t xHandle;
} xQueueRegistryItem;

typedef xQueueRegistryItem QueueRegistryItem_t;

PRIVILEGED_DATA QueueRegistryItem_t xQueueRegistry[ configQUEUE_REGISTRY_SIZE ];

队列的注册表是一个简单的数组。

8.2.4. 队列的私有函数

在私有函数中,比较重要的是为信号量和互斥锁预留的内部接口:

QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType ) PRIVILEGED_FUNCTION;
QueueHandle_t xQueueCreateMutexStatic( const uint8_t ucQueueType, StaticQueue_t *pxStaticQueue ) PRIVILEGED_FUNCTION;

QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount ) PRIVILEGED_FUNCTION;
QueueHandle_t xQueueCreateCountingSemaphoreStatic( const UBaseType_ uxMaxCount, const UBaseType_t uxInitialCount, StaticQueue_t *pxStaticQueue ) PRIVILEGED_FUNCTION;

void *xQueueGetMutexHolder( QueueHandle_t xSemaphore ) PRIVILEGED_FUNCTION;

BaseType_t xQueueTakeMutexRecursive( QueueHandle_t xMutex, TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
BaseType_t xQueueGiveMutexRecursive( QueueHandle_t xMutex ) PRIVILEGED_FUNCTION;

FreeRTOS专用的接口:

void vQueueWaitForMessageRestricted( QueueHandle_t xQueue, TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely) PRIVILEGED_FUNCTION;

vQueueWaitForMessageRestricted()是专门为软件定时器的实现而提供的函数。用于等待Timer命令队列中的消息。

与调试相关的接口:

void vQueueSetQueueNumber( QueueHandle_t xQueue, UBaseType_t xuQueueNumber ) PRIVILEGED_FUNCTION;
UBaseType_t uxQueueGetQueueNumber( QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;
uint8_t ucQueueGetQueueType( QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;

这些接口用于第三方的调试工具。功能如其名,无需过多解释。

你可能感兴趣的:(操作系统,学习,c语言,iot,arm开发,开源软件)