FreeRTOS任务通知

目录

所谓"任务通知",你可以反过来读"通知任务"。发送者和接收者是多对1的关系

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

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

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

1.特性

任务通知(Task Notification)是 FreeRTOS 为任务间通信和同步提供的一种轻量级机制,其主要特点在于直接嵌入在任务的控制块(TCB)中,不需要额外创建队列、信号量或事件组结构,因此具有高效率和低内存占用的优势。

1.1 优势以及限制

优势:

  • 任务通知操作直接更新目标任务的TCB中的通知值和状态,省去了复杂的数据拷贝和对象管理过程。
  • 相比于队列、信号量或事件组,任务通知所需的CPU周期更少,适用于需要频繁发送通知的场景。
  • 任务通知的数据(通常为一个32位整数)嵌入在任务的TCB中,不需要额外分配单独的结构体内存。
  • 这对于嵌入式系统或资源受限的环境尤为重要,可以减少系统整体内存开销。

限制:

  1. 不能发送数据给ISR:
    • 由于 ISR 没有自己的 TCB,因此无法直接使用任务通知机制发送数据给 ISR。
    • 不过 ISR 可以通过任务通知的 API(例如 xTaskNotifyFromISR())将数据发送给任务,从而实现中断到任务的通信。
  1. 数据只能给目标任务独享:
    • 通知的数据存放在目标任务的 TCB 中,只有该任务能读取和操作。
    • 如果需要广播或共享数据,使用队列、信号量或事件组更合适。
  1. 无法缓冲多个数据:
    • 每个任务的通知存储区域仅能保存一个32位通知值,相当于深度为1的缓冲区。
    • 如果通知还未被读取而又发送了新的通知,之前的通知数据会被覆盖(视具体 API 行为而定)。
  1. 无法广播给多个任务:
    • 任务通知只能针对单个任务进行,不能像事件组那样同时唤醒多个任务。
    • 这意味着当需要将同一通知发送给多个任务时,任务通知就不适用了。
  1. 发送方无法阻塞等待:
    • 如果目标任务的通知区域正被使用,发送任务不能像队列那样进入阻塞状态等待目标任务清空通知值。
    • 发送操作会立即返回错误,发送方需要自行处理这种情况。

1.2 TCB结构体中的存储

在TCB结构体中:

c 复制代码
typedef struct tskTaskControlBlock       /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
    //省略
    #if ( configUSE_TASK_NOTIFICATIONS == 1 )
        volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
        volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
    #endif

   //省略
} tskTCB;

ulNotifiedValue,通知值:

  • ulNotifiedValue 是一个 volatile uint32_t 数组。每个数组元素存储一个任务通知值( 存储实际的通知数据,可以表示一个计数、一个位域(类似于事件组)或任意32位数值 )。
  • volatile 关键字确保编译器不会对这些变量进行优化,因为它们可能在任务切换或中断中被修改。
  • 通知值由发送任务或 ISR 写入,接收任务可以读取这个值以获取额外信息。
  • 数组大小由宏 configTASK_NOTIFICATION_ARRAY_ENTRIES 决定。默认情况下这个值通常为 1,但也可以配置为大于1,从而让任务拥有多个通知槽(数组中的每个槽都可以独立使用)。

ucNotifyState,通知状态:

  • taskNOT_WAITING_NOTIFICATION (0): 表示任务当前没有在等待通知。
  • taskWAITING_NOTIFICATION (1): 表示任务正在等待通知。
  • taskNOTIFICATION_RECEIVED (2): 表示任务已接收到通知(处于 pending 状态)。

2.函数

2.1 两类函数

任务通知在 FreeRTOS 中提供了一种轻量级的任务间通信与同步机制。为了满足不同场景的需求,任务通知提供了两套 API:简化版专业版

2.2 简化版

2.2.1 Give

xTaskNotifyGive 与 vTaskNotifyGiveFromISR

这两个函数用于向目标任务发送通知,主要特点是:

  • 将目标任务的通知值增加 1(类似于计数器),
  • 更新通知状态为 "pending",表明有通知等待处理。

xTaskNotifyGive:

  • 在任务上下文中调用,用于将通知值加 1,表示向目标任务发送一个简单的通知或事件。
