示例8: 队列的基本使用

本节代码为:FreeRTOS_08_queue

本程序会创建一个队列,然后创建2个发送任务、1个接收任务:

  • 发送任务优先级为1,分别往队列中写入100、200
  • 接收任务优先级为2,读队列、打印数值

main函数中创建的队列、创建了发送任务、接收任务,代码如下:

/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;

int main( void )
{
	prvSetupHardware();
	
    /* 创建队列: 长度为5,数据大小为4字节(存放一个整数) */
    xQueue = xQueueCreate( 5, sizeof( int32_t ) );

	if( xQueue != NULL )
	{
		/* 创建2个任务用于写队列, 传入的参数分别是100、200
		 * 任务函数会连续执行,向队列发送数值100、200
		 * 优先级为1
		 */
		xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
		xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );

		/* 创建1个任务用于读队列
		 * 优先级为2, 高于上面的两个任务
		 * 这意味着队列一有数据就会被读走
		 */
		xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );

		/* 启动调度器 */
		vTaskStartScheduler();
	}
	else
	{
		/* 无法创建队列 */
	}

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

发送任务的函数中,不断往队列中写入数值,代码如下:

static void vSenderTask( void *pvParameters )
{
	int32_t lValueToSend;
	BaseType_t xStatus;

	/* 我们会使用这个函数创建2个任务
	 * 这些任务的pvParameters不一样
 	 */
	lValueToSend = ( int32_t ) pvParameters;

	/* 无限循环 */
	for( ;; )
	{
		/* 写队列
		 * xQueue: 写哪个队列
		 * &lValueToSend: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
		 * 0: 不阻塞, 如果队列满的话, 写入失败, 立刻返回
		 */
		xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );

		if( xStatus != pdPASS )
		{
			printf( "Could not send to the queue.\r\n" );
		}
	}
}

接收任务的函数中,读取队列、判断返回值、打印,代码如下:

static void vReceiverTask( void *pvParameters )
{
	/* 读取队列时, 用这个变量来存放数据 */
	int32_t lReceivedValue;
	BaseType_t xStatus;
	const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );

	/* 无限循环 */
	for( ;; )
	{
		/* 读队列
		 * xQueue: 读哪个队列
		 * &lReceivedValue: 读到的数据复制到这个地址
		 * xTicksToWait: 如果队列为空, 阻塞一会
		 */
		xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );

		if( xStatus == pdPASS )
		{
			/* 读到了数据 */
			printf( "Received = %d\r\n", lReceivedValue );
		}
		else
		{
			/* 没读到数据 */
			printf( "Could not receive from the queue.\r\n" );
		}
	}
}

程序运行结果如下:

任务调度情况如下图所示:

 示例9: 分辨数据源

本节代码为:FreeRTOS_09_queue_datasource

当有多个发送任务,通过同一个队列发出数据,接收任务如何分辨数据来源?数据本身带有"来源"信息,比如写入队列的数据是一个结构体,结构体中的lDataSouceID用来表示数据来源:

typedef struct {
    ID_t eDataID;
    int32_t lDataValue;
}Data_t;

不同的发送任务,先构造好结构体,填入自己的eDataID,再写队列;接收任务读出数据后,根据eDataID就可以知道数据来源了,如下图所示:

  • CAN任务发送的数据:eDataID=eMotorSpeed
  • HMI任务发送的数据:eDataID=eSpeedSetPoint

FreeRTOS_09_queue_datasource程序会创建一个队列,然后创建2个发送任务、1个接收任务:

  • 创建的队列,用来发送结构体:数据大小是结构体的大小
  • 发送任务优先级为2,分别往队列中写入自己的结构体,结构体中会标明数据来源
  • 接收任务优先级为1,读队列、根据数据来源打印信息

main函数中创建了队列、创建了发送任务、接收任务,代码如下:

/* 定义2种数据来源(ID) */
typedef enum
{
	eMotorSpeed,
	eSpeedSetPoint
} ID_t;

