FreeRTOS学习笔记——(4)同步与互斥之队列

系列文章目录

FreeRTOS学习笔记—— 系列文章目录

文章目录

  • 系列文章目录
  • 同步与互斥
  • 队列
    • 一、队列函数
      • 1、创建
      • 2、复位
      • 3、删除
      • 4、写队列
      • 5、读队列
      • 6、查询
      • 7、覆盖、偷看
    • 二、队列的应用
      • 1、串口发送字符串
      • 2、分辨数据源( 多个数据来源,一个队列 )
      • 3、队列集( 可监测多个队列 )


同步与互斥

同步:就是:我(A)正在用会议室,你(B)等会再用。
互斥:就是:我(A)正在用会议室,你(B)不能进来用。
互斥可以使用同步来实现,你(B)等我(A)用完会议室,你(B)再用会议室。这就是使用同步来实现互斥
在这个过程中,A、B是互斥地访问会议室,会议室被称之为临界资源。这里使用了同步机制实现了“临界资源”的“互斥访问”。
任务A、B都要使用串口来打印,串口就是临界资源。如果A、B同时使用串口,那么打印出来的信息就是A、B混杂,无法分辨。所以使用串口时,应该:A用完,B再用;B用完,A再用。

能实现同步、互斥的内核方法有:队列(queue)、信号量(semaphoe)、互斥量(mutex)、事件组(event group)、任务通知(task notification)。

队列

队列包含若干个数据:队列中有若干项,这被称为"长度"(length),每个数据大小固定,创建队列时就要指定长度、数据大小。

一、队列函数

1、创建

队列的创建有两种方法:动态分配内存、静态分配内存

动态创建队列函数:xQueueCreate,在函数内部动态分配内存。函数说明如下:

/* 函数原型
 * 参数:uxQueueLength:队列长度,最多能存放多少个数据(item)
 * 参数:uxItemSize   :每个数据(item)的大小,以字节为单位。
 * 返回值:非 0 :创建成功,返回句柄,使用这个句柄操作这个队列。
 *		   NULL:创建失败,内存不足
 */
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );

静态创建队列函数:xQueueCreateStatic,队列的内存事先分配好,函数说明如下:

/* 函数原型
 * 参数1:uxQueueLength:队列长度,最多能存放多少个数据(item)
 * 参数2:uxItemSize   :每个数据(item)的大小,以字节为单位。
 * 参数3:pucQueueStorageBuffer:必须指向一个 uint8_t 数组,此数组大小至少为  uxQueueLength * uxItemSize
 * 参数4:pxQueueBuffer:必须定义一个StaticQueue_t结构体,用来保存队列的数据结构
 * 返回值:非 0 :创建成功,返回句柄,使用这个句柄操作这个队列。
 *		   NULL:创建失败,pxQueueBuffer 为 NULL
 */
QueueHandle_t xQueueCreateStatic(
             UBaseType_t uxQueueLength,
             UBaseType_t uxItemSize,
             uint8_t *pucQueueStorageBuffer,
             StaticQueue_t *pxQueueBuffer
           );

2、复位

复位队列函数: xQueueReset,把队列恢复为初始状态

/* 参数:pxQueue : 复位队列的句柄
 * 返回值: pdPASS(必定成功)
 */
BaseType_t xQueueReset( QueueHandle_t pxQueue);

3、删除

删除队列函数: vQueueDelete,只能删除动态方法创建的队列,释放内存

/* 参数:xQueue : 要删除队列的句柄
 */
void vQueueDelete( QueueHandle_t xQueue );

4、写队列

可以把数据写到队列的头部,也可以写到尾部,写队列函数有两个版本:在任务中使用、在中断中使用(函数后缀为 ISR 的是在中断中使用)

/* 
 * 参数:xQueue        :队列句柄,要写哪个队列
 * 
 * 参数:pvItemToQueue :数据指针,这个数据的值会被复制进队列
 * 
 * 参数:xTicksToWait  :如果队列满则无法写入新数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。
						如果被设为0,无法写入数据时函数会立刻返回;
						如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写
						
 * 返回值:pdPASS       :数据成功写入了队列
 *		  errQUEUE_FULL:写入失败,因为队列满了。
 */



