FreeRTOS学习笔记八【队列-下】

本文深入探讨FreeRTOS中队列的高级应用,包括如何在队列中传输大数据和大小可变的数据,如何使用队列集从多个队列中高效获取数据,以及如何利用队列创建邮箱进行数据存储。文章提供了详细的代码示例,帮助读者理解队列在实时操作系统中的灵活运用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

队列中传输大数据(一个包)或大小可变的数据

传输大数据

如果存储在队列中的数据项很大,则最好将指向数据的指针存放在队列中,这种方法可以有效的节省数据的拷贝时间以及降低创建队列时所需的空间。但使用指针时必须注意一下两点:

  1. 指针指向的RAM的所有者是明确定义的。
    通过指针在任务之间共享内存时,必须确保各个任务不会同时修改内存的内容。理想情况下,在将指针入队前只允许发送任务访问内存,在指针出队后只允许接收任务访问内存。
  2. 指针指向的RAM是有效的。
    如果指向的内存时动态分配(或者从预先分配的内存池中获得)的,那么应该只有一个任务负责释放任务。在释放该内存后,任何任务后不可以再访问该内存。并且不应该使用指针访问存放在栈上的变量(自动变量)。

下面通过示例来展示使用指针传递数据。
创建一个可以存放5个指针的队列:

QueueHandle_t xPointerQueue;
xPointerQueue = xQueueCreate( 5, sizeof( char * ) );

分配缓冲区,将数据存入缓冲区,然后将指针入队:

void vStringSendingTask( void *pvParameters )
{
	char *pcStringToSend;
	const size_t xMaxStringLength = 50;
	BaseType_t xStringNumber = 0;
	for( ;; )
	{
		/* 分配缓存区 */
		pcStringToSend = ( char * ) prvGetBuffer( xMaxStringLength );
		/* 在缓冲区存放数据 */
		snprintf( pcStringToSend, xMaxStringLength, "String number %d\r\n", xStringNumber );
		
		xStringNumber++;
		/* 指针入队 */
		xQueueSend( xPointerQueue, &pcStringToSend, portMAX_DELAY );
	}
}

从队列获取指针,然后打印数据,最后释放缓存区:

void vStringReceivingTask( void *pvParameters )
{
	char *pcReceivedString;
	for( ;; )
	{
		/* 获取指针 */
		xQueueReceive( xPointerQueue, &pcReceivedString, portMAX_DELAY );
		/* 打印数据 */
		vPrintString( pcReceivedString );
		/* 释放缓存区 */
		prvReleaseBuffer( pcReceivedString );
	}
}

传输不同类型和长度的数据

前面介绍了通过队列传输结构体和指针,通过这两者的组合就可以在单个队列中从任何数据源接收任意数据类型。

从多个队列中获取数据

队列集合

应用程序中通常需要单个任务来接收不同大小、不同含义、不同数据源的数据,在前面介绍了解决这些问题的方法,但有时应用程序运行在受限的设备或者程序不能满足前面的一些要求,因此需要为某些数据使用单独的队列。例如,第三方代码可能会假设某个专用队列存在,这种情况下可以使用队列集。队列集运行从多个队列中获取数据,但任务不会依次轮询灭个队列来确定哪个队列还包含数据。
与使用接收结构体的单个队列实现相同功能的代码比,使用队列集从多个源接收数据的代码并不那么整洁并且效率较低,因此建议仅在有约束条件时而不能满足前面介绍的方法时才使用队列集。

如果要使用队列集功能,需要将FreeRTOSConfig.h中configUSE_QUEUE_SETS设置为1。通过以下几个步骤使用一个队列集:

  1. 创建队列集。
  2. 向集合添加队列。
    信号量也可以添加到队列集中。 信号量将在后面介绍。
  3. 从队列集中读取以确定集合中的哪些队列包含数据。
    当队列集中的队列中有数据时,在调用从队列集中获取数据的函数会返回收到数据的队列的句柄,此时就可直接从该句柄中获取数据。
    注意:如果队列是队列集的成员,则只能使用队列集返回的队列句柄,否则不要直接从队列中读取数据。

xQueueCreateSet()

在使用队列集之前必须显示创建它之后才能使用。QueueSetHandle_t是队列集句柄的类型,通过调用xQueueCreateSet()可以创建一个队列集,它的原型如下:

QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );

参数:

  • uxEventQueueLength
    当队列集中的队列收到数据时,接收到数据队列的句柄会被发送到队列集。uxEventQueueLength定义创建的队列集可以容纳的最大句柄数。只有当集合中的队列接收到数据时,才会将队列句柄发送到队列集。如果队列已满,则无法将队列句柄发送到队列集。因此队列集可容纳的最大句柄数就是队列集中每个队列的数据项数之和。例如,集合中有5个队列,每个队列可以容纳3个数据项,那么uxEventQueueLength的值就是15。信号量也可以添加到队列,二值信号量的长度为1,计数信号量的长度是最大计数值,例如,集合中有一个二值信号量,一个长度为10的队列,那么uxEventQueueLength = 10 + 1 = 11。

返回值:

  • NULL 队列集创建失败,因为没有足够的内存来存储队列集。
  • 非NULL,队列集创建成功,返回的是队列集的句柄。

xQueueAddToSet()

xQueueAddToSet()将队列或信号量添加到队列集。原型如下:

BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet );

参数:

  • xQueueOrSemaphore
    要添加到队列集的队列或信号量的句柄。
    队列句柄和信号量句柄都可以强制转换为QueueSetMemberHandle_t类型。
  • xQueueSet
    队列集的句柄。

返回值:

  • pdPASS
    仅当队列或信号量成功添加到队列集时,才会返回pdPASS。
  • pdFAIL
    如果无法将队列或信号量添加到队列集,则将返回pdFAIL。
    队列和二值信号量只能在为空时添加到集合中。 计数信号量只能在计数为零时添加到集合中。 队列和信号量都只能被添加到一个集合中,不可以同时添加到两个队列集。

xQueueSelectFromSet()

xQueueSelectFromSet()从队列集中获取队列的句柄。当集合中队列或信号量收到数据时,队列或信号量的句柄会被发送到队列集,在任务调用xQueueSelectFromSet()时返回句柄,当该函数返回的句柄时,表示该句柄的队列(信号量)已有数据,可以直接从中获取数据。
注意:只能从xQueueSelectFromSet()返回的句柄中获取数据,而不能直接从加入集合中的队列或信号量里获取数据。同时每次从返回的句柄中只能获取一项数据。
xQueueSelectFromSet()的原型如下:

QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, const TickType_t xTicksToWait );

参数:

  • xQueueSet
    队列集的句柄。
  • xTicksToWait
    如果xTicksToWait为0,并且队列集中没有队列收到数据,立即返回,如果xTicksToWait不为0,表示队列集中没有队列收到数据的最长等大时间。指定的时间可以通过pdMS_TO_TICKS()转换。如果将FreeRTOSConfig.h中INCLUDE_vTaskSuspend设置为1,将xTicksToWait设置为portMAX_DELAY,任务将无限期地等待。

返回值:

  • 非NULL值表示收到数据的队列或信号量的句柄。 句柄返回QueueSetMemberHandle_t类型,但可以强制转换为QueueHandle_t类型或SemaphoreHandle_t类型。
  • NULL则无法从队列集中读取句柄。 如果xTicksToWait不为零,则该时间内都没有收到数据。

示例

示例中创建了两个发送任务和一个接收任务。 两个发送任务分别在两个队列上发送数据, 两个队列被添加到队列集中,接收任务从队列集中读取以确定两个队列中的哪一个包含数据。

static QueueHandle_t xQueue1 = NULL, xQueue2 = NULL;
/* 队列集的句柄 */
static QueueSetHandle_t xQueueSet = NULL;

void vSenderTask1( void *pvParameters )
{
	const TickType_t xBlockTime = pdMS_TO_TICKS( 100 );
	const char * const pcMessage = "Message from vSenderTask1\r\n";
	for( ;; )
	{
		vTaskDelay( xBlockTime );
		/* 在队列1中发送数据 */
		xQueueSend( xQueue1, &pcMessage, 0 );
	}
}

void vSenderTask2( void *pvParameters )
{
	const TickType_t xBlockTime = pdMS_TO_TICKS( 200 );
	const char * const pcMessage = "Message from vSenderTask2\r\n";
	for( ;; )
	{
		vTaskDelay( xBlockTime );
		/* 在队列2中发送数据*/
		xQueueSend( xQueue2, &pcMessage, 0 );
	}
}
void vReceiverTask( void *pvParameters )
{
	QueueHandle_t xQueueThatContainsData;
	char *pcReceivedString;
	for( ;; )
	{
		/* 获取收到数据的队列的句柄 */
		xQueueThatContainsData = ( QueueHandle_t ) xQueueSelectFromSet( xQueueSet,
		portMAX_DELAY );
		/* 从队列中获取数据 */
		xQueueReceive( xQueueThatContainsData, &pcReceivedString, 0 );
		
		vPrintString( pcReceivedString );
	}
}

