FreeRTOS任务通知

1.任务通知概念及特性

1.1任务通知与之前队列,信号量,互斥量,事件组通知任务的区别

在之前的队列,信号量,互斥量,事件组的学习中我们想要发送“消息”给别的任务我们需要先创建一个“中间的信号基站”(环形buffer,计数值,二进制整数),并且发起通知的任务并不知道要通知给谁,接收通知的任务也不知道是谁发起的通知。

而任务通知不需要创建“中间信号基站”,通过FreeRTOS可以直接进行通知。并且发起通知的任务可以知道发给谁,接收通知的任务也可以知道是谁发起的。

1.2任务通知的特性

1.2.1 优势及限制

任务通知的优势:

  • 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
  • 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。

任务通知的限制:

  • 不能发送数据给ISR:
  • ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
  • 数据只能给该任务独享
  • 使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
  • 在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
  • 无法缓冲数据
  • 使用队列时,假设队列深度为N,那么它可以保持N个数据。
  • 使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
  • 无法广播给多个任务
  • 使用事件组可以同时给多个任务发送事件。
  • 使用任务通知,只能发个一个任务。
  • 如果发送受阻,发送方无法进入阻塞状态等待
  • 假设队列已经满了,使用 xQueueSendToBack() 给队列发送数据时,任务可以进入阻塞状态等待发送完成。
  • 使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。

1.2.2 通知状态和通知值

每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:

  • 一个是uint8_t类型,用来表示通知状态
  • 一个是uint32_t类型,用来表示通知值
typedef struct tskTaskControlBlock
{
    ......
    /* configTASK_NOTIFICATION_ARRAY_ENTRIES = 1 */
    volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    ......
} tskTCB;

通知状态有3种取值:

  • taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
  • taskWAITING_NOTIFICATION:任务在等待通知
  • taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)
##define taskNOT_WAITING_NOTIFICATION              ( ( uint8_t ) 0 )  /* 也是初始状态 */
##define taskWAITING_NOTIFICATION                  ( ( uint8_t ) 1 )  /*等待任务通知*/
##define taskNOTIFICATION_RECEIVED                 ( ( uint8_t ) 2 )   /*接收任务通知*/

通知值可以有很多种类型:

  • 计数值
  • 位(类似事件组)
  • 任意数值

 2.任务通知相关API函数

2.1 两类函数

任务通知有2套函数,简化版、专业版,列表如下:

  • 简化版函数的使用比较简单,它实际上也是使用专业版函数实现的
  • 专业版函数支持很多参数,可以实现很多功能
简化版专业版
发出通知xTaskNotifyGive vTaskNotifyGiveFromISRxTaskNotify xTaskNotifyFromISR
取出通知ulTaskNotifyTakexTaskNotifyWait

2.2 xTaskNotifyGive/ulTaskNotifyTake

在任务中使用xTaskNotifyGive函数,在ISR中使用vTaskNotifyGiveFromISR函数,都是直接给其他任务发送通知:

  • 使得通知值加一
  • 并使得通知状态变为"pending",也就是taskNOTIFICATION_RECEIVED,表示有数据了、待处理

可以使用ulTaskNotifyTake函数来取出通知值:

  • 如果通知值等于0,则阻塞(可以指定超时时间)
  • 当通知值大于0时,任务从阻塞态进入就绪态
  • 在ulTaskNotifyTake返回之前,还可以做些清理工作:把通知值减一,或者把通知值清零

使用ulTaskNotifyTake函数可以实现轻量级的、高效的二进制信号量、计数型信号量。

这几个函数的原型如下:

BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken );

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );

xTaskNotifyGive函数的参数说明如下:

参数说明
xTaskToNotify任务句柄(创建任务时得到),给哪个任务发通知
返回值必定返回pdPASS

vTaskNotifyGiveFromISR函数的参数说明如下:

参数说明
xTaskHandle任务句柄(创建任务时得到),给哪个任务发通知
pxHigherPriorityTaskWoken被通知的任务,可能正处于阻塞状态。 此函数发出通知后,会把它从阻塞状态切换为就绪态。 如果被唤醒的任务的优先级,高于当前任务的优先级, 则"*pxHigherPriorityTaskWoken"被设置为pdTRUE, 这表示在中断返回之前要进行任务切换。

ulTaskNotifyTake函数的参数说明如下:

参数说明
xClearCountOnExit函数返回前是否清零: pdTRUE:把通知值清零 pdFALSE:如果通知值大于0,则把通知值减一
xTicksToWait任务进入阻塞态的超时时间,它在等待通知值大于0。 0:不等待,即刻返回; portMAX_DELAY:一直等待,直到通知值大于0; 其他值:Tick Count,可以用*pdMS_TO_TICKS()*把ms转换为Tick Count
返回值函数返回之前,在清零或减一之前的通知值。 如果xTicksToWait非0,则返回值有2种情况: 1. 大于0:在超时前,通知值被增加了 2. 等于0:一直没有其他任务增加通知值,最后超时返回0

2.3 xTaskNotify/xTaskNotifyWait

xTaskNotify 函数功能更强大,可以使用不同参数实现各类功能,比如:

  • 让接收任务的通知值加一:这时 xTaskNotify() 等同于 xTaskNotifyGive()
  • 设置接收任务的通知值的某一位、某些位,这就是一个轻量级的、更高效的事件组
  • 把一个新值写入接收任务的通知值:上一次的通知值被读走后,写入才成功。这就是轻量级的、长度为1的队列
  • 用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。类似 xQueueOverwrite() 函数,这就是轻量级的邮箱。

xTaskNotify() 比 xTaskNotifyGive() 更灵活、强大,使用上也就更复杂。xTaskNotifyFromISR() 是它对应的ISR版本。

这两个函数用来发出任务通知,使用哪个函数来取出任务通知呢?

使用 xTaskNotifyWait() 函数!它比 ulTaskNotifyTake() 更复杂:

  • 可以让任务等待(可以加上超时时间),等到任务状态为"pending"(也就是有数据)
  • 还可以在函数进入、退出时,清除通知值的指定位

这几个函数的原型如下:

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );

BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
                               uint32_t ulValue, 
                               eNotifyAction eAction, 
                               BaseType_t *pxHigherPriorityTaskWoken );

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, 
                            uint32_t ulBitsToClearOnExit, 
                            uint32_t *pulNotificationValue, 
                            TickType_t xTicksToWait );

xTaskNotify函数的参数说明如下:

参数说明
xTaskToNotify任务句柄(创建任务时得到),给哪个任务发通知
ulValue怎么使用ulValue,由eAction参数决定
eAction见下表
返回值pdPASS:成功,大部分调用都会成功 pdFAIL:只有一种情况会失败,当eAction为eSetValueWithoutOverwrite, 并且通知状态为"pending"(表示有新数据未读),这时就会失败。

eNotifyAction参数说明:

eNotifyAction取值说明
eNoAction仅仅是更新通知状态为"pending",未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量。
eSetBits通知值 = 原来的通知值 | ulValue,按位或。 相当于轻量级的、更高效的事件组。
eIncrement通知值 = 原来的通知值 + 1,未使用ulValue。 相当于轻量级的、更高效的二进制信号量、计数型信号量。 相当于**xTaskNotifyGive()**函数。
eSetValueWithoutOverwrite不覆盖。 如果通知状态为"pending"(表示有数据未读), 则此次调用xTaskNotify不做任何事,返回pdFAIL。 如果通知状态不是"pending"(表示没有新数据), 则:通知值 = ulValue。
eSetValueWithOverwrite覆盖。 无论如何,不管通知状态是否为"pending", 通知值 = ulValue。

