FreeRTOS个人笔记-消息队列

根据个人的学习方向,学习FreeRTOS。由于野火小哥把FreeRTOS讲得比较含蓄,打算在本专栏尽量细化一点。作为个人笔记,仅供参考或查阅。

配套资料:FreeRTOS内核实现与应用开发实战指南、野火FreeRTOS配套视频源码、b站野火FreeRTOS视频。搭配来看更佳哟!!!

消息队列

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

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

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

消息队列的结构体如下

typedef struct QueueDefinition
{
	int8_t *pcHead;					//头指针,指向队列消息存储区起始位置地址
	int8_t *pcTail;					//尾指针,指向队列消息存储区结束位置地址
	int8_t *pcWriteTo;				//指向队列消息存储区下一个可用消息空间

	//pcReadFrom 与 uxRecursiveCallCount 是一对互斥变量, 使用联合体用来确保两个互斥的结构体成员不会同时出现。 
	//当结构体用于队列时, pcReadFrom 指向出队消息空间的最后一个,即读取消息时候是从 pcReadFrom 指向的空间读取消息内容
	//当结构体用于互斥量时, uxRecursiveCallCount 用于计数,记录递归互斥量被“调用” 的次数。
	union							
	{
		int8_t *pcReadFrom;			
		UBaseType_t uxRecursiveCallCount;
	}u;

	List_t xTasksWaitingToSend;		//发送消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序,由于队列已满,想要发送消息的任务无法发送消息。
	List_t xTasksWaitingToReceive;	//获取消息阻塞列表,用于保存阻塞在此队列的任务,任务按照优先级进行排序,由于队列是空的,想要获取消息的任务无法获取到消息。

	volatile UBaseType_t uxMessagesWaiting;	//记录当前消息队列的消息个数, 如果消息队列被用于信号量的时候,这个值就表示有效信号量个数
	UBaseType_t uxLength;					//队列长度
	UBaseType_t uxItemSize;					//单个消息大小

	//这两个成员变量为 queueUNLOCKED 时,表示队列未上锁;当这两个成员变量为 queueLOCKED_UNMODIFIED 时,表示队列上锁。
	volatile int8_t cRxLock;		//队列上锁后,储存从队列收到的列表项数目,也就是出队的数量,如果队列没有上锁,设置为 queueUNLOCKED。
	volatile int8_t cTxLock;		//队列上锁后,储存发送到队列的列表项数目,也就是入队的数量,如果队列没有上锁,设置为 queueUNLOCKED。

	#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
		uint8_t ucStaticallyAllocated;	
	#endif

	#if ( configUSE_QUEUE_SETS == 1 )
		struct QueueDefinition *pxQueueSetContainer;
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t uxQueueNumber;
		uint8_t ucQueueType;
	#endif

} xQUEUE;
typedef xQUEUE Queue_t;

使用队列模块的典型流程如下:
创建消息队列。
写队列操作。
读队列操作。
删除队列。

创建消息队列

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

xQueueCreate()函数如下

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

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
	//uxQueueLength为队列长度。uxItemSize为队列中消息单元的大小,以字节为单位。
	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 )
		{
			xQueueSizeInBytes = ( size_t ) 0;
		}
		else
		{
			//分配足够存储消息的空间,消息存储空间大小=队列长度*单个消息大小
			xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
		}
		//向系统申请内存,内存大小为消息队列控制块大小+消息存储空间大小
		pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );

		if( pxNewQueue != NULL )
		{
			//计算出消息存储空间的起始地址
			pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + 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 );
		}

		return pxNewQueue;
	}

#endif /* configSUPPORT_STATIC_ALLOCATION */

上述函数用到了初始化消息队列函数,prvInitialiseNewQueue()。

prvInitialiseNewQueue()函数如下

