【FreeRTOS】详细讲解FreeRTOS中消息队列并通过示例讲述其用法

    • @TOC

      使用消息队列的原因

      在裸机系统中,两个程序间需要共享某个资源通常使用全局变量来实现;但在含操作系统(下文就拿FreeRTOS举例)的开发中,则使用消息队列完成。那么这两者有啥区别呢?🤔🤔🤔   其实在FreeRTOS系统中也能够像裸机似的,使用全局变量实现多程序共享某个资源(这里资源就可称为临界资源),则多个程序都能随时访问同一个临界资源,这时若两个程序同时访问同一个临界资源来完成两次资源读写操作,假如两个程序读取操作是同时完成,但是写入操作有先后之别,那么最后实际完成的操作就会是一个。例如下图:

      看完上图后,大家可能会想:两者结果相同,无所谓了。但是呢,如果此时再来个C程序恰好读取到的值为456,那么是不是跟最终结果789存在偏差呢!!!😅😅😅

      因此,在FreeRTOS系统中,引入了消息队列来实现某个资源共享,其不仅仅实现临界资源共享,也给临界资源提供保护,使得程序更加稳定。


      消息队列

      消息队列,是一种用于任务与任务间、中断和任务间传递一条或多条信息的数据结构 ,实现了任务接收 来自其他任务中断不固定或固定长度的消息 。   任务从队列里面读取消息时,如果队列中消息为空 ,读取消息的任务将被阻塞 ;否则任务就读取消息并且处理。用户还可以指定阻塞任务时间 xTicksToWait() ,在指定阻塞时间内,如果队列为空,该任务将保持阻塞状态以等待队列数据有效 。   有多个消息发送到消息队列时,通常将先进入队列的消息 先传给任务,也就是说,任务一般读取到的消息是最先进入消息队列的消息,即先进先出原则(FIFO),但也支持后进先出原则(LIFO)。   FreeRTOS 中使用队列数据结构实现任务异步通信工作,其具有如下特性:

      • 消息支持先进先出 的方式排队,支持异步读写的工作方式;
      • 读写队列均支持超时机制
      • 消息支持后进先出方式排队,即直接往队首发送消息(LIFO);
      • 允许不同长度(不超过队列节点最大值)的任意类型消息;
      • 一个任务 能够与任意一个消息队列接收和发送消息的操作;
      • 多个任务 能够与同一个消息队列接收和发送消息的操作;
      • 当队列使用结束 后,可以通过删除队列函数进行删除函数

      消息队列收发双方处理机制

      • 创建消息队列,FreeRTOS系统会分配一块单个消息大小与消息队列长度乘积 的空间;(创建成功后,每个消息的大小及消息队列长度无法更改,不能写入大于单个消息大小的数据 ,并且只有删除消息队列时,才能释放队列占用的内存。)

      • 写入消息队列,当消息队列未满或允许覆盖入队 时,FreeRTOS系统会直接将消息复制到队列末端;否则,程序会根据指定的阻塞时间进入阻塞状态 ,直到消息队列未满或者是阻塞时间超时,程序就会进入就绪状态; 写入紧急消息,本质上与普通消息差不多,不同的是其将消息直接复制到消息队列队首

      • 读取消息队列,在指定阻塞时间内,未读取到 消息队列中的数据 (消息队列为空),程序进入阻塞状态 ,等待消息队列中有数据;一旦阻塞时间超时 ,程序进入就绪态

      • 一旦消息队列不再使用时,应该将其删除;(此时会永久删除)

      (图有点丑,大家伙将就一下吧🤣🤣🤣)

      函数解析

      消息队列通用创建 函数原型

      c 复制代码
      QueueHandle_t xQueueGenericCreate( 
      								   const UBaseType_t uxQueueLength,
                                         const UBaseType_t uxItemSize,
                                         const uint8_t ucQueueType 
                                       );

      参数解析

      • const UBaseType_t uxQueueLength:设置消息队列长度;
      • const UBaseType_t uxItemSize:设置消息队列中单个消息大小;
      • const uint8_t ucQueueType:设置消息队列的类型;

      函数说明   一个通用的消息队列创建函数,该函数自己给其他函数提供API,自己也调用函数prvInitialiseNewQueue()完成消息队列创建功能。

      消息队列动态创建 函数原型

      c 复制代码
      QueueHandle_t xQueueCreate(
                                  UBaseType_t uxQueueLength,
                                  UBaseType_t uxItemSize
                               );

      参数解析

      • UBaseType_t uxQueueLength:设置消息队列长度;
      • UBaseType_t uxItemSize:设置消息队列中单个消息的大小;

      函数说明   创建函数,实际上使用还是调用函数 xQueueGenericCreate()完成消息队列创建工作。   当消息队列创建成功时,返回一个消息队列的控制句柄,用于访问创建的队列;否则,返回NULL,可能原因是创建队列需要的 RAM 无法分配成功。

      消息队列静态创建 函数原型

      c 复制代码
      QueueHandle_t xQueueCreateStatic(
       	                             UBaseType_t uxQueueLength,
          	                         UBaseType_t uxItemSize,
              	                     uint8_t *pucQueueStorage,
                  	                 StaticQueue_t *pxQueueBuffer
                      		        );

      参数解析

      • UBaseType_t uxQueueLength:设置消息队列长度;
      • UBaseType_t uxItemSize:设置消息队列中单个消息大小;
      • uint8_t *pucQueueStorage:传递消息队列中单个消息的存储结构;
      • StaticQueue_t *pxQueueBuffer:传递自定义的消息队列;

      函数说明

      xQueueCreateStatic()用于创建一个新的队列并返回可用于访问这个队列的队列句柄,队列句柄其实就是一个指向队列数据结构类型的指针。   当返回值为NULL时,创建失败,失败原因与动态创建类似,即可能是创建队列需要的 RAM 无法分配成功。

      消息队列删除 函数原型

      c 复制代码
      void vQueueDelete( QueueHandle_t xQueue );

      参数解析

      • QueueHandle_t xQueue:消息队列的控制句柄;

      函数说明   使用函数vQueueDelete()可以将一个消息队列中的所有信息都清空回收,并且该队列将无法继续使用。值得注意的是,一个没有创建的消息队列,是无法删除的

      发送消息到消息队列 函数原型

      c 复制代码
      BaseType_t xQueueSend(
                             QueueHandle_t xQueue,
                             const void * pvItemToQueue,
                             TickType_t xTicksToWait
                           );

      参数解析

      • QueueHandle_t xQueue:传入消息队列的控制句柄;
      • const void * pvItemToQueue:传入需要发送到消息队列中的数据;
      • TickType_t xTicksToWait:设置阻塞超时时间,设置成0,可直接返回;

      函数说明   发送函数实际上调用PAI函数xQueueGenericSend();该函数也等同于函数xQueueSendToBack()。   发送消息函数xQueueSend(),其处理机制为:当消息队列未满或允许覆盖入队 时,FreeRTOS系统会直接将消息复制到队列末端;否则,程序会根据指定的阻塞时间进入阻塞状态,直到消息队列未满或者是阻塞时间超时,程序就会进入就绪状态。

      中断中发送消息到消息队列 函数原型

      c 复制代码
       BaseType_t xQueueSendFromISR(
                                     QueueHandle_t xQueue,
                                     const void *pvItemToQueue,
                                     BaseType_t *pxHigherPriorityTaskWoken
                                   );

      参数解析

      • QueueHandle_t xQueue:传递消息队列的控制权柄;
      • const void *pvItemToQueue:传递需要发送的消息;
      • BaseType_t *pxHigherPriorityTaskWoken:若消息入队列时产生一个更高优先级的任务,那么改参数就会被设置成pdTRUE,系统在中断函数结束前会切换任务,去执行更高优先级的任务。在FReeRTOS V7.3.0起,该函数为一个可选参数。

      函数说明   该函数实际上调用FreeRTOS系统APIxQueueGenericSendFromISR()来完成在中断中发送消息。该函数功能上与xQueueSendToBackFromISR()相同,并且两者参数完全一致。

      发送消息到消息队列队首 函数原型

      c 复制代码
      BaseType_t xQueueSendToToFront(
                                       QueueHandle_t    xQueue,
                                       const void       *pvItemToQueue,
                                       TickType_t       xTicksToWait
                                    );

      参数解析

      • QueueHandle_t xQueue:消息队列的控制句柄;
      • const void *pvItemToQueue:需要发送的消息;
      • TickType_t xTicksToWait:函数阻塞超时时间;

      函数说明   该函数实际上还是调用函数xQueueGenericSend()来完成其功能的。   xQueueSendToToFront()向队列队首发送一个消息;发送消息成功返回pdTRUE,否则返回 errQUEUE_FULL

      中断中发送消息到消息队列队首 函数原型

      c 复制代码
       BaseType_t xQueueSendToFrontFromISR(
                                             QueueHandle_t xQueue,
                                             const void *pvItemToQueue,
                                             BaseType_t *pxHigherPriorityTaskWoken
                                          );

      参数解析

      • QueueHandle_t xQueue:消息队列的控制句柄;
      • const void *pvItemToQueue:需要发送的消息;
      • BaseType_t *pxHigherPriorityTaskWoken:与xQueueSendFromISR中的该参数类似,都是与队列插入数据时产生的更高优先级任务有关;

      函数说明

      消息队列接收函数 函数原型

      c 复制代码
      BaseType_t xQueueReceive( 
      						  QueueHandle_t xQueue,
                                void * const pvBuffer,
                                TickType_t xTicksToWait
                              );

      参数解析

      • QueueHandle_t xQueue:消息队列的控制权柄;
      • void * const pvBuffer:指向存储消息队列数据的存储空间;
      • TickType_t xTicksToWait:消息队列接收函数的最大阻塞时间。若该函数设置为0,则函数立刻返回;

      函数说明   一旦消息队列接收成功后会返回pdTRUE;否则,返回pdFALSE。接收消息队列后会删除该消息,倘若不想删除该信息,可使用函数xQueuePeek()

      在中断中接收消息队列中消息 函数原型

      c 复制代码
      BaseType_t xQueueReceiveFromISR( 
      							QueueHandle_t xQueue,
                                  void * const pvBuffer,
                                  BaseType_t * const pxHigherPriorityTaskWoken
                                      );

      参数解析

      • QueueHandle_t xQueue:消息队列的控制权柄;
      • void * const pvBuffer:需要发送的消息
      • BaseType_t * const pxHigherPriorityTaskWoken:任务在往队列发送信息时,如果队列满,则任务将阻塞在该队列上,若xQueueReceiveFromISR()函数碰都一个任务,则*pxHigherPriorityTaskWoken=pdTRUE;否则,其值为NULL

      函数说明   函数xQueueReceiveFromISR()是函数xQueueReceive的中断版本,功能上一样,即接收函数后,也会将该消息删除,若不想删除可使用函数 xQueuePeekFromISR。同样,函数 xQueuePeekFromISR()是函数xQueuePeek()的中断版,功能上也是一样的。


      示例

      示例1   先创建两个任务,再通过消息队列实现任务间一对一通信 ,来完成任务2控制任务1实现依次反转LED1-LED8的状态。此时消息队列传递的是一个整型数据

      c 复制代码
      //任务控制权柄
      TaskHandle_t xHandleTsak[4];
      //消息队列控制权柄
      QueueHandle_t xMyQueueHandle;
      
      int main(void)
      {
      	//存储创建任务的返回值
      	BaseType_t xReturn[5] ;
      	
      	xMyQueueHandle = xQueueCreate(20,sizeof(uint16_t));
      	
      	if(xMyQueueHandle == 0)
      		//点亮LED7
      		changeLedStateByLocation(LED7,ON);
      	
      	//动态创建任务1
      	xReturn[0] = xTaskCreate(
      				(TaskFunction_t )queueMesageTask1,
      				(const char *)"queueMesageTask1",(uint16_t)512,
      				(void*)NULL,2,&xHandleTsak[0]
      				);
      	
      	//动态创建任务2
      	xReturn[1] = xTaskCreate(
      				(TaskFunction_t )queueMesageTask2,
      				(const char *)"queueMesageTask2",
      				(uint16_t)512,(void*)NULL,1,
      				&xHandleTsak[1]
      				);		
      	//创建成功 
      	if (pdPASS == xReturn[0] == xReturn[1])
      		//启动任务,开启调度 
      		vTaskStartScheduler(); 
      	//创建失败
      	else
      		//点亮LED6
      		changeLedStateByLocation(LED6,ON);
      }
      
      /********************************************
      * 函数功能:消息队列测试函数1
      * 函数参数:无
      * 函数返回值:无
      ********************************************/
      void queueMesageTask1(void)
      {
      	// 定义一个接收消息的变量
      	uint16_t r_queue;
      	while(1)
      	{
      		 if( pdTRUE == xQueueReceive( xMyQueueHandle,&r_queue,portMAX_DELAY) )
      			rollbackLedByLocation(r_queue);
      	}
      }
      
      /********************************************
      * 函数功能:消息队列测试函数2
      * 函数参数:无
      * 函数返回值:无
      ********************************************/
      void queueMesageTask2(void)
      {
      	uint16_t data[] = {LED1,LED2,LED3,LED4,LED5,LED6,LED7,LED8};
      	//保存需要发送的数据
      	static uint16_t i = 0;
      	//保存系统时间
      	static portTickType myPreviousWakeTime;
      	//保存阻塞时间
      	const volatile TickType_t xDelay1500ms = pdMS_TO_TICKS( 1500UL );
      	//获取当前时间
      	myPreviousWakeTime = xTaskGetTickCount();
      	while(1)
      	{
      		xQueueSend( xMyQueueHandle,&data[i],0 );
      		if(++i == 8) i = 0;
      		
      		//非阻塞延时1.5s
      		xTaskDelayUntil( &myPreviousWakeTime,xDelay1500ms );
      	}
      }

      示例2   使用xTaskCreate()函数创建三个任务,其中有两个发送消息任务,一个接收消息任务,以完成一个多对一消息队列 的实验。   在创建任务时,为了使得代码更加舒服,采用带参数创建FreeRTOS任务 的方法,即先将函数需要用到的参数使用结构体保存,然后再通过xTaskCreate()函数的第四个参数传递给相应任务。   相对于示例1,示例2对任务间传递的消息也进行了优化,使其传递的数据由一个整型变量改成一个结构体改变后,即有利于消息数据的扩展,同时,也方便多个多种类型消息同时传递

      c 复制代码
      // 消息队列传输的数据类型
      struct messageQueue{
      	int id;
      	char msg[100];
      };
      
      //创建任务时传递参数结构体
      struct taskParameters{
      	// 任务ID
      	int id;
      	// 绝对延时延时时间
      	uint16_t delayTime;
      	// LED灯位置
      	uint16_t LEDLOCATION;
      	// LCD显示行
      	u8 lcdLine;
      	// 整型参数,用于变量倍增
      	int number;
      };
      
      void queueMesageTask1(void);
      void queueMesageTask2(struct taskParameters* params);
      
      //任务控制权柄
      TaskHandle_t xHandleTsak[4];
      //消息队列控制权柄
      QueueHandle_t xMyQueueHandle;
      // 任务参数 
      struct taskParameters param[2] = {{0,1500,LED1,Line6,1},{1,1000,LED2,Line7,2}};
      //任务名字
      char*taskName[] = {"task1","task2"};
      
      /*****************************************
      * 函数功能:freertos工作函数
      * 函数参数:无
      * 函数返回值:无
      *****************************************/
      void freertosWork(void)
      {
      //创建两个任务 用于测试消息队列
      	unsigned  int i = 0;
      	
      	//存储创建任务的返回值
      	BaseType_t xReturn[5] ;
      	
      	// 创建消息队列
      	xMyQueueHandle = xQueueCreate(20,sizeof(struct messageQueue));
      	if(xMyQueueHandle == 0)
      		//点亮LED7
      		changeLedStateByLocation(LED7,ON);
      	
      	//动态创建任务
      	for(i=0;i<2;i++)
      		xReturn[i] = xTaskCreate((TaskFunction_t )queueMesageTask2,
      					(const char *)taskName[i],(uint16_t)128,
      					(struct taskParameters*) &param[i],1,&xHandleTsak[i]
      					);
      	
      	//动态创建任务1
      	xReturn[3] = xTaskCreate(
      					(TaskFunction_t )queueMesageTask1,
      					(const char *)"queueMesageTask3",
      					(uint16_t)128,(void*)NULL,1,&xHandleTsak[3]
      				);		
      				
      	LCD_DisplayStringLine(Line0,(uint8_t*)"receve:");
      	LCD_DisplayStringLine(Line5,(uint8_t*)"send:");
      	
      	if (pdPASS == xReturn[0] == xReturn[1] == xReturn[2])
      		//点亮LED6
      		changeLedStateByLocation(LED6,ON);				
      	else
      		vTaskStartScheduler();
      }
      
      /********************************************
      * 函数功能:消息队列测试函数1
      * 函数参数:无
      * 函数返回值:无
      ********************************************/
      void queueMesageTask1(void)
      {	
      	// 定义一个接收消息的变量
      	struct messageQueue r_queue;
      	char temp[100];
      	int j1=1;
      	while(1)
      	{
      		//接收数据  如果数据接收成功就处理 否则就点亮LED4
      		if( pdTRUE == xQueueReceive( xMyQueueHandle,&r_queue,portMAX_DELAY) )
      		{
      			sprintf(temp,"(%s;%s;%d;)",pcTaskGetName(xHandleTsak[r_queue.id]),r_queue.msg,r_queue.id);
      			
      			//任务2发送的数据
      			if(r_queue.id == 0)
      				LCD_DisplayStringLine(Line1,(uint8_t*)temp);
      			//任务3发送的数据
      			else
      				LCD_DisplayStringLine(Line2,(uint8_t*)temp);
      			//每次接收数据后闪烁一次LED3
      			changeAllLedByStateNumber(OFF);
      			changeLedStateByLocation(LED3,j1++%2);
      		}
      		//数据接收失败 点亮LED4
      		else
      		{
      			changeAllLedByStateNumber(OFF);
      			changeLedStateByLocation(LED4,ON);
      		}
      	}
      }
      
      /********************************************
      * 函数功能:消息队列测试函数2
      * 函数参数:无
      * 函数返回值:无
      ********************************************/
      void queueMesageTask2(struct taskParameters* params)
      {
      	//初始化以及保存需要发送的数据
      	struct messageQueue _sData;
      	struct messageQueue*sData = &_sData;
      	sData->id = params->id;
      	//显示需要发送的数据
      	char temp[50];
      	uint16_t count = 0;
      	//保存系统时间
      	portTickType myPreviousWakeTime;
      	//保存阻塞时间
      	TickType_t xDelayms = pdMS_TO_TICKS( params->delayTime );
      	//获取当前时间
      	myPreviousWakeTime = xTaskGetTickCount();
      	while(1)
      	{
      		// 改变本次发送的数据
      		sprintf(sData->msg,"%s%d","Sender2:",count+=params->number);
      		sprintf(temp,"(%s;%d)",sData->msg,sData->id);
      		LCD_DisplayStringLine(params->lcdLine,(uint8_t*)temp);
      		
      		//关闭所有LED灯 避免LCD带来的影响
      		changeAllLedByStateNumber(0);
      		//发送数据 如果发送成功就点亮一次LED1
      		if( xQueueSend( xMyQueueHandle,&_sData,0 ) == pdTRUE)
      			changeLedStateByLocation(params->LEDLOCATION,ON);
      		
      		//非阻塞延时(ms)
      		xTaskDelayUntil( &myPreviousWakeTime,xDelayms );
      	}
      }

      遇到的问题

      keil报错展示

      报错分析   该报错是由于结构体初始化时引起的,keil中不支持不完整定义的变量;但是可以看看小编目前使用的变量struct taskParameters param[2] = {{0,1500,LED1,Line6,1},{1,1000,LED2,Line7,2}};其一样可以啊!😢🤔因此到底什么原因小编暂时也不得而知了。

      小编的解决方案   不知道小编这样子到底算不算解决了该问题🤣🤣🤣:   首先,小编整理了代码,将一些不必要的变量全部都删除,并且优化了代码架构,最后这个程序莫名其妙就可以使用了,没有丝毫报错与警告。🤔😅😅😅

      还有个问题是关于变量struct taskParameters param[2] = {{0,1500,LED1,Line6,1},{1,1000,LED2,Line7,2}};的,该变量最高定义为全局变量,否则程序就会跑飞;如果实在要将其定义为局部变量也行,但是需要换一种结构体初始化的方式。


      小编这里也有其他的一些相关文章,欢迎各位点击观看😉😉😉

      最后 ,欢迎大家留言或私信交流,共同进步!😁😁😁

