FreeRTOS任务间通信

一、消息队列

消息队列,一种用于任务与任务间、中断和任务间传递信息的数据结构,实现了任务接收来自其他任务或中断的不等长的消息。

消息队列能够接收来自线程或中断服务例程中不固定长度的消息,并把消息缓存在自己的内存空间中。其他线程也能够从消息队列中读取相应的消息,而当消息队列是空的时候,可以挂起读取线程。当有新的消息到达时,挂起的线程将被唤醒以接收并处理消息。消息队列是一种异步的通信方式。

创建消息队列 ,FreeRTOS系统会分配一块单个消息大小与消息队列长度乘积的空间;(创建成功后,每个消息的大小及消息队列长度无法更改,不能写入大于单个消息大小的数据,并且只有删除消息队列时,才能释放队列占用的内存) 。写入消息队列 ,当消息队列未满或允许覆盖入队时,FreeRTOS系统会直接将消息复制到队列末端;否则,程序会根据指定的阻塞时间进入阻塞状态,直到消息队列未满或者是阻塞时间超时,程序就会进入就绪状态;写入紧急消息,本质上与普通消息差不多,不同的是其将消息直接复制到消息队列队首;读取消息队列,在指定阻塞时间内,未读取到消息队列中的数据(消息队列为空),程序进入阻塞状态,等待消息队列中有数据;一旦阻塞时间超时,程序进入就绪态;

二、消息队列应用实例

cpp 复制代码
函数解释
//创建队列
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,//队列长度
							UBaseType_t uxItemSize );//队列中消息单元的大小,以字节为单位

//队列发送消息,队尾写入
BaseType_t xQueueSend(QueueHandle_t xQueue,//队列句柄
				const void * pvItemToQueue,//指针,指向要发送到队列尾部的队列消息
				TickType_t xTicksToWait);//等待时间

//中断服务程序中使用的发送函数
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
							const void *pvItemToQueue,
						    BaseType_t *pxHigherPriorityTaskWoken//为一个可选参数, 可以设置为 NULL。
						    );

//向队列首发送消息
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
						const void * pvItemToQueue,
							TickType_t xTicksToWait );

//从一个队列中接收消息并把消息从队列中删除
BaseType_t xQueueReceive(QueueHandle_t xQueue,
							void *pvBuffer,
					TickType_t xTicksToWait);

//在中断服务程序中接收消息队列的函数,总之就是在任务中调用和在中断服务程序中调用函数不同。
BaseType_t xQueueReceiveFromISR( 
							QueueHandle_t xQueue,
                            void * const pvBuffer,
                            BaseType_t * const pxHigherPriorityTaskWoken
                                );


举例说明
QueueHandle_t Test_Queue =NULL;
#define  QUEUE_LEN    4   /*消息队列长度*/
#define  QUEUE_SIZE   4   /*每个消息大小 */

static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;
·taskENTER_CRITICAL();     //进入临界区
  /* 创建Test_Queue */
  Test_Queue = xQueueCreate((UBaseType_t ) QUEUE_LEN,(UBaseType_t ) QUEUE_SIZE);
  
 创建任务一:Send_Task
 创建任务二:Receive_Task

  vTaskDelete(AppTaskCreate_Handle); 
  taskEXIT_CRITICAL();  
}

/
  
//任务一:发送
static void Send_Task(void* parameter)
{	 
  BaseType_t xReturn = pdPASS;
  uint32_t send_data1 = 1;
  uint32_t send_data2 = 2;
  while (1)
  {
      xReturn = xQueueSend( Test_Queue,&send_data1,0 );       
      if(pdPASS == xReturn)
        printf("send_data1 发送成功!\n\n");
    vTaskDelay(20);
  }
}

//任务二:接收
static void Receive_Task(void* parameter)
{	
  BaseType_t xReturn = 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);
  }
}                             

三、信号量

二值信号量任务之间同步或临界资源的互斥访问

同步:比如说,买包子

我要去买包子,如果包子店没有包子了,则需要等待卖包子的把包子做出来我才能买到包子,这个等待的过程就叫做同步。(在实际应用中:一个采集数据的传感器任务,一个处理数据的任务,则处理数据的任务需要等待传感器去采用数据,则在FreeRTOS系统中等待不能干等着,在该任务等待的过程中,CPU转而可以去执行其他任务,则就可以提高效率,则就是队列的阻塞机制)

互斥:比如说,抢厕所