/*
	消息队列类型:
 	queueQUEUE_TYPE_BASE:表示队列。
 	queueQUEUE_TYPE_SET:表示队列集合。
 	queueQUEUE_TYPE_MUTEX:表示互斥量。
	queueQUEUE_TYPE_COUNTING_SEMAPHORE:表示计数信号量。
 	queueQUEUE_TYPE_BINARY_SEMAPHORE:表示二进制信号量。
 	queueQUEUE_TYPE_RECURSIVE_MUTEX:表示递归互斥量。
*/
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 )
	{
		/* 	没有为消息存储分配内存,但是 pcHead 指针不能设置为 NULL,
			因为队列用作互斥量时,pcHead 要设置成 NULL。
			这里只是将 pcHead 指向一个已知的区域 */
		pxNewQueue->pcHead = ( int8_t * ) pxNewQueue;
	}
	else
	{
		//设置 pcHead 指向存储消息的起始地址 
		pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage;
	}

	/* 初始化消息队列控制块的其他成员 */
	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 );
}

上述函数用到了重置消息队列函数,xQueueGenericReset()。

xQueueGenericReset()如下

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;									//当前消息队列中的消息个数
		pxQueue->pcWriteTo = pxQueue->pcHead;												//pcWriteTo 指向队列消息存储区下一个可用消息空间
		pxQueue->u.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - ( UBaseType_t ) 1U ) * pxQueue->uxItemSize );		//pcReadFrom 指向消息队列最后一个消息空间
		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 ) );
			vListInitialise( &( pxQueue->xTasksWaitingToReceive ) );
		}
	}
	taskEXIT_CRITICAL();

	return pdPASS;
}

至此,消息队列的创建内容就已经结束,多数函数都已经被官方封装好,我们直接用就行。

xQueueCreate()

删除消息队列

如果删除消息队列时,有任务正在等待消息, 则不应该进行删除操作。

vQueueDelete()函数如下

void vQueueDelete( QueueHandle_t xQueue )	//删除消息队列和信号量
{
    Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	configASSERT( pxQueue );
	traceQUEUE_DELETE( pxQueue );			//删除消息队列

	#if ( configQUEUE_REGISTRY_SIZE > 0 )
	{
		vQueueUnregisterQueue( pxQueue );	//将消息队列从注册表中删除
	}
	#endif

	#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
	{
		vPortFree( pxQueue );				//释放消息队列的内存
	}
	#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
	{
		if( pxQueue->ucStaticallyAllocated == ( uint8_t ) pdFALSE )
		{
			vPortFree( pxQueue );
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#else
	{
		( void ) pxQueue;
	}
	#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
}

至此,消息队列的删除内容就已经结束,多数函数都已经被官方封装好,我们直接用就行。

vQueueDelete()

消息队列发送消息

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

消息队列常规发送消息

xQueue,                          队列句柄,
pvItemToQueue,             指向要发送到队列尾部/首部的队列消息的指针
xTicksToWait为队列满时,等待队列空闲的最大超时时间。如果队列满并且 xTicksToWait 被设置成 0,函数立刻返回。    
消息发送成功返回pdTRUE,否则返回errQUEUE_FULL。

//向队列尾部发送一个队列消息,该函数绝对不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的 xQueueSendFromISR()来代替。
//向队列首部发送一个队列消息,该函数绝对不能在中断服务程序里面被调用,而是必须使用带有中断保护功能的 xQueueSendToFrontFromISR ()来代替。
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )

xQueueSend()函数实例

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! \n");
			xReturn = xQueueSend( 	Test_Queue, /* 消息队列的句柄 */
									&send_data1,/* 发送的消息内容 */
									0 ); 		/* 等待时间 0 */
			if (xReturn == pdPASS)
			printf("消息 send_data1 发送成功!\n\n");
		}
		if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) 	/* K2 被按下 */
		{
			printf("发送消息 send_data2! \n");
			xReturn = xQueueSend( 	Test_Queue, /* 消息队列的句柄 */
									&send_data2,/* 发送的消息内容 */
									0 ); 		/* 等待时间 0 */
			if (pdPASS == xReturn)
				printf("消息 send_data2 发送成功!\n\n");
		}
		vTaskDelay(20);/* 延时 20 个 tick */
	}	
}

消息队列发送消息的中断保护版本

