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 发通知和取通知流程
发通知:
- 关中断
- 获取任务通知的原状态,发送通知,State更新为taskNOTIFICATION_RECEIVED
- 根据 eAction 更新任务通知值
- 被通知任务由于等待通知而挂起,即State= taskWAITING_NOTIFICATION,唤醒:
* DelayList ——>ReadList
* 如果唤醒的任务优先级 > 当前任务
+ 任务切换 - 开中断
等通知:
ulTaskNotifyTake:
- 关中断
- 没收到通知(value = 0)
- 标记 state为 等待通知,taskWAITING_NOTIFICATION
- 休眠
- 根据超时时间放入延时链表
- 切换任务
- 收到了通知(若其他任务或中断向该任务发送了通知,或超时唤醒)
- 取 value,
- 是否清除,不清除,则减一。
- 重设 State = taskNOT_WAITING_NOTIFICATION。
- 开中断
xTaskNotifyWait:
- 关中断
- 没有收到通知,即 State != taskNOTIFICATION_RECEIVED
- 在收通知前是否将某些位清除,value &= ~ulBitsToClearOnEntry
- 标识 State = taskWAITING_NOTIFICATION,等待通知
- 休眠(阻塞)
- 根据超时时间放入延时链表
- 切换任务
- 收到了通知(若其他任务或中断向该任务发送了通知,或超时唤醒)
- 取出通知值 value;
- 退出前清零,Value &= ~ulBitsToClearOnExit
- 重设 State = taskNOT_WAITING_NOTIFICATION。
- 开中断
2. 任务通知函数
使用任务通知,可以实现轻量级的队列(长度为1)、邮箱(覆盖的队列)、计数型信号量、二进制信号量、事件组。
2.1 两类函数
任务通知有2套函数,简化版、专业版,列表如下:
- 简化版函数的使用比较简单,它实际上也是使用专业版函数实现的
- 专业版函数支持很多参数,可以实现很多功能
简化版 | 专业版 | |
---|---|---|
发出通知 | xTaskNotifyGive vTaskNotifyGiveFromISR | xTaskNotify xTaskNotifyFromISR |
取出通知 | ulTaskNotifyTake | xTaskNotifyWait |
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才会启动。