任务通知

Q: 什么是任务通知?

A: FreeRTOS 从版本 V8.2.0 开始提供任务通知这个功能,每个任务都有一个 32 位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队列)。 在大多数情况下任务通知可以替代二值信号量、计数信号量、事件标志组,可以替代长度为 1 的队列(可以保存一个 32 位整数或指针值),并且任务通知速度更快、使用的RAM更少!

任务通知值的更新方式

FreeRTOS 提供以下几种方式发送通知给任务 :

  • 发送消息给任务,如果有通知未读,不覆盖通知值
  • 发送消息给任务,直接覆盖通知值
  • 发送消息给任务,设置通知值的一个或者多个位
  • 发送消息给任务,递增通知值

通过对以上方式的合理使用,可以在一定场合下替代原本的队列、信号量、事件标志组等

任务通知的优势和劣势

任务通知的优势

  • 使用任务通知向任务发送事件或数据,比使用队列、事件标志组或信号量快得多。
  • 使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。

任务通知的劣势

  • 只有任务可以等待通知,中断服务函数中不可以,因为中断没有 TCB(TCB可以简易理解为任务创建时在内存中开辟的空间) 。
  • 通知只能一对一,因为通知必须指定任务。
  • 等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待。(而信号量或者事件标志组就可以阻塞等待
  • 任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保 持一个数据。(这也是为什么任务通知只能模拟长度为1的队列

任务通知相关 API 函数

发送通知

发送通知,带有通知值 

 

  • xTaskToNotify:需要接收通知的任务句柄
  • ulValue:用于更新接收任务通知值, 具体如何更新由形参 eAction 决定
  • eAction:一个枚举,代表如何使用任务通知的值
  • 返回值: 如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE(按照枚举的表格,这种情况可能出现在枚举被设置为“eSetValueWithoutOverWrite”时出现), 而其他情况均返回pdPASS

关于枚举值:

其中,eSetBits可以模拟事件标志组elncrement可以模拟信号量eSetValueWithOverWrite可以模拟为覆写的消息队列eSetValueWithoutOverwrite可以模拟为不覆写的消息队列

发送通知,带有通知值并且保留接收任务的原通知值

  • xTaskToNotify:需要接收通知的任务句柄
  • ulValue:用于更新接收任务通知值, 具体如何更新 由形参 eAction 决定
  • eAction:一个枚举,代表如何使用任务通知的值
  • pulPreviousNotifyValue:对象任务的上一个任务通知值,如果为 NULL, 则不需要回传, 这个时候就等价于函数 xTaskNotify()
  • 返回值: 如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE(按照枚举的表格,这种情况可能出现在枚举被设置为“eSetValueWithoutOverWrite”时出现), 而其他情况均返回pdPASS
发送通知,不带通知值

 

  • xTaskToNotify:接收通知的任务句柄, 并让其自身的任务通知值加 1 (模拟信号量
  • 返回值: 总是返回 pdPASS

等待通知

注意!等待通知API函数只能用在任务,不可应用于中断中!

 获取任务通知的函数1

  • xClearCountOnExit:指定在成功接收通知后,将通知值清零或减 1,pdTRUE:把通知值清零(类似二值信号量);pdFALSE:把通知值减一(类似计数型信号量
  • xTicksToWait:阻塞等待任务通知值的最大时间
  • 返回值: 0:接收失败 ;非0:接收成功,返回任务通知的通知值
 获取任务通知的函数2

 

  • ulBitsToClearOnEntry:函数执行前清零任务通知值那些位,1清零,0不清零
  • ulBitsToClearOnExit:表示在函数退出前,清零任务通知值那些位,1清零,0不清零,在清 0 前,接收到的任务通知值会先被保存到形参 *pulNotificationValue 中

注意“1清零,0不清零”是对于“位”来说,即如果希望全部清零,那应该是0xFFFFFFFF

  • pulNotificationValue:用于保存接收到的任务通知值。 如果 不需要使 用,则设置为 NULL 即可 
  • xTicksToWait:等待消息通知的最大等待时间 

实操演示

在 C:\mjm_CubeMX_proj 路径下,复制一份Cube的母版并重命名为 :mjm_freeRTOS_note:

打开相应的Cube文件:

1. 配置按钮的GPIO:

2. 找到左侧的Middleware --> FREERTOS,然后在下方找到"Task and Queues",然后创建两个任务:

3. 生成代码打开Keil:

模拟二值信号量
#include "stdio.h"

void StartTask_send(void const * argument)
{
  for(;;)
  {
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){ //按键消抖
				printf("KEY1 has been pressed\r\n");
				xTaskNotifyGive(Task_receiveHandle); //相当于一个指定了对象,一对一的二值信号量释放,使通知量加1	
				printf("Task note: successfully give\r\n");
			}
			while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务			
		} 

    osDelay(1);
  }
}


void StartTask_receive(void const * argument)
{
  uint32_t recv = 0;

  for(;;)
  {
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){ //按键消抖
				printf("KEY2 has been pressed\r\n");
				recv = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); //收到通知将通知清零,模拟二值信号量获取
				if(recv != 0){
					printf("Task note: successfully take\r\n");		
				}
			}
			while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务			
		} 
		
    osDelay(1);
  }
}
实现效果1

打开串口助手:

分别按下KEY1和KEY2: 

但如果再按一下KEY2:

由于模拟二值信号量已经被获取了,再次获取注定是不成功的,且我将等待时间拉到了最长,所以会一直死等,进入接收阻塞

模拟计数型信号量