xQueue,                                队列句柄,
pvItemToQueue,                   指向要发送到队列首部/尾部的队列消息的指针
pxHigherPriorityTaskWoken,如果入队导致一个任务解锁,并且解锁的任务优先级高于当前被中断的任务,则将*pxHigherPriorityTaskWoken设置成 pdTRUE,然后在中断退出前需要进行一次上下文切换,去执行被唤醒的优先级更高的任务 。
消息发送成功返回pdTRUE,否则返回errQUEUE_FULL。

//xQueueSendToFront的中断保护版本,在中断服务程序中向队列首部发送一个队列消息
//		 xQueueSend的中断保护版本,在中断服务程序中向队列尾部发送一个队列消息
#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )
//xQueueSendToBackFromISR和xQueueSendFromISR等同
#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )  xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
#define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) 		 xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )

xQueueSendFromISR()函数实例

void vBufferISR( void )
{
	char cIn;
	BaseType_t xHigherPriorityTaskWoken;

	/* 在 ISR 开始的时候,我们并没有唤醒任务 */
	xHigherPriorityTaskWoken = pdFALSE;

	/* 直到缓冲区为空 */
	do 
	{
		/* 从缓冲区获取一个字节的数据 */
		cIn = portINPUT_BYTE( RX_REGISTER_ADDRESS );
		
		/* 发送这个数据 */
		xQueueSendFromISR( xRxQueue, &cIn, &xHigherPriorityTaskWoken );	
	}while( portINPUT_BYTE( BUFFER_COUNT ) );
	
	/* 这时候 buffer 已经为空,如果需要则进行上下文切换 */
	if ( xHigherPriorityTaskWoken ) 
	{
		/* 上下文切换,这是一个宏,不同的处理器,具体的方法不一样 */
		taskYIELD_FROM_ISR ();
	}
}

至此,消息队列的发送消息内容就已经结束,多数函数都已经被官方封装好,我们直接用就行。
消息队列发送函数
xQueueSend( xQueue, pvItemToQueue, xTicksToWait )                                                      向队列尾部发送一个队列消息
xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )    xQueueSend函数的中断保护版本

xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait )                                          向队列首部发送一个队列消息
xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )     xQueueSendToFront函数的中断保护版本
xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )                 xQueueSendToFront函数的中断保护版本
上述都是在两个函数内展开的,xQueueGenericSend()函数和xQueueGenericSendFromISR()函数。

xQueueGenericSend()函数如下

