实时操作系统Freertos开坑学习笔记:(八):信号量、事件标志组、任务通知机制

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

这一节的信息还是非常庞大的,同时也是非常重要的。
在这里插入图片描述

一、信号量的简介

在这里插入图片描述
在这里插入图片描述

看这个图来理解的话:就是说信号量就是停车位,信号量计数值大于0,说明有车位,=1有一个车位,=3有三个车位,等于0说明没有车位了。如果有一个车位被占了,那计数值就减一,空出来一个车位就加一

信号量可以比喻成一把钥匙,用于控制多个人对同一个资源的访问。每个人在想要使用这个资源之前,都必须先去拿这把钥匙。如果钥匙可用,那么这个人就可以使用资源;如果钥匙不可用,那么这个人就必须等待,直到有其他人归还钥匙。

举一个实际的例子来说明信号量的概念。假设有一个公共厕所,只有一个坑位可供使用。多个人需要使用这个坑位,但是只能有一个人在同一时间使用。为了避免多个人同时进入厕所,可以使用信号量来控制访问。

二进制信号量的例子:假设有三个人,分别是A、B、C,他们想要使用公共厕所。在公共厕所门口放置一个锁,初始状态为锁上。当一个人想要使用厕所时,他必须先去看锁是否上锁,如果上锁就等待,如果没上锁就上锁并进入厕所使用。当他使用完毕后,他会将锁解开,让其他人可以进入使用。

计数信号量的例子:假设有一个饭店,只有五个座位可供客人用餐。多个客人来到饭店,但是只能有五个客人同时用餐。在饭店门口放置一个计数器,初始值为5。当一个客人想要进入饭店用餐时,他必须先查看计数器的值,如果大于0,表示还有座位可用,他可以进入用餐,并将计数器的值减少1;如果计数器的值为0,表示没有座位可用,他就必须等待,直到有其他客人用餐并离开,释放座位。

在这些例子中,信号量就像是一把钥匙,控制着多个人对资源(厕所、座位)的访问。只有当钥匙可用时,才能进入使用资源;否则,需要等待其他人释放钥匙。这种通过信号量来控制资源访问的机制,可以有效避免资源竞争和混乱。

1.信号量与队列的区别?

在这里插入图片描述
信号量和队列是两种不同的概念,它们在实际应用中有着不同的作用和用途。

功能和作用:

信号量:信号量主要用于实现任务同步和互斥的机制。它用于协调多个任务之间的并发执行,控制对共享资源的访问,避免竞争条件和数据不一致的问题。
队列:队列是一种数据结构,用于存储和管理数据。它遵循先进先出(FIFO)的原则,通过队头和队尾的操作来实现数据的入队和出队。
数据结构:

信号量:信号量本身并不是一个数据结构,它可以理解为一个计数器或标志,用于控制任务的执行和访问共享资源的权限。
队列:队列是一种数据结构,可以使用数组或链表来实现。它在内部维护一个元素的集合,并提供入队和出队的操作。
应用场景:

信号量:主要应用于操作系统中,用于实现任务的同步和互斥,控制对共享资源的访问。
队列:队列广泛应用于许多领域,如操作系统调度算法、消息传递、网络通信等。它可以用于实现任务队列、消息队列、请求排队等。

二、二值信号量及其实例

1.什么是二值信号量

在这里插入图片描述
二值信号量是一种特殊类型的信号量,它只有两个可能的取值:0和1。它可以用于实现互斥操作,即在某一时刻只允许一个任务或线程访问共享资源。

二值信号量的工作方式类似于一把锁。当信号量的值为1时,表示资源可用,任何一个任务或线程都可以获取该资源并执行操作。当一个任务或线程获取了资源后,它将信号量的值减少为0,表示资源已被占用

当信号量的值为0时,表示资源不可用,其他任务或线程必须等待。当一个任务或线程释放了资源后,它将信号量的值增加为1,表示资源可用,等待的任务或线程将有机会获取该资源。

二值信号量的经典应用是实现互斥访问共享资源,如临界区的保护。在多个任务或线程需要访问临界区时,可以使用一个二值信号量来控制对临界区的访问,保证同一时间只有一个任务或线程可以进入临界区执行操作,从而避免竞争条件和数据不一致的问题。

