当前位置: 首页 > article >正文

FreeRTOS - 任务通知

1. 任务通知

所谓"任务通知",你可以反过来读"通知任务"。

我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。

使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信:

使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知":

1.1 任务通知的特性

每个任务都有一个 32 位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件组,也可以替代 长度为 1 的队列(可以保存一个 32位整数或指针值)。

1.1.1 优势和限制

任务通知的优势:

  • 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
  • 更节省内存:使用其他方法时都要先创建对应的结构体。使用任务通知时无需额外创建结构体。 由于任务通知的数据结构包含在任务控制块中,只要任务存在,任务通知数据结构就已经创建完毕, 可以直接使用

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

  • 发送通知给任务, 如果有通知未读,不覆盖通知值。
  • 发送通知给任务,直接覆盖通知值。
  • 发送通知给任务,设置通知值的一个或者多个位,可以当做事件组来使用。
  • 发送通知给任务,递增通知值,可以当做计数信号量使用。

任务通知的限制:

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

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

  • ulNotifiedValue :uint32_t类型,任务通知值,可以保存一个32位整数或指针值
  • ucNotifyState:uint8_t类型,任务通知状态,用于标识任务是否在等待通知。

通知状态有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 )

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

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

1.2 发通知和取通知流程

发通知:

  1. 关中断
  2. 获取任务通知的原状态,发送通知,State更新为taskNOTIFICATION_RECEIVED
  3. 根据 eAction 更新任务通知值
  4. 被通知任务由于等待通知而挂起,即State= taskWAITING_NOTIFICATION,唤醒:
    * DelayList ——>ReadList
    * 如果唤醒的任务优先级 > 当前任务
    + 任务切换
  5. 开中断

等通知:

ulTaskNotifyTake:

  1. 关中断
  2. 没收到通知(value = 0)
    1. 标记 state为 等待通知,taskWAITING_NOTIFICATION
    2. 休眠
      1. 根据超时时间放入延时链表
      2. 切换任务
  3. 收到了通知(若其他任务或中断向该任务发送了通知,或超时唤醒)
    1. 取 value,
    2. 是否清除,不清除,则减一。
    3. 重设 State = taskNOT_WAITING_NOTIFICATION。
  4. 开中断

xTaskNotifyWait:

  1. 关中断
  2. 没有收到通知,即 State != taskNOTIFICATION_RECEIVED
    1. 在收通知前是否将某些位清除,value &= ~ulBitsToClearOnEntry
    2. 标识 State = taskWAITING_NOTIFICATION,等待通知
    3. 休眠(阻塞)
      1. 根据超时时间放入延时链表
      2. 切换任务
  3. 收到了通知(若其他任务或中断向该任务发送了通知,或超时唤醒)
    1. 取出通知值 value;
    2. 退出前清零,Value &= ~ulBitsToClearOnExit
    3. 重设 State = taskNOT_WAITING_NOTIFICATION。
  4. 开中断

2. 任务通知函数

使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。

2.1 两类函数

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

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

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

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

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

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

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

2.1.2 专业版–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 );

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

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

2.2 发送任务通知函数

2.2.1 发送任务通知函数xTaskGenericNotify()
  • xTaskToNotify:被通知的任务句柄;
  • ulValue:发送的通知值;
  • eAction:枚举类型,指明更新通知值的方式。
typedef enum
{
	eNoAction = 0,				/* 不更新通知值就通知任务 */
	eSetBits,					/* 在任务通知值中设置位 */
	eIncrement,					/* 任务通知值递增 1 */
	eSetValueWithOverwrite,		/* (可覆盖)将任务通知值设置为特定值 */
	eSetValueWithoutOverwrite	/* (不可覆盖)将任务通知值设置为特定值*/
} eNotifyAction;
  • pulPreviousNotificationValue:任务原本的通知值返回。