c 复制代码
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);

// xTaskToNotify:目标任务的句柄(通常在创建任务时获得)。它指明了通知应该发给哪个任务。
// 返回值:此函数必定返回 pdPASS,表示通知已成功发送。
  • 当发送者任务想通知目标任务"有新事件发生"或"计数加1"时,调用该函数非常高效。
  • 不需要传递复杂数据,只是简单地增加计数并改变通知状态。

vTaskNotifyGiveFromISR:

  • 在中断服务例程(ISR)中调用,功能与 xTaskNotifyGive 类似,用于在中断中向任务发送通知。
c 复制代码
void vTaskNotifyGiveFromISR(TaskHandle_t xTaskHandle, BaseType_t *pxHigherPriorityTaskWoken);

/*
xTaskHandle:目标任务的句柄。
pxHigherPriorityTaskWoken:一个指向 BaseType_t 的指针,供 ISR 使用,若通知导致某个等待任务进入就
                          绪态且其优先级高于当前任务,则将该变量设置为 pdTRUE,指示在中断结束后
                          需要进行上下文切换。
返回值:此函数不返回值,主要通过 pxHigherPriorityTaskWoken 参数反馈信息。
*/
  • 当中断发生时,需要快速通知任务(例如传感器数据就绪、外设事件等),使用该函数可以迅速将通知发送给任务,并确保高优先级任务能在中断退出前被调度。

2.2.2 Take

该函数用于在任务中等待并取出通知,是任务获取通知的主要接口之一,类似于"获取"操作。

  • 如果通知值为 0,则任务会阻塞等待,直到在指定超时时间内有通知值增加。
  • 当通知值大于0时,任务从阻塞状态恢复,并在返回之前,根据参数选择将通知值清零或仅减一。
c 复制代码
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);

参数:

  • xClearCountOnExit:

    • pdTRUE :在函数返回前,将目标任务的通知值清零。
      这种方式适用于二进制信号量风格,即接收到通知后立即清除。
    • pdFALSE :在返回前,不会完全清零,而是将通知值减 1。
      这适用于计数型信号量的场景,允许连续接收多个通知。
  • xTicksToWait:

    • 指定任务进入阻塞等待的时间(以 Tick 为单位)。
    • 如果设为 0,则函数立即返回当前通知值,不进行阻塞。
    • 如果设为 portMAX_DELAY,则任务将无限等待直到收到通知。
    • 其他值则表示等待的 Tick 数,可以使用 pdMS_TO_TICKS() 将毫秒转换为 Tick 数。

返回值:

  • 返回在清零或减一之前的通知值:

    • 如果在等待期间有通知到来,则返回大于 0 的值(表示接收到的通知数量);
    • 如果一直没有通知到来而超时,则返回 0。

当一个任务需要等待某个事件或计数器增加时,调用 ulTaskNotifyTake() 可使任务阻塞,直到事件发生。

例如,生产者任务使用 xTaskNotifyGive() 发出通知,消费者任务则调用 ulTaskNotifyTake() 来等待通知并"消费"该通知。


2.3 专业版

2.3.1 Give

任务上下文:

c 复制代码
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
                          uint32_t ulValue,
                          eNotifyAction eAction );

ISR:

c 复制代码
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
                               uint32_t ulValue,
                               eNotifyAction eAction,
                               BaseType_t *pxHigherPriorityTaskWoken );
  • xTaskToNotify / 任务句柄:指定要发送通知的目标任务。这个句柄在任务创建时获得。

  • ulValue:32 位数值,其具体作用由 eAction 决定:

    • 当使用 eNoAction 时,不会对通知值做任何修改,只是更新通知状态为"pending"(即有通知)。
    • 当使用 eSetBits 时,通知值将按位"或"(OR)运算,即:
      新通知值 = 原通知值 | ulValue
      这种方式适合将各个事件以位的形式累积,类似轻量级事件组。
    • 当使用 eIncrement 时,忽略 ulValue,通知值将递增 1。
      这种方式实现了计数功能,与 xTaskNotifyGive() 的行为一致。
    • 当使用 eSetValueWithoutOverwrite 时,只有当目标任务的通知状态不是"pending"(表示没有未读通知)时,才将通知值设为 ulValue;如果当前已有未处理通知,则此次调用不会更新通知值,并返回 pdFAIL。
      这种方式适用于需要确保新通知不会覆盖旧通知的场合。
    • 当使用 eSetValueWithOverwrite 时,无论当前通知状态如何,通知值都将被设为 ulValue,即直接覆盖之前的通知值。
      这种方式类似于轻量级邮箱,可以确保写入新值,不受旧通知的影响。
  • eAction (eNotifyAction)

    指定如何操作通知值,上述说明中列出的各个取值分别实现不同的通知行为。

  • pxHigherPriorityTaskWoken (仅用于 ISR 版本)

    是一个指针,指向一个 BaseType_t 变量。

    • 当 ISR 发送通知后,如果被通知的任务正处于阻塞状态且其优先级高于当前任务,则该变量将被设置为 pdTRUE,提示中断服务例程在退出时进行上下文切换。
  • 返回值:

    • 对于大多数调用,返回值为 pdPASS 表示成功。
    • 唯一可能返回 pdFAIL 的情况是在使用 eSetValueWithoutOverwrite 时,如果目标任务的通知状态已经处于"pending",说明之前的通知还未被读取,此时调用将失败,不更新通知值。

使用场景:

  • 作为事件组:
    如果希望将不同事件以位的形式累加到同一通知值中,可以使用 eSetBits。多个通知可以同时累加,不会相互覆盖。
  • 作为计数型信号量:
    使用 eIncrement 使通知值递增 1,每次通知代表一个事件或资源释放。
  • 作为邮箱或单深度队列:
    使用 eSetValueWithOverwrite 将新的数据写入通知值,无论是否有未读通知;或使用 eSetValueWithoutOverwrite 防止覆盖未处理的数据(如果需要确保数据不被覆盖)。
  • 在 ISR 中发送通知:
    使用 xTaskNotifyFromISR,可以在中断上下文中快速发送通知给任务,同时利用 pxHigherPriorityTaskWoken 实现实时响应。

2.3.2 Take

xTaskNotifyWait 用于在任务中等待通知到来,并提供在进入等待和退出等待时清除通知值特定位的功能。这使得任务在等待期间可以"清理"旧的通知数据,确保只处理新到来的通知。

c 复制代码
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
                            uint32_t ulBitsToClearOnExit,
                            uint32_t *pulNotificationValue,
                            TickType_t xTicksToWait );
  • ulBitsToClearOnEntry

    • 在进入等待前,清除通知值的哪些位(仅在通知状态不是"pending"的情况下执行)。
    • 例如,传入 0x01 表示在进入等待之前先清除通知值中的 bit0。
    • 如果传入 ULONG_MAX,则表示清除所有位(即将通知值清零)。
  • ulBitsToClearOnExit

    • 当等待成功(非超时返回)时,在函数退出前清除通知值的哪些位。
    • 在清除之前,通知值会先被赋值给 *pulNotificationValue,以便任务获取通知值。
    • 例如,传入 0x03 表示清除通知值的 bit0 和 bit1;传入 ULONG_MAX 则表示清除所有位。
  • pulNotificationValue

    • 用来取出通知值。
    • 在函数退出时,使用ulBitsToClearOnExit清除之前,把通知值赋给"*pulNotificationValue"。
    • 如果不需要取出通知值,可以设为NULL。
  • xTicksToWait

    • 指定任务等待通知的最大时间(Tick 数)。
    • 0 表示不等待,立即返回当前通知值;
    • portMAX_DELAY 表示无限等待,直到通知状态变为"pending";
    • 其他值表示等待指定的 Tick 数(可以通过 pdMS_TO_TICKS() 转换为 Tick 数)。

返回值:

  • 返回 pdPASS 表示任务成功获得通知,即等待期间通知状态变为了"pending",任务解除阻塞。
  • 返回 pdFAIL 表示任务在指定时间内没有收到通知而超时返回。

