文章目录
- 一、任务通知是什么?
- 1.1任务通知的优势
- 1.2任务通知的限制
- 1.3通知状态和通知值
- 二、任务通知的使用
- 2.1任务通知使用_轻量级信号量
- 2.2任务通知使用_轻量级队列
- 2.3任务通知使用_轻量级事件组
一、任务通知是什么?
我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。
使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信。
使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知"。
1.1任务通知的优势
效率高: 使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都
有大的优势。
省内存: 使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。启用任务通知功能的开销固定为每个任务8个字节的RAM
1.2任务通知的限制
- 不能发送数据给ISR: ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
- 数据只能给该任务独享: 使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
- 无法缓冲数据: 使用队列时,假设队列深度为N,那么它可以保持N个数据。使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
- 无法广播给多个任务: 事件组可以同时给多个任务发送事件。使用任务通知,只能发个一个任务。
- 发送方无法进入阻塞状态: 接收方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。
1.3通知状态和通知值
每个任务都有一个结构体TCB(Task Control Block),里面有2个成员:
一个是uint8_t类型,用来表示通知状态
一个是uint32_t类型,用来表示通知值
通知状态
ulNotifiedValue
有3种取值:
状态 | 说明 |
---|---|
taskNOT_WAITING_NOTIFICATION | 任务没有在等待通知 |
taskWAITING_NOTIFICATION | 任务在等待通知 |
taskNOTIFICATION_RECEIVED | 任务接收到了通知,也被称为pending (有数据了,待处理) |
通知值可以有很多种类型:计数值、位(类似事件组)、任意数值
二、任务通知的使用
使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、(计数型/二进制)信号量、事件组。
发送函数 | 作用 | 接收函数 | 作用 |
---|---|---|---|
xTaskNotifyGive | val++ | ulTaskNotifyTake | val-- 或 val = 0 |
xTaskNotify | xTaskNotifyWait | ||
不使用val,只起通知作用 | 可以在函数进入时清除val的某些位 可以在函数退出前清除val的某些位 可以取得val的值 | ||
val |= (bits) | |||
val++ | |||
val = xxx 不覆盖, 当ucNotifyState表示在等待才起效 | |||
val = xxx 覆盖 |
2.1任务通知使用_轻量级信号量
在任务中使用xTaskNotifyGive
函数,在ISR中使用vTaskNotifyGiveFromISR
函数,都是直接给其他任务发送通知:
- 使得通知值加一
- 使得通知状态变为"pending",也就是
taskNOTIFICATION_RECEIVED
,表示有数据了、待处理可以使用ulTaskNotifyTake
函数来取出通知值: - 如果通知值等于0,则阻塞(可以指定超时时间)
- 当通知值大于0时,任务从阻塞态进入就绪态
- 在ulTaskNotifyTake返回之前,还可以做些清理工作:把通知值减一,或者把通知值清零
信号量与任务通知信号量对比如下:
信号量 | 使用任务通知实现信号量 | |
---|---|---|
创建 | SemaphoreHandle_t xSemaphoreCreateCounting ( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount ); | 无 |
Give | xSemaphoreGive ( SemaphoreHandle_t xSemaphore ); | BaseType_t xTaskNotifyGive ( TaskHandle_t xTaskToNotify ); |
Take | xSemaphoreTake (SemaphoreHandle_t xSemaphore, TickType_t xBlockTime ); | uint32_t ulTaskNotifyTake ( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ); |
xTaskNotifyGive
函数的参数说明如下:
参数 | 说明 |
---|---|
xTaskToNotify | 任务句柄(创建任务时获得),表示要发送通知的任务 |
返回值 | 必定返回pdPASS,表示操作成功 |
ulTaskNotifyTake
函数的参数说明如下:
参数 | 说明 |
---|---|
xClearCountOnExit | 函数返回前是否清零通知值。pdTRUE 表示清零,pdFALSE 表示如果通知值大于0,则减一 |
xTicksToWait | 任务进入阻塞态的超时时间,等待通知值大于0。0表示立即返回,portMAX_DELAY 表示一直等待直到通知值大于0,其他值表示以Tick Count 的方式等待 |
返回值 | 函数返回之前,在清零或减一之前的通知值。如果xTicksToWait 非0,有两种情况:1. 大于0,表示在超时前通知值被增加了;2. 等于0,表示一直没有其他任务增加通知值,最后超时返回0 |
实验1: 任务1发送数据通知,任务2接收
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);flagCalcEnd = 1;printf("sum = %d, NotifyVal = %d, i = %d\r\n", sum, val, i++);}
}
pdFALSE
,表示通知值大于0,则减一。所以可以接收到所有通知。结果如下:
pdTRUE
表示通知值清零,所以只能接收一次。结果如下:
2.2任务通知使用_轻量级队列
因为TCB结构体value
只能存一个数据,所以任务通知实现队列可以只能容纳一个32位数据,且写队列不可阻塞,数据可覆盖也可不覆盖。正常队列可以容纳多个数据,且数据大小可指定,写队列可以阻塞。
队列与使用任务通知实现队列对比如下:
队列 | 使用任务通知实现队列 | |
---|---|---|
创建 | QueueHandle_t xQueueCreate (UBaseType_t uxQueueLength, UBaseType_t uxItemSize ); | 无 |
发送 | BaseType_t xQueueSend (QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait ); | BaseType_t xTaskNotify ( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction ); |
接收 | BaseType_t xQueueReceive ( QueueHandle_t xQueue,void * const pvBuffer, TickType_t xTicksToWait ); | BaseType_t xTaskNotifyWait ( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait ); |
xTaskNotify
函数的参数说明如下:
参数 | 说明 |
---|---|
xTaskToNotify | 任务句柄(创建任务时得到),表示要给哪个任务发通知 |
ulValue | 通知值,如何使用取决于eAction参数 |
eAction | 操作类型,具体含义见下表 |
返回值 | pdPASS表示成功,大部分调用都会成功;pdFAIL表示失败,仅当eAction为eSetValueWithoutOverwrite且通知状态为"pending"时会失败 |
eNotifyAction取值 | 说明 |
---|---|
eNoAction | 仅仅是更新通知状态为"pending",未使用ulValue。 这个选项相当于轻量级的、更高效的二进制信号量。 |
eSetBits | 通知值 = 原来的通知值 | ulValue,按位或。 相当于轻量级的、更高效的事件组。 |
eIncrement | 通知值 = 原来的通知值 + 1,未使用ulValue。 相当于轻量级的、更高效的二进制信号量、计数型信号量。 相当于 xTaskNotifyGive() 函数。 |
eSetValueWithoutOverwrite | 不覆盖。 如果通知状态为"pending"(表示有数据未读), 则此次调用xTaskNotify不做任何事,返回pdFAIL。 如果通知状态不是"pending"(表示没有新数据), 则:通知值 = ulValue。 |
eSetValueWithOverwrite | 覆盖。 无论如何,不管通知状态是否为"pendng", 通知值 = ulValue。 |
xTaskNotifyWait
函数的参数说明如下:
参数 | 说明 |
---|---|
ulBitsToClearOnEntry | 在进入xTaskNotifyWait时要清除通知值的哪些位。只有在通知状态不是"pending"时才会清除。清除的方式是通知值 = 通知值 & ~(ulBitsToClearOnEntry)。 |
ulBitsToClearOnExit | 在退出xTaskNotifyWait时,如果是因为接收到了数据而退出(而不是超时),要清除通知值的哪些位。清除的方式是通知值 = 通知值 & ~(ulBitsToClearOnExit)。在清除之前,通知值会被赋给pulNotificationValue。 |
pulNotificationValue | 用于接收通知值。在函数退出时,使用ulBitsToClearOnExit清除通知值之前,通知值会被赋给pulNotificationValue。如果不需要接收通知值,可以将其设为NULL。 |
xTicksToWait | 任务进入阻塞态的超时时间,等待通知状态变为"pending"。0表示立即返回,portMAX_DELAY表示一直等待直到通知状态变为"pending",其他值表示以Tick Count的方式等待。 |
返回值 | pdPASS表示成功,表示xTaskNotifyWait成功获得了通知。pdFAIL表示失败,表示没有得到通知。 |
实验2: 任务1发送,任务2接收
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);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++);}
}
eSetValueWithoutOverwrite
不覆盖 结果
eSetValueWithOverwrite
覆盖 结果
2.3任务通知使用_轻量级事件组
使用任务通知实现轻量级的事件组,我们将eAction
配置为 eSetBits
,通知值 = 原来的通知值 | ulValue。并且只要调用xTaskNotify就会唤醒目标任务,这与事件组 只有设置某一位或某几位满足条件才会唤醒任务 不符合。所以在 目标任务需要做一些判断才能模拟事件组。
实验3: 任务1、任务2发出事件,任务3等待事件。任务1设置
bit0
,任务2设置bit1
,任务3等待这两个都发生才退出
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);}}
}
结果: 任务2循环时间是任务1十倍,所以任务1将
bit0
置1,任务3响应后发现bits & 0x3
仍未0,直到任务2将bit1
置1,任务3打印出val1
和val2