int main( void )
{
	/* 创建两个队列 */
	xQueue1 = xQueueCreate( 1, sizeof( char * ) );
	xQueue2 = xQueueCreate( 1, sizeof( char * ) );
	/* 创建队列集 */
	xQueueSet = xQueueCreateSet( 1 * 2 );
	/* 添加队列到队列集 */
	xQueueAddToSet( xQueue1, xQueueSet );
	xQueueAddToSet( xQueue2, xQueueSet );
	/* 创建两个发送任务 */
	xTaskCreate( vSenderTask1, "Sender1", 1000, NULL, 1, NULL );
	xTaskCreate( vSenderTask2, "Sender2", 1000, NULL, 1, NULL );
	/* 创建一个接收任务 */
	xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
	vTaskStartScheduler();
	for( ;; );
	return 0;
}

运行结果:
在这里插入图片描述

使用队列创建邮箱

“邮箱”用于表示长度为1的队列。由于队列在应用程序中的使用方式,队列可能会被描述为邮箱,而不是因为它与队列有功能差异:

  • 队列用于将数据从一个任务发送到另一个任务,或从中断服务例程发送到任务。发件方将一个数据项放入队列中,接收方从队列中删除该数据项。数据通过队列从发送方传递到接收方。
  • 邮箱用于保存任何任务或任何中断服务例程都可以读取的数据。数据不会经过邮箱,而是保留在邮箱中,直到被覆盖。发件方覆盖邮箱中的数据,接收方从邮箱中读取数据,但不从邮箱中删除该数据。
    创建一个邮箱的方法如下:
/* 实际使用中可以可以是任意类型数据,不一定像本例这样, 这里包含一个更新数据时时间戳,和存放的数据*/
typedef struct xExampleStructure
{
	TickType_t xTimeStamp;
	uint32_t ulValue;
} Example_t;
QueueHandle_t xMailbox;
void vAFunction( void )
{
	/* 创建一个邮箱 */
	xMailbox = xQueueCreate( 1, sizeof( Example_t ) );
}

下面介绍两个允许将队列用作邮箱的队列API函数。

xQueueOverwrite()

与xQueueSendToBack()函数一样,xQueueOverwrite()函数将数据发送到队列。 与xQueueSendToBack()不同的是如果队列已满,则xQueueOverwrite()将覆盖队列中已有的数据。xQueueOverwrite()只应与长度为1的队列一起使用。 该限制避免了实现在队满时需要选择队列中的哪个数据项被覆盖的实现。
注意:不要从中断服务程序调用xQueueOverwrite()。中断安全版本为xQueueOverwriteFromISR()。
函数原型如下:

BaseType_t xQueueOverwrite( QueueHandle_t xQueue, const void * pvItemToQueue );

参数:

  • xQueue
    队列的句柄。
  • pvItemToQueue
    指向写入队列的数据。

返回值:

  • 只会返回pdPASS,因为队满时也会写入数据。

使用示例:

void vUpdateMailbox( uint32_t ulNewValue )
{
	Example_t xData;
	
	xData.ulValue = ulNewValue;

	xData.xTimeStamp = xTaskGetTickCount();
	
	xQueueOverwrite( xMailbox, &xData );
}

xQueuePeek()

xQueuePeek()用于从队列接收(读取)数据,但不删除数据。 它从队列头部接收数据,而不修改存储在队列中的数据,或者数据存储在队列中的顺序。
注意:不要从中断服务程序调用xQueuePeek()。 中断安全版本为xQueuePeekFromISR()。
函数原型:

BaseType_t xQueuePeek( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );

它的参数和返回值与xQueueReceive()的相同。
使用示例:

BaseType_t vReadMailbox( Example_t *pxData )
{
	TickType_t xPreviousTimeStamp;
	BaseType_t xDataUpdated;
	/* 记录上一次数据更新时间 */
	xPreviousTimeStamp = pxData->xTimeStamp;
	/* 获取数据 */
	xQueuePeek( xMailbox, pxData, portMAX_DELAY );
	/* 如果数据已更新返回pdTRUE,否则返回pdFALSE */
	if( pxData->xTimeStamp > xPreviousTimeStamp )
	{
		xDataUpdated = pdTRUE;
	}
	else
	{
		xDataUpdated = pdFALSE;
	}
	return xDataUpdated;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值