/*
xCopyPosition参数选择:
#define    queueSEND_TO_BACK        ( ( BaseType_t ) 0 )        //发送到队尾
#define    queueSEND_TO_FRONT        ( ( BaseType_t ) 1 )        //发送到队首
#define queueOVERWRITE            ( ( BaseType_t ) 2 )        //以覆盖的方式发送,无论队列满还是不满,都可以发送。
*/

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;

	/* 省略断言代码 */

	for( ;; )
	{
		taskENTER_CRITICAL();
		{
			if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )		//队列未满或者允许覆盖
			{
				traceQUEUE_SEND( pxQueue );
				xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );					//将消息拷贝到消息队列中

				#if ( configUSE_QUEUE_SETS == 1 )	//使用队列集
				{
					//没用到,省略代码
				}
				#else /* configUSE_QUEUE_SETS */
				{
					/* 如果有任务在等待获取此消息队列 */
					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();
				return pdPASS;
			}
			else	//队列已满
			{
				if( xTicksToWait == ( TickType_t ) 0 )	//用户不指定阻塞超时时间,退出
				{
					taskEXIT_CRITICAL();

					traceQUEUE_SEND_FAILED( pxQueue );
					return errQUEUE_FULL;
				}
				else if( xEntryTimeSet == pdFALSE )		
				{
					vTaskInternalSetTimeOutState( &xTimeOut );	//初始化阻塞超时结构体变量,初始化进入阻塞的时间 xTickCount 和溢出次数 xNumOfOverflows
					xEntryTimeSet = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		taskEXIT_CRITICAL();

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

		/* 检查超时时间是否已经过去了 */
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
		{
			if( prvIsQueueFull( pxQueue ) != pdFALSE )		// 如果队列还是满的
			{
				/* 将当前任务添加到队列的等待发送列表中以及阻塞延时列表,延时时间为用户指定的超时时间 xTicksToWait */
				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;
		}
	}
}

xQueueGenericSendFromISR()函数只能用于中断中执行,是不带阻塞机制的。

xQueueGenericSendFromISR()函数如下

BaseType_t xQueueGenericSendFromISR( 	QueueHandle_t xQueue, 							//消息队列句柄
										const void * const pvItemToQueue,               //指向要发送的信息的指针
										BaseType_t * const pxHigherPriorityTaskWoken,   //如果入队导致一个任务解锁,并且解锁的任务优先级高于当前运行的任务,则该函数将*pxHigherPriorityTaskWoken设置成pdTRUE。如果值为 pdTRUE,则中断退出前需要一次上下文切换。
										const BaseType_t xCopyPosition )                //发送数据到消息队列的位置
{
    BaseType_t xReturn;
    UBaseType_t uxSavedInterruptStatus;
    Queue_t * const pxQueue = ( Queue_t * ) xQueue;

	/* 省略断言代码 */
	
	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 ( configUSE_QUEUE_SETS == 1 )		//使用队列集
				{
					//没用到,省略代码
				}
				#else /* configUSE_QUEUE_SETS */
				{
					/* 如果有任务在等待获取此消息队列 */
					if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
					{
						if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )	//将等待接收队列删除,即将任务从阻塞中恢复
						{
							if( pxHigherPriorityTaskWoken != NULL )
							{
								*pxHigherPriorityTaskWoken = pdTRUE;	/* 解除阻塞的任务优先级比当前任务高,记录上下文切换请求,等返回中断服务程序后,就进行上下文切换 */
							}
							else
							{
								mtCOVERAGE_TEST_MARKER();
							}
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
				#endif /* configUSE_QUEUE_SETS */
			}
			else	//队列上锁
			{
				pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 );	//记录上锁次数,等到任务解除队列锁时,使用这个计录数就可以知道有多少数据入队
			}

			xReturn = pdPASS;
		}
		else	//队列已满
		{
			traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );	//因为 API 执行的上下文环境是中断,所以不能阻塞,直接返回队列已满错误代码 errQUEUE_FULL
			xReturn = errQUEUE_FULL;
		}
	}
	portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

	return xReturn;
}

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

消息队列读取消息

当任务试图读队列中的消息时,可以指定一个阻塞超时时间,当且仅当消息队列中有消息的时候,任务才能读取到消息。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务程序往其等待的队列中写入了数据,该任务将自动由阻塞态转为就绪态。当任务等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
xQueue,                          队列句柄,
pvItemToQueue,             指向接收到要保存的数据的指针
xTicksToWait,                  队列空时,阻塞超时的最大时间。如果 xTicksToWait 被设置成 0,函数立刻返回。    
消息发送成功返回pdTRUE,否则返回pdFALSE。

//xQueueReceive()函数用于从一个队列中接收消息并把消息从队列中删除。
//xQueuePeek()	 函数用于从一个队列中接收消息但不从队列中删除消息。
#define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) 	xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdFALSE )
#define xQueuePeek( xQueue, pvBuffer, xTicksToWait ) 		xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdTRUE )

接收的消息是以拷贝的形式进行的,所以我们必须提供一个足够大空间的缓冲区。具体能够拷贝多少数据到缓冲区,这个在队列创建的时候已经设定。

xQueueReceive()函数实例

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); 
		else 
			printf("数据接收出错,错误代码: 0x%lx\n",xReturn); 
	}
}

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

xQueueReceiveFromISR()是 xQueueReceive ()的中断版本。
xQueuePeekFromISR()     是 xQueuePeek()      的中断版本。
这两个函数只能用于中断,是不带有阻塞机制的,并且是在中断中可以安全调用。

xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken )函数简介

