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,其它任务打算让数据入队时会死等(期间任务将会进入阻塞态),一直等到可以入队为止。 ![](https://i-blog.csdnimg.cn/direct/8990cbddcd1e4f7398e92ee4d06466f8.png) 假设此时队列已空无法出队(出队阻塞): \[1\]若阻塞时间为0,其它任务打算让数据出队时直接无法出队,并返回pdFALSE。 \[2\]若阻塞时间为0\~port_MAX_DELAY,其它任务打算让数据出队时会等待设定的阻塞时间(期间任务将会进入阻塞态),若在该时间内还无数据可出队,超时后直接返回pdFALSE,不再等待。 \[3\]若阻塞时间为port_MAX_DELAY,其它任务打算让数据出队时会死等(期间任务将会进入阻塞态),一直等到可以出队为止。 ![](https://i-blog.csdnimg.cn/direct/0223bfff678146b59d08f942d23f88a2.png) 当多个任务写入消息给一个"满队列"时,这些任务都会进入阻塞状态,也就是说有多个任务在等待同一个队列的空间,当队列中有空间时,优先级最高的任务会最先进入就绪态并获得队列"使用权",如果诸任务的优先级相同,那等待时间最久的任务会进入就绪态。 (5)队列操作基本过程举例: ![](https://i-blog.csdnimg.cn/direct/f2ca3e0277ab40c99119ecb87e8ce7b2.png) ### 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)队列结构体整体示意图: ![](https://i-blog.csdnimg.cn/direct/b0b0d324c8fd4f499f119968dc2fcda6.png) ### 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教程中的一致): ![](https://i-blog.csdnimg.cn/direct/1cb022cf753a4796adb313dbf80245cd.png) (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函数。 ![](https://i-blog.csdnimg.cn/direct/641252689ea042dbaf318315006b64a4.png) (2)测试函数全流程: ①创建记录按键次数的队列和大容量数据队列(下图未示出)。 ②创建任务start_task。 ③开启任务调度器。 ![](https://i-blog.csdnimg.cn/direct/015c3d3d06da4e41bc9debb96257f594.png) (3)多任务调度执行阶段(发生在开启任务调度器以后): ①在前面的实验中已对任务调度方面做了多次详细解释,从本实验开始简单的任务调度将不再详解。 ②按下按键1,task1函数中记录的按键1按下次数自增,并将按键次数写进队列key_queue中;紧接着,原本task2函数中的xQueueReceive函数因为key_queue为空读不到数据而进入无限阻塞,现在key_queue有数据,xQueueReceive函数则将key_queue中的数据,也就是新的按键1按下次数取出,并显示在OLED屏上;以此往复,每次按下按键1,task1就会往key_queue中写一个数据,而task2会将它马上取出。 ![](https://i-blog.csdnimg.cn/direct/07582a0a2c0a4f7b8fcd6520f6fec522.png) ![](https://i-blog.csdnimg.cn/direct/a528122043ae422c84385c96a6d4c57e.png) ③按下按键2,task1函数先修改大容量数据本身的值,然后将大容量数据的地址写进队列big_data_queue中;紧接着,原本task3函数中的xQueueReceive函数因为big_data_queue为空读不到数据而进入无限阻塞,现在big_data_queue有数据,xQueueReceive函数则将big_data_queue中的数据,也就是大容量数据的地址取出,并通过这个地址访问大容量数据,显示一部分在OLED屏上;以此往复,每次按下按键2,task1就会往big_data_queue中写一个数据的地址,而task3会将它马上取出,然后可以通过地址访问数据。 ![](https://i-blog.csdnimg.cn/direct/a0b243ece1de4007b8586ea5dd76197e.png) ![](https://i-blog.csdnimg.cn/direct/ba6ed11f1c5a4c2882087f0daaf2068a.png)

相关推荐
YdaMooc9 小时前
STM32-FreeRTOS的详细配置
stm32·单片机·嵌入式硬件
云山工作室9 小时前
基于STM32的智能门禁系统
stm32·单片机·毕业设计·毕设
无处在10 小时前
STM32 四足机器人常见问题汇总
stm32·嵌入式硬件·机器人
南梦也要学习12 小时前
STM32江科大-----PWR电源控制
stm32·单片机·嵌入式硬件
时光の尘13 小时前
FreeRTOS菜鸟入门(五)·空闲任务与阻塞延时的实现
c语言·stm32·嵌入式硬件·mcu·物联网·freertos
正点原子14 小时前
【正点原子STM32MP257连载】第四章 ATK-DLMP257B功能测试——EEPROM、SPI FLASH测试 #AT24C64 #W25Q128
linux·stm32·单片机·嵌入式硬件·stm32mp257
光芒Shine17 小时前
【STM32-代码】
stm32·单片机·嵌入式硬件
无垠的广袤17 小时前
【树莓派 PICO 2 测评】采集 DS18B20 数据及 OLED 显示
单片机·嵌入式硬件·物联网
冻结的鱼17 小时前
两个 STM32G0 I2C 通信异常的案例分析
stm32·单片机·嵌入式硬件
总结所学17 小时前
arm_math.h、arm_const_structs.h 和 arm_common_tables.h
arm开发·单片机·嵌入式硬件