/* 
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend( //等同于xQueueSendToBack
                QueueHandle_t   xQueue,
                const void    *pvItemToQueue,
                TickType_t    xTicksToWait
             );
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
                QueueHandle_t   xQueue,
                const void    *pvItemToQueue,
                TickType_t    xTicksToWait
             );
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
                   QueueHandle_t xQueue,
                   const void *pvItemToQueue,
                   BaseType_t *pxHigherPriorityTaskWoken
                 );
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
                QueueHandle_t   xQueue,
                const void    *pvItemToQueue,
                TickType_t    xTicksToWait
                );
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
                   QueueHandle_t xQueue,
                   const void *pvItemToQueue,
                   BaseType_t *pxHigherPriorityTaskWoken
                   );

5、读队列

读到一个数据后,队列中该数据会被移除。读队列函数有两个版本:在任务中使用、在中断中使用

/* 
 * 参数:xQueue        :队列句柄,要读哪个队列
 * 
 * 参数:pvBuffer      :bufer指针,队列的数据会被复制到这个buffer
 * 
 * 参数:xTicksToWait  :如果队列空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。
						如果被设为0,无法读出数据时函数会立刻返回;
						如果被设为portMAX_DELAY,则会一直阻塞直到有数据可读
						
 * 返回值:pdPASS        :数据成功读出
 *		  errQUEUE_EMPTY:读取失败,因为队列空了。
 */

BaseType_t xQueueReceive( QueueHandle_t xQueue,
            			 void * const pvBuffer,
             			 TickType_t xTicksToWait 
                         );
BaseType_t xQueueReceiveFromISR(
                  QueueHandle_t   xQueue,
                  void       *pvBuffer,
                  BaseType_t    *pxTaskWoken
               );

6、查询

可以查询队列中有多少个数据、有多少空余的空间。

// 返回队列中可用数据的个数
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );

// 返回队列中可用空间的个数
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

7、覆盖、偷看

覆盖:当队列长度为1时,可以使用 xQueueOverwrite() 或 xQueueOverwriteFromISR() 来覆盖数据。队列长度必须为1。

/* 覆盖队列
* xQueue: 写队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
             QueueHandle_t xQueue,
             const void * pvItemToQueue
          );
BaseType_t xQueueOverwriteFromISR(
             QueueHandle_t xQueue,
             const void * pvItemToQueue,
             BaseType_t *pxHigherPriorityTaskWoken
          );

偷看:队列中的数据供多方读取,也就是读取队列中的数据但不移除队列中的数据。从队列中复制出数据,但不移除,如果队列中没有数据,那么偷看时会导致阻塞;一旦队列中有数据,以后每次偷看都会成功。

/* 偷看队列
* xQueue: 偷看的队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 如果没有数据,阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
             QueueHandle_t xQueue,
             void * const pvBuffer,
             TickType_t xTicksToWait
          );
BaseType_t xQueuePeekFromISR(
                QueueHandle_t xQueue,
                void *pvBuffer,
              );

二、队列的应用

1、串口发送字符串

  1. 创建队列:长度为 1 ,传输 char * 指针
  2. 发送任务的优先级为 1 ,将字符串写入 参数,将参数的地址写入队列。
  3. 接收任务的优先级为 2 ,读队列得到 char * 的值,通过串口打印

在main函数中创建了队列、创建了发送任务、接收任务


QueueHandle_t xQueue;   //队列句柄

int main( void )
{
		
	prvSetupHardware();
	
	xQueue = xQueueCreate(1, sizeof( char * ));  // 创建队列: 长度为1,数据大小为4字节(存放一个char指针)
	if(xQueue == NULL)
	{
		printf("can not create queue\r\n");
	}
	
	printf( "Runing.\r\n" );
	/* 创建2个任务用于写队列
	*  优先级为1
	*/
	xTaskCreate(vSenderTask, "Sender1", 100, "Task1 is Runing!\r\n", 1, NULL); 
	xTaskCreate(vSenderTask, "Sender2", 100, "Task2 is Runing!\r\n", 1, NULL);
	
	/* 创建1个任务用于读队列
	*  优先级为2,高于写队列的任务,队列里一有数据就被读走
	*/
	xTaskCreate(vReceiverTask, "Recevier", 100, NULL, 2, NULL);

	/* 启动调度器. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

写队列函数:将字符串地址写入队列

static void vSenderTask(void * param)
{
	char * ValToSend; //接收到的字符存入 ValToSend
	BaseType_t xStatus;  // 写队列函数的返回值,用来判断是否写入成功
	
	ValToSend = ( char * )param;
	
	while (1)
	{
		
		xStatus = xQueueSend(xQueue, &ValToSend, portMAX_DELAY);
		
		if(xStatus != pdTRUE)
		{
			printf( "Could not send to the queue.\r\n" );
		}

	}
}

读队列函数:

static void vReceiverTask(void * param)
{
	BaseType_t  xStatus;
	char *    ValToReceived;
	const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
	
	while(1)
	{
		xStatus = xQueueReceive( xQueue, &ValToReceived, xTicksToWait );
		if(xStatus == pdTRUE)
		{
			printf("%s", ValToReceived);
		}
		else
		{
			printf( "Could not receive from the queue.\r\n" );
		}
	}
}

运行结果:
FreeRTOS学习笔记——(4)同步与互斥之队列_第1张图片

2、分辨数据源( 多个数据来源,一个队列 )

当有多个发送任务,通过同一个队列发出数据,这就要区分数据的来源。可以通过写入队列结构体,在结构体中定义成员表明数据来源。接下来上代码。

  1. 创建队列:用来发送结构体,数据的大小就是结构体的大小
  2. 发送任务的优先级为 2 ,往队列里写入结构体(结构体里标明了数据来源)
  3. 接收任务的优先级为 1 ,读队列、根据数据来源打印信息

main 函数中

typedef enum 
{
	Source01,
	Source02
} ID_t;  //  定义 枚举 类型,数据来源

typedef struct 
{
	ID_t eDataID;
	char * lDataValue;
	uint32_t val;
} Data_t;    //定义队列中的传输格式  结构体

// 定义结构体 数组,不同的数据来源,不同的数据
static const Data_t xStructToSend[ 2 ] = { {Source01, "Hello world", 6},
						                   {Source02, "Welcome", 8}
									     };
QueueHandle_t xQueue;   //队列句柄

int main( void )
{
	prvSetupHardware();
	
	xQueue = xQueueCreate(5, sizeof( Data_t )); // 创建队列: 长度为5,数据大小为结构体的大小
	if(xQueue == NULL)
	{
		printf("can not create queue\r\n");
	}
	
	/* 创建2个任务用于写队列,传入的参数是  结构体的地址
	*  优先级为2
	*/
	xTaskCreate(vSenderTask, "Source01 Task", 1000, (void *) &(xStructToSend[0]), 2, NULL);
	xTaskCreate(vSenderTask, "Source02 Task", 1000, (void *) &(xStructToSend[1]), 2, NULL);
	
	/* 创建1个任务用于读队列
	*  优先级为1,低于写队列的任务,发送任务优先执行,队列是满的
	*/
	xTaskCreate(vReceiverTask, "Recevier", 1000, NULL, 1, NULL);
	

	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