只有一个厕所,一个人进去上了,另一个人也要上,则必须等待前人上完厕所才能上,等待的过程就是同步,而保护厕所只能一个人上的过程叫做互斥,厕所就是所谓的临界资源,同一时间只能一个人使用厕所。

以生活中的停车场为例来理解信号量的概念:

①当停车场空的时候,停车场的管理员发现有很多空车位,此时会让外面的车陆续进入停车场获得停车位;

②当停车场的车位满的时候,管理员发现已经没有空车位,将禁止外面的车进入停车场,车辆在外排队等候;

③当停车场内有车离开时,管理员发现有空的车位让出,允许外面的车进入停车场;待空车位填满后,又禁止外部车辆进入。

在此例子中,管理员就相当于信号量,管理员手中空车位的个数就是信号量的值(非负数,动态变化);停车位相当于公共资源(临界区),车辆相当于线程。车辆通过获得管理员的允许取得停车位,就类似于线程通过获得信号量访问公共资源。

四、信号量实例

  • 二值信号量用于同步:在多任务系统中,经常会使用二值信号量来实现任务之间或者任务与中断之间的同步,比如,某个任务需要等待一个标记,那么任务可以在轮询中查询这个标记有没有被置位,则任务在等待的过程也会消耗CPU的资源,代码如下所示:
cpp 复制代码
SemaphoreHandle_t BinarySem_Handle =NULL;

static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;
  taskENTER_CRITICAL(); 
  
  /* 创建二值信号量*/
  BinarySem_Handle = xSemaphoreCreateBinary();	 
  if(NULL != BinarySem_Handle)
    printf("BinarySem_Handle create successful!\r\n");

	创建任务一:Receive_Task
	创建任务二:Send_Task
  
  vTaskDelete(AppTaskCreate_Handle); 
  taskEXIT_CRITICAL();  
}

static void Receive_Task(void* parameter)
{	
  BaseType_t xReturn = pdPASS;
  while (1) {
  	//获取二值信号量,没有获取则一直等待
	xReturn = xSemaphoreTake(BinarySem_Handle,portMAX_DELAY); 
    if(pdTRUE == xReturn)
      printf("BinarySem_Handle  get successful |!\n\n");
  }
}


static void Send_Task(void* parameter)
{	 
  BaseType_t xReturn = pdPASS;
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {
      xReturn = xSemaphoreGive( BinarySem_Handle );//给出信号量
      if( xReturn == pdTRUE )
        printf("BinarySem_Handle  释放成功\r\n");
      else
        printf("BinarySem_Handle  释放失败\r\n");
    } 
    vTaskDelay(20);
  }
}

五、事件组

事件是一种实现任务/中断间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。其实事件组的本质就是一个整数(16/32位)。

事件组与队列/信号量的区别:

1.信号量/队列当事件发生时只会去唤醒一个任务,而事件组可以唤醒多个任务 起到一个广播的作用。

2.信号量/队列是一个消耗性资源,即数据读走了则就减少,而事件组可以选择清除事件也可以选择保留事件

3.事件组只能是起到一个同步的作用,并不能传递数据

4.最重要的一点事件组可以实现多个任务之间的同步,队列/信号量则只能是两个任务之间的同步

事件组的特点:

1.一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24 位,还有8位用于管理事件),其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生)。

2.事件仅用于同步,不提供数据传输功能。

  1. 与信号量/队列不同设置事件组不会阻塞,即多次向任务设置同一事件等效于只设置一次。

  2. 支持事件等待超时机制,即等待该事件类型(该事件还未发生)的任务会进入阻塞态。

5.事件获取的时候,有两个选择:1.逻辑或:任务所期望的事件中只要有任意一个事件发生,任务即可被唤醒。2.逻辑或:任务所期望的事件必须全部发生,任务才能被唤醒。

六、事件组实例

cpp 复制代码
static EventGroupHandle_t Event_Handle =NULL;

#define KEY1_EVENT  (0x01 << 0) 
#define KEY2_EVENT  (0x01 << 1) 

static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;
  taskENTER_CRITICAL(); 
  
  /* 创建事件 Event_Handle */
  Event_Handle = xEventGroupCreate();	 
  if(NULL != Event_Handle)
    printf("Event_Handle创建成功\r\n");
    
	创建任务一:LED_Task
	创建任务二:KEY_Task

  vTaskDelete(AppTaskCreate_Handle); 
  taskEXIT_CRITICAL(); 
}

