欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 名人名企 > FreeRTOS菜鸟入门(十)·消息队列

FreeRTOS菜鸟入门(十)·消息队列

2025/5/5 20:15:28 来源:https://blog.csdn.net/MANONGDKY/article/details/147593970  浏览:    关键词:FreeRTOS菜鸟入门(十)·消息队列

目录

1.  基本概念

2.  数据存储

3.  运作机制

4.  阻塞机制

4.1  出队阻塞

4.2  入队阻塞

5.  操作示意图

5.1  创建队列

5.2  向队列发送第一个消息

5.3  向队列发送第二个消息

5.4  从队列读取消息

6.  消息队列控制块

7.  消息队列常用函数

7.1  消息队列创建函数 xQueueCreate()

7.2  消息队列静态创建函数  xQueueCreateStatic()

7.3  消息队列删除函数  vQueueDelete()

7.4  向消息队列发送消息函数

7.4.1  xQueueSend()与 xQueueSendToBack()

7.4.2  xQueueSendFromISR()与 xQueueSendToBackFromISR()

7.4.3  xQueueSendToFront()

7.4.4  xQueueSendToFrontFromISR()

7.4.5  通用消息队列发送函数 xQueueGenericSend()

7.4.6  消息队列发送函数 xQueueGenericSendFromISR()

7.5  从消息队列读取消息函数

7.5.1  xQueueReceive()与 xQueuePeek()

7.5.2  xQueueReceiveFromISR()与 xQueuePeekFromISR()


1.  基本概念

        队列又称消息队列,是一种常用于任务间通信的数据结构,消息队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。消息队列是一种异步的通信方式。

        FreeRTOS 中使用队列数据结构实现任务异步通信工作,具有如下特性:

  • 消息支持先进先出方式排队,支持异步读写工作方式。
  • 读写队列均支持超时机制。
  • 消息支持后进先出方式排队,往队首发送消息(LIFO)。
  • 可以允许不同长度(不超过队列节点最大值)的任意类型消息。
  • 一个任务能够从任意一个消息队列接收和发送消息。
  • 多个任务能够从同一个消息队列接收和发送消息。
  • 当队列使用结束后,可以通过删除队列函数进行删除。

2.  数据存储

        通过消息队列服务,任务或中断服务例程可以将一条或多条消息放入消息队列中。同样,一个或多个任务可以从消息队列中获得消息。当有多个消息发送到消息队列时,通常是将先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入消息队列的消息,即先进先出原则(FIFO),但是也支持后进先出原则(LIFO)。

        数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传递

3.  运作机制

        创建消息队列时 FreeRTOS 会先给消息队列分配一块内存空间,这块内存的大小等于消息队列控制块大小加上(单个消息空间大小与消息队列长度的乘积),接着再初始化消息队列,此时消息队列为空。

        FreeRTOS 的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针 pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength 等。(先大概有个了解,后续讲解代码在着重讲解)同时每个消息队列都与消息空间在同一段连续的内存空间中,在创建成功的时候,这些内存就被占用了,只有删除了消息队列的时候,这段内存才会被释放掉,创建成功的时候就已经分配好每个消息空间与消息队列的容量,无法更改,每个消息空间可以存放不大于消息大小 uxItemSize 的任意类型的数据,所有消息队列中的消息空间总数即是消息队列的长度,这个长度可在消息队列创建时指定。

        任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队,FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码 errQUEUE_FULL。

        发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的位置是消息队列队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。

        当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

        当消息队列不再被使用时,应该删除它以释放系统资源,一旦操作完成,消息队列将被永久性的删除。

4.  阻塞机制

4.1  出队阻塞

当任务尝试从一个队列读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列读取消息无效的时候任务阻塞的时间。

        假设有一个任务 A 对某个队列进行读操作的时候(也就是我们所说的出队),发现它没有消息,那么此时任务 A有3个选择:

        第一个选择,任务 A 扭头就走,既然队列没有消息,那我也不等了,干其它事情去,这样子任务 A 不会进入阻塞态;

        第二个选择,任务 A 还是在这里等等吧,可能过一会队列就有消息,此时任务 A 会进入阻塞状态,在等待着消息的道来,而任务 A 的等待时间就由我们自己定义,比如设置 1000 个系统时钟节拍 tick 的等待,在这 1000 个 tick 到来之前任务 A 都是处于阻塞态,当阻塞的这段时间任务 A 等到了队列的消息,那么任务 A 就会从阻塞态变成就绪态,如果此时任务 A 比当前运行的任务优先级还高,那么,任务 A 就会得到消息并且运行;假如 1000 个 tick 都过去了,队列还没消息,那任务 A 就不等了,从阻塞态中唤醒,返回一个没等到消息的错误代码,然后继续执行任务 A 的其他代码;

        第三个选择,任务 A 死等,不等到消息就不走了,这样子任务 A 就会进入阻塞态,直到完成读取队列的消息。

4.2  入队阻塞

入队说的是向队列中发送消息,将消息加入到队列中,和出队阻塞一样,当一个任务向队列发送消息的话也可以设置阻塞时间。

        而在发送消息操作的时候,为了保护数据,当且仅当队列允许入队的时候,发送者才能成功发送消息;队列中无可用消息空间时,说明消息队列已满,此时,系统会根据用户指定的阻塞超时时间将任务阻塞,在指定的超时时间内如果还不能完成入队操作,发送消息的任务或者中断服务程序会收到一个错误码 errQUEUE_FULL,然后解除阻塞状态;当然,只有在任务中发送消息才允许进行阻塞状态,而在中断中发送消息不允许带有阻塞机制的,需要调用在中断中发送消息的 API 函数接口,因为发送消息的上下文环境是在中断中,不允许有阻塞的情况。假如有多个任务阻塞在一个消息队列中,那么这些阻塞的任务将按照任务优先级进行排序,优先级高的任务将优先获得队列的访问权。

5.  操作示意图

5.1  创建队列

        首先创建两个任务A和B,任务A想向任务B发送消息,这个消息变量为x,我们在创建队列的时候需要创建其对列的长度以及每个消息的长度,消息的长度需要看好其数据类型,此时x的数据类型为int,占据四个字节,因此消息的长度为4:

5.2  向队列发送第一个消息

        任务 A 的变量 x 值为 10,将这个值发送到消息队列中。此时队列剩余长度就是 3 了。前面说了向队列中发送消息是采用拷贝的方式,所以一旦消息发送完成变量 x 就可以再次被使用,赋其他的值:

5.3  向队列发送第二个消息

        任务 A 又向队列发送了一个消息,即新的 x 的值,这里是 20。紧跟在上一个数据的后面,此时队列剩余长度为 2。

5.4  从队列读取消息

        任务 B 从队列中读取消息,并将读取到的消息值赋值给 y,这样 y 就等于 10 了。任务 B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。当选择清除这个 消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加一,变成 3。如果 不清除的话其他任务或中断也可以获取这个消息,而队列剩余大小依旧是 2。