#if( configUSE_TASK_NOTIFICATIONS == 1 )

	BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,
                                  uint32_t ulValue, 
                                  eNotifyAction eAction, 
                        uint32_t *pulPreviousNotificationValue )
	{
	TCB_t * pxTCB;
	BaseType_t xReturn = pdPASS;
	uint8_t ucOriginalNotifyState;

		configASSERT( xTaskToNotify );
		pxTCB = ( TCB_t * ) xTaskToNotify;

		taskENTER_CRITICAL();
		{
			if( pulPreviousNotificationValue != NULL )
			{
                /* 回传未被更新的任务通知值 */
				*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
			}
            /* 获取任务通知的状态,
            看看任务是否在等待通知,方便在发送通知后恢复任务 */
			ucOriginalNotifyState = pxTCB->ucNotifyState;
            /* 不管状态是怎么样的,反正现在发送通知,
                任务就收到任务通知 */
			pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
            /* 指定更新任务通知的方式 */
			switch( eAction )
			{
              /*通知值按位或上 ulValue。
                使用这种方法可以某些场景下代替事件组,
                但执行速度更快。*/  
				case eSetBits	:
					pxTCB->ulNotifiedValue |= ulValue;
					break;
                    
              /* 被通知任务的通知值增加 1,
                 这种发送通知方式,参数 ulValue 未使用 */
				case eIncrement	:
					( pxTCB->ulNotifiedValue )++;
					break;
              /* 将被通知任务的通知值设置为 ulValue。
                 无论任务是否还有通知,都覆盖当前任务通知值。
                 使用这种方法,
                 可以在某些场景下代替 xQueueoverwrite()函数,
                  但执行速度更快。 */
				case eSetValueWithOverwrite	:
					pxTCB->ulNotifiedValue = ulValue;
					break;

            /* 如果被通知任务当前没有通知,
               则被通知任务的通知值设置为 ulValue;
               在某些场景下替代长度为 1 的 xQueuesend(),
                    但速度更快。 */        
				case eSetValueWithoutOverwrite :
					if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
					{
						pxTCB->ulNotifiedValue = ulValue;
					}
					else
					{
						/* 如果被通知任务还没取走上一个通知,本次发送通知,
                        任务又接收到了一个通知,则这次通知值丢弃,
                        在这种情况下,函数调用失败并返回pdFAIL
                        */
						xReturn = pdFAIL;
					}
					break;
                /* 发送通知但不更新通知值,这意味着参数 ulValue 未使用*/
				case eNoAction:
					/* The task is being notified without its notify value being
					updated. */
					break;
			}

			traceTASK_NOTIFY();

			/* 如果被通知任务由于等待任务通知而挂起 */
			if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
			{
                /* 唤醒,从阻塞链表删除,放入就绪链表 */
				( void ) uxListRemove( &( pxTCB->xStateListItem ) );
				prvAddTaskToReadyList( pxTCB );

                /* 刚刚唤醒的任务优先级比当前任务高 */
				if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
				{
					/* 任务切换 */
					taskYIELD_IF_USING_PREEMPTION();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		taskEXIT_CRITICAL();

		return xReturn;
	}

#endif /* configUSE_TASK_NOTIFICATIONS */

创建了三个任务:

2.3 获取任务通知函数
2.3.1 ulTaskNotifyTake
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, 
                          TickType_t xTicksToWait )

对于这个函数,任务通知值为 0,对应信号量无效,如果任务设置了阻塞等待,任务 被阻塞挂起。当其他任务或中断发送了通知值使其不为 0 后,通知变为有效,等待通知的 任务将获取到通知,并且在退出时候根据用户传递的第一个参数 xClearCountOnExit 选择清 零通知值或者执行减一操作。

作为二值信号量和计数信号量的一种轻量级实现,速度更快。

退出的时候处理任务的通知值的时候有两种方法:

  • 在函数退出时将通知值清零,这种方法适用于实现二值信号量;
  • 在函数退出时将通知 值减 1,这种方法适用于实现计数信号量。
#if( configUSE_TASK_NOTIFICATIONS == 1 )

	uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, 
                               TickType_t xTicksToWait )
	{
	uint32_t ulReturn;

		taskENTER_CRITICAL();//进入中断临界区
		{
			/* 如果通知值为 0 ,阻塞任务,
            默认初始化通知值为 0, 说明没有未读通知 */
			if( pxCurrentTCB->ulNotifiedValue == 0UL )
			{
				/* 标记任务状态:等待消息通知 */
				pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
                /* 用户指定超时时间,那就进入等待状态 */
				if( xTicksToWait > ( TickType_t ) 0 )
				{
                    /* 根据用户指定超时时间将任务添加到延时链表*/
					prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
					traceTASK_NOTIFY_TAKE_BLOCK();

					/* 切换任务*/
					portYIELD_WITHIN_API();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		taskEXIT_CRITICAL();
        /* 到这里说明其他任务或中断向这个任务发送了通知,
        或者任务阻塞超时,
            现在继续处理*/
		taskENTER_CRITICAL();
		{
            // 获取任务通知值
			traceTASK_NOTIFY_TAKE();
			ulReturn = pxCurrentTCB->ulNotifiedValue;
            
            // 看看任务通知是否有效,有效则返回
			if( ulReturn != 0UL )
			{
                //是否需要清除通知
				if( xClearCountOnExit != pdFALSE )
				{
					pxCurrentTCB->ulNotifiedValue = 0UL;
				}
				else
				{   // 不清除,就减一
					pxCurrentTCB->ulNotifiedValue = ulReturn - ( uint32_t ) 1;
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
            //恢复任务通知状态
			pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
		}
		taskEXIT_CRITICAL();

		return ulReturn;
	}

#endif /* configUSE_TASK_NOTIFICATIONS */

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

用于实现全功能版的等待任务通知,根据用户指定的参数的不 同,可以灵活的用于实现轻量级的消息队列队列、二值信号量、计数信号量和事件组功能, 并带有超时等待。

#if( configUSE_TASK_NOTIFICATIONS == 1 )

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

		taskENTER_CRITICAL(); /* 进入临界段 */
		{
			/* 只有任务当前没有收到任务通知,才会将任务阻塞 */
			if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED )
			{
				/* 使用任务通知值之前,根据用户指定参数 
                ulBitsToClearOnEntry
                将通知值的某些或全部位清零 */
				pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;

				/* 设置任务状态标识:等待通知 */
				pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
                
                /* 挂起任务等待通知或者进入阻塞态 */
				if( xTicksToWait > ( TickType_t ) 0 )
				{
                    /* 根据用户指定超时时间将任务添加到延时列表 */
					prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
					traceTASK_NOTIFY_WAIT_BLOCK();

					/* 任务切换 */
					portYIELD_WITHIN_API();
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		taskEXIT_CRITICAL();
        /*程序能执行到这里说明其它任务或中断向这个任务发送了通知
        或者任务阻塞超时,
            现在继续处理*/
		taskENTER_CRITICAL();
		{
			traceTASK_NOTIFY_WAIT();

			if( pulNotificationValue != NULL )
			{
				/* 返回当前通知值,通过指针参数传递 */
				*pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
			}

			/* 没有收到任务通知 */
			if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED )
			{
				/* A notification was not received. */
				xReturn = pdFALSE;
			}
			else
			{
				/* 收到任务值,先将参数 ulBitsToClearOnExit 
                   取反后与通知值位做按位与运算
                   在退出函数前,将通知值的某些或者全部位清零.  */
				pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;
				xReturn = pdTRUE;
			}
            /* 重新设置任务通知状态 */
			pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
		}
		taskEXIT_CRITICAL();

		return xReturn;
	}

#endif /* configUSE_TASK_NOTIFICATIONS */

3. 任务通知实验

3.1 代码-任务通知

本节代码为:27_tasknotification_car_game,主要看nwatch\game2.c。

car1运行到终点后,给car2发送轻量级信号量给car3发送数值。car2等待轻量级信号量,car3等待特定的通知值。

使用任务通知时,需要知道对方的任务句柄,创建任务时要记录任务句柄,代码如下:

40 static TaskHandle_t g_TaskHandleCar2;

41 static TaskHandle_t g_TaskHandleCar3;

/* 省略 */

315 xTaskCreate(Car1Task, "car1", 128, &g_cars[0], osPriorityNormal, NULL);

316 xTaskCreate(Car2Task, "car2", 128, &g_cars[1], osPriorityNormal+2, &g_TaskHandleCar2);

317 xTaskCreate(Car3Task, "car3", 128, &g_cars[2], osPriorityNormal+2, &g_TaskHandleCar3);	

car1到达终点后,向car2、car3发出任务通知,代码如下:

145   /* 发出任务通知给car2 */

146   xTaskNotifyGive(g_TaskHandleCar2);

147		/* 发出任务通知给car3 */
        /* 覆盖。 无论如何,不管通知状态是否为"pendng", 
         * 通知值 = ulValue。 */
148   xTaskNotify(g_TaskHandleCar3, 100, eSetValueWithOverwrite);

car2等待轻量级信号量,代码如下:

/* 函数返回前清零,一直等待,直到通知值大于0*/
176   ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

car3等待通知值为100,代码如下:

224   uint32_t val;

/* 省略 */

241   do

242   {
        /* 入口处全部清零,出口处全部清零,取出通知值给val,一直等待*/
243      xTaskNotifyWait(~0, ~0, &val, portMAX_DELAY);

244  } while (val != 100);

实验现象:car1到达终点后,car2、car3才会启动。


http://www.kler.cn/news/354033.html

相关文章:

  • 锥线性规划【分布鲁棒、两阶段鲁棒方向知识点】
  • 基于SpringBoot的校园兼职管理系统
  • Scrapy | 爬取网易招聘信息来认识start_urls是POST请求时如何重写start_requests方法以及翻页问题的处理
  • 力扣题解(鸡蛋掉落,两枚鸡蛋)
  • Bug剖析
  • 数据权限的设计与实现系列12——前端筛选器组件Everright-filter集成功能完善3
  • 优化SpringBoot接口:异步处理提升系统吞吐量策略
  • SQL语句查询
  • 【IC设计】复旦微行业分析
  • 为什么你总碰到渣男?伯克森悖论
  • 博客搭建之路:hexo使用next主题渲染流程图
  • 技术总结(七)
  • 解决ultralytics中的YOLOv8在执行task.py文件添加模块操作出现的KeyError报错
  • Linux-lvs
  • 芒果YOLOv10改进136:注意力机制MLLA|即插即用:融合Mamba设计精髓的线性注意力视觉Transformer
  • Ubuntu(Linux)tcpdump使用方法详解
  • 金融信用评分卡建模项目:AI辅助
  • mysql指令笔记(基本)
  • C#/WinForm 自定义控件绘制章鱼
  • 【2022工业3D异常检测文献】Patch+FPFH: 结合3D手工点云描述符和颜色特征的异常检测方法