xTaskNotifyFromISR函数跟xTaskNotify很类似,就多了最后一个参数pxHigherPriorityTaskWoken。在很多ISR函数中,这个参数的作用都是类似的,使用场景如下:

  • 被通知的任务,可能正处于阻塞状态
  • xTaskNotifyFromISR函数发出通知后,会把接收任务从阻塞状态切换为就绪态
  • 如果被唤醒的任务的优先级,高于当前任务的优先级,则"*pxHigherPriorityTaskWoken"被设置为pdTRUE,这表示在中断返回之前要进行任务切换。

xTaskNotifyWait函数列表如下:

参数说明
ulBitsToClearOnEntry在xTaskNotifyWait入口处,要清除通知值的哪些位? 通知状态不是"pending"的情况下,才会清除。 它的本意是:我想等待某些事件发生,所以先把"旧数据"的某些位清零。 能清零的话:通知值 = 通知值 & ~(ulBitsToClearOnEntry)。 比如传入0x01,表示清除通知值的bit0; 传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0
ulBitsToClearOnExit在xTaskNotifyWait出口处,如果不是因为超时推出,而是因为得到了数据而退出时: 通知值 = 通知值 & ~(ulBitsToClearOnExit)。 在清除某些位之前,通知值先被赋给"*pulNotificationValue"。 比如入0x03,表示清除通知值的bit0、bit1; 传入0xffffffff即ULONG_MAX,表示清除所有位,即把值设置为0
pulNotificationValue用来取出通知值。 在函数退出时,使用ulBitsToClearOnExit清除之前,把通知值赋给"*pulNotificationValue"。 如果不需要取出通知值,可以设为NULL。
xTicksToWait任务进入阻塞态的超时时间,它在等待通知状态变为"pending"。 0:不等待,即刻返回; portMAX_DELAY:一直等待,直到通知状态变为"pending"; 其他值:Tick Count,可以用*pdMS_TO_TICKS()*把ms转换为Tick Count
返回值1. pdPASS:成功 这表示xTaskNotifyWait成功获得了通知: 可能是调用函数之前,通知状态就是"pending"; 也可能是在阻塞期间,通知状态变为了"pending"。 2. pdFAIL:没有得到通知。

3.任务通知机制

假设有两个任务A,B任务A发送通知给任务B。有两种情景: 

情景1:任务A虽然给任务B发通知但是任务B并不去理会任务A发来的通知。

情景2:任务A给任务B发送通知,任务B理会任务A的通知。

情景1讲解:就好比舔狗,一直给喜欢的女生发消息,女生根本不理会所以也不知道男生发了什么消息。但是女生微信上的消息内容取决于男生行为(eAction ),当有一天女生回心转意去看这个消息她就接收到通知了,并且微信的未读消息数也清0了。好我们来转移到任务。

当任务A使用xTaskNotify 函数给任务B发送通知时,任务B虽然不理会这个消息,但是任务A将任务B的TCB结构体的通知值已经改变了(改变成什么取决于eAction 的取值),任务B的状态一直都是taskNOT_WAITING_NOTIFICATION,虽然任务B表现出taskNOT_WAITING_NOTIFICATION,但是任务B的状态已经改变为taskNOTIFICATION_RECEIVED 只是没有表现出来,当任务B想去读这个通知时调用了xTaskNotifyWait函数,任务B会立即从taskNOT_WAITING_NOTIFICATION状态转变为taskNOTIFICATION_RECEIVED ,任务B不会阻塞,因为任务A已经通知过,只是任务A通知的数据没被任务B处理(pending)而已。 

任务B调用xTaskNotifyWait()在这个函数内部会做那些事呢?

·第一步:在函数入口处判断任务B的TCB里的通知状态,(情景2)如果不是taskNOTIFICATION_RECEIVED,就清除任务B的TCB通知值的哪些位。(情景1)如果已经接收到通知了,那就不会清除任务B的TCB通知值的哪些位。

·第二步:收到通知后,将通知值保存在pulNotificationValue这个地址里,并返回给调用者(任务B)