6.  消息队列控制块

        FreeRTOS 的消息队列控制块由多个元素组成,当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的一些信息如消息的存储位置,头指针 pcHead、尾指针 pcTail、消息大小 uxItemSize 以及队列长度 uxLength,以及当前队列消息个数 uxMessagesWaiting 等:

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;          /*< 存储在队列锁定时从队列中接收(移除)的项数。当队列未锁定时设置为 `queueUNLOCKED`。 */volatile int8_t cTxLock;          /*< 存储在队列锁定时发送到队列(添加)的项数。当队列未锁定时设置为 `queueUNLOCKED`。 */#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )uint8_t ucStaticallyAllocated; /*< 如果队列使用的内存是静态分配的,则设置为 `pdTRUE`,以确保不尝试释放内存。 */#endif#if ( configUSE_QUEUE_SETS == 1 )struct QueueDefinition *pxQueueSetContainer; /*< 指向包含此队列的队列集合的指针。 */#endif#if ( configUSE_TRACE_FACILITY == 1 )UBaseType_t uxQueueNumber;      /*< 队列的唯一编号。 */uint8_t ucQueueType;            /*< 队列的类型。 */#endif} xQUEUE;

7.  消息队列常用函数

7.1  消息队列创建函数 xQueueCreate()

        xQueueCreate()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。队列句柄其实就是一个指向队列数据结构类型的指针。

        队列就是一个数据结构,用于任务间的数据的传递。每创建一个新的队列都需要为其分配 RAM,一部分用于存储队列的状态,剩下的作为队列消息的存储区域。使用xQueueCreate()创建队列时,使用的是动态内存分配,所以要想使用该函数必须在FreeRTOSConfig.h 中把 configSUPPORT_DYNAMIC_ALLOCATION 定义为 1 来使能,这是个用于使能动态内存分配的宏,通常情况下,在 FreeRTOS 中,凡是创建任务,队列,信号量和互斥量等内核对象都需要使用动态内存分配,所以这个宏默认在 FreeRTOS.h 头文件中已经使能(即定义为 1)。如果想使用静态内存,则可以使用 xQueueCreateStatic() 函数来创建一个队列。使用静态创建消息队列函数创建队列时需要的形参更多,需要的内存由编译的时候预先分配好,一般很少使用这种方法。

函数原型:

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xQueueCreate( uxQueueLength, uxItemSize ) \ 
xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) ) 
#endif

函数说明:

函数原型QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_tuxItemSize);
功能用于创建一个新的队列。
参数uxQueueLength队列能够存储的最大消息单元数目,即队列长度。
uxltemSize队列中消息单元的大小,以字节为单位。
返回值如果创建成功则返回一个队列句柄,用于访问创建的队列。如果创建不成功则返回NULL,可能原因是创建队列需要的RAM无法分配成功。

        从函数原型中,我们可以看到,创建队列真正使用的函数是 xQueueGenericCreate(),消息队列创建函数,顾名思义,就是创建一个队列,与任务一样,都是需要先创建才能使用的东西,FreeRTOS 肯定不知道我们需要什么样的队列,比如队列的长度,消息的大小这些信息都是需要我们自己定义的,FreeRTOS 提供给我们这个创建函数,爱怎么搞都是我们自己来实现,下面来看看 xQueueGenericCreate()函数源码:

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType ){Queue_t *pxNewQueue;size_t xQueueSizeInBytes;uint8_t *pucQueueStorage;configASSERT( uxQueueLength > ( UBaseType_t ) 0 );if( uxItemSize == ( UBaseType_t ) 0 ){/* There is not going to be a queue storage area. 消息空间大小为 0*/xQueueSizeInBytes = ( size_t ) 0;//如果 uxItemSize 为 0,也就是单个消息空间大小为 0,这样子就不需要申请内存了,那么 xQueueSizeInBytes 也设置为 0 即可,设置为 0 是可以的,用作信号量的时候这个就可以设置为 0。}else{/* Allocate enough space to hold the maximum number of items thatcan be in the queue at any time. 分配足够消息存储空间,空间的大小为队列长度*单个消息大小*/xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */}/* 向系统申请内存,内存大小为消息队列控制块大小+消息存储空间大小 */pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );if( pxNewQueue != NULL ){/* Jump past the queue structure to find the location of the queuestorage area. 计算出消息存储空间的起始地址*/pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t );#if( configSUPPORT_STATIC_ALLOCATION == 1 ){/* Queues can be created either statically or dynamically, sonote this task was created dynamically in case it is laterdeleted. */pxNewQueue->ucStaticallyAllocated = pdFALSE;}#endif /* configSUPPORT_STATIC_ALLOCATION */prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );//①}return pxNewQueue;}#endif /* configSUPPORT_STATIC_ALLOCATION */

        FreeRTOS 调用 pvPortMalloc()函数向系统申请内存空间,内存大小为消息队列控制块大小加上消息存储空间大小,因为这段内存空间是需要保证连续的:

        ①:调用 prvInitialiseNewQueue()函数将消息队列进行初始化。其实xQueueGenericCreate()主要是用于分配消息队列内存的,消息队列初始化函数源码:

/*
const UBaseType_t uxQueueLength:消息队列长度;
const UBaseType_t uxItemSize:单个消息大小;
uint8_t *pucQueueStorage:存储消息起始地址;
const uint8_t ucQueueType:消息队列类型● queueQUEUE_TYPE_BASE:表示队列。● queueQUEUE_TYPE_SET:表示队列集合 。● queueQUEUE_TYPE_MUTEX:表示互斥量。● queueQUEUE_TYPE_COUNTING_SEMAPHORE:表示计数信号量。● queueQUEUE_TYPE_BINARY_SEMAPHORE:表示二进制信号量。● queueQUEUE_TYPE_RECURSIVE_MUTEX :表示递归互斥量。
Queue_t *pxNewQueue:消息队列控制块
*/
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, uint8_t *pucQueueStorage, const uint8_t ucQueueType, Queue_t *pxNewQueue )
{/* Remove compiler warnings about unused parameters shouldconfigUSE_TRACE_FACILITY not be set to 1. */( void ) ucQueueType;if( uxItemSize == ( UBaseType_t ) 0 ){/* No RAM was allocated for the queue storage area, but PC head cannotbe set to NULL because NULL is used as a key to say the queue is used asa mutex.  Therefore just set pcHead to point to the queue as a benignvalue that is known to be within the memory map. 没有为消息存储分配内存,但是 pcHead 指针不能设置为 NULL,因为队列用作互斥量时,pcHead 要设置成 NULL。这里只是将 pcHead 指向一个已知的区域*/pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;}else{/* Set the head to the start of the queue storage area.设置 pcHead 指向存储消息的起始地址 */pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;}/* Initialise the queue members as described where the queue type isdefined. 初始化消息队列控制块的其他成员*/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 );
}

        ②重置消息队列,在消息队列初始化的时候,需要重置一下相关参数,具体如下:

BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{
Queue_t * const pxQueue = ( Queue_t * ) xQueue;configASSERT( pxQueue );taskENTER_CRITICAL();//进入临界段{pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->uxItemSize );//重置消息队列的成员变量,pcTail 指向存储消息内存空间的结束地址。pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U;//当前消息队列中的消息个数 uxMessagesWaiting 为 0。pxQueue->pcWriteTo = pxQueue->pcHead;//pcWriteTo 指向队列消息存储区下一个可用消息空间,因为是重置消息队列,就指向消息队列的第一个消息空间,也就是 pcHead 指向的空间。pxQueue->u.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - ( UBaseType_t ) 1U ) * pxQueue->uxItemSize );//pcReadFrom 指向消息队列最后一个消息空间。pxQueue->cRxLock = queueUNLOCKED;//消息队列没有上锁,设置为 queueUNLOCKED。pxQueue->cTxLock = queueUNLOCKED;if( xNewQueue == pdFALSE ){/* If there are tasks blocked waiting to read from the queue, thenthe tasks will remain blocked as after this function exits the queuewill still be empty.  If there are tasks blocked waiting to write tothe queue, then one should be unblocked as after this function exitsit will be possible to write to it. 如果不是新建一个消息队列,那么之前的消息队列可能阻塞了一些任务,需要将其解除阻塞。如果有发送消息任务被阻塞,那么需要将它恢复,而如果任务是因为读取消息而阻塞,那么重置之后的消息队列也是空的,则无需被恢复。*/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{/* Ensure the event queues start in the correct state. */vListInitialise( &( pxQueue->xTasksWaitingToSend ) );vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );}}taskEXIT_CRITICAL();/* A value is returned for calling semantic consistency with previousversions. */return pdPASS;
}

消息队列创建完成示意图:

        在创建消息队列的时候,是需要用户自己定义消息队列的句柄的,但是注意了,定义了队列的句柄并不等于创建了队列,创建队列必须是调用消息队列创建函数进行创建(可以是静态也可以是动态创建),否则,以后根据队列句柄使用消息队列的其它函数的时候会发生错误,创建完成会返回消息队列的句柄,用户通过句柄就可使用消息队列进行发送与读取消息队列的操作,如果返回的是 NULL 则表示创建失败,举个例子:

在此模版上进行修改:

基于STM32F103ZET6的FreeRTOS移植任务创建-动态创建多个任务资源-CSDN文库

        创建一个内核对象句柄:

/********************************** 内核对象句柄 *********************************/
/** 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我* 们就可以通过这个句柄操作这些内核对象。** 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数* 来完成的* */
QueueHandle_t Test_Queue =NULL;

        我们知道要想创建一个消息队列,需要对队列长度,消息队列的消息大小进行分配:

#define  QUEUE_LEN    4   /* 队列的长度,最大可包含多少个消息 */
#define  QUEUE_SIZE   4   /* 队列中每个消息大小(字节) */

        消息队列创建函数:

//任务创建函数
static void AppTaskCreate(void)
{BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */taskENTER_CRITICAL();           //进入临界区/* 创建Test_Queue */Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */if(NULL != Test_Queue)printf("创建Test_Queue消息队列成功!\r\n");vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务taskEXIT_CRITICAL();            //退出临界区
}

        消息队列创建函数默认的是动态创建的方法。通常情况下,在 FreeRTOS 中,凡是创建任务,队列, 信号量和互斥量等内核对象都需要使用动态内存分配,使用静态创建消息队列函数创建队列时需要的形参更多,需要的内存 由编译的时候预先分配好,一般很少使用这种方法。

完整工程:

FreeRTOS消息队列-创建消息队列.zip资源-CSDN文库

运行结果:

7.2  消息队列静态创建函数  xQueueCreateStatic()

        xQueueCreateStatic()用于创建一个新的队列并返回可用于访问这个队列的队列句柄。队列句柄其实就是一个指向队列数据结构类型的指针。

        使用xQueueCreateStatic()创建队列时,使用的是静态内存分配,所以要想使用该函数必须在FreeRTOSConfig.h 中把 configSUPPORT_STATIC_ALLOCATION 定义为 1 来使能。这是个用于使能静态内存分配的宏,需要的内存在程序编译的时候分配好,由用户自己定义,其实创建过程与 xQueueCreate()都是差不多的:

函数原型

QueueHandle_t xQueueCreateStatic(UBaseType_tuxQueueLength,

                                                            UBaseType_t uxltemSize,

                                                            uint8_t *pucQueueStorageBuffer,

                                                            StaticQueue_t *pxQueueBuffer);

功能用于创建一个新的队列。
参数uxQueueLength队列能够存储的最大单元数目,即队列深度。
uxItemSize队列中数据单元的长度,以字节为单位。
pucQueueStorageBuffer指针,指向一个uint8_t类型的数组,数组的大小至少有uxQueueLength* uxltemSize个字节。当uxItemSize为0时,pucQueueStorageBuffer可以为NULL.
pxQueueBuffer指针,指向 StaticQueue_t 类型的变量,该变量用于存储队列的数据结构。
返回值如果创建成功则返回一个队列句柄,用于访问创建的队列。如果创建不成功则返回NULL,可能原因是创建队列需要的RAM无法分配成功。

静态任务创建模版:

基于STM32F103ZET6的FreeRTOS静态任务创建模版资源-CSDN文库

        用静态方法创建和动态方法差不多,只是静态方法需要自己手动分配内存,比较麻烦一些,首先对队列长度以及队列数据大小进行一个宏定义:

/* 创建一个可以最多可以存储 10 个 64 位变量的队列 */
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint64_t )

        创建一个变量用于用于存储队列的数据结构:

static StaticQueue_t xStaticQueue;

        通过上面创建函数我们了解到为了分配足够消息存储空间,空间的大小为队列长度*单个消息大小,静态变量需要自己分配内存,因此我们将队列的存储区域分配为:

uint8_t ucQueueStorageArea[ QUEUE_LENGTH * ITEM_SIZE ];

        静态创建函数代码:

static void AppTaskCreate(void)
{taskENTER_CRITICAL();           //进入临界区QueueHandle_t xQueue;/* 创建一个队列 */ xQueue = xQueueCreateStatic( QUEUE_LENGTH, /* 队列深度 */ ITEM_SIZE, /* 队列数据单元的单位 */ ucQueueStorageArea,/* 队列的存储区域 */ &xStaticQueue ); /* 队列的数据结构 */ if(xQueue != NULL) {printf("静态队列创建成功!\r\n");} else {printf("静态队列创建失败!\r\n");}vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务taskEXIT_CRITICAL();            //退出临界区
}

