FreeRTOS从入门到精通 第十二章(FreeRTOS消息队列)

参考教程:【正点原子】手把手教你学FreeRTOS实时系统_哔哩哔哩_bilibili

一、队列介绍

1、概述

(1)队列是任务到任务、任务到中断、中断到任务数据交流的一种机制,它不同于全局变量。假设有一个全局变量a,现有两个任务都在写这个变量a,如下所示,变量自增分为三个步骤,如果在任务1读数据以后、修改数据以前发生任务切换,这将导致任务2和任务1读取相同的数据,并且基于相同的数据做相同的修改,这显然是有问题的,而使用队列可以避免这种问题(指访问冲突)。

(2)FreeRTOS基于队列实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、递归互斥信号量,并且读写队列做好了保护(主要依靠临界区),防止多任务同时访问冲突,用户只需要直接调用API函数即可。

(3)在队列中可以存储数量有限、大小固定的数据,队列中的每一个数据叫做"队列项目",队列能够存储"队列项目"的最大数量称为队列的长度,在创建队列时,用户要指定队列长度以及队列项目的大小。

(4)FreeRTOS队列的特点:

①数据入队出队方式:队列通常采用"先进先出"(FIFO)的数据存储缓冲机制,即先入队的数据会先从队列中被读取;FreeRTOS中也可以配置为"后进先出"(LIFO)方式。

②数据传递方式:FreeRTOS中队列可采用实际值传递,即将数据拷贝到队列中进行传递, 也可以采用指针传递,在传递较大的数据时,将数据的地址拷贝到队列中进行传递。

③队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息。

④当任务向一个队列发送消息时,可以指定一个阻塞时间。

假设此时队列已满无法入队(入队阻塞):

[1]若阻塞时间为0,其它任务打算让数据入队时直接无法入队,并返回errQUEUE_FULL。

[2]若阻塞时间为0~port_MAX_DELAY,其它任务打算让数据入队时会等待设定的阻塞时间(期间任务将会进入阻塞态),若在该时间内还无空位可入队,超时后直接返回errQUEUE_FULL,不再等待。

[3]若阻塞时间为port_MAX_DELAY,其它任务打算让数据入队时会死等(期间任务将会进入阻塞态),一直等到可以入队为止。

假设此时队列已空无法出队(出队阻塞):

[1]若阻塞时间为0,其它任务打算让数据出队时直接无法出队,并返回pdFALSE。

[2]若阻塞时间为0~port_MAX_DELAY,其它任务打算让数据出队时会等待设定的阻塞时间(期间任务将会进入阻塞态),若在该时间内还无数据可出队,超时后直接返回pdFALSE,不再等待。

[3]若阻塞时间为port_MAX_DELAY,其它任务打算让数据出队时会死等(期间任务将会进入阻塞态),一直等到可以出队为止。

当多个任务写入消息给一个"满队列"时,这些任务都会进入阻塞状态,也就是说有多个任务在等待同一个队列的空间,当队列中有空间时,优先级最高的任务会最先进入就绪态并获得队列"使用权",如果诸任务的优先级相同,那等待时间最久的任务会进入就绪态。

(5)队列操作基本过程举例:

2、队列的数据结构

(1)队列的结构体:

cpp 复制代码
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;

(2)上面的结构体可用于队列,也可用于互斥信号量和递归互斥信号量。

①当用于队列使用时:

cpp 复制代码
typedef struct QueuePointers
{
     int8_t * pcTail; 			/* 存储区的结束地址 */
     int8_t * pcReadFrom;		/* 最后一个读取队列的地址 */
}QueuePointers_t;

②当用于互斥信号量和递归互斥信号量时:

cpp 复制代码
typedef struct SemaphoreData
{
    TaskHandle_t xMutexHolder;			 /* 互斥信号量持有者 */
    UBaseType_t uxRecursiveCallCount;	/* 递归互斥信号量的获取计数器 */
}SemaphoreData_t;

(3)队列结构体整体示意图:

3、队列相关API函数介绍

(1)FreeRTOS基于队列实现了多种功能,每一种功能对应一种队列类型,队列类型在 queue.h文件中有定义:

cpp 复制代码
#define queueQUEUE_TYPE_BASE                ( ( uint8_t ) 0U )	//队列
#define queueQUEUE_TYPE_SET                 ( ( uint8_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 )	//递归互斥信号量

(2)创建队列相关API函数介绍:

|----------------------|----------|
| 函数 | 描述 |
| xQueueCreate() | 动态方式创建队列 |
| xQueueCreateStatic() | 静态方式创建队列 |

①动态和静态创建队列之间的区别:动态创建队列所需的内存空间由FreeRTOS 从FreeRTOS管理的堆中分配,而静态创建需要用户自行分配内存。

②动态方式创建队列函数xQueueCreate的宏函数接口:

cpp 复制代码
#define xQueueCreate (uxQueueLength, uxItemSize)   						 \
	  xQueueGenericCreate ((uxQueueLength),(uxItemSize),(queueQUEUE_TYPE_BASE ))

[1]函数参数:

|---------------|---------|
| 形参 | 描述 |
| uxQueueLength | 队列长度 |
| uxItemSize | 队列项目的大小 |

[2]返回值:

|------|---------------|
| 返回值 | 描述 |
| NULL | 队列创建失败 |
| 其它值 | 队列创建成功,返回队列句柄 |

(3)往队列写入消息相关API函数介绍:

|----------------------------|--------------------------|
| 函数 | 描述 |
| xQueueSend() | 往队列的尾部写入消息 |
| xQueueSendToBack() | 同 xQueueSend() |
| xQueueSendToFront() | 往队列的头部写入消息 |
| xQueueOverwrite() | 覆写队列消息(只用于队列长度为1的情况) |
| xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
| xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
| xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
| xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为1的情况) |

①上述8个函数可分为任务级与中断级。

②队列一共有3个写入位置,定义如下:

cpp 复制代码
#define queueSEND_TO_BACK         ( ( BaseType_t ) 0 )		/* 写入队列尾部 */
#define queueSEND_TO_FRONT        	( ( BaseType_t ) 1 )		/* 写入队列头部 */
#define queueOVERWRITE             ( ( BaseType_t ) 2 )		/* 覆写队列*/
//覆写方式写入队列,只有在队列的队列长度为1时才能够使用

③任务级写入函数的宏函数接口(中断级暂时不做介绍):

cpp 复制代码
#define  xQueueSend(xQueue,pvItemToQueue,xTicksToWait)	\    xQueueGenericSend((xQueue),(pvItemToQueue),(xTicksToWait), queueSEND_TO_BACK)
#define  xQueueSendToBack(xQueue,pvItemToQueue,xTicksToWait)		\    
xQueueGenericSend((xQueue),(pvItemToQueue),(xTicksToWait), queueSEND_TO_BACK)
#define  xQueueSendToFront(xQueue,pvItemToQueue,xTicksToWait) 		\   
xQueueGenericSend((xQueue),(pvItemToQueue),(xTicksToWait), queueSEND_TO_FRONT)
#define  xQueueOverwrite(xQueue, pvItemToQueue) 					\    
xQueueGenericSend((xQueue), (pvItemToQueue), 0, queueOVERWRITE)

这几个写入函数调用的是同一个函数xQueueGenericSend,只是指定了不同的写入位置。

④xQueueGenericSend函数的入口参数及返回值解析:

cpp 复制代码
BaseType_t  xQueueGenericSend
(
    QueueHandle_t 		xQueue,
	const void * const 		pvItemToQueue,
	TickType_t 			xTicksToWait,
	const BaseType_t 		xCopyPosition
)

[1]函数参数:

|---------------|----------|
| 形参 | 描述 |
| xQueue | 待写入的队列 |
| pvItemToQueue | 待写入消息的地址 |
| xTicksToWait | 阻塞超时时间 |
| xCopyPosition | 写入的位置 |

[2]返回值:

|---------------|--------|
| 返回值 | 描述 |
| pdTRUE | 队列写入成功 |
| errQUEUE_FULL | 队列写入失败 |

(4)从队列读取消息相关API函数介绍:

|------------------------|----------------------------------|
| 函数 | 描述 |
| xQueueReceive() | 从队列头部读取消息,消息读取成功后,会将消息从队列中移除 |
| xQueuePeek() | 从队列头部读取消息 |
| xQueueReceiveFromISR() | 在中断中从队列头部读取消息,消息读取成功后,会将消息从队列中移除 |
| xQueuePeekFromISR() | 在中断中从队列头部读取消息 |

①上述4个函数可分为任务级与中断级,这里暂时仅介绍任务级的函数。

②xQueueReceive函数的入口参数及返回值解析:

cpp 复制代码
BaseType_t  xQueueReceive
(
    QueueHandle_t   xQueue,
    void * const 		pvBuffer,
    TickType_t   	xTicksToWait
)

[1]函数参数:

|--------------|------------|
| 形参 | 描述 |
| xQueue | 待读取的队列 |
| pvBuffer | 信息读取缓冲区的地址 |
| xTicksToWait | 阻塞超时时间 |

[2]返回值:

|---------|------|
| 返回值 | 描述 |
| pdTRUE | 读取成功 |
| pdFALSE | 读取失败 |

③xQueuePeek函数的入口参数及返回值解析:

cpp 复制代码
BaseType_t  xQueuePeek
(
    QueueHandle_t   	xQueue, 
    void * const   	pvBuffer,
    TickType_t   	xTicksToWait
)

[1]函数参数:

|--------------|------------|
| 形参 | 描述 |
| xQueue | 待读取的队列 |
| pvBuffer | 信息读取缓冲区的地址 |
| xTicksToWait | 阻塞超时时间 |

[2]返回值:

|---------|------|
| 返回值 | 描述 |
| pdTRUE | 读取成功 |
| pdFALSE | 读取失败 |

二、常用队列相关API函数的源码剖析

1、xQueueCreate函数

(1)如上所述,该函数用于创建队列(动态方法)。

(2)xQueueCreate函数的底层实际上是xQueueGenericCreate函数,相比之下xQueueGenericCreate函数多出一个参数------queueQUEUE_TYPE_BASE,它表示创建的队列为普通队列(而非队列集、互斥信号量等)。