相关推荐
OpenAnolis小助手3 小时前
开源生态发展合作倡议
开源·操作系统·龙蜥社区·龙蜥·openanolis
OpenAnolis小助手9 小时前
Cloud Kernel SIG 月度动态:发布ANCK 5.10-017.3小版本,引入SMC、TDX等多项特性
操作系统·龙蜥社区·龙蜥sig·anolisos·openanolis
敲上瘾12 小时前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc
不爱学习的YY酱1 天前
【操作系统不挂科】<CPU调度(13)>选择题(带答案与解析)
java·linux·前端·算法·操作系统
钰爱&1 天前
【操作系统】Linux之网络编程(UDP)(头歌作业)
linux·操作系统
清酒伴风(面试准备中......)2 天前
操作系统基础——针对实习面试
笔记·面试·职场和发展·操作系统·实习
架构师Wu老七6 天前
【软考】系统架构设计师-计算机系统基础(2):操作系统
系统架构·操作系统·软考·系统架构设计师
不爱学习的YY酱6 天前
【操作系统不挂科】<线程概念(6)>选择题&简答题(带答案与解析)
linux·开发语言·操作系统
修修修也9 天前
【Linux】进程间通信
linux·运维·服务器·操作系统·进程通信
Pandaconda11 天前
【操作系统】每日 3 题(十八)
linux·服务器·开发语言·数据结构·笔记·后端·操作系统