static void LED_Task(void* parameter)
{	
    EventBits_t r_event;
    while (1)
	{
    r_event = xEventGroupWaitBits(Event_Handle, 
                                  KEY1_EVENT|KEY2_EVENT,//事件
                                  pdTRUE,pdTRUE,  portMAX_DELAY);
                        
    if((r_event & (KEY1_EVENT|KEY2_EVENT)) == (KEY1_EVENT|KEY2_EVENT)) 
    {
      printf ( "收到事件\n");		
    }
    else
      printf ( "事件 错误\n");	
  }
}

static void KEY_Task(void* parameter)
{	 
  while (1)
  {
       if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )  {
			xEventGroupSetBits(Event_Handle,KEY1_EVENT); //触发事件一 					
		}
   
		if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )  {
			xEventGroupSetBits(Event_Handle,KEY2_EVENT); 触发事件二				
		}
		vTaskDelay(20); 
  }
}

七、任务通知

前言:任务间通信的机制,包括队列、事件组和各种不同类型的信号量。使用这些机制都需要创建一个通信对象。事件和数据不会直接发送到接收任务或接收ISR,而是发送到通信对象(也就是发送到队列、事件组、信号量)。同样,任务和ISR从通信对象接收事件和数据,而不是直接从发送事件或数据的任务或ISR接收事件和数据。

**任务通知允许任务与其他任务交互,并与ISR同步,而不需要单独的通信对象。**通过使用任务通知,任务或ISR可以直接向接收任务发送事件。

每个任务都有一个 32 位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件组,也可以替代长度为 1 的队列;任务通知的使用无需创建队列;

(1)任务通知的缺点:按照FreeRTOS官方的说法使用任务通知比通过队列、事件标志组或信号量通信方式解除阻塞的任务要快 45%,并且更加省 RAM 内存空间,因为像队列、信号量、事件组这些通信方式使用前必须先创建,拿队列来说如下图所示,申请内存的时候至少需要下图这么多变量,而任务通知是任务结构体中自带的一个32位的无符号整数,一个8位的通知状态变量,一共就5个字节。

(2).任务通知的缺点

虽然说任务通知可以模拟这么多通信方式,但是肯定有限制、有缺点,不然还要这些队列、信号量、事件组干嘛。

1.不能发送通知到中断

原因很简单,任务通知、任务通知,人家通知的是任务,是修改任务控制块中那个32位无符号整数的值,中断并没有任务控制块这一说,但为什么队列、信号量、事件组这些就可以呢,说到底人家创建了一个独立的队列、信号量、事件组结构体当然谁都可以访问里面的内容,但是可以在中断中发送通知给其他任务,这个是没毛病的。

2.不能发送通知给多个任务

任务通知只能指定发送给某一个任务而不能广播,而队列、信号量、事件组任何中断和任务都能访问,不过很少出现多个任务或中断接收同一个通讯对象的情况

3.发送通知的任务不能进入阻塞

只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态,像队列:写队列当队列满的时候,可以进入阻塞态

八、任务通知实例

cpp 复制代码
获取任务通知 
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, //任务句柄
                                      uint32_t ulValue, //值
                                      eNotifyAction eAction ); 
     eNotifyAction 的取值                                 
       * eNoAction = 0//通知任务而不更新其通知值
       * eSetBits//设置任务通知值中的值
       * eIncrement//增加任务的通道值
       * eSetvaluewithoverwrite//覆盖当前通知
       * eSetValueWithoutoverwrite//不覆盖当前通知
cpp 复制代码
等待任务通知
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,//将从任务的通知值中清除的位(Bit)。这个参数可以用来指定只关心特定的位。可以使用 0 来表示不清除任何位。
							uint32_t ulBitsToClearOnExit,//从任务的通知值中清除的位。如果希望在任务等待结束时清除某些位,可以通过此参数指定。如果不需要清除,可以使用 0
							uint32_t *pulNotificationValue,//函数将把接收到的通知值存储在这个指针指向的内存中。如果不需要接收到的通知值,可以传递 NULL
							TickType_t xTicksToWait );//任务没有接收到通知值的情况下等待的时间(以滴答计时,毫秒为单位)。如果这个时间到了还没有收到通知,函数将返回。使用 portMAX_DELAY 可以让任务无限期等待,直到收到通知。
cpp 复制代码
#define EVENTBIT_0	(1<<0)				//CAN_0
#define EVENTBIT_1	(1<<1)              //CAN_1
#define EVENTBIT_2	(1<<2)              //USART_0
#define EVENTBIT_3	(1<<3)              //USART_1

举例说明
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;
  taskENTER_CRITICAL(); 

  创建任务一: Receive1_Task
  创建任务二 :Send_Task
  
  vTaskDelete(AppTaskCreate_Handle);
  taskEXIT_CRITICAL();  
}