完整工程:
FreeRTOS消息队列-静态创建消息队列.zip资源-CSDN文库

运行结果:

7.3  消息队列删除函数  vQueueDelete()

        队列删除函数是根据消息队列句柄直接删除的,删除之后这个消息队列的所有信息都会被系统回收清空,而且不能再次使用这个消息队列了,但是需要注意的是,如果某个消息队列没有被创建,那也是无法被删除的,xQueue 是 vQueueDelete()函数的形参,是消息队列句柄,表示的是要删除哪个想队列:

void vQueueDelete( QueueHandle_t xQueue )
{Queue_t * const pxQueue = ( Queue_t * ) xQueue;/* 断言 */configASSERT( pxQueue ); //(1)traceQUEUE_DELETE( pxQueue );#if ( configQUEUE_REGISTRY_SIZE > 0 ){/* 将消息队列从注册表中删除,我们目前没有添加到注册表中,暂时不用理会 */vQueueUnregisterQueue( pxQueue ); //(2)}#endif#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) ) {/* 因为用的消息队列是动态分配内存的,所以需要调用vPortFree 来释放消息队列的内存 */vPortFree( pxQueue ); //(3)}
}

(1):对传入的消息队列句柄进行检查,如果消息队列是有效的才允许进行删除操作。
(2):将消息队列从注册表中删除,我们目前没有添加到注册表中,暂时不用理会。
(3):因为用的消息队列是动态分配内存的,所以需要调用 vPortFree()函数来释放消息队列的内存。

        消息队列删除函数 vQueueDelete()的使用也是很简单的,只需传入要删除的消息队列的句柄即可,调用函数时,系统将删除这个消息队列。需要注意的是调用删除消息队列函数前,系统应存在 xQueueCreate()或 xQueueCreateStatic()函数创建的消息队列。此外vQueueDelete()也可用于删除信号量。如果删除消息队列时,有任务正在等待消息,则不应该进行删除操作(官方说的是不允许进行删除操作,但是源码并没有禁止删除的操作,使用的时候注意一下就行了),举个例子:

        这里我们在上面7.1的代码的基础上进行修改,消息队列删除函数 vQueueDelete()的使用非常的简单,只需传入要删除的消息队列的句柄即可:

vQueueDelete( Test_Queue );//删除已创建的消息队列
//任务创建函数
static void AppTaskCreate(void)
{BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */taskENTER_CRITICAL();           //进入临界区/* 创建Test_Queue */Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */if(NULL != Test_Queue){printf("创建Test_Queue消息队列成功!\r\n");vQueueDelete( Test_Queue );//删除已创建的消息队列}else{printf("创建Test_Queue消息队列失败!\r\n");	}vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务taskEXIT_CRITICAL();            //退出临界区
}

        不过这样我们发现,我们无法看到消息队列是否删除成功,我们可以加一个if语句进行判断,开始我是想判断if(NULL == Test_Queue),即:

//任务创建函数
static void AppTaskCreate(void)
{BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */taskENTER_CRITICAL();           //进入临界区/* 创建Test_Queue */Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */if(NULL != Test_Queue){printf("创建Test_Queue消息队列成功!\r\n");vQueueDelete( Test_Queue );//删除已创建的消息队列if(NULL == Test_Queue){printf("删除Test_Queue消息队列成功!\r\n");}else{printf("删除Test_Queue消息队列失败!\r\n");			}}else{printf("创建Test_Queue消息队列失败!\r\n");	}vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务taskEXIT_CRITICAL();            //退出临界区
}

        但是了解发现在 FreeRTOS 中,消息队列被删除后,其句柄会变为无效,但 FreeRTOS 不会自动将句柄设为 NULL,需要在删除队列后,手动将句柄置为 NULL,后续通过检查 NULL 判断队列是否存在:

//任务创建函数
static void AppTaskCreate(void)
{BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */taskENTER_CRITICAL();           //进入临界区/* 创建Test_Queue */Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */if(NULL != Test_Queue){printf("创建Test_Queue消息队列成功!\r\n");vQueueDelete( Test_Queue );//删除已创建的消息队列Test_Queue = NULL;//手动置空if(NULL == Test_Queue){printf("删除Test_Queue消息队列成功!\r\n");}else{printf("删除Test_Queue消息队列失败!\r\n");			}}else{printf("创建Test_Queue消息队列失败!\r\n");	}vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务taskEXIT_CRITICAL();            //退出临界区
}

        这时候运行可以发现: 

解释一下原因:

在 FreeRTOS 中,当调用 vQueueDelete() 删除一个队列时,系统会释放该队列占用的内存,但不会自动将队列句柄(指针)设为 NULL。这意味着句柄变成"悬空指针",删除队列后,句柄仍然指向原来的内存地址,但该地址已被 FreeRTOS 回收,如果后续误用该句柄(如调用 xQueueSend()),可能导致内存访问冲突(HardFault),调试时难以区分 "有效队列" 和 "已删除队列"。

        根据上面我们又需要思考,这一块的条件成立,是因为手动置空的原因还是删除的原因:

		Test_Queue = NULL;//手动置空if(NULL == Test_Queue){printf("删除Test_Queue消息队列成功!\r\n");}else{printf("删除Test_Queue消息队列失败!\r\n");			}

        我们可以在删除前面加一个判断进行验证:

static void AppTaskCreate(void)
{BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */taskENTER_CRITICAL();           //进入临界区/* 创建Test_Queue */Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */if(NULL != Test_Queue){printf("创建Test_Queue消息队列成功!\r\n");Test_Queue = NULL;//手动置空if(NULL == Test_Queue){printf("Test_Queue被手动删除!\r\n");}else{printf("Test_Queue未被手动删除!\r\n");			}vQueueDelete( Test_Queue );//删除已创建的消息队列Test_Queue = NULL;//手动置空if(NULL == Test_Queue){printf("删除Test_Queue消息队列成功!\r\n");}else{printf("删除Test_Queue消息队列失败!\r\n");			}}else{printf("创建Test_Queue消息队列失败!\r\n");	}vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务taskEXIT_CRITICAL();            //退出临界区
}

        运行后会发现,由于Test_Queue被手动置空,当删除函数调用时,会发生编译错误:

那是因为,队列刚创建成功就被丢弃句柄,导致队列内存无法被访问或删除,造成内存泄漏,导致内部崩溃。

完整工程:

FreeRTOS消息队列-删除消息队列.zip资源-CSDN文库