一般工作流程:

  • 进入等待之前

    • 调用 xTaskNotifyWait 时,首先根据 ulBitsToClearOnEntry 清除通知值中指定的位(如果当前通知状态不是 pending),从而丢弃旧通知。
  • 阻塞等待

    • 任务进入阻塞状态,直到目标任务的通知状态变为"pending"(即有新通知到来),或等待时间超时。
  • 退出等待前

    • 在成功获得通知后,函数先将当前通知值保存到 *pulNotificationValue(如果提供),然后根据 ulBitsToClearOnExit 清除通知值中相应的位。

这种机制允许任务在等待通知时同时对通知值进行预处理和后处理,以确保任务接收到的是新鲜的数据或正确的事件标志。

3.实现轻量级信号量

创建信号量时,可以指定最大值、初始值;使用任务通知实现轻量级的信号量时呢

  • 不能设置最大值
  • 初始值为0,不能指定初始值
  • 最小值是0,跟信号量是一样的
c 复制代码
static int sum = 0;
static volatile int flagCalcEnd = 0;
static volatile int flagUARTused = 0;

static SemaphoreHandle_t xSemCalc;
static SemaphoreHandle_t xSemUART;

static TaskHandle_t xHandleTask2;


void Task1Function(void * param)
{
	volatile int i = 0;
	while (1)
	{
		for (i = 0; i < 10000; i++)
			sum++;
		//printf("1");
		for (i = 0; i < 10; i++)
		{
			// xSemaphoreGive(xSemCalc);
			xTaskNotifyGive(xHandleTask2);
		}
		vTaskDelete(NULL);
	}
}

void Task2Function(void * param)
{
	int i = 0;
	int val;
	while (1)
	{
		//if (flagCalcEnd)
		flagCalcEnd = 0;
		//xSemaphoreTake(xSemCalc, portMAX_DELAY);
		val = ulTaskNotifyTake(pdFALSE, portMAX_DELAY); //pdFALSE:每取出一次通知值减1
		flagCalcEnd = 1;
		printf("sum = %d, NotifyVal = %d, i = %d\r\n", sum, val, i++);
	}
}

/*-----------------------------------------------------------*/