总结来说,二值信号量是一种特殊类型的信号量,只有两个可能的取值:0和1。它可以用于实现互斥访问共享资源,保证在某一时刻只有一个任务或线程可以执行操作。

2.二值信号量相关API函数

在这里插入图片描述
这些函数是FreeRTOS中用于创建和操作二值信号量的函数。下面是对这些函数的简要说明:

xSemaphoreCreateBinary():使用动态方式创建一个二值信号量。该函数返回一个指向创建的二值信号量的句柄。可以在任务中使用该句柄来获取或释放信号量。

xSemaphoreCreateBinaryStatic():使用静态方式创建一个二值信号量。与动态方式创建的函数类似,但是需要提供一个静态分配的内存作为信号量的存储空间。该函数返回一个指向创建的二值信号量的句柄。

xSemaphoreGive():释放一个二值信号量。将信号量的计数器加1,表示资源可用。如果有其他任务在等待该信号量,则其中一个任务将获取到该信号量。

xSemaphoreGiveFromISR():在中断服务程序(ISR)中释放一个二值信号量。与xSemaphoreGive()函数类似,但是可以在中断上下文中使用。这是因为在中断上下文中,不能直接调用一些阻塞式的函数。

xSemaphoreTake():获取一个二值信号量。将信号量的计数器减1,表示资源不可用。如果信号量的计数器为零,则任务将阻塞等待,直到有其他任务释放该信号量。

xSemaphoreTakeFromISR():在中断服务程序(ISR)中获取一个二值信号量。与xSemaphoreTake()函数类似,但是可以在中断上下文中使用。

3.二值信号量实例

在这里插入图片描述
代码:

/* 任务一,释放二值信号量 */
void task1( void * pvParameters )
{
    
    
    uint8_t key = 0;
    BaseType_t err;
    while(1) 
    {
    
    
        key = key_scan(0);
        if(key == KEY0_PRES)
        {
    
    
            if(semphore_handle != NULL)
            {
    
    
                err = xSemaphoreGive(semphore_handle);
                if(err == pdPASS)
                {
    
    
                    printf("信号量释放成功!!\r\n");
                }else printf("信号量释放失败!!\r\n");
            }
            
        }
        vTaskDelay(10);
    }
}

/* 任务二,获取二值信号量 */
void task2( void * pvParameters )
{
    
    
    uint32_t i = 0;
    BaseType_t err;
    while(1)
    {
    
    
        err = xSemaphoreTake(semphore_handle,portMAX_DELAY); /* 获取信号量并死等 */
        if(err == pdTRUE)
        {
    
    
            printf("获取信号量成功\r\n");
        }else printf("已超时%d\r\n",++i);
        
    }
}

上述代码是一个示例,演示了在FreeRTOS中如何使用二值信号量进行任务间的同步操作。

任务一是一个周期性的任务,通过key_scan()函数检测按键,并在按下KEY0时释放二值信号量。如果二值信号量的句柄非空,就调用xSemaphoreGive()函数释放信号量。如果释放成功,则打印"信号量释放成功!“,否则打印"信号量释放失败!”。

任务二是一个循环任务,通过调用xSemaphoreTake()函数获取二值信号量。使用portMAX_DELAY参数表示在没有获取到信号量时,任务会一直等待。如果获取成功,则打印"获取信号量成功",否则打印"已超时"并增加计数器。

这样,当任务一释放信号量时,任务二就能够获取到信号量并执行相应的操作。通过二值信号量的获取和释放,可以实现任务之间的同步和互斥操作,保证对共享资源的安全访问。

需要注意的是,示例中的semphore_handle是一个指向二值信号量的句柄,需要在任务创建时进行初始化。

三、计数型信号量

在这里插入图片描述
在这里插入图片描述

四、优先级翻转

在这里插入图片描述
举个例子:假设有三个任务:Task1、Task2和Task3,它们的优先级分别为高、中和低。这三个任务需要访问一个共享资源,例如一个全局变量。