7.4  向消息队列发送消息函数

        任务或者中断服务程序都可以给消息队列发送消息,当发送消息时,如果队列未满或者允许覆盖入队,FreeRTOS 会将消息拷贝到消息队列队尾,否则,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列一直不允许入队,该任务将保持阻塞状态以等待队列允许入队。当其它任务从其等待的队列中读取入了数据(队列未满),该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中还不允许入队,任务也会自动从阻塞态转移为就绪态,此时发送消息的任务或者中断程序会收到一个错误码 errQUEUE_FULL。

        发送紧急消息的过程与发送消息几乎一样,唯一的不同是,当发送紧急消息时,发送的位置是消息队列队头而非队尾,这样,接收者就能够优先接收到紧急消息,从而及时进行消息处理。

        其实消息队列发送函数有好几个,都是使用宏定义进行展开的,有些只能在任务调用,有些只能在中断中调用。

7.4.1  xQueueSend()与 xQueueSendToBack()

xQueueSend()函数原型:

#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), \( xTicksToWait ), queueSEND_TO_BACK )

xQueueSendToBack()函数原型:

#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) \xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), \( xTicksToWait ), queueSEND_TO_BACK )

        xQueueSend()是一个宏,宏展开是调用函数 xQueueGenericSend(),该宏是为了向后兼容没 有包含 xQueueSendToFront() 和 xQueueSendToBack() 这两个宏的 FreeRTOS 版本 。xQueueSend() 等同于xQueueSendToBack()。

        xQueueSend()用于向队列尾部发送一个队列消息。消息以拷贝的形式入队,而不是以引用的形式。

该函数绝对不能在中断服务程序里面被调用,中断中必须使用带有中断保护功能的 xQueueSendFromISR()来代替。

函数原型

BaseType_t xQueueSend(QueueHandle_t xQueue,

                                           const void * pvltemToQueue,

                                           TickType_txTicksToWait);

功能用于向队列尾部发送一个队列消息。
参数xQueue队列句柄。
pvItemToQueue指针,指向要发送到队列尾部的队列消息。
xTicksToWait队列满时,等待队列空闲的最大超时时间。如果队列满并且xTicksToWait被设置成0,函数立刻返回。超时时间的单位为系统节拍周期,常量portTICK-PERIOD-MS用于辅助计算真实的时间,单位为ms。如果INCLUDE_vTaskSuspend设置成1,并且指定延时为 portMAX_DELAY 将导致任务挂起(没有超时)。
返回值消息发送成功成功返回pdTRUE,否则返回errQUEUE-FULL。

实例演示:

        我们以7.1的任务创建函数为模版,首先我们先创建一个发送任务的任务句柄:

static TaskHandle_t Send_Task_Handle = NULL;/* 发送任务句柄 */

        然后创建一个发送任务主体,任务完成的功能,我们通过按键按下进行发送不同的信息,这里我们不进行等待:

//发送任务主体
static void Send_Task(void* parameter)
{	 BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */uint32_t send_data1 = 1;uint32_t send_data2 = 2;while (1){if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ){/* K1 被按下 */printf("发送消息send_data1! \r\n");xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */&send_data1,/* 发送的消息内容 */0 );        /* 等待时间 0 */if(pdPASS == xReturn)printf("消息send_data1发送成功! \r\n");} if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ){/* K2 被按下 */printf("发送消息send_data2! \r\n");xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */&send_data2,/* 发送的消息内容 */0 );        /* 等待时间 0 */if(pdPASS == xReturn)printf("消息send_data2发送成功! \r\n");}vTaskDelay(20);/* 延时20个tick */}
}

        然后在任务创建函数中对该任务进行栈空间,优先级等的分配:

//任务创建函数
static void AppTaskCreate(void)
{BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */taskENTER_CRITICAL();           //进入临界区/* 创建Test_Queue */Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */if(NULL != Test_Queue)printf("创建Test_Queue消息队列成功!\r\n");/* 创建Send_Task任务 */xReturn = xTaskCreate((TaskFunction_t )Send_Task,  /* 任务入口函数 */(const char*    )"Send_Task",/* 任务名字 */(uint16_t       )512,  /* 任务栈大小 */(void*          )NULL,/* 任务入口函数参数 */(UBaseType_t    )3, /* 任务的优先级 */(TaskHandle_t*  )&Send_Task_Handle);/* 任务控制块指针 */ if(pdPASS == xReturn)printf("创建Send_Task任务成功!\r\n"); vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务taskEXIT_CRITICAL();            //退出临界区
}

运行结果:

完整工程:

FreeRTOS消息队列-发送消息-非中断.zip资源-CSDN文库

7.4.2  xQueueSendFromISR()与 xQueueSendToBackFromISR()

xQueueSendFromISR()函数原型:

#define xQueueSendToFrontFromISR(xQueue,pvItemToQueue,pxHigherPriorityTaskWoken) \xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ),\( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )

xQueueSendToBackFromISR()函数原型:

#define xQueueSendToBackFromISR(xQueue,pvItemToQueue,pxHigherPriorityTaskWoken) \xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), \( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )

        xQueueSendFromISR()是一个宏,宏展开是调用函数 xQueueGenericSendFromISR()。该宏是 xQueueSend()的中断保护版本,用于在中断服务程序中向队列尾部发送一个队列消息,等价于 xQueueSendToBackFromISR()。xQueueSendFromISR()函数具体说明:

函数原型

BaseType_t xQueueSendFromISR(QueueHandle_txQueue,

                                                         const void *pvItemToQueue,

                                                         BaseType_t*pxHigherPriority TaskWoken);

功能在中断服务程序中用于向队列尾部发送一个消息。
参数xQueue队列句柄。
pvltemToQueue指针,指向要发送到队列尾部的消息。
pxHigherPriority TaskWoken如果入队导致一个任务解锁,并且解锁的任务优先级高于当前被中断的任务,则将*pxHigherPriorityTaskWoken设置成 pdTRUE,然后在中断退出前需要进行一次上下文切换,去执行被唤醒的优先级更高的任务。从FreeRTOS V7.3.0 起, pxHigherPriorityTaskWoken作为一个可选参数,可以设置为 NULL。
返回值消息发送成功返回pdTRUE,否则返回errQUEUE-FULL。

7.4.3  xQueueSendToFront()

xQueueSendToFront()函数原型:

#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) \xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), \( xTicksToWait ), queueSEND_TO_FRONT )

        xQueueSendToFron() 是一个宏,宏展开也是调用函数 xQueueGenericSend() 。xQueueSendToFront()用于向队列队首发送一个消息。消息以拷贝的形式入队,而不是以引用的形式。该函数绝不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的xQueueSendToFrontFromISR ()来代替。

函数原型

BaseType_t xQueueSendToFront(QueueHandle_t xQueue,

                                                       const void * pvItemToQueue,

                                                       TickType_t xTicksToWait);