xQueue,                                  队列句柄,
pvBuffer,                                 指向接收到要保存的数据的指针
pxHigherPriorityTaskWoken , 任务在往队列发送信息时,如果队列满,则任务将阻塞在该队列上。

如果 xQueueReceiveFromISR() 到账了一个任务解锁了,则将 *pxHigherPriorityTaskWoken 设置为 pdTRUE ,否则 *pxHigherPriorityTaskWoken 的值将不变。
消息发送成功返回pdTRUE,否则返回pdFALSE。

xQueuePeekFromISR( QueueHandle_t xQueue, void * const pvBuffer )函数简介

xQueue,                                  队列句柄,
pvBuffer,                                 指向接收到要保存的数据的指针
消息发送成功返回pdTRUE,否则返回pdFALSE。

xQueueReceiveFromISR()函数实例

QueueHandle_t xQueue;

/* 创建一个队列,并往队列里面发送一些数据 */
void vAFunction( void *pvParameters )
{
	char 	cValueToPost;
	const 	TickType_t xTicksToWait = ( TickType_t )0xff;

	/* 创建一个可以容纳 10 个字符的队列 */
	xQueue = xQueueCreate( 10, sizeof( char ) );
	if ( xQueue == 0 ) 
	{
		/* 队列创建失败 */
	}
 
	/* ... 任务其他代码 */
 
	/* 往队列里面发送两个字符,如果队列满了则等待 xTicksToWait 个系统节拍周期*/
	cValueToPost = 'a';
	xQueueSend( xQueue, ( void * ) &cValueToPost, xTicksToWait );
	cValueToPost = 'b';
	xQueueSend( xQueue, ( void * ) &cValueToPost, xTicksToWait );
	cValueToPost = 'c';
	xQueueSend( xQueue, ( void * ) &cValueToPost, xTicksToWait );
 }
 
/* 中断服务程序:输出所有从队列中接收到的字符 */
void vISR_Routine( void )
{
	BaseType_t xTaskWokenByReceive = pdFALSE;
	char cRxedChar;
 
	while ( xQueueReceiveFromISR( xQueue, ( void * ) &cRxedChar, &xTaskWokenByReceive) ) 
	{ 
		/* 接收到一个字符,然后输出这个字符 */
		vOutputCharacter( cRxedChar );
 
		/* 如果从队列移除一个字符串后唤醒了向此队列投递字符的任务,那么参数 xTaskWokenByReceive 将会设置成 pdTRUE,
		这个循环无论重复多少次,仅会有一个任务被唤醒 */
	}
 
	if ( xTaskWokenByReceive != pdFALSE ) 
	{ 
		/* 我们应该进行一次上下文切换,当 ISR 返回的时候则执行另外一个任务 */
		/* 这是一个上下文切换的宏,不同的处理器,具体处理的方式不一样 */ 
 		taskYIELD (); 
	} 
}

至此,消息队列的发送消息内容就已经结束,多数函数都已经被官方封装好,我们直接用就行。

xQueueReceive()                        从一个队列中接收消息并把消息从队列中删除。
xQueuePeek()                             从一个队列中接收消息但不从队列中删除消息。

xQueueReceiveFromISR()         xQueueReceive()函数的中断保护版本。

xQueuePeekFromISR()              xQueuePeek()函数的中断保护版本。

xQueueReceive()和xQueuePeek()都是在同一个函数内展开的,从队列读取消息函数,xQueueGenericReceive()。

xQueueGenericReceive()函数如下