根据上面所讲,模拟二值和模拟计数信号量的唯一区别,就是将ulTaskNotifyTake()函数的第一个参数从 pdTRUE 改成 pdFALSE

#include "stdio.h"

void StartTask_send(void const * argument)
{
  for(;;)
  {
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){ //按键消抖
				printf("KEY1 has been pressed\r\n");
				xTaskNotifyGive(Task_receiveHandle); //相当于一个指定了对象,一对一的二值信号量释放,使通知量加1	
				printf("Task note: successfully give\r\n");
			}
			while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务			
		} 

    osDelay(1);
  }
}


void StartTask_receive(void const * argument)
{
  uint32_t recv = 0;

  for(;;)
  {
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){ //按键消抖
				printf("KEY2 has been pressed\r\n");
				recv = ulTaskNotifyTake(pdFALSE, portMAX_DELAY); //收到通知将通知减1,模拟计数信号量获取
				if(recv != 0){
					printf("Task note: successfully take\r\n");		
				}
			}
			while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务			
		} 
		
    osDelay(1);
  }
}
实现效果2

打开串口助手:

按三下KEY1(模拟计数信号量,使得计数量为3)

再按三下KEY2 (使得计数量归0)

最后再按一下KEY2,由于模拟的计数信号量值为0,所以会开始死等,进入接收阻塞

模拟事件标志组
#include "stdio.h"

void StartTask_send(void const * argument)
{
  for(;;)
  {
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){ //按键消抖
				printf("KEY1 has been pressed\r\n");
				printf("bit0 has been set to 1\r\n");
				xTaskNotify(Task_receiveHandle, 0x01, eSetBits); //被通知任务的任务值按位或0x01,相当于将bit0置1
				//printf("bit0 has been set to 1\r\n"); 注意这句话不能加在这里,任务通知之后,会立刻跳转到始终等待阻塞的其他任务,这句话的存在可能会导致程序的错误
			}
			while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务			
		} 
		
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){ //按键消抖
				printf("KEY2 has been pressed\r\n");
				printf("bit1 has been set to 1\r\n");
				xTaskNotify(Task_receiveHandle, 0x02, eSetBits); //被通知任务的任务值按位或0x02,相当于将bit1置1
				//printf("bit1 has been set to 1\r\n"); 注意这句话不能加在这里,任务通知之后,会立刻跳转到始终等待阻塞的其他任务,这句话的存在可能会导致程序的错误
			}
			while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务			
		}

    osDelay(1);
  }
}


void StartTask_receive(void const * argument)
{
	uint32_t recv = 0;
	uint32_t event_bit = 0; 

  for(;;)
  {
		xTaskNotifyWait(0, 0xFFFFFFFF, &recv, portMAX_DELAY); //函数执行前不清零,将通知值保存到recv中,在函数结束前清零,并一直等待消息通知
		//为什么有8个F,是因为变量类型是32位的整型
		if(recv & 0x01){ //此处其实也可以直接写成 “recv == 0x01”,但是如果代码量大,可能会出现各种状况,“recv & 0x01”的写法是最好且不会出错的,因为这句话就只判断特定的位的值
			event_bit |= 0x01; //将event_bit的 bit0 置1
			printf("bit0 qualified\r\n");
		}
		if(recv & 0x02){
			event_bit |= 0x02; //将event_bit的 bit1 置1
			printf("bit1 qualified\r\n");
		}
		if(event_bit == 0x03){
			printf("good good good\r\n");
			event_bit = 0;
		}
		
    osDelay(1);
  }
}
实现效果3

打开串口助手:

只有KEY1和KEY2都被按下过,才会发送“good good good” 

模拟邮箱

所谓邮箱,其实就是消息的收发,也就是“任务通知”最原始的含义。

其实,和模拟事件标志组的代码差不多,主要区别就是将枚举换成“eSetValueWithOverwrite”或“eSetValueWithoutOverwrite”:

此处使用覆写eSetValueWithOverwrite)或不覆写eSetValueWithoutOverwrite)都可以,因为不管覆不覆写,在我的代码中,只要发送了任意一条通知,接收通知的任务在退出前都会将通知值全部清0,所以实现的效果是一样的。

#include "stdio.h"

void StartTask_send(void const * argument)
{
  for(;;)
  {
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){ //按键消抖
				printf("KEY1 has been pressed\r\n");
				printf("bit0 has been set to 1\r\n");
				xTaskNotify(Task_receiveHandle, 0x01, eSetValueWithOverwrite); //被通知任务的任务值按位或0x01,相当于将bit0置1		
			}
			while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务			
		} 
		
		if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){
			osDelay(20);
			if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){ //按键消抖
				printf("KEY2 has been pressed\r\n");
				printf("bit1 has been set to 1\r\n");
				xTaskNotify(Task_receiveHandle, 0x02, eSetValueWithOverwrite); //被通知任务的任务值按位或0x02,相当于将bit1置1
			}
			while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET); //等待按键松开,防止出现按钮一直按着,就一直删除创建任务			
		}

    osDelay(1);
  }
}


void StartTask_receive(void const * argument)
{
  uint32_t recv = 0; 

  for(;;)
  {
		xTaskNotifyWait(0, 0xFFFFFFFF, &recv, portMAX_DELAY); //函数执行前不清零,将通知值保存到recv中,在函数结束前清零,并一直等待消息通知
		//为什么有8个F,是因为变量类型是32位的整型
		printf("msg received, saying\"%d\"\r\n",recv);
		
    osDelay(1);
  }
}
实现效果4

打开串口助手:

按下KEY1: 

按下KEY2:

就像任务一在向任务二发邮件一样。 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值