//发送任务
static void Send_Task(void* parameter)
{	 
  BaseType_t xReturn = pdPASS;
  uint32_t send1 = 1;
  
  while (1)
  {

    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {

      xReturn = xTaskNotify( Receive1_Task_Handle, /*任务句柄*/
                             (uint32_t)EVENTBIT_3, /*任务内容 */
                             eSetValueWithOverwrite );/*覆盖当前通知*/
      
      if( xReturn == pdPASS )
        printf("Receive1_Task_Handle ÈÎÎñ֪ͨÏûÏ¢·¢Ëͳɹ¦!\r\n");
    } 
    vTaskDelay(20);
  }
}


//接收任务
static void Receive1_Task(void* parameter)
{	
  BaseType_t xReturn = pdTRUE;
  uint32_t r_num;
  xReturn=xTaskNotifyWait(0x0,			//进入函数的时候不清除任务bit
                            ULONG_MAX,	  //退出函数的时候清除所有bit
                            (uint32_t *)&r_num,		  //任务通知值
                            portMAX_DELAY);	//阻塞事件
    if( pdTRUE == xReturn )
      printf("Receive1_Task 任务通知消息 %d \n",r_num);                      
}

九、互斥锁

信号量的一个特例,信号量可以实现一对一和一对多,而对于互斥锁只能实现一对一。互斥锁是一种用于保护共享资源的机制。当一个任务需要使用一个共享资源时,它必须首先获取互斥锁。如果互斥锁已经被另一个任务获取,那么这个任务就需要等待,直到互斥锁被释放。在FreeRTOS中,可以使用xSemaphoreCreateMutex()函数来创建一个互斥锁。

互斥量又叫相互排斥的信号量,是一种特殊的二值信号量。互斥量类似于只有一个车位的停车场:当有一辆车进入的时候,将停车场大门锁住,其他车辆在外面等候。当里面的车出来时,将停车场大门打开,下一辆车才可以进入。

cpp 复制代码
SemaphoreHandle_t xMutex;

void vTask(void  pvParameters)
{
    for(;;)
    {
        if(xSemaphoreTake(xMutex, (TickType_t)10) == pdTRUE)
        {
            // The mutex was successfully taken, so the shared resource can be accessed.
            printf("Task: Mutex taken!\n");

            // ...
            // Access the shared resource.
            // ...

            // Release the mutex.
            xSemaphoreGive(xMutex);
        }
        else
        {
            // The mutex could not be taken.
            printf("Task: Mutex not taken!\n");
        }
    }
}

信号量 vs 互斥锁区别:

  • 互斥锁是一种所有权的概念,即一个任务获取了互斥锁后,只有它自己可以释放这个互斥锁,其他任务不能释放。而信号量没有所有权的概念,任何任务都可以释放信号量。
  • 在FreeRTOS中,互斥锁有优先级翻转的解决机制,当一个低优先级的任务获取了互斥锁,而高优先级的任务需要这个互斥锁时,低优先级的任务的优先级会被提升,以减少优先级反转的问题。而信号量没有这个机制。
  • 互斥锁通常用于保护共享资源,即在同一时间只能有一个任务访问某个资源。信号量则更多是用于任务同步,它可以被用来唤醒一个或多个等待的任务。
  • 在FreeRTOS中,信号量可以有计数的概念,即可以被"给"多次,每次"给"都会增加一个计数,而互斥锁没有这个概念,它只有锁定和解锁两种状态。
  • 信号量可以被用作二元信号量(即只有两种状态,0和1,类似互斥锁),而互斥锁不能被用作计数信号量。

十、总结

消息队列:不同任务或进程之间传递消息

互斥锁:侧重于保证同一时刻只有一个线程可以访问共享资源

信号量:控制对共享资源访问的同步机制,用于控制多个线程对共享资源的访问数量。

事件组:管理多个事件的同步机制,允许多个任务等待多个事件的发生

任务通知:任务之间直接发送通知,任务间通信

线程间同步:信号量,互斥锁,事件组

线程间通信:消息队列,任务通知

相关推荐
百事老饼干2 分钟前
Java[面试题]-真实面试
java·开发语言·面试
customer0810 分钟前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
2402_8575893620 分钟前
SpringBoot框架:作业管理技术新解
java·spring boot·后端
HBryce2424 分钟前
缓存-基础概念
java·缓存
一只爱打拳的程序猿38 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
杨荧40 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
minDuck42 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
白子寰1 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
王俊山IT1 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习