在某个时间点,Task1先开始执行,并且获取了该共享资源的互斥量。此时,Task2和Task3处于阻塞状态,因为它们的优先级低于Task1。
然后,Task2的中优先级任务需要处理一个紧急事件,但由于Task1持有共享资源,Task2无法访问该资源。此时,Task2无法及时处理紧急事件,导致系统的实时性能受到影响。

为了解决这个问题,可以使用FreeRTOS中的优先级翻转技术。当Task1获取共享资源的互斥量时,它的优先级会被提升到Task2的优先级。这样,Task2就能够及时访问共享资源并处理紧急事件,保证了系统的实时性能。

当Task1释放共享资源的互斥量时,它的优先级会恢复到原来的值,即高优先级。这样,Task3可以继续执行,并且Task2的优先级也会恢复到中优先级。

通过使用优先级翻转技术,可以解决实时任务调度中的优先级反转问题,确保高优先级任务能够及时访问共享资源,提高系统的实时性能。

五、互斥信号量

在这里插入图片描述
互斥信号量是一种用于实现互斥访问共享资源的机制。它是一种二进制信号量,只能取两个值:0和1。当互斥信号量的值为1时,表示共享资源可供访问;当互斥信号量的值为0时,表示共享资源已被占用。

在多任务系统中,当一个任务需要访问共享资源时,它会首先尝试获取互斥信号量。如果互斥信号量的值为1,表示共享资源可供访问,该任务会将互斥信号量的值设置为0,标记共享资源已被占用。如果互斥信号量的值为0,表示共享资源已被其他任务占用,该任务会进入阻塞状态,等待互斥信号量的值变为1。
当占用共享资源的任务完成对共享资源的访问后,它会释放互斥信号量,将其值重新设置为1,表示共享资源可供其他任务访问。此时,处于阻塞状态的任务中优先级最高的任务会被唤醒,获取互斥信号量,继续执行访问共享资源的操作。

通过使用互斥信号量,可以实现对共享资源的互斥访问,避免多个任务同时访问共享资源导致的竞争和冲突。这有助于提高系统的可靠性和并发性。在FreeRTOS中,互斥信号量可以使用xSemaphoreCreateMutex()函数创建,并通过xSemaphoreTake()和xSemaphoreGive()函数来获取和释放互斥信号量。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当多个任务需要访问共享资源时,信号量的作用可以通过以下例子来说明:

假设有两个任务:Task1和Task2,它们需要同时访问一个共享的串口资源,例如一个串口缓冲区。

在这种情况下,可以使用一个二进制信号量(互斥信号量)来实现资源的互斥访问。

初始化信号量:在系统初始化时,创建一个互斥信号量,初始值为1。

xSemaphoreHandle mutex = xSemaphoreCreateMutex();

Task1和Task2的实现:

// Task1
void Task1(void *parameters) {
    
    
    while (1) {
    
    
        // 尝试获取互斥信号量
        if (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE) {
    
    
            // 临界区代码,访问共享资源
            // ...
            
            // 释放互斥信号量
            xSemaphoreGive(mutex);
        }
    }
}

// Task2
void Task2(void *parameters) {
    
    
    while (1) {
    
    
        // 尝试获取互斥信号量
        if (xSemaphoreTake(mutex, portMAX_DELAY) == pdTRUE) {
    
    
            // 临界区代码,访问共享资源
            // ...
            
            // 释放互斥信号量
            xSemaphoreGive(mutex);
        }
    }
}

在这个例子中,当Task1和Task2需要访问共享资源时,它们首先尝试获取互斥信号量。如果互斥信号量的值为1,表示资源可供访问,任务可以进入临界区代码,并将互斥信号量的值设置为0,以标记共享资源已被占用。当任务完成对共享资源的访问后,它会释放互斥信号量,将其值重新设置为1,表示共享资源可供其他任务访问。

通过使用互斥信号量,可以确保在任意时刻只有一个任务能够访问共享资源,避免了数据的冲突和损坏。这样可以保证系统的正确性和一致性。

六、信号量这一节中比较重要的知识:

1.最重要的是保证资源的同步和互斥访问。

资源的同步是指多个任务之间对共享资源的访问需要进行协调,以确保数据的正确性和一致性。通过信号量,可以控制任务的执行顺序,使得在某个任务使用共享资源之前,必须等待其他任务释放该资源。这样可以避免竞态条件和数据不一致的问题