BaseType_t xQueueGenericReceive( QueueHandle_t 		xQueue, 		//消息队列句柄
								 void * const 		pvBuffer, 		//指向接收到要保存的数据的指针
								 TickType_t 		xTicksToWait, 	//队列空时,用户指定的阻塞超时时间。如果该参数设置为 0,函数立刻返回。
								 const BaseType_t   xJustPeeking ) 	//标记消息是否需要出队。pdFALSE,读取消息后会进行出队操作。pdTRUE,读取消息后不会进行出队操作。
{
	BaseType_t 			xEntryTimeSet = pdFALSE;
	TimeOut_t 			xTimeOut;
	int8_t 				*pcOriginalReadPosition;
	Queue_t * const 	pxQueue = ( Queue_t * ) xQueue;
 
	/* 已删除一些断言 */
	for ( ;; ) 
	{
		taskENTER_CRITICAL(); 
		{
			const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;	//uxMessagesWaiting为消息个数
		
			/* 看看队列中有没有消息 */
			if ( uxMessagesWaiting > ( UBaseType_t ) 0 ) 	//当前队列有消息
			{ 
				/*记录读消息位置,防止仅仅是读取消息,而不进行消息出队操作*/
				pcOriginalReadPosition = pxQueue->u.pcReadFrom; 
				/* 拷贝消息到用户指定存放区域 pvBuffer */
				prvCopyDataFromQueue( pxQueue, pvBuffer ); 
		
				if ( xJustPeeking == pdFALSE ) 				//读取消息后会进行出队操作
				{ 
					/* 读取消息并且消息出队 */
					traceQUEUE_RECEIVE( pxQueue );
		
					/* 获取了消息,当前消息队列的消息个数需要减一 */
					pxQueue->uxMessagesWaiting = uxMessagesWaiting - 1; 
					/* 判断一下消息队列中是否有等待发送消息的任务 */
					if ( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) 	/* 消息队列中有等待发送消息的任务,就要将任务从阻塞中恢复 */
					{
						//将等待的任务从队列的等待发送列表xTasksWaitingToSend中删除,并且添加到就绪列表中。
						if ( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE ) 
						{
							/* 将任务从阻塞中恢复,如果被恢复的任务优先级比当前任务高,会进行一次任务切换 */
							queueYIELD_IF_USING_PREEMPTION(); (13)
						} else 
						{
							mtCOVERAGE_TEST_MARKER();
						}
					} else 
					{
						mtCOVERAGE_TEST_MARKER();
					}
				} else 	//读取消息后不会进行出队操作
				{ 
					traceQUEUE_PEEK( pxQueue );
		
					/* 因为是只读消息 所以还要还原读消息位置指针 */
					pxQueue->u.pcReadFrom = pcOriginalReadPosition; 
					
					/* 判断一下消息队列中是否还有等待获取消息的任务 */
					if ( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE ) 
					{
						/* 将任务从阻塞中恢复 */
						if ( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE ) 
						{
							/* 如果被恢复的任务优先级比当前任务高,会进行一次任务切换 */
							queueYIELD_IF_USING_PREEMPTION();
						}else 
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}else 
					{
						mtCOVERAGE_TEST_MARKER();
					}
				}
		
			taskEXIT_CRITICAL(); 
			return pdPASS;
			}else 	//当前队列没有消息
			{ 
				/* 消息队列中没有消息可读 */
				if ( xTicksToWait == ( TickType_t ) 0 )
				{ 
					/* 不等待,直接返回 */
					taskEXIT_CRITICAL();
					traceQUEUE_RECEIVE_FAILED( pxQueue );
					return errQUEUE_EMPTY;
				}else if ( xEntryTimeSet == pdFALSE ) 
				{
					/* 初始化阻塞超时结构体变量,初始化进入阻塞的时间 xTickCount 和溢出次数 xNumOfOverflows */
					vTaskSetTimeOutState( &xTimeOut ); 
					xEntryTimeSet = pdTRUE;
				}else 
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		taskEXIT_CRITICAL();
		

		//接下来的操作系统不允许其他任务访问队列,简单粗暴挂起调度器就不会进行任务切换,但是挂起调度器并不会禁止中断的发生,所以还需给队列上锁。
		//因为系统不希望突然有中断操作这个队列的 xTasksWaitingToReceive 列表和 xTasksWaitingToSend 列表。
		vTaskSuspendAll();
		prvLockQueue( pxQueue ); 
 
		/* 检查超时时间是否已经过去了*/
		if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) 
		{
			/* 如果队列还是空的 */
			if( prvIsQueueEmpty( pxQueue ) != pdFALSE ) 
			{
				traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue ); 
				/* 将当前任务添加到队列的等待接收列表中以及阻塞延时列表,阻塞时间为用户指定的超时时间 xTicksToWait */
				vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
				prvUnlockQueue( pxQueue );
				if( xTaskResumeAll() == pdFALSE ) 
				{
					/* 如果有任务优先级比当前任务高,会进行一次任务切换 */
					portYIELD_WITHIN_API();
				}else 
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}else 
			{
				/* 如果队列有消息了,就再试一次获取消息 */
				prvUnlockQueue( pxQueue ); 
				( void ) xTaskResumeAll();
			}
		}else 
		{
			/* 超时时间已过,退出 */
			prvUnlockQueue( pxQueue ); 
			( void ) xTaskResumeAll();

			if ( prvIsQueueEmpty( pxQueue ) != pdFALSE ) 
			{
				/* 如果队列还是空的,返回错误代码 errQUEUE_EMPTY */
				traceQUEUE_RECEIVE_FAILED( pxQueue );
				return errQUEUE_EMPTY; 
			}else 
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
	}
}

在获取队列中的消息时候,我们必须要定义一个存储读取数据的地方,并且该数据区域大小不小于消息大小,否则,很可能引发地址非法的错误。
无论是发送或者是接收消息都是以拷贝的方式进行,如果消息过于庞大,可以将消息的地址作为消息进行发送、接收。
队列是具有自己独立权限的内核对象,并不属于任何任务。所有任务都可以向同一队列写入和读出。
一个队列由多任务或中断写入是经常的事,但由多个任务读出倒是用的比较少。

总实验案例

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "bsp_led.h"
#include "bsp_usart.h"
#include "bsp_key.h"

static TaskHandle_t AppTaskCreate_Handle = NULL;	/* 创建任务句柄 */
static TaskHandle_t Receive_Task_Handle = NULL;		/* LED 任务句柄 */
static TaskHandle_t Send_Task_Handle = NULL;		/* KEY 任务句柄 */

QueueHandle_t Test_Queue =NULL;
 
#define QUEUE_LEN 	4 	/* 队列的长度,最大可包含多少个消息 */ 
#define QUEUE_SIZE  4  	/* 队列中每个消息大小(字节) */
 
int main(void)
{
	BaseType_t xReturn = pdPASS;	/* 定义一个创建信息返回值,默认为 pdPASS */
 
	BSP_Init();
	printf("按下 KEY1 或者 KEY2 发送队列消息\n");
	printf("Receive 任务接收到消息在串口回显\n\n");
	
	/* 创建 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(); /* 启动任务,开启调度 */
	else
		return -1;
 
	while (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 ( Test_Queue != NULL ) 
		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 (xReturn == pdPASS)
		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 (xReturn == pdPASS)
		printf("创建 Send_Task 任务成功!\n\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\n\n",r_queue); 
		else 
			printf("数据接收出错,错误代码: 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 ) 
		{ 
			/* KEY1 被按下 */ 
			printf("发送消息 send_data1!\n"); 
			xReturn = xQueueSend( 	Test_Queue, 		/* 消息队列的句柄 */ 
									&send_data1,		/* 发送的消息内容 */ 
									0 ); 				/* 等待时间 0 */ 
			if (pdPASS == xReturn) 
				printf("消息 send_data1 发送成功!\n\n"); 
		} 
		if ( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON ) 
		{ 
			/* KEY2 被按下 */ 
			printf("发送消息 send_data2!\n"); 
			xReturn = xQueueSend( 	Test_Queue, 		/* 消息队列的句柄 */ 
									&send_data2,		/* 发送的消息内容 */ 
									0 ); 				/* 等待时间 0 */ 
			if (pdPASS == xReturn) 
				printf("消息 send_data2 发送成功!\n\n"); 
		} 
		vTaskDelay(20);/* 延时 20 个 tick */ 
	} 
}

你可能感兴趣的:(FreeRTOS个人笔记,操作系统,c语言,单片机)