int main( void )
{
	TaskHandle_t xHandleTask1;
		
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello, world!\r\n");
	xSemCalc = xSemaphoreCreateCounting(10, 0);
	xSemUART = xSemaphoreCreateBinary();
	xSemaphoreGive(xSemUART);

	xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
	xTaskCreate(Task2Function, "Task2", 100, NULL, 1, &xHandleTask2);

	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

4.实现轻量级队列

队列、使用任务通知实现的轻量级队列,有什么异同?

  • 队列:可以容纳多个数据,数据大小可以指定
  • 任务通知:只有1个数据,数据时32位的
  • 队列:写队列时,可以阻塞
  • 任务通知:写队列时,不可以阻塞
  • 队列:如果队列长度是1,可以选择覆盖队列
  • 任务通知:可以覆盖,也可不覆盖
c 复制代码
static int sum = 0;
static volatile int flagCalcEnd = 0;
static volatile int flagUARTused = 0;
static QueueHandle_t xQueueCalcHandle;
static QueueHandle_t xQueueUARTcHandle;

static TaskHandle_t xHandleTask2;

int InitUARTLock(void)
{	
	int val;
	xQueueUARTcHandle = xQueueCreate(1, sizeof(int));
	if (xQueueUARTcHandle == NULL)
	{
		printf("can not create queue\r\n");
		return -1;
	}
	xQueueSend(xQueueUARTcHandle, &val, portMAX_DELAY);
	return 0;
}

void GetUARTLock(void)
{	
	int val;
	xQueueReceive(xQueueUARTcHandle, &val, portMAX_DELAY);
}

void PutUARTLock(void)
{	
	int val;
	xQueueSend(xQueueUARTcHandle, &val, portMAX_DELAY);
}


void Task1Function(void * param)
{
	volatile int i = 0;
	while (1)
	{
		for (i = 0; i < 10000; i++)
			sum++;
		//printf("1");
		//flagCalcEnd = 1;
		//vTaskDelete(NULL);
		for (i = 0; i < 10; i++)
		{
			//xQueueSend(xQueueCalcHandle, &sum, portMAX_DELAY);
			//xTaskNotify(xHandleTask2, sum, eSetValueWithoutOverwrite);  // 不覆盖
			xTaskNotify(xHandleTask2, sum, eSetValueWithOverwrite); // 覆盖value
			sum++;
		}
		vTaskDelete(NULL);
		//sum = 1;
	}
}

void Task2Function(void * param)
{
	int val;
	int i = 0;
	
	while (1)
	{
		//if (flagCalcEnd)
		flagCalcEnd = 0;
		//xQueueReceive(xQueueCalcHandle, &val, portMAX_DELAY);
		xTaskNotifyWait(0, 0, &val, portMAX_DELAY);
		flagCalcEnd = 1;
		printf("sum = %d, i = %d\r\n", val, i++);
	}
}


/*-----------------------------------------------------------*/

int main( void )
{
	TaskHandle_t xHandleTask1;
		
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello, world!\r\n");

	xQueueCalcHandle = xQueueCreate(10, sizeof(int));
	if (xQueueCalcHandle == NULL)
	{
		printf("can not create queue\r\n");
	}

	InitUARTLock();

	xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
	xTaskCreate(Task2Function, "Task2", 100, NULL, 1, &xHandleTask2);

	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

5.实现轻量级事件组

任务通知并不能实现真正的事件组,为什么?

  • 它不能等待指定的事件
  • 它不能等待若干个事件中的任意一个
  • 一旦有事件,总会唤醒任务

发送方可以设置事件,但是接收方不能挑选事件,即使不是它关心的事件,它也会被唤醒

  • TCB结构体中的uint_32的通知值,不管修改哪一位表示事件的发生,正在等待的接收方都会被通知
c 复制代码
static int sum = 0;
static int dec = 0;
static volatile int flagCalcEnd = 0;
static volatile int flagUARTused = 0;
static QueueHandle_t xQueueCalcHandle;

static EventGroupHandle_t xEventGroupCalc;
static TaskHandle_t xHandleTask3;



void Task1Function(void * param)
{
	volatile int i = 0;
	while (1)
	{
		for (i = 0; i < 100000; i++)
			sum++;
		xQueueSend(xQueueCalcHandle, &sum, 0);
		/* 设置事件0 */
		//xEventGroupSetBits(xEventGroupCalc, (1<<0));
		xTaskNotify(xHandleTask3, (1<<0), eSetBits);
		printf("Task 1 set bit 0\r\n");
		vTaskDelete(NULL);
	}
}

void Task2Function(void * param)
{
	volatile int i = 0;
	while (1)
	{
		for (i = 0; i < 1000000; i++)
			dec--;
		xQueueSend(xQueueCalcHandle, &dec, 0);
		/* 设置事件1 */
		//xEventGroupSetBits(xEventGroupCalc, (1<<1));
		xTaskNotify(xHandleTask3, (1<<1), eSetBits);
		printf("Task 2 set bit 1\r\n");
		vTaskDelete(NULL);
	}
}


void Task3Function(void * param)
{
	int val1, val2;
	int bits;
	while (1)
	{
		/*等待事件  */
		//xEventGroupWaitBits(xEventGroupCalc, (1<<0)|(1<<1), pdTRUE, pdTRUE, portMAX_DELAY);
		xTaskNotifyWait(0, 0, &bits, portMAX_DELAY);
		if ((bits & 0x3) == 0x3)
		{
			vTaskDelay(20);
			xQueueReceive(xQueueCalcHandle, &val1, 0);
			xQueueReceive(xQueueCalcHandle, &val2, 0);
			
			printf("val1 = %d, val2 = %d\r\n", val1, val2);
		}
		else
		{
			vTaskDelay(20);
			printf("have not get all bits, get only 0x%x\r\n", bits);
		}
	}
}



/*-----------------------------------------------------------*/

int main( void )
{
	TaskHandle_t xHandleTask1;
		
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello, world!\r\n");

	/* 创建事件组 */
	xEventGroupCalc = xEventGroupCreate();

	xQueueCalcHandle = xQueueCreate(2, sizeof(int));
	if (xQueueCalcHandle == NULL)
	{
		printf("can not create queue\r\n");
	}


	xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
	xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
	xTaskCreate(Task3Function, "Task3", 100, NULL, 1, &xHandleTask3);
	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}

6.源码分析

上面就提到过一个任务的"通知状态"有三种:

  • taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
  • taskWAITING_NOTIFICATION:任务在等待通知
  • taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为 pending(有数据了,待处理)

一个任务想等待别人发来通知,可以调用ulTaskNotifyTake xTaskNotifyWait

  • 可能别人早就发来通知:"通知状态"为taskNOTIFICATION_RECEIVED,那么函数立刻返回
  • 可能别人还没发来通知:这些函数把"通知状态"从taskNOT_WAITING_NOTIFICATION改为taskWAITING_NOTIFICATION,然后休眠

别的任务可以使用xTaskNotifyGivexTaskNotify 给某个任务发通知:

  • 会马上唤醒对方
  • 无条件唤醒对方,不管对方期待什么数据

6.1 等待通知

这里以专业版的 xTaskGenericNotifyWait进行诉说:

c 复制代码
BaseType_t xTaskGenericNotifyWait( UBaseType_t uxIndexToWait,
                                   uint32_t ulBitsToClearOnEntry,
                                   uint32_t ulBitsToClearOnExit,
                                   uint32_t * pulNotificationValue,
                                   TickType_t xTicksToWait )
{
    BaseType_t xReturn;

    /* 确保传入的索引有效,索引必须小于任务通知数组的总条目数 */
    configASSERT( uxIndexToWait < configTASK_NOTIFICATION_ARRAY_ENTRIES );

    /* 进入临界区,确保接下来的操作原子执行,不被中断 */
    taskENTER_CRITICAL();
    {
        /* 如果当前任务的通知状态未标记为"已接收通知",则说明当前没有通知等待结果
         * 只有当通知状态不是 taskNOTIFICATION_RECEIVED 时,才允许任务进入等待状态 */
        if( pxCurrentTCB->ucNotifyState[ uxIndexToWait ] != taskNOTIFICATION_RECEIVED )
        {
            /* 在进入等待之前,根据 ulBitsToClearOnEntry 参数清除任务通知值中的指定位
             * 这样做可以在等待开始前清除旧的通知位(例如将通知值清零) */
            pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ] &= ~ulBitsToClearOnEntry;

            /* 将任务的通知状态标记为"等待通知" */
            pxCurrentTCB->ucNotifyState[ uxIndexToWait ] = taskWAITING_NOTIFICATION;

            /* 如果指定的等待时间大于0,则任务将进入阻塞状态等待通知 */
            if( xTicksToWait > ( TickType_t ) 0 )
            {
                /* 将当前任务添加到延时列表中,等待 xTicksToWait 个时钟节拍后超时 */
                prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
                /* 跟踪记录任务进入等待通知阻塞状态 */
                traceTASK_NOTIFY_WAIT_BLOCK( uxIndexToWait );

                /* 尽管处于临界区内,内核各移植层允许在 API 中进行任务切换(yield)
                 * 这可能会导致立即进行上下文切换,但应用代码一般不会直接调用此 yield */
                portYIELD_WITHIN_API();
            }
            else
            {
                /* 如果 xTicksToWait 为0,则不进入延时列表 */
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            /* 如果任务的通知状态已经是"已接收通知",则无需改变状态 */
            mtCOVERAGE_TEST_MARKER();
        }
    }
    /* 退出第一个临界区 */
    taskEXIT_CRITICAL();

    /* 第二个临界区用于处理解除阻塞后的通知值读取和状态恢复 */
    taskENTER_CRITICAL();
    {
        /* 跟踪记录任务通知等待完成 */
        traceTASK_NOTIFY_WAIT( uxIndexToWait );

        /* 如果调用者提供了 pulNotificationValue 指针,则将当前任务的通知值写入该指针中
         * 这个通知值可能在等待期间被更新 */
        if( pulNotificationValue != NULL )
        {
            *pulNotificationValue = pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ];
        }

        /* 判断通知状态:
         * 如果通知状态不是 taskNOTIFICATION_RECEIVED,则说明任务未因通知解除阻塞,而是因超时解除 */
        if( pxCurrentTCB->ucNotifyState[ uxIndexToWait ] != taskNOTIFICATION_RECEIVED )
        {
            /* 没有收到通知,返回 pdFALSE */
            xReturn = pdFALSE;
        }
        else
        {
            /* 如果通知状态为 taskNOTIFICATION_RECEIVED,则任务在等待期间收到了通知
             * 根据 ulBitsToClearOnExit 参数清除通知值中的相应位 */
            pxCurrentTCB->ulNotifiedValue[ uxIndexToWait ] &= ~ulBitsToClearOnExit;
            /* 设置返回值为 pdTRUE,表示成功接收到通知 */
            xReturn = pdTRUE;
        }

        /* 无论通知是否接收,将任务的通知状态重置为"未等待通知" */
        pxCurrentTCB->ucNotifyState[ uxIndexToWait ] = taskNOT_WAITING_NOTIFICATION;
    }
    /* 退出第二个临界区 */
    taskEXIT_CRITICAL();

    /* 返回任务是否成功接收到通知 */
    return xReturn;
}

根据当前任务的TCB结构体pxCurrentTCB中的状态标志来决定需不需要等待

  • 如果是taskNOTIFICATION_RECEIVED,已经收到通知但是被pending了,不需要等待
  • 如果是其它状态,将状态设置为taskWAITING_NOTIFICATION,等待通知,然后调用prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );将该任务放入到任务等待链表当中,使其等待通知