资源的互斥访问是指同一时间只能有一个任务访问共享资源,以防止数据的冲突和损坏。通过使用互斥信号量,可以保证在一个任务访问共享资源时,其他任务无法同时访问该资源,从而避免了并发访问带来的问题

在并发系统中,资源的同步和互斥访问是非常重要的,尤其是在多任务环境下。它们可以保证任务之间的协作和正确性,避免数据竞争和冲突,提高系统的可靠性和性能。

2.总结的一些知识点:

在信号量中,有一些常用的知识点和概念,包括:

二进制信号量和计数信号量:二进制信号量只能取两个值(0和1),用于实现互斥访问共享资源;计数信号量可以取多个非负整数值,用于控制对共享资源的并发访问。

信号量的创建和删除:可以使用特定的函数来创建和删除信号量,例如xSemaphoreCreateBinary()创建二进制信号量,xSemaphoreCreateCounting()创建计数信号量,vSemaphoreDelete()删除信号量。

信号量的获取和释放:任务可以使用xSemaphoreTake()函数来获取信号量,如果信号量不可用,则任务会进入阻塞状态等待;任务可以使用xSemaphoreGive()函数来释放信号量,使得其他任务可以获取该信号量。

信号量的优先级逆转问题:当一个低优先级任务持有一个信号量,而一个高优先级任务需要获取该信号量时,可能会导致优先级逆转问题。为了解决这个问题,可以使用优先级继承或者优先级屏蔽等技术。

信号量的超时处理:任务在获取信号量时,可以指定一个超时时间,如果在指定时间内无法获取到信号量,则任务可以执行相应的超时处理。

死锁问题:在使用多个信号量时,如果任务之间的互斥和同步关系不正确,可能会导致死锁问题,即任务相互等待对方释放资源而无法继续执行。为了避免死锁,需要合理设计信号量的获取和释放顺序,以及避免出现资源竞争的情况。

七、事件标志组

1.事件标志组概念

在这里插入图片描述
在这里插入图片描述

2.事件标志组与队列、信号量的区别?

在这里插入图片描述
事件标志组、队列和信号量是FreeRTOS中常用的同步机制,它们在功能和应用场景上有一些区别:

事件标志组(Event Flag Group):事件标志组用于任务间的事件通知和等待。它允许任务等待多个事件中的任意一个或多个事件发生,并在事件发生后立即返回。可以通过设置和清除位来表示事件,任务可以等待特定的位或位组合。事件标志组提供了一种灵活的方式来实现任务间的事件通知,适用于多任务并发处理的场景。

队列(Queue):队列用于任务间的数据传输和共享。它可以在任务之间传递消息、数据和事件。队列有固定的大小,可以用来实现任务之间的异步通信和同步操作。任务可以通过向队列发送消息或从队列接收消息来实现数据的传输和共享。

信号量(Semaphore):信号量用于任务间的互斥和同步。它可以用来保护共享资源的访问,防止多个任务同时访问共享资源。信号量通常用于限制资源的并发访问,控制任务的执行顺序,或者用于任务间的同步操作。

区别总结如下:

功能不同:事件标志组用于事件通知和等待,队列用于数据传输和共享,信号量用于互斥和同步。
数据传输:队列可以传输数据和消息,而事件标志组和信号量不直接传输数据。
同步方式:事件标志组用于等待事件发生;队列和信号量用于任务间的同步和互斥操作。
大小限制:队列有固定的大小,而事件标志组和信号量没有大小限制。
使用场景:事件标志组适用于任务间的事件通知和并发处理;队列适用于任务间的数据传输和共享;信号量适用于任务间的互斥和同步操作。
根据具体的应用需求,可以选择适合的同步机制来实现任务间的通信和协调。通常情况下,这三种同步机制可以相互结合使用,以满足复杂的应用场景。

3.API函数

在这里插入图片描述
这些函数是FreeRTOS中用于事件标志组操作的函数。下面是每个函数的功能和用法:

xEventGroupCreate():使用动态方式创建事件标志组。它返回一个EventGroupHandle_t类型的句柄,用于对事件标志组进行操作。