cpp 复制代码
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	#define xQueueCreate(uxQueueLength, uxItemSize)    xQueueGenericCreate((uxQueueLength), (uxItemSize), (queueQUEUE_TYPE_BASE))
#endif

(3)xQueueGenericCreate函数的实现:

cpp 复制代码
QueueHandle_t xQueueGenericCreate(const UBaseType_t uxQueueLength,
                                   const UBaseType_t uxItemSize,
                                   const uint8_t ucQueueType)
{
    Queue_t * pxNewQueue = NULL;
    size_t xQueueSizeInBytes;
    uint8_t * pucQueueStorage;

    traceENTER_xQueueGenericCreate( uxQueueLength, uxItemSize, ucQueueType );

    if( ( uxQueueLength > ( UBaseType_t ) 0 ) &&       //判断队列长度是否合法,即大于0
        ( ( SIZE_MAX / uxQueueLength ) >= uxItemSize ) && 
        ( ( UBaseType_t ) ( SIZE_MAX - sizeof( Queue_t ) ) >= ( uxQueueLength * uxItemSize ) ) )                                    //判断是否会出现溢出的情况
    {
        xQueueSizeInBytes = (size_t) ( (size_t)uxQueueLength * (size_t) uxItemSize);   
//计算队列项总的占用空间
        pxNewQueue = (Queue_t *)pvPortMalloc(sizeof(Queue_t) + xQueueSizeInBytes);  
//为队列项及队列结构体申请内存空间

        if( pxNewQueue != NULL )  //判断内存是否申请成功,成功则计算出队列项存储区的首地址
        {
            pucQueueStorage = ( uint8_t * ) pxNewQueue;
            pucQueueStorage += sizeof( Queue_t );    //求出队列项存储区的首地址

            #if ( configSUPPORT_STATIC_ALLOCATION == 1 )
            {
                pxNewQueue->ucStaticallyAllocated = pdFALSE;
            }
            #endif /* configSUPPORT_STATIC_ALLOCATION */

            prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );   //初始化队列结构体成员变量
        }
        else
        {
            traceQUEUE_CREATE_FAILED( ucQueueType );
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else
    {
        configASSERT( pxNewQueue );
        mtCOVERAGE_TEST_MARKER();
    }

    traceRETURN_xQueueGenericCreate( pxNewQueue );

   return pxNewQueue;   //返回队列结构体首地址
}

(4)xQueueGenericCreate函数中调用到的函数剖析:

①prvInitialiseNewQueue函数的实现:

cpp 复制代码
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 )
    {
        //用作信号量时,不需要队列项存储区,队头指针指向自己
        pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
    }
    else
    {
        //否则队头指针指向队列项存储区首地址
        pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;
    }

    pxNewQueue->uxLength = uxQueueLength;   //初始化队列长度
    pxNewQueue->uxItemSize = uxItemSize;     //初始化队列项大小
    ( void ) xQueueGenericReset( pxNewQueue, pdTRUE );    //传入pdTRUE是为了初始化接收列表WaitingToReceive和发送列表xTasksWaitingToSend

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

②xQueueGenericReset函数的实现:

cpp 复制代码
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
    BaseType_t xReturn = pdPASS;
    Queue_t * const pxQueue = xQueue;

    traceENTER_xQueueGenericReset( xQueue, xNewQueue );

    configASSERT( pxQueue );

    if( ( pxQueue != NULL ) && ( pxQueue->uxLength >= 1U ) && ( ( SIZE_MAX / pxQueue->uxLength ) >= pxQueue->uxItemSize ) )   //合法性检查
    {
        taskENTER_CRITICAL();   //关中断(进入临界区)
        {
            pxQueue->u.xQueue.pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize );                                      //初始化队尾指针
            pxQueue->uxMessagesWaiting = (UBaseType_t) 0U; //初始化非空闲队列项数
            pxQueue->pcWriteTo = pxQueue->pcHead; //初始化下一个队列项写入的位置
            pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - 1U ) * pxQueue->uxItemSize );                //初始化上一个队列项读取的位置
            pxQueue->cRxLock = queueUNLOCKED;  //初始化------不上队列锁
            pxQueue->cTxLock = queueUNLOCKED;  //初始化------不上队列锁

            if( xNewQueue == pdFALSE )       //针对旧队列进行处理(复位)
            {
                if(listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToSend)) ==pdFALSE)
                {   //清空等待发送列表中的任务
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
                    {
                        queueYIELD_IF_USING_PREEMPTION();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else                             //针对新队列进行处理
            {
                vListInitialise( &( pxQueue->xTasksWaitingToSend ) );  
                //初始化接收列表WaitingToReceive
                vListInitialise( &( pxQueue->xTasksWaitingToReceive ) ); 
                //初始化发送列表xTasksWaitingToSend
            }
        }
        taskEXIT_CRITICAL();   //开中断(退出临界区)
    }
    else
    {
        xReturn = pdFAIL;
    }

    configASSERT( xReturn != pdFAIL );

    traceRETURN_xQueueGenericReset( xReturn );

    return xReturn;
}

