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博客
相关文章:
FreeRTOS菜鸟入门(十)·消息队列
目录 1. 基本概念 2. 数据存储 3. 运作机制 4. 阻塞机制 4.1 出队阻塞 4.2 入队阻塞 5. 操作示意图 5.1 创建队列 5.2 向队列发送第一个消息 5.3 向队列发送第二个消息 5.4 从队列读取消息 6. 消息队列控制块 7. 消息队列常用函数 7.1 消息队列创建…...
Day3:设置页面全局渐变线性渐变背景色uniapp壁纸实战
效果: 页面全局渐变线性渐变背景色会让这个设计更加有质感 想给页面加背景的时候,就给最外部的布局容器添加一个pageBg这样一个类别 接下来,我们就可以在之前写的common的公共的style里的common-style.css里改变它就行了 线性渐变࿱…...
Ray开源程序 是用于扩展 AI 和 Python 应用程序的统一框架。Ray 由一个核心分布式运行时和一组用于简化 ML 计算的 AI 库组成
一、软件介绍 文末提供程序和源码下载 Ray开源程序 是用于扩展 AI 和 Python 应用程序的统一框架。Ray 由一个核心分布式运行时和一组用于简化 ML 计算的 AI 库组成 二、Ray AI 库的更多信息 数据:适用于 ML 的可扩展数据集训练:分布式训练Tune&…...
19、权限控制:分院帽系统——React 19 RBAC实现
一、分院帽的魔法本质 "RBAC是霍格沃茨城堡的智能分院帽,用角色编织的星轨矩阵阻隔黑魔法入侵!" 魔法部安全司官员挥舞魔杖,角色-权限的量子纠缠态在空中交织成防护结界。 ——基于《国际魔法联合会》第7号安全协议,RB…...
P1802 5 倍经验日
P1802 5 倍经验日 - 洛谷 题目背景 复制Markdown {展开 进入IDE模式 现在乐斗有活动了!每打一个人可以获得5倍经验!absi2011却无奈的看着那一些比他等级高的好友,想着能否把他们干掉。干掉能拿不少经验的。 题目描述 现在abs…...
赋予网页健壮的灵魂 —— TypeScript(下)
7 DOM 操作与类型定义:赋予网页真正的交互 回到最初的目标:让网页动起来。在 TypeScript 中,我们如何安全地操作 HTML 元素并响应用户的交互呢?TypeScript 提供了内置的类型定义来描述浏览器环境中的各种对象(如 docu…...
ARM Linux 设备树
Linux 设备驱动开发详解:基于最新的Linux 4.0内核, 机械工业出版社, 宋宝华, 2015 1. 设备树的起源 • 背景: ARM架构中大量板级代码冗余,硬编码在mach-xxx目录,设备树(Device Tree)引入结构化描述硬件。 • 目的: 减…...
35、C# 中的反射(Reflection)
反射是 C# 和 .NET 框架中的一个重要特性,它允许程序在运行时检查、访问和操作类型、对象、属性和方法等元数据信息。 反射的核心概念 运行时类型检查:反射允许你在程序运行时获取类型的详细信息,而不需要在编译时知道这些信息。动态操作&a…...
驱动中的 mmap() 函数和 file_operations 中的 mmap() 原型的区别
在 Linux 驱动开发中,mmap() 系统调用和 file_operations 结构中的 mmap() 方法虽然功能相关,但原型和用途有显著区别。以下是两者的详细对比: 1. 系统调用 mmap() 的原型 这是用户空间程序调用的系统调用接口,定义在 <sys/mma…...
Go语言八股文之Map详解
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 非常期待和您一起在这个小…...
矿泉水瓶的绘制
1.制作中心矩形,大小为60,注意设置矩形的两条边相等 2.点击拉伸,高度为150mm 3.使用圆角命令,点击连接到开始面,同时选中4条边,进行圆角转化,圆角大小为10mm,点击多半径圆角…...
LeetCode 热题 100 54. 螺旋矩阵
LeetCode 热题 100 | 54. 螺旋矩阵 大家好,今天我们来解决一道经典的算法题——螺旋矩阵。这道题在LeetCode上被标记为中等难度,要求我们按照顺时针螺旋顺序返回矩阵中的所有元素。下面我将详细讲解解题思路,并附上Python代码实现。 问题描述…...
方案精读:业财融合转型路径和华为实践【附全文阅读】
在当今快速变化、竞争激烈的时代,业务面临不确定性,业财融合至关重要。以华为为例,其从财务到财经的转型,历经财务四统一变革、IFS 变革等,构建了包含财经能力中心(COE)、业务伙伴(BP)和财经共享中心(SSC)的财务组织架构 。通过实现财务四算拉通、提升预算预测、项目…...
AIDC智算中心建设:计算力核心技术解析
目录 一、智算中心发展概览 二、计算力核心技术解析 一、智算中心发展概览 智算中心是人工智能发展的关键基础设施,基于人工智能计算架构,提供人工智能应用所需算力服务、数据服务和算法服务的算力基础设施,融合高性能计算设备、高速网络以…...
Javase 基础加强 —— 02 泛型
本系列为笔者学习Javase的课堂笔记,视频资源为B站黑马程序员出品的《黑马程序员JavaAI智能辅助编程全套视频教程,java零基础入门到大牛一套通关》,章节分布参考视频教程,为同样学习Javase系列课程的同学们提供参考。 01 认识泛型…...
PowerShell 备份 Windows10/11 还原计算机驱动程序SOP
一、现在计算机C目录下创建一个新的文件夹名称为 driverbackup 二、打开cmd 以管理员身份执行 dism /online /export-driver /destination: C:\driverbackup 在正常情况下,Windows 10会自动检测您的设备所需的驱动程序,并将其安装到您的PC上。 但是&am…...
Python 中的 collections 库:高效数据结构的利器
Python 中的 collections 库:高效数据结构的利器 在 Python 编程中,数据结构的高效使用往往能极大地提升代码的性能和可读性。今天,就让我们来深入了解一下 Python 的 collections 库,它是一个非常实用且强大的工具库,…...
2025年- H24-Lc132-94. 二叉树的中序遍历(树)---java版。
1.题目描述 2.思路 递归遍历:返回值,中序遍历的节点值列表 List。 (1)首先是一个中序遍历的结果函数,传入root参数,定义一个节点值列表result,然后递归调用中序遍历的函数 (2&#…...
第十六届蓝桥杯单片机组省赛(第一套)
看到很多人在问第十六届蓝桥杯单片机难不难,以及实现多少功能可以获得省一。 先介绍下我的作答情况吧,选择题只对一题,程序题的求连续两次距离差值没有考虑负数的情况,其他功能都实现了,成绩是福建省省一第一页&#x…...
使用python写多文件#inlcude
使用下面的程序可以将当前文件夹下面的.c文件的写入main.h文件,我这里是将自己的基于标准库stm32初始化io文件为例。 import osbase ["#ifndef main_H","#define main_H\n","#endif" ]includes set() for file in os.listdir():if…...
深度学习中保存最优模型的实践与探索:以食物图像分类为例
深度学习中保存最优模型的实践与探索:以食物图像分类为例 在深度学习的模型训练过程中,训练一个性能良好的模型往往需要耗费大量的时间和计算资源。而保存最优模型不仅可以避免重复训练,还能方便后续使用和部署。本文将结合食物图像分类的代…...
OpenCv实战笔记(2)基于opencv和qt对图像进行灰度化 → 降噪 → 边缘检测预处理及显示
一、实现效果 二、应用场景 这三步是经典的 “灰度化 → 降噪 → 边缘检测” 预处理流程,常用于: 计算机视觉任务(如物体识别、特征提取)。 图像分析(如文档扫描、车牌识别)。 减少后续算法的计算复杂度&a…...
个人文章不设置vip
今天没登录打开自己文章,发现自己读自己文章还要充值。就感觉很扯。 继而去查看个人的历史文章,发现很多被标注为高质量的文章设置成了VIP文章。 如果仅有一两篇,还可能是本人手滑误设置,这么多就解释不通了。 (大概有…...
【聚类分析】基于copula的风光联合场景生成与缩减
目录 1 主要内容 风光出力场景生成方法 2 部分程序 3 程序结果 4 下载链接 1 主要内容 该程序方法复现《融合风光出力场景生成的多能互补微网系统优化配置》风光出力场景生成部分,目前大多数研究的是不计风光出力之间的相关性影响,但是地理位置相近…...
CSDN积分详解(介绍、获取、用途)
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 积分**一、积分类型及用途****二、积分获取途…...
运维--计划任务
计划任务分为一次性和循环性的计划任务 1.date的用法 date 月日时分年 eg:date 100118222023 date:查看时间和日期、修改时间和日期 获取当前日期:date +%F F:日期 获取当前时间:date +%H:%M:%S H:时 M:分 S:秒 获取周几: date +%w w:周 …...
AVL树(2):
我们之前讲AVL树,我们讲到了旋转,然后讲了左单旋和右单旋。 但是我们这里的单旋产生的条件都是纯粹的一边比较高, 我们看上面这个图片,当我们插入新的结点导致一边的子树比较高的时候,我们必须是存粹的一边高…...
架构思维:异构数据的同步一致性方案
文章目录 一、引言二、全景架构回顾三、潜在问题问题1:Binlog 延迟——理想 vs 实际问题2:Binlog 格式解析问题3:高可靠消费1. 串行 ACK 消费2. 并行消费+乱序风险3. 解决方案 问题4:缓存数据结构设计1. Key–Value 冗…...
nginx 正反向代理和nginx正则
目录 一. 正向代理 1. 编译安装Nginx 2. 配置正向代理 二. 反向代理 1. 配置nginx七层代理 2. 配置nginx四层代理 三. Nginx 缓存 1. 缓存功能的核心原理和缓存类型 2. 代理缓存功能设置 四. Nginx rewrite和正则 1. Nginx正则 2. nginx location 3. Rewrite …...
SAM-Decoding_ 后缀自动机助力大模型推理加速!
SAM-Decoding: 后缀自动机助力大模型推理加速! 大语言模型(LLMs)的推理效率一直是研究热点。本文介绍的SAM-Decoding方法,借助后缀自动机(Suffix Automaton,SAM)实现推测解码,在提升…...
使用Scrapy构建高效网络爬虫:从入门到数据导出全流程
在数据驱动的时代,网络爬虫已成为获取公开信息的核心工具。本文将带您通过Scrapy框架完成一个实战项目,涵盖从零搭建爬虫到多格式数据导出的完整流程,并深入解析Scrapy的Feed Exports功能。 一、项目背景与目标 我们将爬取书籍网站ÿ…...
Docker安装Gitblit(图文教程)
本章教程,使用Docker安装部署Gitblit。 一、Gitblit简介 Gitblit 是一个基于 Java 的 Git 仓库管理工具,主要用于在局域网或小型团队环境中搭建私有 Git 服务器。它提供了一个简单易用的 Web 界面,用于浏览代码、管理仓库和用户权限等。 二、拉取镜像 sudo docker pull git…...
SpringBoot的汽车商城后台管理系统源码开发实现
概述 汽车商城后台管理系统专为汽车4S店和经销商设计,提供全面的汽车管理系统解决方案。 主要内容 1. 核心功能模块 系统提供以下主要功能: 销售管理:记录销售信息,跟踪交易进度客户管理:维护客户…...
组合模式(Composite Pattern)
非常棒!你现在进入了结构型设计模式中最典型的「树形结构」设计模式 —— 组合模式(Composite Pattern)。 我将通过简明解释 清晰代码 类图演示,一步步帮你理解它。 🧠 一句话定义 组合模式允许你将对象组合成树形结…...
Java捕获InterruptedException异常后,会自动清空中断状态
InterruptedException异常一般是在一个线程处于等待(像Thread.sleep()、Object.wait()、Thread.join()等方法)状态时被另一个线程调用interrupt()方法中断而抛出的。一旦捕获到InterruptedException,Java 会自动清除该线程的中断状态。 以下…...
HTML04:图像标签
图像标签 常见的图像标签 JPGGIFPNGBMP <img src"路径" alt"名称" title"悬停名称" width"高" height"宽"/><!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8&quo…...
供应链算法整理(二)--- 智能补货
供应链业务的目标价值是:优化货品的供给、销售提供支撑,以降低成本,提高时效、收益,最终提升用户体验。基于目标价值,整体的算法模块分为:智能选品、智能预测、品仓铺货、智能补货、智能调拨、仓网路由、快…...
【毕设通关】——Word交叉引用
📖 前言:在论文中,我们经常会在文段贴图片时,写“如图x所示”的内容,如果每次都手动写数字,那么当需要在前面内容插入图片时,后续更新会很繁琐,这时就需要交叉引用功能。 ǵ…...
java技术总监简历模板
模板信息 简历范文名称:java技术总监简历模板,所属行业:其他 | 职位,模板编号:XDNUTA 专业的个人简历模板,逻辑清晰,排版简洁美观,让你的个人简历显得更专业,找到好工作…...
视频编解码学习三之显示器
整理自:显示器_百度百科,触摸屏_百度百科,百度安全验证 分为阴极射线管显示器(CRT),等离子显示器PDP,液晶显示器LCD 液晶显示器的组成。一般来说,液晶显示器由以下几个部分组成: […...
【人工智能】大模型安全的深度剖析:DeepSeek漏洞分析与防护实践
《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 随着大语言模型(LLM)的广泛应用,其安全性问题日益凸显。DeepSeek作为中国领先的开源AI模型,以低成本和高性能著称,但近期暴露的数据库…...
架构思维:使用懒加载架构实现高性能读服务
文章目录 一、引言二、读服务的功能性需求三、两大基本设计原则1. 架构尽量不要分层2. 代码尽可能简单 四、实战方案:懒加载架构及其四大挑战五、改进思路六、总结与思考题 一、引言 在任何后台系统设计中,「读多写少」的业务场景占据主流:浏…...
【AI提示词】黑天鹅模型专家
提示说明 详细解释黑天鹅模型的理论背景、定义、分类及其在不同领域的应用。 提示词 # Role: 黑天鹅模型专家## Profile - language: 中文 - description: 详细解释黑天鹅模型的理论背景、定义、分类及其在不同领域的应用 - background: 黑天鹅模型是尼尔斯莫尔提出的理论&a…...
pip安装包时网络不畅,替换国内PyPI镜像源
1、PyPI 镜像源 1.1、定义 PyPI 镜像源是对 Python Package Index(PyPI)官方仓库的复制。 PyPI 是 Python 社区中最大的软件包仓库,存储着大量的 Python 包,供开发者们下载和使用。 然而,由于 PyPI 服务器位于国外&a…...
TS 类型推论
应用场景: 1.变量初始化 仅声明不初始化无法推断是什么类型,必须手动添加类型注解 2.决定函数返回值 根据函数体内的运算可以推断出返回值的类型 函数参数的类型声明建议一定要手写...
Java基于SaaS模式多租户ERP系统源码
目录 一、系统概述 二、开发环境 三、系统功能介绍 一、系统概述 ERP,全称 Enterprise Resource Planning 即企业资源计划。是一种集成化的管理软件系统,它通过信息技术手段,将企业的各个业务流程和资源管理进行整合,以提高企业…...
PHP的include和require
文章目录 环境require和includerequire VS includerequire(include) VS require_once(include_once)路径问题当前工作目录对相对路径的影响题外话总结其它 参考 环境 Windows 11 专业版XAMPP v3.3.0 PHP 8.2.12Apache 2.4.58 VSC…...
日本人工智能发展全景观察:从技术革新到社会重构的深度解析
一、日本IT产业演进与AI技术崛起的历史脉络 1.1 信息化时代的奠基(1990-2010) 日本IT产业的腾飞始于"信息高速公路计划"的实施。1994年NTT推出全球首个商用光纤网络,至2005年实现全国光纤覆盖率突破80%。这一时期培育出富士通、N…...
什么是DGI数据治理框架?
DGI数据治理框架是由数据治理研究所(Data Governance Institute, DGI)提出的一套系统性方法论,旨在帮助企业或组织建立有效的数据治理体系,确保数据资产的高质量管理、合规使用和价值释放。以下是关于DGI数据治理框架的核心内容&a…...
[硬件电路-12]:LD激光器与DFB激光器功能概述、管脚定义、功能比较
一、LD激光器(普通半导体激光器)功能 核心功能: LD激光器通过半导体材料的电子-空穴复合实现受激辐射,将电能直接转换为高相干性激光,是光电子系统的核心光源。 基础光发射功能 工作原理:正向偏置电流注入…...