xEventGroupCreateStatic():使用静态方式创建事件标志组。与xEventGroupCreate()类似,但是可以使用静态分配的内存来创建事件标志组。

xEventGroupClearBits():清零事件标志位。通过指定要清除的位的位掩码,将事件标志组中的一个或多个位清零。

xEventGroupClearBitsFromISR():在中断中清零事件标志位。与xEventGroupClearBits()类似,但是可以在中断服务函数中使用。

xEventGroupSetBits():设置事件标志位。通过指定要设置的位的位掩码,将事件标志组中的一个或多个位设置为1。

xEventGroupSetBitsFromISR():在中断中设置事件标志位。与xEventGroupSetBits()类似,但是可以在中断服务函数中使用。

xEventGroupWaitBits():等待事件标志位。任务可以调用此函数来等待事件标志组中的一个或多个位满足指定的条件,以及可选的超时时间。函数将返回满足条件的位。

xEventGroupSync():设置事件标志位,并等待事件标志位。此函数用于将事件标志组设置为指定的位,并等待满足条件的位。与xEventGroupWaitBits()类似,但是可以在一个原子操作中完成设置和等待。

4.举一个应用了:事件标志组的知识的智能门锁的例程

智能门锁的应用十分贴合事件标志组,因为会有很多任务触发事件,导致同一个任务:开锁,的发生。
基于FreeRTOS操作系统下创建了一个开始任务和四个控制任务。
任务与任务之间使用事件标志组进行通信,当TFTLCD屏幕触屏输入密码的时,当密码输入正确,则向舵机任务发送一个事件标志位;
当指纹识别成功时,则向舵机任务发送一个事件标志位;
当射频识别卡识别成功卡号时候;则向舵机任务发送一个事件标志位;
当蓝牙从手机串口发生密码时,当密码识别成功,则向舵机任务发送一个事件标志位。

舵机任务只要接收到其中一个事件标志位时候,舵机就会转动180°来模拟解锁成功。当如果每解锁失败一次之后,err全局变量加一,当err等于3的时候,则舵机任务被挂起。

①宏定义事件标志组

EventGroupHandle_t EventGroupHandler;	//事件标志组句柄
#define EVENTBIT_0	(1<<0)				//事件位
#define EVENTBIT_1	(1<<1)
#define EVENTBIT_2	(1<<2)
#define EVENTBIT_ALL	(EVENTBIT_0|EVENTBIT_1|EVENTBIT_2)

这些宏定义可以用来在代码中指定特定的事件位,以进行事件的设置、清除和检查。例如,通过使用EVENTBIT_0,可以指定事件标志组中的第0位进行相应的操作。同时,EVENTBIT_ALL可以用于检查所有事件位是否都被触发。

②任务一:触摸屏输入密码,若正确则设置事件标志组使得发送开锁指令

void LCD_task(void * pvParameters)
{
    
    
	while(1)
	{
    
    
			  if(sg90flag==1||GET_NUM())
				{
    
    
					 BEEP=1;
					 delay_xms(100);
					 BEEP=0;
				   printf("密码输入正确\r\n");
		       xEventGroupSetBits(EventGroupHandler,EVENTBIT_0);
	 
				}
        else
				{
    
    
					 BEEP=1;
					 delay_xms(50);
					 BEEP=0;
					 delay_xms(50);
					 BEEP=1;
					 delay_xms(50);
					 BEEP=0;
					delay_xms(50);
					 BEEP=1;
					 delay_xms(50);
					 BEEP=0;
					printf("密码输入错误\r\n");
					err++;
					if(err==3)
					{
    
    
					  vTaskSuspend(SG90Task_Handler);
						printf("舵机任务挂起\r\n");
					}			
				}					
			vTaskDelay(100); //延时10ms,也就是10个时钟节拍
	}	
}

任务进入一个无限循环(while(1)),表示任务会一直执行。

在循环中,任务会检查sg90flag变量的值以及调用GET_NUM()函数的返回值。这些条件用于判断密码是否输入正确。如果密码输入正确,任务会打印"密码输入正确"的消息,并执行一系列操作。这些操作包括发出蜂鸣器声音,延时一段时间,显示密码匹配的消息在LCD上,以及设置事件标志组中的位。