功能于向队列队首发送一个消息。
参数xQueue队列句柄。
pvltemToQueue指针,指向要发送到队首的消息。
xTicksToWait队列满时,等待队列空闲的最大超时时间。如果队列满并且xTicksToWait被设置成0,函数立刻返回。超时时间的单位为系统节拍周期,常量 portTICK_PERIOD_MS 用于辅助计算真实的时间,单位为 ms。如果 INCLUDE_vTaskSuspend 设置成 1,并且指定延时为 portMAX_DELAY 将导致任务无限阻塞(没有超时)。
返回值发送消息成功返回pdTRUE,否则返回errQUEUE-FULL。

7.4.4  xQueueSendToFrontFromISR()

xQueueSendToFrontFromISR()函数原型:

#define xQueueSendToFrontFromISR( xQueue,pvItemToQueue,pxHigherPriorityTaskWoken ) \xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ),\( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )

        xQueueSendToFrontFromISR() 是一个宏,宏展开是调用函数xQueueGenericSendFromISR()。该宏是 xQueueSendToFront()的中断保护版本,用于在中断服务程序中向消息队列队首发送一个消息。

函数原型

BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,

                                                                     const void *pvItemToQueue,

                                                                     BaseType_t*pxHigherPriorityTaskWoken);

功能在中断服务程序中向消息队列队首发送一个消息。
参数xQueue队列句柄。
pvltemToQueue指针,指向要发送到队首的消息。
pxHigherPriority TaskWoken如果入队导致一个任务解锁,并且解锁的任务优先级高于当前被中断的任务,则将*pxHigherPriorityTaskWoken设置成 pdTRUE,然后在中断退出前需要进行一次上下文切换,去执行被唤醒的优先级更高的任务。从FreeRTOS V7.3.0 起, pxHigherPriorityTaskWoken作为一个可选参数,可以设置为 NULL。
返回值队列项投递成功返回pdTRUE,否则返回errQUEUE_FULL。

7.4.5  通用消息队列发送函数 xQueueGenericSend()

        上面看到的那些在任务中发送消息的函数都是 xQueueGenericSend()展开的宏定义,真正起作用的就是 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;/* 参数断言检查(实际代码中通常启用) */configASSERT(pxQueue);configASSERT(!(pvItemToQueue == NULL && pxQueue->uxItemSize != 0));configASSERT(!(xCopyPosition == queueOVERWRITE && pxQueue->uxLength != 1));for (;;) {taskENTER_CRITICAL();{/* 检查队列是否有空间 */if ((pxQueue->uxMessagesWaiting < pxQueue->uxLength) || (xCopyPosition == queueOVERWRITE)) {traceQUEUE_SEND(pxQueue);/* 将数据写入队列 */xYieldRequired = prvCopyDataToQueue(pxQueue, pvItemToQueue, xCopyPosition);/* 如果有任务在等待接收数据 */if (listLIST_IS_EMPTY(&(pxQueue->xTasksWaitingToReceive)) == pdFALSE) {/* 唤醒最高优先级的接收任务 */if (xTaskRemoveFromEventList(&(pxQueue->xTasksWaitingToReceive)) != pdFALSE) {/* 如果唤醒的任务优先级更高,触发任务切换 */queueYIELD_IF_USING_PREEMPTION();}} else if (xYieldRequired != pdFALSE) {/* 特殊情况下(如覆盖写入)也需要切换 */queueYIELD_IF_USING_PREEMPTION();}taskEXIT_CRITICAL();return pdPASS;}/* 队列已满时的处理 */else {if (xTicksToWait == (TickType_t)0) {/* 不阻塞,直接返回错误 */taskEXIT_CRITICAL();traceQUEUE_SEND_FAILED(pxQueue);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) {traceBLOCKING_ON_QUEUE_SEND(pxQueue);/* 将当前任务加入发送等待列表 */vTaskPlaceOnEventList(&(pxQueue->xTasksWaitingToSend), xTicksToWait);prvUnlockQueue(pxQueue);/* 恢复调度器(可能触发切换) */if (xTaskResumeAll() == pdFALSE) {portYIELD_WITHIN_API();}} else {/* 队列有空闲,重试 */prvUnlockQueue(pxQueue);(void)xTaskResumeAll();}} else {/* 超时处理 */prvUnlockQueue(pxQueue);(void)xTaskResumeAll();traceQUEUE_SEND_FAILED(pxQueue);return errQUEUE_FULL;}}
}

        我们来解释一下上面代码,首先函数原型:

BaseType_t xQueueGenericSend(QueueHandle_t xQueue,          // (1) 队列句柄const void *pvItemToQueue,     // (2) 待发送数据指针TickType_t xTicksToWait,       // (3) 阻塞超时时间const BaseType_t xCopyPosition // (4) 数据写入位置(队首/队尾/覆盖)
);

        队列未满时的快速路径:

if (pxQueue->uxMessagesWaiting < pxQueue->uxLength || xCopyPosition == queueOVERWRITE) {xYieldRequired = prvCopyDataToQueue(pxQueue, pvItemToQueue, xCopyPosition); // (7)if (有任务在等待接收) {xTaskRemoveFromEventList(&pxQueue->xTasksWaitingToReceive); // (9)if (需任务切换) queueYIELD_IF_USING_PREEMPTION(); // (10)}return pdPASS;
}

(9):如果有任务在等待获取此消息,就要将任务从阻塞中恢复,调用xTaskRemoveFromEventList() 函数将等待的任务从队列的等待接收列表 xTasksWaitingToReceive 中删除,并且添加到就绪列表中。

(10):将任务从阻塞中恢复,如果恢复的任务优先级比当前运行任务的优先级高,那么需要进行一次任务切换。

        队列已满时的阻塞处理:

if (xTicksToWait == 0) {return errQUEUE_FULL; // (14) 非阻塞模式直接返回
} else {vTaskSetTimeOutState(&xTimeOut); // (15) 初始化超时计时vTaskSuspendAll();               // 挂起调度器prvLockQueue(pxQueue);           // 锁队列if (!xTaskCheckForTimeOut(&xTimeOut, &xTicksToWait)) { // (17)if (prvIsQueueFull(pxQueue)) { // (18)vTaskPlaceOnEventList(&pxQueue->xTasksWaitingToSend, xTicksToWait); // (19)prvUnlockQueue(pxQueue);  // (20)xTaskResumeAll();         // 恢复调度器(可能触发切换)}} else {return errQUEUE_FULL; // (22) 超时退出}
}

(14):如果用户不指定阻塞超时时间,则直接退出,不会发送消息。