写队列函数:将结构体地址写入队列

static void vSenderTask(void * param)
{
	
	BaseType_t xStatus; // 写队列函数的返回值,用来判断是否写入成功
	
	while (1)
	{
		
		xStatus = xQueueSend(xQueue, param, portMAX_DELAY);
		
		if(xStatus != pdTRUE)
		{
			printf( "Could not send to the queue.\r\n" );
		}

	}
}

读队列函数:

static void vReceiverTask(void * param)
{
	BaseType_t  xStatus;
	Data_t   DateToReceived;  /* 读取队列时, 用这个变量来存放数据 */
	
	while(1)
	{
		
		
		xStatus = xQueueReceive( xQueue, &DateToReceived, 0 );
		if(xStatus == pdTRUE)
		{
			//读到数据
			if(DateToReceived.eDataID == Source01)  //判断数据源,做不同的事情
			{
				printf("From Source01 \r\nval = %d \r\n%s \r\n\r\n",  DateToReceived.val,  DateToReceived.lDataValue);
			}
			else if(DateToReceived.eDataID == Source02)
			{
				printf("From Source02 \r\nval = %d \r\n%s \r\n\r\n",  DateToReceived.val,  DateToReceived.lDataValue);
			}

		}
		else
		{
			printf( "Could not receive from the queue.\r\n" );
		}
	}
}

运行结果:
FreeRTOS学习笔记——(4)同步与互斥之队列_第2张图片

3、队列集( 可监测多个队列 )

如果某系统支持鼠标输入、按键输入、触摸屏输入等,每种输入设备都会产生数据,产生的数据就可以放到对应的队列中,在应用程序中要去读取这三个队列,任意一个队列有数据都可以唤醒应用,这时就需要队列集

队列集可以监测多个队列,从多个队列中挑出有数据的队列,去读队列。