下半部分则是用于处理解除阻塞后的通知值读取和状态恢复(第二个临界区),注释很清楚了,这里不赘述

6.2 发送通知

还是以专业版的xTaskNotify为例子:

c 复制代码
#define xTaskNotify( xTaskToNotify, ulValue, eAction ) \
    xTaskGenericNotify( ( xTaskToNotify ), ( tskDEFAULT_INDEX_TO_NOTIFY ), ( ulValue ), ( eAction ), NULL )
c 复制代码
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,
                               UBaseType_t uxIndexToNotify,
                               uint32_t ulValue,
                               eNotifyAction eAction,
                               uint32_t * pulPreviousNotificationValue )
{
    /* 定义指向目标任务控制块(TCB)的指针 */
    TCB_t * pxTCB;
    /* 默认返回值设置为 pdPASS,表示通知成功 */
    BaseType_t xReturn = pdPASS;
    /* 用于保存原始通知状态的变量 */
    uint8_t ucOriginalNotifyState;

    /* 断言检查:确保传入的通知索引在任务通知数组范围内 */
    configASSERT( uxIndexToNotify < configTASK_NOTIFICATION_ARRAY_ENTRIES );
    /* 断言检查:确保要通知的任务不为空 */
    configASSERT( xTaskToNotify );
    /* 将任务句柄转换为 TCB 指针,便于访问任务的通知数据 */
    pxTCB = xTaskToNotify;

    /* 进入临界区,确保以下操作是原子性的,防止中断或任务切换干扰 */
    taskENTER_CRITICAL();
    {
        /* 如果调用者要求获取通知前的值,则将目标任务对应索引的通知值保存到 pulPreviousNotificationValue */
        if( pulPreviousNotificationValue != NULL )
        {
            *pulPreviousNotificationValue = pxTCB->ulNotifiedValue[ uxIndexToNotify ];
        }

        /* 保存当前任务的通知状态,以便后续判断任务是否处于等待通知状态 */
        ucOriginalNotifyState = pxTCB->ucNotifyState[ uxIndexToNotify ];

        /* 将任务的通知状态设置为"已接收通知",表示本次调用已触发通知 */
        pxTCB->ucNotifyState[ uxIndexToNotify ] = taskNOTIFICATION_RECEIVED;

        /* 根据通知操作类型(eAction)来更新任务的通知值 */
        switch( eAction )
        {
            case eSetBits:
                /* eSetBits:将 ulValue 指定位与当前通知值进行按位或运算 */
                pxTCB->ulNotifiedValue[ uxIndexToNotify ] |= ulValue;
                break;

            case eIncrement:
                /* eIncrement:将当前通知值递增 */
                ( pxTCB->ulNotifiedValue[ uxIndexToNotify ] )++;
                break;

            case eSetValueWithOverwrite:
                /* eSetValueWithOverwrite:无条件覆盖当前通知值,设置为 ulValue */
                pxTCB->ulNotifiedValue[ uxIndexToNotify ] = ulValue;
                break;

            case eSetValueWithoutOverwrite:
                /* eSetValueWithoutOverwrite:仅当通知状态之前未标记为"已接收通知"时才写入新值 */
                if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
                {
                    pxTCB->ulNotifiedValue[ uxIndexToNotify ] = ulValue;
                }
                else
                {
                    /* 如果目标任务已处于"已接收通知"状态,则不允许覆盖,返回失败 */
                    xReturn = pdFAIL;
                }
                break;

            case eNoAction:
                /* eNoAction:不改变通知值,仅更新通知状态,用于仅唤醒任务 */
                break;

            default:
                /* 默认情况:如果传入了未处理的操作类型,则强制断言失败 */
                configASSERT( xTickCount == ( TickType_t ) 0 );
                break;
        }

        /* 记录通知事件,用于调试和跟踪 */
        traceTASK_NOTIFY( uxIndexToNotify );

        /* 检查目标任务是否正处于等待通知的阻塞状态 */
        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
        {
            /* 如果是,则将该任务从阻塞列表中移除 */
            listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
            /* 并将任务添加到就绪列表中,以便它可以被调度执行 */
            prvAddTaskToReadyList( pxTCB );

            /* 确保该任务不在任何事件列表中 */
            configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL );

            #if ( configUSE_TICKLESS_IDLE != 0 )
            {
                /* 如果使用 Tickless Idle 模式,可能需要重置下一个任务解除阻塞时间,
                 * 以便更快进入睡眠模式 */
                prvResetNextTaskUnblockTime();
            }
            #endif

            /* 如果被通知的任务优先级高于当前任务,
             * 则立即进行任务切换,使得高优先级任务尽快运行 */
            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
            {
                taskYIELD_IF_USING_PREEMPTION();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            /* 如果目标任务原来并不处于等待通知状态,则不需要将其从阻塞列表中移除 */
            mtCOVERAGE_TEST_MARKER();
        }
    }
    /* 退出临界区 */
    taskEXIT_CRITICAL();

    /* 返回操作结果,pdPASS 表示通知成功,pdFAIL 表示未能写入通知值(如 eSetValueWithoutOverwrite 情况) */
    return xReturn;
}