(15):而如果用户指定了超时时间,系统就会初始化阻塞超时结构体变量,初始化进入阻塞的时间 xTickCount 和溢出次数 xNumOfOverflows,为后面的阻塞任务做准备。

        从消息队列的入队操作我们可以看出:如果阻塞时间不为 0,则任务会因为等待入队而进入阻塞,在将任务设置为阻塞的过程中,系统不希望有其它任务和中断操作这个队列的xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表,因为可能引起其它任务解除阻塞,这可能会发生优先级翻转。比如任务 A 的优先级低于当前任务,但是在当前任务进入阻塞的过程中,任务 A 却因为其它原因解除阻塞了,这显然是要绝对禁止的。因此FreeRTOS 使用挂起调度器禁止其它任务操作队列,因为挂起调度器意味着任务不能切换并且不准调用可能引起任务切换的 API 函数。但挂起调度器并不会禁止中断,中断服务函数仍然可以操作队列事件列表,可能会解除任务阻塞、可能会进行上下文切换,这也是不允许的。于是,解决办法是不但挂起调度器,还要给队列上锁,禁止任何中断来操作队列。

7.4.6  消息队列发送函数 xQueueGenericSendFromISR()

        既然有任务中发送消息的函数,当然也需要有在中断中发送消息函数,其实这个函数跟 xQueueGenericSend() 函数很像,只不过是执行的上下文环境是不一样的,xQueueGenericSendFromISR()函数只能用于中断中执行,是不带阻塞机制的:

BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue,//消息队列句柄const void * const pvItemToQueue,//指针,指向要发送的消息。BaseType_t * const pxHigherPriorityTaskWoken,//(1)const BaseType_t xCopyPosition )//(2)
{BaseType_t xReturn;UBaseType_t uxSavedInterruptStatus;Queue_t * const pxQueue = ( Queue_t * ) xQueue;/* 中断安全检查(实际使用时需根据移植层实现) */configASSERT(pxQueue);configASSERT(!( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 )));configASSERT(!(pvItemToQueue == NULL && pxQueue->uxItemSize != 0));/* 保存当前中断状态并禁用中断 */uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();{/* 检查队列是否有空间 */if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) ){const int8_t cTxLock = pxQueue->cTxLock;traceQUEUE_SEND_FROM_ISR( pxQueue );/* 将数据写入队列 */( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );/* 如果队列未上锁 */if( cTxLock == queueUNLOCKED ){/* 检查是否有任务在等待接收数据 */if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ){/* 唤醒接收任务 */if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ){/* 标记需要上下文切换 */if( pxHigherPriorityTaskWoken != NULL ){*pxHigherPriorityTaskWoken = pdTRUE;}}}}else{/* 队列已上锁,增加发送锁计数 */pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );}xReturn = pdPASS;}else{traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );xReturn = errQUEUE_FULL;}}/* 恢复中断状态 */portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );return xReturn;
}

(1):如果入队导致一个任务解锁,并且解锁的任务优先级高于当前运行的任务,则该函数将 *pxHigherPriorityTaskWoken 设置成 pdTRUE 。 如果xQueueSendFromISR()设置这个值为 pdTRUE,则中断退出前需要一次上下文切换。从 FreeRTOS V7.3.0 起,pxHigherPriorityTaskWoken 称为一个可选参数,并可以设置为 NULL。 

(2):发送数据到消息队列的位置,有以下 3 个选择,在 queue.h 中有定义,queueSEND_TO_BACK:发送到队尾;queueSEND_TO_FRONT:发送到队头;queueOVERWRITE:以覆盖的方式发送。

        xQueueGenericSendFromISR()函数没有阻塞机制,只能用于中断中发送消息,代码简单了很多,当成功入队后,如果有因为等待出队而阻塞的任务,系统会将该任务解除阻塞,要注意的是,解除了任务并不是会马上运行的,只是任务会被挂到就绪列表中。在执行解除阻塞操作之前,会判断队列是否上锁。如果没有上锁,则可以解除被阻塞的任务,然后根据任务优先级情况来决定是否需要进行任务切换;如果队列已经上锁,则不能解除被阻塞的任务,只能是记录 xTxLock 的值,表示队列上锁期间消息入队的个数,也用来记录可以解除阻塞任务的个数,在队列解锁中会将任务解除阻塞。

7.5  从消息队列读取消息函数

        当任务试图读队列中的消息时,可以指定一个阻塞超时时间,当且仅当消息队列中有消息的时候,任务才能读取到消息。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。

7.5.1  xQueueReceive()与 xQueuePeek()

xQueueReceive()函数原型:

#define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) \xQueueGenericReceive( ( xQueue ), ( pvBuffer ), \( xTicksToWait ), pdFALSE )

        xQueueReceive() 是 一 个 宏 , 宏 展 开 是 调 用 函 数 xQueueGenericReceive() 。xQueueReceive()用于从一个队列中接收消息并把消息从队列中删除。接收的消息是以拷贝的形式进行的,所以我们必须提供一个足够大空间的缓冲区。具体能够拷贝多少数据到缓冲区,这个在队列创建的时候已经设定。该函数绝不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的 xQueueReceiveFromISR ()来代替:

函数原型

BaseType_t xQueueReceive(QueueHandle_t xQueue,

                                              void *pvBuffer,

                                              TickType_txTicksToWait);

功能用于从一个队列中接收消息,并把接收的消息从队列中删除。
参数xQueue队列句柄。
pvBuffer指针,指向接收到要保存的数据。
xTicksToWait队列空时,阻塞超时的最大时间。如果该参数设置为0,函数立刻返回。超时时间的单位为系统节拍周期,常量portTICK_PERIOD_MS用于辅助计算真实的时间,单位为ms。如果INCLUDE_vTaskSuspend设置成1,并且指定延时为portMAX_DELAY将导致任务无限阻塞(没有超时)。
返回值

队列项接收成功返回pdTRUE,否则返回pdFALSE。

        下面我们在7.4.1的基础上增加接收队列的东西,首先创建一个接收任务句柄:

static TaskHandle_t Receive_Task_Handle = NULL;/* 接收任务句柄 */

        对于接收任务主体,我们通过按键发送的数值给到消息队列,我们接收消息队列的数据,如果未接收到就选择移植等待直到接收到为止:

//接收任务主体
static void Receive_Task(void* parameter)
{	BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdTRUE */uint32_t r_queue;	/* 定义一个接收消息的变量 */while (1){xReturn = xQueueReceive( Test_Queue,    /* 消息队列的句柄 */&r_queue,      /* 发送的消息内容 */portMAX_DELAY); /* 等待时间 一直等 */if(pdTRUE == xReturn)printf("本次接收到的数据是%d\n\n",r_queue);elseprintf("数据接收出错,错误代码0x%lx\n",xReturn);}
}

        然后在AppTaskCreate()增加接收任务的创建:

  /* 创建Receive_Task任务 */xReturn = xTaskCreate((TaskFunction_t )Receive_Task, /* 任务入口函数 */(const char*    )"Receive_Task",/* 任务名字 */(uint16_t       )512,   /* 任务栈大小 */(void*          )NULL,	/* 任务入口函数参数 */(UBaseType_t    )2,	    /* 任务的优先级 */(TaskHandle_t*  )&Receive_Task_Handle);/* 任务控制块指针 */if(pdPASS == xReturn)printf("创建Receive_Task任务成功!\r\n");

        完整代码:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"#include "LED.h"