如果密码输入错误,任务会打印"密码输入错误"的消息,并执行一系列操作。这些操作包括连续发出蜂鸣器声音,延时一段时间,显示密码错误的消息在LCD上,并增加错误计数err。如果错误计数达到3次,任务会挂起舵机任务,并打印"舵机任务挂起"的消息,并在LCD上显示相应的信息。

最后,任务调用vTaskDelay(100)函数进行延时,延时时间为100个时钟节拍。

③任务二:刷NFC卡,若识别到则设置事件标志组使得发送开锁指令

void RFID_task(void * pvParameters)
{
    
    
	
//	 rfidflag=shibieka();
   while(1)
	 {
    
    
	    if(rfidflag==1||shibieka())
			{
    
    
				   BEEP=1;
					 delay_xms(100);
					 BEEP=0;
				xEventGroupSetBits(EventGroupHandler,EVENTBIT_1);
				printf("识别卡号成功\r\n");
			}
			else if(shibieka()==0)
			{
    
    
				BEEP=1;
					 delay_xms(50);
					 BEEP=0;
					 delay_xms(50);
					 BEEP=1;
					 delay_xms(50);
					 BEEP=0;
					delay_xms(50);
					 BEEP=1;
					 delay_xms(50);
					 BEEP=0;
			  printf("识别卡号失败\r\n");
				err++;
					if(err==3)
					{
    
    
					  vTaskSuspend(SG90Task_Handler);
						printf("舵机任务挂起\r\n");
					}
			}
			
	   vTaskDelay(100); //延时10ms,也就是10个时钟节拍
	 }
 
}

进入一个无限循环while(1),表示任务将一直执行。

在循环中,首先判断rfidflag是否为1或者shibieka()函数返回值为真。如果是,则执行一系列操作,包括控制蜂鸣器发声、设置事件标志组中的第1位(通过调用xEventGroupSetBits(EventGroupHandler,EVENTBIT_1)),并输出一条成功识别卡号的提示信息。

如果上述条件不满足,即rfidflag不为1且shibieka()函数返回值为假,执行另外一系列操作。这些操作包括控制蜂鸣器发出一段特定的声音序列,输出一条识别卡号失败的提示信息,并增加一个错误计数器err。如果错误计数器达到3,将挂起名为SG90Task_Handler的舵机任务,并输出一条舵机任务被挂起的提示信息。

最后,通过调用vTaskDelay(100)进行延时,即任务每次执行完后会暂停100个时钟节拍(大约是10毫秒),然后再次进入下一次循环。

④任务三:舵机任务,接收各个事件标志位来做出反应

void SG90_task(void * pvParameters)
{
    
    
	 volatile EventBits_t EventValue;	
	while(1)
	{
    
    
   EventValue=xEventGroupWaitBits(EventGroupHandler,EVENTBIT_ALL,pdTRUE,pdFALSE,portMAX_DELAY);
		   
				  printf("接收事件成功\r\n");
		      set_Angle(180);
			    delay_xms(1000);
			    delay_xms(1000);
			    set_Angle(0);
			    LCD_ShowString(80,150,260,16,16,"              ");
 
			vTaskDelay(100); //延时10ms,也就是10个时钟节拍
				
	}	
}

首先,在任务的开头定义了一个EventBits_t类型的变量EventValue,用于存储事件标志组的值。

接下来,进入一个无限循环while(1),表示任务将一直执行。

在循环中,通过调用xEventGroupWaitBits(EventGroupHandler,EVENTBIT_ALL,pdTRUE,pdFALSE,portMAX_DELAY)等待事件标志组中的所有位都被置位。函数的参数说明如下:

EventGroupHandler:事件标志组的句柄。
EVENTBIT_ALL:要等待的事件位,这里是等待所有事件位都被置位。
pdTRUE:设置为pdTRUE,表示等待所有事件位都被置位。
pdFALSE:设置为pdFALSE,表示不需要等待所有事件位都被清除。
portMAX_DELAY:表示等待时间的最大值,即无限等待。
当所有事件位都被置位后,执行一系列操作,包括输出一条接收事件成功的提示信息、设置舵机角度为180度、延时1秒、再延时1秒、设置舵机角度为0度,并在LCD上显示一段空白字符串。