/* 定义在队列中传输的数据的格式 */
typedef struct {
    ID_t eDataID;
    int32_t lDataValue;
}Data_t;

/* 定义2个结构体 */
static const Data_t xStructsToSend[ 2 ] =
{
	{ eMotorSpeed,    10 }, /* CAN任务发送的数据 */
	{ eSpeedSetPoint, 5 }   /* HMI任务发送的数据 */
};

/* vSenderTask被用来创建2个任务,用于写队列
 * vReceiverTask被用来创建1个任务,用于读队列
 */
static void vSenderTask( void *pvParameters );
static void vReceiverTask( void *pvParameters );

/*-----------------------------------------------------------*/

/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;

int main( void )
{
	prvSetupHardware();
	
    /* 创建队列: 长度为5,数据大小为4字节(存放一个整数) */
    xQueue = xQueueCreate( 5, sizeof( Data_t ) );

	if( xQueue != NULL )
	{
		/* 创建2个任务用于写队列, 传入的参数是不同的结构体地址
		 * 任务函数会连续执行,向队列发送结构体
		 * 优先级为2
		 */
		xTaskCreate(vSenderTask, "CAN Task", 1000, (void *) &(xStructsToSend[0]), 2, NULL);
		xTaskCreate(vSenderTask, "HMI Task", 1000, (void *) &( xStructsToSend[1]), 2, NULL);

		/* 创建1个任务用于读队列
		 * 优先级为1, 低于上面的两个任务
		 * 这意味着发送任务优先写队列,队列常常是满的状态
		 */
		xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );

		/* 启动调度器 */
		vTaskStartScheduler();
	}
	else
	{
		/* 无法创建队列 */
	}

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

发送任务的函数中,不断往队列中写入数值,代码如下:

static void vSenderTask( void *pvParameters )
{
	BaseType_t xStatus;
	const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );

	/* 无限循环 */
	for( ;; )
	{
		/* 写队列
		 * xQueue: 写哪个队列
		 * pvParameters: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
		 * xTicksToWait: 如果队列满的话, 阻塞一会
		 */
		xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );

		if( xStatus != pdPASS )
		{
			printf( "Could not send to the queue.\r\n" );
		}
	}
}

接收任务的函数中,读取队列、判断返回值、打印,代码如下:

static void vReceiverTask( void *pvParameters )
{
	/* 读取队列时, 用这个变量来存放数据 */
	Data_t xReceivedStructure;
	BaseType_t xStatus;

	/* 无限循环 */
	for( ;; )
	{
		/* 读队列
		 * xQueue: 读哪个队列
		 * &xReceivedStructure: 读到的数据复制到这个地址
		 * 0: 没有数据就即刻返回,不阻塞
		 */
		xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );

		if( xStatus == pdPASS )
		{
			/* 读到了数据 */
			if( xReceivedStructure.eDataID == eMotorSpeed )
			{
				printf( "From CAN, MotorSpeed = %d\r\n", xReceivedStructure.lDataValue );
			}
			else if( xReceivedStructure.eDataID == eSpeedSetPoint )
			{
				printf( "From HMI, SpeedSetPoint = %d\r\n", xReceivedStructure.lDataValue );
			}
		}
		else
		{
			/* 没读到数据 */
			printf( "Could not receive from the queue.\r\n" );
		}
	}
}

运行结果如下:

任务调度情况如下图所示:

  • t1:HMI是最后创建的最高优先级任务,它先执行,一下子向队列写入5个数据,把队列都写满了
  • t2:队列已经满了,HMI任务再发起第6次写操作时,进入阻塞状态。这时CAN任务是最高优先级的就绪态任务,它开始执行
  • t3:CAN任务发现队列已经满了,进入阻塞状态;接收任务变为最高优先级的就绪态任务,它开始运行
  • t4:现在,HMI任务、CAN任务的优先级都比接收任务高,它们都在等待队列有空闲的空间;一旦接收任务读出1个数据,会马上被抢占。被谁抢占?谁等待最久?HMI任务!所以在t4时刻,切换到HMI任务。
  • t5:HMI任务向队列写入第6个数据,然后再次阻塞,这是CAN任务已经阻塞很久了。接收任务变为最高优先级的就绪态任务,开始执行。
  • t6:现在,HMI任务、CAN任务的优先级都比接收任务高,它们都在等待队列有空闲的空间;一旦接收任务读出1个数据,会马上被抢占。被谁抢占?谁等待最久?CAN任务!所以在t6时刻,切换到CAN任务。
  • t7:CAN任务向队列写入数据,因为仅仅有一个空间供写入,所以它马上再次进入阻塞状态。这时HMI任务、CAN任务都在等待空闲空间,只有接收任务可以继续执行。

示例10: 传输大块数据

本节代码为:FreeRTOS_10_queue_bigtransfer

FreeRTOS的队列使用拷贝传输,也就是要传输uint32_t时,把4字节的数据拷贝进队列;要传输一个8字节的结构体时,把8字节的数据拷贝进队列。

如果要传输1000字节的结构体呢?写队列时拷贝1000字节,读队列时再拷贝1000字节?不建议这么做,影响效率!

这时候,我们要传输的是这个巨大结构体的地址:把它的地址写入队列,对方从队列得到这个地址,使用地址去访问那1000字节的数据。

使用地址来间接传输数据时,这些数据放在RAM里,对于这块RAM,要保证这几点:

  • RAM的所有者、操作者,必须清晰明了 这块内存,就被称为"共享内存"。要确保不能同时修改RAM。比如,在写队列之前只有由发送者修改这块RAM,在读队列之后只能由接收者访问这块RAM。
  • RAM要保持可用 这块RAM应该是全局变量,或者是动态分配的内存。对于动然分配的内存,要确保它不能提前释放:要等到接收者用完后再释放。另外,不能是局部变量。

FreeRTOS_10_queue_bigtransfer程序会创建一个队列,然后创建1个发送任务、1个接收任务:

  • 创建的队列:长度为1,用来传输"char *"指针
  • 发送任务优先级为1,在字符数组中写好数据后,把它的地址写入队列
  • 接收任务优先级为2,读队列得到"char *"值,把它打印出来

这个程序故意设置接收任务的优先级更高,在它访问数组的过程中,发送任务无法执行、无法写这个数组。

main函数中创建了队列、创建了发送任务、接收任务,代码如下:

/* 定义一个字符数组 */
static char pcBuffer[100];


/* vSenderTask被用来创建2个任务,用于写队列
 * vReceiverTask被用来创建1个任务,用于读队列
 */
static void vSenderTask( void *pvParameters );
static void vReceiverTask( void *pvParameters );

/*-----------------------------------------------------------*/

/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;

int main( void )
{
	prvSetupHardware();
	
    /* 创建队列: 长度为1,数据大小为4字节(存放一个char指针) */
    xQueue = xQueueCreate( 1, sizeof(char *) );

	if( xQueue != NULL )
	{
		/* 创建1个任务用于写队列
		 * 任务函数会连续执行,构造buffer数据,把buffer地址写入队列
		 * 优先级为1
		 */
		xTaskCreate( vSenderTask, "Sender", 1000, NULL, 1, NULL );

		/* 创建1个任务用于读队列
		 * 优先级为2, 高于上面的两个任务
		 * 这意味着读队列得到buffer地址后,本任务使用buffer时不会被打断
		 */
		xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );

		/* 启动调度器 */
		vTaskStartScheduler();
	}
	else
	{
		/* 无法创建队列 */
	}

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

发送任务的函数中,现在全局大数组pcBuffer中构造数据,然后把它的地址写入队列,代码如下:

static void vSenderTask( void *pvParameters )
{
	BaseType_t xStatus;
	static int cnt = 0;
	
	char *buffer;

	/* 无限循环 */
	for( ;; )
	{
		sprintf(pcBuffer, "www.100ask.net Msg %d\r\n", cnt++);
		buffer = pcBuffer; // buffer变量等于数组的地址, 下面要把这个地址写入队列
		
		/* 写队列
		 * xQueue: 写哪个队列
		 * pvParameters: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
		 * 0: 如果队列满的话, 即刻返回
		 */
		xStatus = xQueueSendToBack( xQueue, &buffer, 0 ); /* 只需要写入4字节, 无需写入整个buffer */

		if( xStatus != pdPASS )
		{
			printf( "Could not send to the queue.\r\n" );
		}
	}
}

接收任务的函数中,读取队列、得到buffer的地址、打印,代码如下:

static void vReceiverTask( void *pvParameters )
{
	/* 读取队列时, 用这个变量来存放数据 */
	char *buffer;
	const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );	
	BaseType_t xStatus;

	/* 无限循环 */
	for( ;; )
	{
		/* 读队列
		 * xQueue: 读哪个队列
		 * &xReceivedStructure: 读到的数据复制到这个地址
		 * xTicksToWait: 没有数据就阻塞一会
		 */
		xStatus = xQueueReceive( xQueue, &buffer, xTicksToWait); /* 得到buffer地址,只是4字节 */

		if( xStatus == pdPASS )
		{
			/* 读到了数据 */
			printf("Get: %s", buffer);
		}
		else
		{
			/* 没读到数据 */
			printf( "Could not receive from the queue.\r\n" );
		}
	}
}

运行结果如下图所示:

示例11: 邮箱(Mailbox)

本节代码为:FreeRTOS_11_queue_mailbox

FreeRTOS的邮箱概念跟别的RTOS不一样,这里的邮箱称为"橱窗"也许更恰当:

  • 它是一个队列,队列长度只有1
  • 写邮箱:新数据覆盖旧数据,在任务中使用xQueueOverwrite(),在中断中使用xQueueOverwriteFromISR()。 既然是覆盖,那么无论邮箱中是否有数据,这些函数总能成功写入数据。
  • 读邮箱:读数据时,数据不会被移除;在任务中使用xQueuePeek(),在中断中使用xQueuePeekFromISR()。 这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。

main函数中创建了队列(队列长度为1)、创建了发送任务、接收任务:

  • 发送任务的优先级为2,它先执行
  • 接收任务的优先级为1

代码如下:

/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;

int main( void )
{
	prvSetupHardware();
	
    /* 创建队列: 长度为1,数据大小为4字节(存放一个char指针) */
    xQueue = xQueueCreate( 1, sizeof(uint32_t) );

	if( xQueue != NULL )
	{
		/* 创建1个任务用于写队列
		 * 任务函数会连续执行,构造buffer数据,把buffer地址写入队列
		 * 优先级为2
		 */
		xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );

		/* 创建1个任务用于读队列
		 * 优先级为1
		 */
		xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );

		/* 启动调度器 */
		vTaskStartScheduler();
	}
	else
	{
		/* 无法创建队列 */
	}

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

发送任务、接收任务的代码和执行流程如下:

  • A:发送任务先执行,马上阻塞
  • BC:接收任务执行,这是邮箱无数据,打印"Could not ..."。在发送任务阻塞过程中,接收任务多次执行、多次打印。
  • D:发送任务从阻塞状态退出,立刻执行、写队列
  • E:发送任务再次阻塞
  • FG、HI、……:接收任务不断"偷看"邮箱,得到同一个数据,打印出多个"Get: 0"
  • J:发送任务从阻塞状态退出,立刻执行、覆盖队列,写入1
  • K:发送任务再次阻塞
  • LM、……:接收任务不断"偷看"邮箱,得到同一个数据,打印出多个"Get: 1"

运行结果如下图所示:

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