#include "Usart.h"
#include "Key.h"  /* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"/* * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么* 这个句柄可以为NULL。*/
static TaskHandle_t AppTaskCreate_Handle = NULL; /* 创建任务句柄 */
static TaskHandle_t Receive_Task_Handle = NULL;/* 接收任务句柄 */
static TaskHandle_t Send_Task_Handle = NULL;/* 发送任务句柄 *//********************************** 内核对象句柄 *********************************/
/** 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我* 们就可以通过这个句柄操作这些内核对象。** 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数* 来完成的* */
QueueHandle_t Test_Queue =NULL;/******************************* 宏定义 ************************************/
/** 当我们在写应用程序的时候,可能需要用到一些宏定义。*/
#define  QUEUE_LEN    4   /* 队列的长度,最大可包含多少个消息 */
#define  QUEUE_SIZE   4   /* 队列中每个消息大小(字节) *///一些函数声明
static void AppTaskCreate(void);/* 用于创建任务 */
static void Receive_Task(void* pvParameters);/* Receive_Task任务实现 */
static void Send_Task(void* pvParameters);/* Send_Task任务实现 */
static void All_Function_Init(void);/* 用于初始化板载相关资源 */int main(void)
{BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */All_Function_Init();//硬件初始化while (1){/* 创建AppTaskCreate任务 */xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */(const char*    )"AppTaskCreate",/* 任务名字 */(uint16_t       )512,  /* 任务栈大小 */(void*          )NULL,/* 任务入口函数参数 */(UBaseType_t    )1, /* 任务的优先级 */(TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ /* 启动任务调度 */           if(pdPASS == xReturn)vTaskStartScheduler();   /* 启动任务,开启调度 */elsereturn -1;  }
}//任务创建函数
static void AppTaskCreate(void)
{BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */taskENTER_CRITICAL();           //进入临界区/* 创建Test_Queue */Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,/* 消息队列的长度 */(UBaseType_t ) QUEUE_SIZE);/* 消息的大小 */if(NULL != Test_Queue)printf("创建Test_Queue消息队列成功!\r\n");/* 创建Receive_Task任务 */xReturn = xTaskCreate((TaskFunction_t )Receive_Task, /* 任务入口函数 */(const char*    )"Receive_Task",/* 任务名字 */(uint16_t       )512,   /* 任务栈大小 */(void*          )NULL,	/* 任务入口函数参数 */(UBaseType_t    )2,	    /* 任务的优先级 */(TaskHandle_t*  )&Receive_Task_Handle);/* 任务控制块指针 */if(pdPASS == xReturn)printf("创建Receive_Task任务成功!\r\n");/* 创建Send_Task任务 */xReturn = xTaskCreate((TaskFunction_t )Send_Task,  /* 任务入口函数 */(const char*    )"Send_Task",/* 任务名字 */(uint16_t       )512,  /* 任务栈大小 */(void*          )NULL,/* 任务入口函数参数 */(UBaseType_t    )3, /* 任务的优先级 */(TaskHandle_t*  )&Send_Task_Handle);/* 任务控制块指针 */ if(pdPASS == xReturn)printf("创建Send_Task任务成功!\r\n");vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务taskEXIT_CRITICAL();            //退出临界区
}//接收任务主体
static void Receive_Task(void* parameter)
{	BaseType_t xReturn = pdTRUE;/* 定义一个创建信息返回值,默认为pdTRUE */uint32_t r_queue;	/* 定义一个接收消息的变量 */while (1){xReturn = xQueueReceive( Test_Queue,    /* 消息队列的句柄 */&r_queue,      /* 发送的消息内容 */portMAX_DELAY); /* 等待时间 一直等 */if(pdTRUE == xReturn)printf("本次接收到的数据是%d\r\n",r_queue);elseprintf("数据接收出错,错误代码0x%lx\n",xReturn);}
}//发送任务主体
static void Send_Task(void* parameter)
{	 BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */uint32_t send_data1 = 1;uint32_t send_data2 = 2;while (1){if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ){/* K1 被按下 */printf("发送消息send_data1! \r\n");xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */&send_data1,/* 发送的消息内容 */0 );        /* 等待时间 0 */if(pdPASS == xReturn)printf("消息send_data1发送成功! \r\n");} if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ){/* K2 被按下 */printf("发送消息send_data2! \r\n");xReturn = xQueueSend( Test_Queue, /* 消息队列的句柄 */&send_data2,/* 发送的消息内容 */0 );        /* 等待时间 0 */if(pdPASS == xReturn)printf("消息send_data2发送成功! \r\n");}vTaskDelay(20);/* 延时20个tick */}
}//初始化声明
static void All_Function_Init(void)
{/** STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,* 都统一用这个优先级分组,千万不要再分组,切忌。*/NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );/* LED 初始化 */LED_GPIO_Config();/* 串口初始化	*/USART_Config();//按键初始化Key_GPIO_Config();}

运行结果:

完整工程:

FreeRTOS消息队列-接收消息-非中断.zip资源-CSDN文库

7.5.2  xQueueReceiveFromISR()与 xQueuePeekFromISR()

        xQueueReceiveFromISR()是 xQueueReceive ()的中断版本,用于在中断服务程序中接收一个队列消息并把消息从队列中删除;xQueuePeekFromISR()是 xQueuePeek()的中断版本,用于在中断中从一个队列中接收消息,但并不会把消息从队列中移除。说白了这两个函数只能用于中断,是不带有阻塞机制的,并且是在中断中可以安全调用:

函数原型

BaseType_t xQueueReceiveFromISR(QueueHandle_txQueue,

                                                              void *pvBuffer,

                                                              BaseType_t*pxHigherPriorityTaskWoken);

功能在中断中从一个队列中接收消息,并从队列中删除该消息。
参数xQueue队列句柄。
pvBuffer指针,指向接收到要保存的数据。
pxHigherPriority TaskWoken任务在往队列投递信息时,如果队列满,则任务将阻塞在该队列上。如果 xQueueReceiveFromISR()到账了一个任务解锁了则将*pxHigherPriorityTaskWoken设置为pdTRUE, 否则*pxHigherPriorityTaskWoken的值将不变。从FreeRTOS V7.3.0起, pxHigherPriorityTaskWoken作为一个可选参数,可以设置为NULL。
返回值队列项接收成功返回pdTRUE,否则返回pdFALSE。
函数原型

BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,

                                                         void *pvBuffer);

功能在中断中从一个队列中接收消息,但并不会把消息从该队列中移除。
参数xQueue队列句柄。
pvBuffer指针,指向接收到要保存的数据。
返回值队列项接收(peek)成功返回pdTRUE,否则返回pdFALSE。

FreeRTOS菜鸟入门系列_时光の尘的博客-CSDN博客

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词