队列集也是队列,队列集中放的是队列。队列集的大小是检测队列的长度和,例如:要检测三个队列A、B、C,则队列集的长度为:队列A的长度 + 队列B的长度 + 队列C的长度。

  1. 创建队列( queue )、队列集(queue set)
  2. 与队列建立联系
  3. A设备产生数据就会写 A 对应的队列,同时会将 队列 A 的句柄放到队列集中;也就是写队列一次就会写队列集一次。
  4. 读队列集,回返回某队列的句柄。
  5. 根据队列集返回的队列句柄去读队列

程序
创建两个队列,使用队列集监测这两个队列,队列中有数据就读出来。
main函数

static QueueHandle_t xQueueHandle1;  //队列的句柄
static QueueHandle_t xQueueHandle2;  //队列的句柄
static QueueSetHandle_t xQueueHandle_Set;  //队列集的句柄

int main( void )
{
	
		
	prvSetupHardware();

	//1、创建2个队列
	xQueueHandle1 = xQueueCreate(2, sizeof( int ));
	if(xQueueHandle1 == NULL)
	{
		printf("can not create queue\r\n");
	}
	xQueueHandle2 = xQueueCreate(2, sizeof( int ));
	if(xQueueHandle2 == NULL)
	{
		printf("can not create queue\r\n");
	}
	
	//2、创建队列集
	// xQueueCreateSet();是创建队列集函数,参数为队列集的大小(4) = 队列1的大小(2)  +  队列2的大小(2)
	xQueueHandle_Set = xQueueCreateSet(4); 
	
	//3、队列与队列集建立联系
	xQueueAddToSet(xQueueHandle1, xQueueHandle_Set);  //利用xQueueAddToSet(); 将 xQueueHandle1(队列1) 添加到 xQueueHandle_Set(队列集)中
	xQueueAddToSet(xQueueHandle2, xQueueHandle_Set);  //利用xQueueAddToSet(); 将 xQueueHandle2(队列2) 添加到 xQueueHandle_Set(队列集)中

	//4、创建3个任务
	
	xTaskCreate(Task1Function, "Task1", 100, NULL, 1, NULL);
	xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
	xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
	

	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

三个任务函数:
任务 1:隔一段时间就往队列 1 中写数据

static void Task1Function(void * param)
{
	
	BaseType_t xStatus;
	int i = 0;
	
	while (1)
	{
		// 任务 1:隔一段时间就往队列 1 中写数据
		xStatus = xQueueSend(xQueueHandle1, &i, portMAX_DELAY);
		if(xStatus != pdTRUE)
		{
			printf( "Could not send to the queue.\r\n" );
		}

		i++;
		vTaskDelay(100);
		
	}
}

任务 2:隔一段时间就往队列 2 中写数据

static void Task2Function(void * param)
{
	
	BaseType_t xStatus;
	int i = -1;
	
	while (1)
	{
		// 任务 2:隔一段时间就往队列 2 中写数据
		xStatus = xQueueSend(xQueueHandle2, &i, portMAX_DELAY);
		if(xStatus != pdTRUE)
		{
			printf( "Could not send to the queue.\r\n" );
		}

		i--;
		vTaskDelay(200);
		
	}
}

任务 3:监测队列 1 和 队列 2 ,两个队列中有数据就读出来

static void Task3Function(void * param)
{
	QueueSetMemberHandle_t handler;
	int i;
	
	//任务 3:监测队列 1 和 队列 2  ,两个队列中有数据就读出来
	while (1)
	{
		//读队列集函数xQueueSelectFromSet(); 看哪个队列有数据,返回值为有数据的队列句柄,参数为队列集的句柄和等待时间
		handler = xQueueSelectFromSet(xQueueHandle_Set, portMAX_DELAY);
		
		//读有数据的队列
		xQueueReceive(handler, &i, 0);

		//打印出来
		printf("get data : %d\r\n", i);
	}
}

注意: 在使用函数来创建队列集,使用队列集的相关函数时要配置下面的宏:

#define configUSE_QUEUE_SETS        1

如果要使用 xQueueCreateSet(); 来创建队列集要配置两个宏,如下图:
FreeRTOS学习笔记——(4)同步与互斥之队列_第3张图片
其中第二个宏在FreeRTOS.h中被自动配置为 1 :
在这里插入图片描述
其中第一个宏在FreeRTOS.h中被自动配置为 0 :
在这里插入图片描述
所以需要增加配置宏

#define configUSE_QUEUE_SETS        1

运行结果:
FreeRTOS学习笔记——(4)同步与互斥之队列_第4张图片


你可能感兴趣的:(FreeRTOS学习笔记,学习)