最后,通过调用vTaskDelay(100)进行延时,即任务每次执行完后会暂停100个时钟节拍(大约是10毫秒),然后再次进入下一次循环。

当RFID任务或LCD任务执行了xEventGroupSetBits(EventGroupHandler,EVENTBIT_1)语句后,会立即触发SG90任务的执行。
在RFID任务或LCD任务中,当调用xEventGroupSetBits(EventGroupHandler,EVENTBIT_1)函数时,会将事件标志组中的第1位(即EVENTBIT_1)置位。然后,SG90任务通过调用xEventGroupWaitBits(EventGroupHandler,EVENTBIT_ALL,pdTRUE,pdFALSE,portMAX_DELAY)函数等待所有事件位都被置位。当事件标志组中的所有位都被置位后,SG90任务会立即执行一系列操作。
因此,当RFID任务或LCD任务执行了xEventGroupSetBits(EventGroupHandler,EVENTBIT_1)语句后,会立刻触发SG90任务的执行。

这就是事件标志组的应用。

八、任务通知-消息邮箱

1.任务通知的概念

在这里插入图片描述
在这里插入图片描述

2.任务通知的优缺点

在这里插入图片描述
任务通知(Task Notification)是一种在实时操作系统(RTOS)中用于任务间通信和同步的机制。任务通知允许任务之间直接发送简单的通知消息,而无需使用队列、信号量或事件标志组等更复杂的机制。

优点:
①轻量级:任务通知是一种轻量级的通信机制,不需要额外的数据结构和内存,仅使用一个32位的整数作为通知值。因此,它对系统资源的消耗较小,适合于资源受限的嵌入式系统。
②快速响应:任务通知的发送和接收操作是直接的,不需要中间的缓冲区或队列,因此响应时间较短。任务可以立即收到通知并进行相应的处理,适用于对实时性要求较高的应用场景。
③灵活性:任务通知可以传递任意的32位整数值,任务可以根据通知值的具体含义进行不同的处理。这种灵活性使得任务通知适用于各种不同的应用,可以实现多种通信和同步的需求。

缺点:
①仅适用于一对一通信:任务通知是一对一的通信机制,一个任务发送的通知只能被一个特定的任务接收。如果需要多个任务接收同一通知,就需要在多个任务之间进行复制操作,增加了额外的开销。
②无法传递大量数据:任务通知仅能传递一个32位整数值,无法直接传递大量的数据。如果需要传递大量的数据,就需要使用其他机制(如队列或消息邮箱)来传递数据的指针或句柄。
③没有阻塞选项:任务通知的发送和接收操作是非阻塞的,任务无法在等待通知时挂起,需要不断地进行轮询。这可能会浪费处理器资源,特别是在等待时间较长的情况下。

综上所述,任务通知是一种轻量级、快速响应且灵活的任务间通信机制,适用于对实时性要求较高、通信量较小且一对一通信的应用场景。但需要注意其无法传递大量数据、无阻塞选项且仅适用于一对一通信的限制。在具体应用中,需要根据实际需求和系统资源的考量选择合适的通信机制。

3.相关API函数

在这里插入图片描述

在这里插入图片描述

4.利用任务通知机制的例程

(1)模拟获取二值信号量的任务:

/* 任务一,发送任务通知值 */
void task1( void * pvParameters )
{
    
    
    uint8_t key = 0;
    
    while(1) 
    {
    
    
        key = key_scan(0);
        if(key == KEY0_PRES)
        {
    
    
            printf("任务通知模拟二值信号量释放!\r\n");
            xTaskNotifyGive(task2_handler);
        }
        vTaskDelay(10);
    }
}

/* 任务二,接收任务通知值 */
void task2( void * pvParameters )
{
    
    
    uint32_t rev = 0;
    while(1)
    {
    
    
        rev = ulTaskNotifyTake(pdTRUE , portMAX_DELAY);
        if(rev != 0)
        {
    
    
            printf("接收任务通知成功,模拟获取二值信号量!\r\n");
        }
    }
}