·第三步:接收到任务通知后,记录通知值后,清除任务B得到通知值的若干位,第三步的作用就是在阻塞时接收到的通知值,这个处理由第三步来完成。

这三步做个总结:第一步可能会清除通知值的哪些位,也可能不清除,取决于任务B刚进入xTaskNotifyWait()的通知状态,如果任务B进入函数时的状态为taskNOTIFICATION_RECEIVED那就不清除,否则就清除。有可能任务B刚进入就获得通知也可能阻塞一会才得到通知,不管是刚进入得到通知还是阻塞一会得到通知,都会将原本未经修改的通知值保存在pulNotificationValue这个地址里,返回给调用者。在函数xTaskNotifyWait阻塞中退出之前,如果接收到通知执行第三步接收到任务通知后,记录通知值后,清除任务B得到通知值的若干位。过程图如下:

问:为什么第一步一定会执行?第二步为什么不一定会执行?那第三步呢? 

情景2:

根据情景1就很好分析情景2了。

4.例程操作

Car1Task执行完成后发送通知给Car2Task,Car2Task

创建任务:

void car_game(void)
{
	int x;
	int i, j;
	g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp);
	draw_init();
	draw_end();
	
	//g_xSemTicks = xSemaphoreCreateCounting(1, 1);
	//g_xSemTicks = xSemaphoreCreateMutex();
	g_xEventCar = xEventGroupCreate();

	/* 画出路标 */
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 8; j++)
		{
			draw_bitmap(16*j, 16+17*i, roadMarking, 8, 1, NOINVERT, 0);
			draw_flushArea(16*j, 16+17*i, 8, 1);
		}
	}
	
	/* 创建3个汽车任务 */
#if 0	
	for (i = 0; i < 3; i++)
	{
		draw_bitmap(g_cars[i].x, g_cars[i].y, carImg, 15, 16, NOINVERT, 0);
		draw_flushArea(g_cars[i].x, g_cars[i].y, 15, 16);
	}
#endif
    xTaskCreate(Car1Task, "car1", 128, &g_cars[0], osPriorityNormal, NULL);	 
    xTaskCreate(Car2Task, "car2", 128, &g_cars[1], osPriorityNormal+2, &g_TaskHandleCar2);
	  xTaskCreate(Car3Task, "car3", 128, &g_cars[2], osPriorityNormal+2, &g_TaskHandleCar3);
}

因为任务通知需要改变任务TCB,因此动态创建任务时需要获得任务句柄,在发送通知函数里传入任务句柄。

 小车1任务

static void Car1Task(void *params)
{
	struct car *pcar = params;
	struct ir_data idata;
	
	/* 创建自己的队列 */
	QueueHandle_t xQueueIR = xQueueCreate(10, sizeof(struct ir_data));
	
	/* 注册队列 */
	RegisterQueueHandle(xQueueIR);

	/* 显示汽车 */
	ShowCar(pcar);
	
	/* 获得信号量 */
	//xSemaphoreTake(g_xSemTicks, portMAX_DELAY);
	
	while (1)
	{
		/* 读取按键值:读队列 */
		//xQueueReceive(xQueueIR, &idata, portMAX_DELAY);
		
		/* 控制汽车往右移动 */
		//if (idata.val == pcar->control_key)
		{
			if (pcar->x < g_xres - CAR_LENGTH)
			{
				/* 隐藏汽车 */
				HideCar(pcar); 
				
				/* 调整位置 */
				pcar->x += 1;
				if (pcar->x > g_xres - CAR_LENGTH)
				{
					pcar->x = g_xres - CAR_LENGTH;
				}
				
				/* 重新显示汽车 */
				ShowCar(pcar);
				
				vTaskDelay(50);
				
				if (pcar->x == g_xres - CAR_LENGTH)
				{
					/* 释放信号量 */
					//xSemaphoreGive(g_xSemTicks);
					
					/* 设置事件组: bit0 */
					//xEventGroupSetBits(g_xEventCar, (1<<0));
					
					/* 发出任务通知给car2,car3 */
					
					
					xTaskNotify(g_TaskHandleCar3, 100, eSetValueWithOverwrite);
					xTaskNotifyGive(g_TaskHandleCar2);
					vTaskDelete(NULL);
				}
			}
		}
	}
}

