1. 任务通知的简介
任务通知:用来通知任务的。
说到通知任务,我们前面所学习到的队列、信号量以及事件标志组,这些它是不是也可以用来通知任务啊。那为什么我们还要这个任务通知啊?
最主要的就是任务通知它的内存消耗比较小。
- 像队列、信号量以及事件标志组这些是不是我们提前使用它之前要提前创建好才能去操作它,任务通知它就不需要你创建了,因为它的一个结构体成员在任务控制块里面,所以你只要一创建任务之后,这个成员变量就已经给它创建好了,所以就不需要像队列、信号量、事件标志组这些另外给它创建一下,这里就不需要了,所以它所需要内存非常小。
任务控制块中的结构体成员变量 ulNotifiedValue 就是这个通知值。当然还不止这一个,还有一个是保存的任务通知状态。所以在任务控制块里面,跟这个任务通知相关的结构体成员变量是有两个的,一个保存通知值,还有一个是保存它的通知状态。
-
使用队列、信号量、事件标志组时都需另外创建一个结构体,通过中间的结构体进行间接通信!
-
使用任务通知时,任务结构体 TCB 中就包含了任务通知所需要的两个结构体成员变量(通知值+通知状态),可以直接接收别人发过来的"通知"
任务一直接操作任务二的 TCB。
1.1 任务通知值的更新方式
- 不覆盖接受任务的通知值(类似队列)
- 覆盖接受任务的通知值(类似队列)
- 更新接受任务通知值的一个或多个bit(类似事件标志组 )
- 增加接受任务的通知值(类似信号量 )
只要合理,灵活的利用任务通知的特点,这四种更新方式就可以让任务通知在一些场合中用来模拟(替代)队列、信号量、事件标志组!
那大家肯定有疑问了。既然你这个任务通知这么牛掰,什么都能做,那为何还要队列、信号量、事件标志组,我还要它们干嘛,你一个都能抵过三个了,你啥都能做,为什么?大家注意,这里说了是在一些场合里面,也就是说任务通知其实它是有局限性的。怎么说呢,我们就来看一下任务通知它的优势以及劣势。
1.2 任务通知的优势及劣势
- 任务通知的优势:
- 效率更高:使用任务通知向任务发送事件或数据比使用队列、事件标志组或信号量快得多。
FreeRTOS 官方经过了一个测试:使用任务通知来模拟二值信号量这样的一个方式来解除任务的一个阻塞时间相对于常规的二值信号量快了百分之四十五左右。所以大家看这个速度是提升非常明显的,足以说明任务通知的效率相对于其他普通的这些是会更高的。
- 使用内存更小:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体
- 任务通知的劣势:
- 无法发送数据给ISR:ISR没有任务结构体,所以无法给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
- 无法广播给多个任务:任务通知只能是被指定的一个任务接收并处理。
- 无法缓存多个数据:任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数据。
- 发送受阻不支持阻塞:发送方无法进入阻塞状态等待
所以大家如果还要针对很多任务来接收这个通知值的,那么大家就使用队列、事件标志组还有信号量这些了,那如果呢大家都是单对单的,那肯定使用任务通知会更好,效率高,而且使用的内存更小,所以大部分我们如果使用单对单的都使用这个任务通知了。
1.3 任务通知值和通知状态
任务都有一个结构体:任务控制块TCB,它里边有两个结构体成员变量:
typedef struct tskTaskControlBlock
{
… …
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue [ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState [ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
endif
… …
} tskTCB;
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1 /* 定义任务通知数组的大小, 默认: 1 */
- 一个是 uint32_t 类型,用来表示通知值
- 一个是 uint8_t 类型,用来表示通知状态
如果要使用任务通知,那么 configUSE_TASK_NOTIFICATIONS 这个宏必须置 1。
1.3.1任务通知值
任务通知值的更新方式有多种类型:
- 计数值(数值累加,类似信号量)
- 相应位置一(类似事件标志组)
- 任意数值(支持覆写和不覆写,类似队列)
1.3.2 任务通知状态
其中任务通知状态共有3种取值:
#define taskNOT_WAITING_NOTIFICATION ( ( uint8_t ) 0 ) /* 任务未等待通知 */
#define taskWAITING_NOTIFICATION ( ( uint8_t ) 1 ) /* 任务在等待通知 */
#define taskNOTIFICATION_RECEIVED ( ( uint8_t ) 2 ) /* 任务在等待接收 */
- 任务未等待通知 :任务通知默认的初始化状态(事不关己高高挂起,不需要等待通知,也不需要等待接收)
- 等待通知:接收方已经准备好了,等待发送方给个通知(调用了接收任务通知函数)
- 等待接收:发送方已经发送出去,等待接收方接收(调用了发送任务通知函数)
2. 任务通知相关API函数介绍
任务通知API函数主要有两类:① 发送通知 ,② 接收通知。
注意:发送通知API函数可以用于任务和中断服务函数中;接收通知API函数只能用在任务中。因为中断它是没有任务控制块这个结构体的,所以接收通知API函数是不能用于中断中的。
①发送通知相关API函数:
函数 | 描述 |
---|---|
xTaskNotify() | 发送通知,带有通知值(三种方式均可使用) |
xTaskNotifyAndQuery() | 发送通知,带有通知值并且保留接收任务的原通知值(三种方式均可使用) |
xTaskNotifyGive() | 发送通知,不带通知值(通知值++,应用于信号量) |
xTaskNotifyFromISR() | 在中断中发送任务通知 |
xTaskNotifyAndQueryFromISR() | 在中断中发送任务通知 |
vTaskNotifyGiveFromISR() | 在中断中发送任务通知 |
我们用信号量的时候,我们就用 xTaskNotifyGive 这种比较简单方便;那 xTaskNotify 和 xTaskNotifyAndQuery 这两个的话就用于事件标志组和队列。
函数原型:
#define xTaskNotifyAndQuery( xTaskToNotify , ulValue , eAction , pulPreviousNotifyValue )
xTaskGenericNotify( ( xTaskToNotify ),
( tskDEFAULT_INDEX_TO_NOTIFY ),
( ulValue ),
( eAction ),
( pulPreviousNotifyValue ) )
tskDEFAULT_INDEX_TO_NOTIFY 其实是 0,代表是数组成员第一个,也就是数组成员第一个成员。我们来看一下,在前面介绍任务通知的结构体的时候,我们是有介绍到的,任务通知值和任务通知状态它们其实都是数组,它们的 configTASK_NOTIFICATION_ARRAY_ENTRIES 这个宏是 1,也就是说它们都只有一个成员,而这一个成员就是 0,数组名[0] 就是第一个成员,那我们这里就来确定是哪一个数组成员,这个宏其实就这么一个功能。
#define xTaskNotify( xTaskToNotify , ulValue , eAction )
xTaskGenericNotify( ( xTaskToNotify ) , ( tskDEFAULT_INDEX_TO_NOTIFY ) , ( ulValue ) , ( eAction ) , NULL )
#define xTaskNotifyGive( xTaskToNotify )
xTaskGenericNotify( ( xTaskToNotify ) , ( tskDEFAULT_INDEX_TO_NOTIFY ) , ( 0 ) , eIncrement , NULL )
它们都是宏定义,并且它们都是调用的同一个函数。唯一的区别就是代入得入口参数有区别。
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,
UBaseType_t uxIndexToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue )
形参 | 描述 |
---|---|
xTaskToNotify | 接收任务通知的任务句柄 |
uxIndexToNotify | 任务的指定通知(任务通知相关数组成员) |
ulValue | 任务通知值 |
eAction | 通知方式(通知值更新方式) |
pulPreviousNotificationValue | 用于保存更新前的任务通知值(为NULL则不保存) |
任务通知方式共有以下几种:
typedef enum
{
eNoAction = 0, /* 无操作 */
eSetBits /* 更新指定bit */
eIncrement /* 通知值加一 */
eSetValueWithOverwrite /* 覆写的方式更新通知值 */
eSetValueWithoutOverwrite /* 不覆写的方式更新通知值 */
} eNotifyAction;
②接收通知相关API函数:
函数 | 描述 |
---|---|
ulTaskNotifyTake() | 获取任务通知,可以设置在退出此函数的时候将任务通知值清零(模拟二值信号量)或者减一(模拟计数型信号量)。当任务通知用作二值信号量或者计数信号量的时候,使用此函数来获取信号量。 |
xTaskNotifyWait() | 获取任务通知,比 ulTaskNotifyTak()更为复杂,可获取通知值和清除通知值的指定位(模拟事件标志组/队列) |
总结:
- 当任务通知用作于信号量时,使用函数获取信号量:ulTaskNotifyTake()
- 当任务通知用作于事件标志组或队列时,使用此函数来获取: xTaskNotifyWait()
#define ulTaskNotifyTake( xClearCountOnExit , xTicksToWait )
ulTaskGenericNotifyTake( ( tskDEFAULT_INDEX_TO_NOTIFY ),
( xClearCountOnExit ),
( xTicksToWait ) )
此函数用于接收任务通知值,可以设置在退出此函数的时候将任务通知值清零或者减一
形参 | 描述 |
---|---|
uxIndexToWaitOn | 任务的指定通知(任务通知相关数组成员) |
xClearCountOnExit | 指定在成功接收通知后,将通知值清零或减 1,pdTRUE:把通知值清零;pdFALSE:把通知值减一 |
xTicksToWait | 阻塞等待任务通知值的最大时间 |
返回值 | 描述 |
---|---|
0 | 接收失败 |
非 0 | 接收成功,返回任务通知的通知值 |
#define xTaskNotifyWait( ulBitsToClearOnEntry,
ulBitsToClearOnExit,
pulNotificationValue,
xTicksToWait)
xTaskGenericNotifyWait( tskDEFAULT_INDEX_TO_NOTIFY, /* 这宏是 0,代表就是数组的第一个成员 */
( ulBitsToClearOnEntry ),
( ulBitsToClearOnExit ),
( pulNotificationValue ),
( xTicksToWait ) )
此函数用于获取通知值和清除通知值的指定位值,适用于模拟队列和事件标志组,使用该函数来获取任务通知 。
BaseType_t xTaskGenericNotifyWait( UBaseType_t uxIndexToWaitOn,
uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t * pulNotificationValue,
TickType_t xTicksToWait );
形参 | 描述 |
---|---|
uxIndexToWaitOn | 任务的指定通知(任务通知相关数组成员) |
ulBitesToClearOnEntry | 等待前清零指定任务通知值的比特位(旧值对应bit清0) |
ulBitesToClearOnExit | 成功等待后清零指定的任务通知值比特位(新值对应bit清0) |
pulNotificationValue | 用来取出通知值(如果不需要取出,可设为NULL) |
xTicksToWait | 阻塞等待任务通知值的最大时间 |
返回值 | 描述 |
---|---|
pdTRUE | 等待任务通知成功 |
pdFALSE | 等待任务通知失败 |
3. 任务通知模拟信号量实验
实验目的:学习使用 FreeRTOS 中的任务通知功能模拟二值信号量和计数型信号量
实验设计:将设计三个任务:start_task、task1、task2
三个任务的功能如下:
start_task:用来创建task1和task2任务
task1:用于按键扫描,当检测到按键KEY0被按下时,将发送任务通知
task2:用于接收任务通知,并打印相关提示信息
3.1 模拟二值信号量
任务函数实现
#include "event_groups.h"
/* 任务一,发送任务通知值 */
void task1( void * pvParameters )
{
uint8_t key = 0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
printf("任务通知模拟二值信号量释放!\r\n");
xTaskNotifyGive(task2_handler); /* 发送任务通知(通知值++) */
}
vTaskDelay(10);
}
}
/* 任务二,接收任务通知值 */
void task2( void * pvParameters )
{
uint32_t rev = 0;
while(1)
{
rev = ulTaskNotifyTake(pdTRUE , portMAX_DELAY); /* 接收任务通知(通知值清零) */
if(rev != 0)
{
printf("接收任务通知成功,模拟获取二值信号量!\r\n");
}
}
}
3.2 模拟计数型信号量
任务函数实现
#include "event_groups.h"
/* 任务一,发送任务通知值 */
void task1( void * pvParameters )
{
uint8_t key = 0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
printf("任务通知模拟计数型信号量释放!\r\n");
xTaskNotifyGive(task2_handler); /* 发送任务通知(通知值++) */
}
vTaskDelay(10);
}
}
/* 任务二,接收任务通知值 */
void task2( void * pvParameters )
{
uint32_t rev = 0;
while(1)
{
rev = ulTaskNotifyTake(pdFALSE , portMAX_DELAY); /* 接收任务通知(通知值--) */
if(rev != 0)
{
printf("rev:%d\r\n",rev);
}
vTaskDelay(1000);
}
}
4. 任务通知模拟消息邮箱实验
实验目的:学习使用 FreeRTOS 中的任务通知功能模拟消息邮箱
实验设计:将设计三个任务:start_task、task1、task2
三个任务的功能如下:
start_task:用来创建task1和task2任务
task1:用于按键扫描,将按下的按键键值通过任务通知发送给指定任务
task2:用于接收任务通知,并根据接收到的数据做相应动作
4.1 模拟队列
任务函数实现
#include "event_groups.h"
/* 任务一,发送任务通知值 */
void task1( void * pvParameters )
{
uint8_t key = 0;
while(1)
{
key = key_scan(0);
if((key != 0) && (task2_handler != NULL)) /* 判断按键是否按下以及任务 2 是否创建成功 */
{
printf("任务通知模拟消息邮箱发送,发送的键值为:%d\r\n",key);
xTaskNotify( task2_handler, key, eSetValueWithOverwrite ); /* 发送任务通知(覆写的方式更新通知值) */
}
vTaskDelay(10);
}
}
/* 任务二,接收任务通知值 */
void task2( void * pvParameters )
{
uint32_t noyify_val = 0;
while(1)
{
xTaskNotifyWait( 0, 0xFFFFFFFF, &noyify_val, portMAX_DELAY ); /* 接收任务通知(成功等待后全部位都给清零) */
switch(noyify_val)
{
case KEY0_PRES:
{
printf("接收到的通知值为:%d\r\n",noyify_val);
LED0_TOGGLE();
break;
}
case KEY1_PRES:
{
printf("接收到的通知值为:%d\r\n",noyify_val);
LED1_TOGGLE();
break;
}
default : break;
}
}
}
5. 任务通知模拟事件标志组实验
实验目的:学习使用 FreeRTOS 中的任务通知功能模拟事件标志组
实验设计:将设计三个任务:start_task、task1、task2
三个任务的功能如下:
start_task:用来创建task1和task2任务
task1:用于按键扫描,当检测到按键按下时,发送任务通知设置不同标志位
task2:用于接收任务通知,并打印相关提示信息
5.1 模拟事件标志组
任务函数实现
#include "event_groups.h"
#define EVENTBIT_0 (1 << 0)
#define EVENTBIT_1 (1 << 1)
/* 任务一,发送任务通知值*/
void task1( void * pvParameters )
{
uint8_t key = 0;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
printf("将bit0位置1\r\n");
xTaskNotify( task2_handler, EVENTBIT_0, eSetBits ); /* 发送任务通知(更新指定 bit) */
}else if(key == KEY1_PRES)
{
printf("将bit1位置1\r\n");
xTaskNotify( task2_handler, EVENTBIT_1, eSetBits ); /* 发送任务通知(更新指定 bit) */
}
vTaskDelay(10);
}
}
/* 任务二,接收任务通知值 */
void task2( void * pvParameters )
{
uint32_t notify_val = 0,event_bit = 0;
while(1)
{
xTaskNotifyWait( 0, 0xFFFFFFFF, ¬ify_val, portMAX_DELAY ); /* 接收任务通知(成功等待后全部位都给清零) */
if(notify_val & EVENTBIT_0)
{
event_bit |= EVENTBIT_0;
}
if(notify_val & EVENTBIT_1)
{
event_bit |= EVENTBIT_1;
}
if(event_bit == (EVENTBIT_0|EVENTBIT_1))
{
printf("任务通知模拟事件标志组接收成功!!\r\n");
event_bit = 0;
}
}
}
如果 xTaskNotifyWait 函数的第二个参数等于 0,效果是怎样的?
按下 key0,把 bit0 位置 1,没问题,按下 key1,把 bit1 位置 1,这时候接收成功,没问题。那这时候呢我按下 bit0,按一个它也接收成功,那这时候肯定不满足我们的一个要求了,所以呢记住 xTaskNotifyWait 这个函数退出之后一定要清零,这个一定要注意。