2、xQueueSend函数

(1)如上所述,该函数用于往队列中写数据。

(2)xQueueSend函数的底层实际上是xQueueGenericSend函数,相比之下xQueueGenericSend函数多出一个参数------queueSEND_TO_BACK,它表示从队列尾部写入数据。

cpp 复制代码
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
    xQueueGenericSend((xQueue), (pvItemToQueue), (xTicksToWait), queueSEND_TO_BACK)

(3)xQueueGenericSend函数的实现:

cpp 复制代码
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
                              const void * const pvItemToQueue,
                              TickType_t xTicksToWait,
                              const BaseType_t xCopyPosition )
{
    BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
    TimeOut_t xTimeOut;
    Queue_t * const pxQueue = xQueue;

    traceENTER_xQueueGenericSend( xQueue, pvItemToQueue, xTicksToWait, xCopyPosition );

    configASSERT( pxQueue );
    configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
    configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );   //如果是覆写方式,需要队列长度为1,否则会报错
    #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
    {
        configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
    }
    #endif

    for( ; ; )
    {
        taskENTER_CRITICAL();   //关中断(进入临界区)
        {
            if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
            {   
				/* 
					先做如下判断,以决定能否继续往队列中写数据:
					①判断非空闲队列项的数目是否小于(或者说不等于)队列长度,是					则代表队列还有空位
					②判断是否为覆写方式,是则无需关注队列还有无空位
				*/
                traceQUEUE_SEND( pxQueue );

                #if ( configUSE_QUEUE_SETS == 1 )  //使用队列集,编译如下代码块
                {
                    const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting;

                    xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );

                    if( pxQueue->pxQueueSetContainer != NULL )
                    {
                        if( ( xCopyPosition == queueOVERWRITE ) && ( uxPreviousMessagesWaiting != ( UBaseType_t ) 0 ) )
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                        else if( prvNotifyQueueSetContainer( pxQueue ) != pdFALSE )
                        {
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    else
                    {
                        if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
                        {
                            if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
                            {
                                queueYIELD_IF_USING_PREEMPTION();
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else if( xYieldRequired != pdFALSE )
                        {
                            queueYIELD_IF_USING_PREEMPTION();
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                }
                #else /* configUSE_QUEUE_SETS */ //使用队列方式,编译如下代码块
                {
                    xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); //复制数据pvItemToQueue到队列pxQueue的xCopyPosition位置(头/尾)
					
                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )  //判断等待接收列表中是否有任务
                    {
					//如果有,移除该任务对应的事件列表项,并判断是否需要任务切换
                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE)
                        {
                            queueYIELD_IF_USING_PREEMPTION(); //进行任务切换
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    else if( xYieldRequired != pdFALSE )
                    {
                        queueYIELD_IF_USING_PREEMPTION();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                #endif /* configUSE_QUEUE_SETS */

                taskEXIT_CRITICAL();   //开中断(退出临界区)

                traceRETURN_xQueueGenericSend( pdPASS );

                return pdPASS;
            }
            else                      
            {
            //如果队列已满
                if( xTicksToWait == ( TickType_t ) 0 )   //如果设定的阻塞时间为0,则不需要阻塞,直接返回写错误errQUEUE_FULL
                {
                    taskEXIT_CRITICAL();   //开中断(退出临界区)
					
                    traceQUEUE_SEND_FAILED( pxQueue );
                    traceRETURN_xQueueGenericSend( errQUEUE_FULL );

                    return errQUEUE_FULL;
                }
                else if( xEntryTimeSet == pdFALSE )
                {
				//记录此时系统节拍计数器和溢出次数,用于下面对阻塞时间进行补偿
                    vTaskInternalSetTimeOutState( &xTimeOut ); 
                    xEntryTimeSet = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
        taskEXIT_CRITICAL();    //开中断(退出临界区)

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

        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )  //判断是否需要继续阻塞(补偿)
        {
            if( prvIsQueueFull( pxQueue ) != pdFALSE )   //判断队列是否已满
            {
                traceBLOCKING_ON_QUEUE_SEND( pxQueue );
                //将任务挂到等待发送列表中
                vTaskPlaceOnEventList(&(pxQueue->xTasksWaitingToSend),xTicksToWait);
                prvUnlockQueue( pxQueue );

                if( xTaskResumeAll() == pdFALSE )  //如需执行任务切换,执行即可
                {
                    taskYIELD_WITHIN_API();
                }
            }
            else
            {
                prvUnlockQueue( pxQueue );   //队列解锁
                ( void ) xTaskResumeAll();    //恢复任务调度器
            }
        }
        else
        {
            prvUnlockQueue( pxQueue );   //队列解锁
            ( void ) xTaskResumeAll();    //恢复任务调度器

            traceQUEUE_SEND_FAILED( pxQueue );
            traceRETURN_xQueueGenericSend( errQUEUE_FULL );

            return errQUEUE_FULL;   //返回队列满的错误信息
        }
    }
}

(4)xQueueSend函数中调用到的函数剖析:

①prvCopyDataToQueue函数的实现:

cpp 复制代码
static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue,
                                  const void * pvItemToQueue,
                                  const BaseType_t xPosition )
{
    BaseType_t xReturn = pdFALSE;
    UBaseType_t uxMessagesWaiting;

    uxMessagesWaiting = pxQueue->uxMessagesWaiting;   //获取非空闲队列项的数目

    if(pxQueue->uxItemSize == ( UBaseType_t ) 0) //队列项数据为零代表队列是信号量
    {
        #if ( configUSE_MUTEXES == 1 )
        {
            if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
            {
                xReturn = xTaskPriorityDisinherit(pxQueue->u.xSemaphore.xMutexHolder);
                pxQueue->u.xSemaphore.xMutexHolder = NULL;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        #endif /* configUSE_MUTEXES */
    }
    else if( xPosition == queueSEND_TO_BACK )    //从队列尾部写入数据
    {
		//将数据pvItemToQueue拷贝至队列pxQueue下一个写入数据的位置pcWriteTo(即尾部)
        ( void ) memcpy( ( void * ) pxQueue->pcWriteTo, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
        //修改下一次写入数据的位置
		pxQueue->pcWriteTo += pxQueue->uxItemSize;

        if( pxQueue->pcWriteTo >= pxQueue->u.xQueue.pcTail )  //当下一次写入数据的位置到达存储区末尾
        {
            pxQueue->pcWriteTo = pxQueue->pcHead;  //下一次写入数据的位置回到队列项存储区头部
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    else    //从队列头部写入数据或覆写队列
    {
		/* 将数据pvItemToQueue拷贝至队列pxQueue上一次读取数据的位置pcReadFrom(即头部) */
        ( void ) memcpy( ( void * ) pxQueue->u.xQueue.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
        pxQueue->u.xQueue.pcReadFrom -= pxQueue->uxItemSize;
        /* 修改上一次读取数据的位置 */

        if( pxQueue->u.xQueue.pcReadFrom < pxQueue->pcHead )  //当上一次读取数据的位置到达存储区头部
        {
            pxQueue->u.xQueue.pcReadFrom = ( pxQueue->u.xQueue.pcTail - pxQueue->uxItemSize );  //当上一次读取数据的位置回到队列项存储区末尾
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        if( xPosition == queueOVERWRITE )    //如果是覆写方式
        {
            if( uxMessagesWaiting > ( UBaseType_t ) 0 )   //如果队列中已有数据
            {
                --uxMessagesWaiting;     //return之前会加一个空闲数据计数,由于覆盖了原本的数据,为保证空闲数据总数不变,这里先扣一个
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }

    pxQueue->uxMessagesWaiting = ( UBaseType_t ) ( uxMessagesWaiting + ( UBaseType_t ) 1 );      //空闲数据数+1

    return xReturn;
}

②xTaskRemoveFromEventList函数的实现:

cpp 复制代码
BaseType_t xTaskRemoveFromEventList( const List_t * const pxEventList )
{
    TCB_t * pxUnblockedTCB;
    BaseType_t xReturn;

    traceENTER_xTaskRemoveFromEventList( pxEventList );

    pxUnblockedTCB = listGET_OWNER_OF_HEAD_ENTRY(pxEventList);   //获取等待接收列表的任务控制块
    configASSERT(pxUnblockedTCB);
    listREMOVE_ITEM(&(pxUnblockedTCB->xEventListItem)); //移除控制块的事件列表项

    if( uxSchedulerSuspended == ( UBaseType_t ) 0U )      //判断调度器是否被挂起
    {
        listREMOVE_ITEM( &( pxUnblockedTCB->xStateListItem ) );  //如未挂起,移除状态列表项,即从阻塞列表中将任务移除
        prvAddTaskToReadyList( pxUnblockedTCB );               //将任务添加进就序列表中,即唤醒任务

        #if ( configUSE_TICKLESS_IDLE != 0 )
        {
            prvResetNextTaskUnblockTime();
        }
        #endif
    }
    else
    {
		/* 否则只能先将任务移动至等待就绪列表 */
        listINSERT_END( &( xPendingReadyList ), &( pxUnblockedTCB->xEventListItem ) );
    }

    #if ( configNUMBER_OF_CORES == 1 )
    {
        if( pxUnblockedTCB->uxPriority > pxCurrentTCB->uxPriority )
        {
            xReturn = pdTRUE;
            xYieldPendings[ 0 ] = pdTRUE;
        }
        else
        {
            xReturn = pdFALSE;
        }
    }
    #else /* #if ( configNUMBER_OF_CORES == 1 ) */
    {
        xReturn = pdFALSE;

        #if ( configUSE_PREEMPTION == 1 )
        {
            prvYieldForTask( pxUnblockedTCB );

            if( xYieldPendings[ portGET_CORE_ID() ] != pdFALSE )
            {
                xReturn = pdTRUE;
            }
        }
        #endif /* #if ( configUSE_PREEMPTION == 1 ) */
    }
    #endif /* #if ( configNUMBER_OF_CORES == 1 ) */

    traceRETURN_xTaskRemoveFromEventList( xReturn );
    return xReturn;
}

③xTaskCheckForTimeOut函数的实现:

cpp 复制代码
BaseType_t xTaskCheckForTimeOut( TimeOut_t * const pxTimeOut,
                               TickType_t * const pxTicksToWait )
{
    BaseType_t xReturn;

    traceENTER_xTaskCheckForTimeOut( pxTimeOut, pxTicksToWait );

    configASSERT( pxTimeOut );
    configASSERT( pxTicksToWait );

    taskENTER_CRITICAL();
    {
        //当前系统节拍计数值
        const TickType_t xConstTickCount = xTickCount;  
		//当前系统节拍计数值 - 之前vTaskInternalSetTimeOutState获取的系统节拍计数值
        const TickType_t xElapsedTime =xConstTickCount -pxTimeOut->xTimeOnEntering;  

        #if ( INCLUDE_xTaskAbortDelay == 1 )
            if( pxCurrentTCB->ucDelayAborted != ( uint8_t ) pdFALSE )
            {
                pxCurrentTCB->ucDelayAborted = ( uint8_t ) pdFALSE;
                xReturn = pdTRUE;
            }
            else
        #endif

        #if ( INCLUDE_vTaskSuspend == 1 )   //如果使能了任务挂起,编译以下代码块
            if( *pxTicksToWait == portMAX_DELAY )   //若阻塞时间设为最大值
            {
                xReturn = pdFALSE;  //需要继续阻塞,返回pdFALSE
            }
            else
        #endif

        if( ( xNumOfOverflows != pxTimeOut->xOverflowCount ) && ( xConstTickCount >= pxTimeOut->xTimeOnEntering ) )
        {
		//判断当前系统节拍溢出次数与之前vTaskInternalSetTimeOutState获取的是否相同
		//同时判断当前系统节拍计数值是否大于等于之前vTaskInternalSetTimeOutState获取的系统节拍计数值
		//由此判断两个时刻的时间差值(判断它们之间相差的时间是不是大于等于2^32个系统节拍)
            xReturn = pdTRUE;  //没有剩余的块时间,并且发生了超时,返回pdTRUE
            *pxTicksToWait = ( TickType_t ) 0;  //阻塞时间更改为0
        }
        else if( xElapsedTime < *pxTicksToWait )
        {
		//原定的阻塞时间还没到,因为已经过去的时间还小于原定的阻塞时间
            *pxTicksToWait -= xElapsedTime;   //原定的阻塞时间减去已经过去的时间,剩下的时间就是需要继续阻塞的时间
            vTaskInternalSetTimeOutState( pxTimeOut );
            xReturn = pdFALSE;  //需要继续阻塞,返回pdFALSE
        }
        else
        {
		//原定的阻塞时间已到
            *pxTicksToWait = ( TickType_t ) 0;
            xReturn = pdTRUE;  //不需要继续阻塞,返回pdTRUE
        }
    }
    taskEXIT_CRITICAL();   //开中断(退出临界区)
    traceRETURN_xTaskCheckForTimeOut( xReturn );  //返回是否需要继续阻塞的结果
    return xReturn;
}

④vTaskPlaceOnEventList函数的实现:

cpp 复制代码
void vTaskPlaceOnEventList( List_t * const pxEventList, const TickType_t xTicksToWait )
{
    traceENTER_vTaskPlaceOnEventList( pxEventList, xTicksToWait );

    configASSERT( pxEventList );

    vListInsert( pxEventList, &( pxCurrentTCB->xEventListItem ) );  //将事件列表项挂到等待发送列表

    prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );  //将任务挂到阻塞列表中

    traceRETURN_vTaskPlaceOnEventList();
}

3、xQueueReceive函数

(1)如上所述,该函数用于从队列中读数据。

(2)xQueueReceive函数的实现:

cpp 复制代码
BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait )
{
    BaseType_t xEntryTimeSet = pdFALSE;
    TimeOut_t xTimeOut;
    Queue_t * const pxQueue = xQueue;

    traceENTER_xQueueReceive( xQueue, pvBuffer, xTicksToWait );

    configASSERT( ( pxQueue ) );

    configASSERT( !( ( ( pvBuffer ) == NULL ) && ( ( pxQueue )->uxItemSize != ( UBaseType_t ) 0U ) ) );

    #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
    {
        configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
    }
    #endif

    for( ; ; )
    {
        taskENTER_CRITICAL();   //关中断(进入临界区)
        {
            const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;  //获取非空闲队列项数目

            if( uxMessagesWaiting > ( UBaseType_t ) 0 )   //若队列中有数据,可执行读数据操作
            {
                prvCopyDataFromQueue( pxQueue, pvBuffer );  //将队列中的一个数据拷贝至pvBuffer中
                traceQUEUE_RECEIVE( pxQueue );
                pxQueue->uxMessagesWaiting = ( UBaseType_t ) ( uxMessagesWaiting - ( UBaseType_t ) 1 ); //非空闲队列项数目-1

                if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )  //如果等待发送列表中有任务
                {
                    if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )  //将等待发送列表中的一个任务移除
                    {
                        queueYIELD_IF_USING_PREEMPTION();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                taskEXIT_CRITICAL();  //开中断(退出临界区)

                traceRETURN_xQueueReceive( pdPASS );

                return pdPASS;
            }
            else
            {
                if( xTicksToWait == ( TickType_t ) 0 )
                {
					//如果预期的阻塞时间为0,则不等待,直接返回读错误信息errQUEUE_EMPTY
                    taskEXIT_CRITICAL();  //开中断

                    traceQUEUE_RECEIVE_FAILED( pxQueue );
                    traceRETURN_xQueueReceive( errQUEUE_EMPTY );

                    return errQUEUE_EMPTY;
                }
                else if( xEntryTimeSet == pdFALSE )
                {
				//记录此时系统节拍计数器和溢出次数,用于下面对阻塞时间进行补偿
                    vTaskInternalSetTimeOutState( &xTimeOut );
                    xEntryTimeSet = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
        taskEXIT_CRITICAL();    //开中断

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

        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )  //判断是否需要继续阻塞(补偿)
        {
            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )   //判断队列是否已空
            {
                traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );
				//将任务挂到等待接收列表中
                vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
                prvUnlockQueue( pxQueue );

                if( xTaskResumeAll() == pdFALSE )  //如需执行任务切换,执行即可
                {
                    taskYIELD_WITHIN_API();
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                prvUnlockQueue( pxQueue );   //队列解锁
                ( void ) xTaskResumeAll();    //恢复任务调度器
            }
        }
        else
        {
            prvUnlockQueue( pxQueue );   //队列解锁
            ( void ) xTaskResumeAll();    //恢复任务调度器

            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )
            {
                traceQUEUE_RECEIVE_FAILED( pxQueue );
                traceRETURN_xQueueReceive( errQUEUE_EMPTY );

                return errQUEUE_EMPTY;   //返回队列空的错误信息
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    }
}

(3)xQueueReceive函数中调用到的prvCopyDataFromQueue函数剖析:

cpp 复制代码
static void prvCopyDataFromQueue( Queue_t * const pxQueue, void * const pvBuffer )
{
    if( pxQueue->uxItemSize != ( UBaseType_t ) 0 )  //队列项不为0,则不是信号量队列
    {
        pxQueue->u.xQueue.pcReadFrom += pxQueue->uxItemSize;  //移动上一次读数据指针指向的位置到这次需要读数据的位置

        if( pxQueue->u.xQueue.pcReadFrom >= pxQueue->u.xQueue.pcTail )
        {
            pxQueue->u.xQueue.pcReadFrom = pxQueue->pcHead;   //如果读数据指针移至队列项存储区尾部,则跳回头部
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        ( void ) memcpy( ( void * ) pvBuffer, ( void * ) pxQueue->u.xQueue.pcReadFrom, ( size_t ) pxQueue->uxItemSize );  //将数据拷贝到pvBuffer中
    }
}

三、队列操作实验

1、原理图与实验目标

(1)原理图(未画出OLED屏,接法与stm32教程中的一致):

(2)实验目标:

①设计4个任务------start_task、task1、task2、task3:

[1]start_task:用于创建其它三个任务,然后删除自身。

[2]task1:当按键key0按下,将当前按键次数拷贝到队列key_queue中;当按键key1按下,将传输大容量数据,拷贝大容量数据的地址到队列big_date_queue中。(入队)

[3]task2:读取队列key_queue中的消息,在OLED屏上打印出按键次数。(出队)

[4]task3:从队列big_date_queue读取大容量数据地址,通过地址访问大容量数据,并通过OLED显示。(出队)

②预期实验现象:

[1]程序下载到板子上后,暂时没有任何现象。

[2]按下相关按键,OLED屏会有相应变化。

2、实验步骤

(1)将"任务创建和删除的动态方法实验"的工程文件夹复制一份,在拷贝版中进行实验。

(2)在FreeRTOS_experiment.c文件中添加头文件queue.h,并定义一个整型数组(全局变量,作为大容量数据)以及两个队列句柄(分别为记录按键次数的队列和大容量数据队列)。

cpp 复制代码
#include "queue.h"

int buffer[5] = {13, 32, 26, 114, 51};

QueueHandle_t key_queue;        //记录按键次数的队列句柄
QueueHandle_t big_data_queue;    //大容量数据队列句柄

(3)在FreeRTOS_Test函数中需要创建记录按键次数的队列和大容量数据队列,与它们的句柄一一对应。

cpp 复制代码
void FreeRTOS_Test(void)
{
	key_queue = xQueueCreate(2, sizeof(uint8_t));       //创建队列key_queue(如果返回值为NULL说明创建失败,可以进行后处理),容量(长度)为2
	big_data_queue = xQueueCreate(2, sizeof(int*));     //创建队列big_data_queue(如果返回值为NULL说明创建失败,可以进行后处理),容量(长度)为2
	
	xTaskCreate((TaskFunction_t)start_task,              //指向任务函数的指针
				"start_task",                         //任务名字
				START_TASK_STACK_SIZE,          //任务堆栈大小,单位为字
				NULL,                             //传递给任务函数的参数
				START_TASK_PRIO,                 //任务优先级
				(TaskHandle_t *) &start_task_handler    //任务句柄
				);
	vTaskStartScheduler();
}

(4)更改task1、task2和task3函数的实现。

cpp 复制代码
void task1(void)
{
	uint8_t key = 0;
	unsigned int num = 0;      //记录按键1按下次数
	int* buf = buffer;         //xQueueSend中不能直接传&buffer
	while(1)
	{
		key = Key_GetNum();    //读取按键键值
		if(key == 1)
		{
			num++;              //修改计数值
			BaseType_t error_1;    //如果出现写失败可做相应后处理
			error_1 = xQueueSend(key_queue, &num, portMAX_DELAY);  //写不进去就死等
		}
		else if(key == 2)
		{
			buffer[1]++;           //修改大容量数据本身的值
			BaseType_t error_2;    //如果出现写失败可做相应后处理
			error_2 = xQueueSend(big_data_queue, &buf, portMAX_DELAY);  //写不进去就死等
		}
		vTaskDelay(10);  //延时(自我阻塞)10ms
	}
}
void task2(void)
{
	int buffer1 = 0;
	while(1)
	{
		BaseType_t error_3;    //如果出现读失败可做相应后处理
		error_3 = xQueueReceive(key_queue, &buffer1, portMAX_DELAY);  //读不出来就死等
		OLED_ShowNum(1, 1, buffer1, 5);
		vTaskDelay(10);  //延时(自我阻塞)10ms
	}
}

void task3(void)
{
	int* buffer2 = NULL;
	while(1)
	{
		BaseType_t error_4;    //如果出现读失败可做相应后处理
		error_4 = xQueueReceive(big_data_queue, &buffer2, portMAX_DELAY);  //读不出来就死等
		OLED_ShowNum(2, 1, buffer[1], 5);  //显示大容量数据本身的一部分(用于对比)
		OLED_ShowNum(3, 1, buffer2[1], 5); //显示通过地址访问的大容量数据的一部分
		vTaskDelay(10);  //延时(自我阻塞)10ms
	}
}

(5)程序完善好后点击"编译",然后将程序下载到开发板上,根据程序注释进行调试。

3、程序执行流程

(1)main函数全流程:

①初始化OLED模块、按键模块、LED模块。

②调用FreeRTOS_Test函数。

(2)测试函数全流程:

①创建记录按键次数的队列和大容量数据队列(下图未示出)。

②创建任务start_task。

③开启任务调度器。

(3)多任务调度执行阶段(发生在开启任务调度器以后):

①在前面的实验中已对任务调度方面做了多次详细解释,从本实验开始简单的任务调度将不再详解。

②按下按键1,task1函数中记录的按键1按下次数自增,并将按键次数写进队列key_queue中;紧接着,原本task2函数中的xQueueReceive函数因为key_queue为空读不到数据而进入无限阻塞,现在key_queue有数据,xQueueReceive函数则将key_queue中的数据,也就是新的按键1按下次数取出,并显示在OLED屏上;以此往复,每次按下按键1,task1就会往key_queue中写一个数据,而task2会将它马上取出。

③按下按键2,task1函数先修改大容量数据本身的值,然后将大容量数据的地址写进队列big_data_queue中;紧接着,原本task3函数中的xQueueReceive函数因为big_data_queue为空读不到数据而进入无限阻塞,现在big_data_queue有数据,xQueueReceive函数则将big_data_queue中的数据,也就是大容量数据的地址取出,并通过这个地址访问大容量数据,显示一部分在OLED屏上;以此往复,每次按下按键2,task1就会往big_data_queue中写一个数据的地址,而task3会将它马上取出,然后可以通过地址访问数据。

相关推荐
弥途2 小时前
自制一个入门STM32 四足机器人具体开发顺序
stm32·单片机·机器人
马浩同学5 小时前
【ESP32】ESP-IDF开发 | WiFi开发 | UDP用户数据报协议 + UDP客户端和服务器例程
c语言·单片机·mcu·网络协议·udp
promising-w5 小时前
单片机基础模块学习——超声波传感器
嵌入式硬件
水饺编程5 小时前
简易CPU设计入门:控制总线的剩余信号(四)
linux·嵌入式硬件·fpga开发·硬件工程
暮雪倾风7 小时前
【硬件介绍】三极管工作原理(图文+典型电路设计)
单片机·嵌入式硬件
aoaoGofei9 小时前
STM32-时钟树
stm32·单片机·嵌入式硬件
Zevalin爱灰灰9 小时前
FreeRTOS从入门到精通 第十三章(信号量)
stm32·单片机·嵌入式硬件·操作系统·freertos
厂太_STAB_丝针11 小时前
【自学嵌入式(6)天气时钟:软硬件准备、串口模块开发】
c语言·单片机·嵌入式硬件
未知陨落13 小时前
冯诺依曼系统及操作系统
linux·操作系统
勿忘初心9115 小时前
Android车机DIY开发之学习篇(七)NDK交叉工具构建
arm开发·单片机·嵌入式硬件·学习