小车二三任务 



static void Car2Task(void *params)
{
	struct car *pcar = params;
	struct ir_data idata;
		
	/* 创建自己的队列 */
	QueueHandle_t xQueueIR = xQueueCreate(10, sizeof(struct ir_data));
	
	/* 注册队列 */
	RegisterQueueHandle(xQueueIR);

	/* 显示汽车 */
	ShowCar(pcar);
	
	/* 获得信号量 */
	//xSemaphoreTake(g_xSemTicks, portMAX_DELAY);
	
	/* 等待事件:bit0 */
	//xEventGroupWaitBits(g_xEventCar, (1<<0), pdTRUE, pdFALSE, portMAX_DELAY);
	ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
	
	while (1)
	{
		/* 读取按键值:读队列 */
		//xQueueReceive(xQueueIR, &idata, portMAX_DELAY);
		
		/* 控制汽车往右移动 */
		//if (idata.val == pcar->control_key)
		{
			if (pcar->x < g_xres - CAR_LENGTH)
			{
				/* 隐藏汽车 */
				HideCar(pcar);
				
				/* 调整位置 */
				pcar->x += 1;
				if (pcar->x > g_xres - CAR_LENGTH)
				{
					pcar->x = g_xres - CAR_LENGTH;
				}
				
				/* 重新显示汽车 */
				ShowCar(pcar);
				
				vTaskDelay(100);
				//mdelay(50);
				
				if (pcar->x == g_xres - CAR_LENGTH)
				{
					/* 释放信号量 */
					//xSemaphoreGive(g_xSemTicks);

					/* 设置事件组: bit1 */
					//xEventGroupSetBits(g_xEventCar, (1<<1));
					
					vTaskDelete(NULL);
				}
			}
		}
	}
}


static void Car3Task(void *params)
{
	struct car *pcar = params;
	struct ir_data idata;
	uint32_t val;
	
	
	/* 创建自己的队列 */
	QueueHandle_t xQueueIR = xQueueCreate(10, sizeof(struct ir_data));
	
	/* 注册队列 */
	RegisterQueueHandle(xQueueIR);

	/* 显示汽车 */
	ShowCar(pcar);
	
	/* 获得信号量 */
	//xSemaphoreTake(g_xSemTicks, portMAX_DELAY);

	/* 等待事件:bit0 and bit1 */
	//xEventGroupWaitBits(g_xEventCar, (1<<0)|(1<<1), pdTRUE, pdTRUE, portMAX_DELAY);
	do 
	{
		xTaskNotifyWait(~0, ~0, &val, portMAX_DELAY);
	} while (val != 100);
		
	
	while (1)
	{
		/* 读取按键值:读队列 */
		//xQueueReceive(xQueueIR, &idata, portMAX_DELAY);
		
		/* 控制汽车往右移动 */
		//if (idata.val == pcar->control_key)
		{
			if (pcar->x < g_xres - CAR_LENGTH)
			{
				/* 隐藏汽车 */
				HideCar(pcar);
				
				/* 调整位置 */
				pcar->x += 1;
				if (pcar->x > g_xres - CAR_LENGTH)
				{
					pcar->x = g_xres - CAR_LENGTH;
				}
				
				/* 重新显示汽车 */
				ShowCar(pcar);
				
				//vTaskDelay(50);
				mdelay(50);
				
				if (pcar->x == g_xres - CAR_LENGTH)
				{
					/* 释放信号量 */
					//xSemaphoreGive(g_xSemTicks);
					vTaskDelete(NULL);
				}
			}
		}
	}
}

 

  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值