这个示例中,任务通知值的作用类似于二值信号量。任务一发送通知值相当于释放了信号量,任务二接收通知值相当于获取了信号量。通过任务通知值的发送和接收,任务一和任务二之间实现了一对一的通信和同步。

需要注意的是,任务通知是非阻塞的操作,即发送和接收任务通知的函数都是立即返回的。因此,任务一和任务二会不断地进行轮询以检查是否有任务通知的发送或接收

(2)模拟获取计数信号量的任务:

/* 任务一,发送任务通知值 */
void task1( void * pvParameters )
{
    
    
    uint8_t key = 0;
    
    while(1) 
    {
    
    
        key = key_scan(0);
        if(key == KEY0_PRES)
        {
    
    
            printf("任务通知模拟计数型信号量释放!\r\n");
            xTaskNotifyGive(task2_handler);
        }
        vTaskDelay(10);
    }
}

/* 任务二,接收任务通知值 */
void task2( void * pvParameters )
{
    
    
    uint32_t rev = 0;
    while(1)
    {
    
    
        rev = ulTaskNotifyTake(pdFALSE , portMAX_DELAY);
        if(rev != 0)
        {
    
    
            printf("rev:%d\r\n",rev);
        }
        vTaskDelay(1000);
    }
}

这个示例中,任务通知值的作用类似于计数型信号量。任务一发送通知值相当于释放了信号量,任务二接收通知值后可以根据接收到的通知值进行相应的处理。在这个示例中,任务二通过打印接收到的通知值来模拟处理的操作。

(3)模拟获取计数信号量的任务:

/* 任务一,发送任务通知值*/
void task1( void * pvParameters )
{
    
    
    uint8_t key = 0;
    while(1) 
    {
    
    
        key = key_scan(0);
        if(key == KEY0_PRES)
        {
    
    
            printf("将bit0位置1\r\n");
            xTaskNotify( task2_handler, EVENTBIT_0, eSetBits );
        }else if(key == KEY1_PRES)
        {
    
    
            printf("将bit1位置1\r\n");
            xTaskNotify( task2_handler, EVENTBIT_1, eSetBits );
        }
        vTaskDelay(10);
    }
}

/* 任务二,接收任务通知值 */
void task2( void * pvParameters )
{
    
    
    uint32_t notify_val = 0,event_bit = 0;
    while(1)
    {
    
    
        xTaskNotifyWait( 0, 0xFFFFFFFF, &notify_val, portMAX_DELAY );
        if(notify_val & EVENTBIT_0)
        {
    
    
            event_bit |= EVENTBIT_0;
        }
        if(notify_val & EVENTBIT_1)
        {
    
    
            event_bit |= EVENTBIT_1;
        }
        if(event_bit == (EVENTBIT_0|EVENTBIT_1))
        {
    
    
            printf("任务通知模拟事件标志组接收成功!!\r\n");
            event_bit = 0;
        }
    }
}

任务一通过调用xTaskNotify(task2_handler, EVENTBIT_0, eSetBits)函数发送任务通知值给任务二。这里的task2_handler是任务二的句柄,用于指定接收通知的任务。EVENTBIT_0是一个宏定义的事件标志位,表示将bit0位置1。

任务二通过调用xTaskNotifyWait(0, 0xFFFFFFFF, &notify_val, portMAX_DELAY)函数等待接收任务通知值。第二个参数0xFFFFFFFF表示等待所有事件标志位都被设置,&notify_val是保存接收到的通知值的变量,portMAX_DELAY表示等待时间设置为最大值,即无限等待。当任务一调用xTaskNotify(task2_handler, EVENTBIT_0, eSetBits)发送通知值后,任务二会接收到通知,并将接收到的通知值保存在notify_val变量中。

在任务二中,通过判断notify_val中的事件标志位是否被设置来检测是否接收到了期望的通知。如果接收到了EVENTBIT_0和EVENTBIT_1,则表示任务通知模拟事件标志组接收成功,并打印相应的提示信息。

这个示例中,任务通知值的作用类似于事件标志组。任务一发送通知值相当于设置了相应的事件标志位,任务二通过检测事件标志位是否被设置来判断是否接收到了期望的通知。

猜你喜欢

转载自blog.csdn.net/qq_53092944/article/details/132673042
今日推荐