通知操作类型(如设置位、递增、覆盖等)更新目标任务的通知值,并在必要时唤醒等待通知的任务 。注释很清楚了,这里就不再额外讲了。看过之前关于队列、信号量、互斥量的相关内部机制讲解,其实看这个也是很简单了

相关推荐
憧憬一下2 小时前
FreeRTOS定时器
嵌入式·freertos·定时器
网易独家音乐人Mike Zhou3 小时前
【Linux应用】Linux系统日志上报服务,以及thttpd的配置、发送函数
linux·运维·服务器·mcu·物联网·嵌入式·iot
程序员打怪兽4 小时前
线程间数据传递机制详解(共享全局内存 + 互斥锁同步)
linux·嵌入式
星羽空间1 天前
项目课题——智能花盆系统设计
物联网·毕业设计·嵌入式·课程设计·嵌入式开发
我命由我123452 天前
STM32 开发 - 中断案例(中断概述、STM32 的中断、NVIC 嵌套向量中断控制器、外部中断配置寄存器组、EXTI 外部中断控制器、实例实操)
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式
SY师弟2 天前
台湾TEMI协会竞赛——2、足球机器人组装教学
c语言·单片机·嵌入式硬件·机器人·嵌入式·台湾temi协会
凉、介2 天前
Linux 下 pcie 初始化设备枚举流程代码分析
linux·运维·服务器·学习·嵌入式·c·pcie
SY师弟3 天前
台湾TEMI协会竞赛——1、龙舟机器人组装教学
c语言·单片机·嵌入式硬件·机器人·嵌入式·台湾temi协会
不脱发的程序猿3 天前
常见的CAN总线协议面试题
嵌入式·can·汽车电子