1. 简介
任务通知本质上就是一种进程间通信机制。之前的文章介绍的消息队列、事件组、信号量等都是一种间接的通信方式,而任务通知则是更加直接的方式,允许两个任务(或中断和任务)之间直接通信。
2. 任务通知的优势和局限性
优势:
- 直接通信的方式性能更好;
- 更少的内存占用,目前的实现每个任务只有5字节的占用。
局限性:
- 无法给中断响应函数发送任务通知;
- 难以支持超过一个接收者的情况,无法广播;
- 传输的数据有限;
- 不支持类似于队列一样的阻塞写。
3. 任务通知的使用
3.1. 发送通知
发送通知的函数原型:需要设置configUSE_TASK_NOTIFICATIONS = 1
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue ) PRIVILEGED_FUNCTION;
#define xTaskNotify( xTaskToNotify, ulValue, eAction ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL)
#define xTaskNotifyAndQuery( xTaskToNotify, ulValue, eAction, pulPreviousNotifyValue ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), ( pulPreviousNotifyValue ) )
开启了configUSE_TASK_NOTIFICATION
后, 每个任务则会记录其私有的notification value,这是一个uint32_t
的整数。与FreeRTOS的其他IPC不同,它将直接把事件发送给指定的任务。在提高效率的同时,它牺牲了灵活性,也即,它能实现的行为有限:在eNotifyAction
中定义了该函数能做的一些操作,例如更新、重写、增加任务的notification value,如下所示:
eSetBits
:使用ulValue
与任务当前的notification value取“或”;这时,该函数永远返回pdPASS
。eIncreament
:将任务当前的notification value自增1,ulValue
将被忽略;此时,该函数永远返回pdPASS
。eSetValueWithOverwrite
:用ulValue
覆盖任务当前的notification value,无论此时是否还有别的notification未被处理。此时,函数永远返回pdPASS
。eSetValueWithoutOverwrite
:如果此时还有别的notification未被处理,那么该函数直接返回pdFAIL
;否则用ulValue
覆盖当前的notification value。eNoAction
:不对notification value进行任何操作,此时函数永远返回pdPASS
。
当一个notification发送到某个任务后,该notification会在将来的某个时候被获取,即当接收者调用了xTaskNotifyWait()
或ulTaskNotifyTake()
时。而如果在notification到达之前,接受者就已经处于等待notifcation的状态(Blocked),notification抵达时,任务将会变成Ready状态,同时清掉notification。
您可能在忧虑应该如何获取被覆盖之前的notification value?pulPreviousNotificationValue
为你排忧解难。它记录了被覆盖前的notification value。
这两个接口都是通过宏定义,最终调用xTaskGenericNotify()
实现,下面我们来看看该函数的具体实现,其活动图如下:
该函数大致的实现流程与消息队列相似。比较重要的区别是,这里没有阻塞的行为,无论是否成功唤醒(除非唤醒的更高优先级的任务),都会直接返回。
另外,FreeRTOS还提供了中断上下文版本的接口:
BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue, BaseType_t *pxHigherPriorityTaskWoken ) PRIVILEGED_FUNCTION;
#define xTaskNotifyFromISR( xTaskToNotify, ulValue, eAction, pxHigherPriorityTaskWoken ) xTaskGenericNotifyFromISR( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL, ( pxHigherPriorityTaskWoken ) )
#define xTaskNotifyAndQueryFromISR( xTaskToNotify, ulValue, eAction, pulPreviousNotificationValue, pxHigherPriorityTaskWoken ) xTaskGenericNotifyFromISR( ( xTaskToNotify ), ( ulValue ), ( eAction ), ( pulPreviousNotificationValue ), ( pxHigherPriorityTaskWoken ) )
与任务上下文中的版本类似,ISR的版本最终通过xTaskGenericNotifyFromISR()
实现,下面就来看看其实现:
{
初始化`xReturn = pdPASS`。
参数校验:Assert `xTaskToNotify != NULL`
检查中断的优先级是否满足要求(`portASSERT_IF_INTERRUPT_PRIORITY_INVALID()`)。
获取TCB(`pxTCB = ( TCB_t * ) xTaskToNotify`)。
进入临界区(`uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR()`)。
如果任务通知接收变量不为空(`pulPreviousNotificationValue != NULL`),设置当前的任务通知(`*pulPreviousNotificationValue = pxTCB->ulNotifiedValue`)。
获取当前任务通知的标志(`ucOriginalNotifyState = pxTCB->ucNotifyState`)。
根据动作类型选择具体操作(`switch ( eAction )`):
{
case eSetBits:设置对应的位图(`pxTCB->ulNotifiedValue |= ulValue`)
break;
case eIncrement:增加任务的通知值(`pxTCB->ulNotifiedValue ++`)。
break;
case eSetValueWithOverwrite:直接设置对应的通知值(`pxTCB->ulNotifiedValue = ulValue`)。
break;
case eSetValueWithoutOverWrite:
{
如果原本任务通知状态不是已接收(`ucOriginalNotifyState != taskNOTIFICATION_RECEIVED`),则更新值`pxTCB->ulNotifiedValue = ulValue`。
否则,设置返回值为失败`xReturn = pdFAIL`。
}
break;
case eNoAction:不做任何操作。
break;
}
如果原本任务通知状态为等待(`ucOriginalNotifyState == taskWAITING_NOTIFICATION`);
{
条件校验:确保任务此时不在任何事件任务队列中(`configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL)`)。
如果调度器未被挂起(`uxSchedulerSuspended == pdFALSE`):
{
将任务从Delayed任务队列中删除(`uxListRemove( &( pxTCB->xStateListItem ) )`)。
将任务加入到Ready任务队列(`prvAddTaskToReadyList( pxTCB )`)。
}
否则
{
将任务将入到PendingReady队列(`vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) )`)。
}
该任务优先级较高(`pxTCB->uxPriority > pxCurrentTCB->uxPriority`),设置更高优先级任务唤醒标志(`*pxHigherPriorityTaskwoken = pdTRUE`),否则设置调度挂起标志(`xYieldPending = pdTRUE`)。
}
恢复临界区(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)。
返回`xReturn`。
}
3.2. 等待任务通知
等待通知的函数原型:需要设置configUSE_TASK_NOTIFICATIONS = 1
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
如果此时没有notification等待被捕获,调用者会进入Blocked状态;该函数的参数需要特别关注一下,因为使用不当会造成意外的结果。首先,因为一个任务可以有多个notification被挂起,因此我们无法预测当前任务的notification value是多少,这时需要一些特殊的前处理和后处理来帮我们确保这个事情。
- 前处理:在设置
pulNotificationValue
前,将当前的notification value与~ulBitsToClearOnEntry
相与,也即清除notification value中ulBitsToClearOnEntry
所指定的位。 - 后处理:在设置
pulNotificationValue
后,将当前的notification value与~ulBitsToClearOnExit
相与,也即清除notification value中ulBitsToClearOnExit
所指定的位。有了后处理,那就可以用notification实现类似于清理外部中断位一样的功能。
有的时候我们只是想看一下是不是有notification在挂起,并不希望无限等待事件的发生,此时,则可以通过xTicksToWait
参数来申明调用者希望等待的最长时间。但由于xTicksToWait
是以tick作为时间度量单位的,通常它还与pdMS_TO_TICKS( value_in_ms )
一起使用,也就是将毫秒值转换成tick值。
如果在指定的时间内,收到了notification,函数则会返回pdPASS
,否则返回pdFAIL
。
该函数实现的活动图如下:
3.3. 用通知实现信号量功能
使用notification可以实现类似于信号量一样的功能,FreeRTOS还提供了这样的一个接口(宏):
#define xTaskNotifyGive( xTaskToNofity ) xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL)
可以看到,它只是对xTaskGenericNotify()
的一个简单封装,用于实现信号量的归还操作。
Give函数还有中断上下文的版本:
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken ) PRIVILEGED_FUNCTION;
其实现如下:
{
参数校验:Assert `xTaskToNotify != NULL`。
中断优先级条件检查(`portASSERT_IF_INTERRUPT_PRIORITY_INVALID()`)。
获取任务TCB(`pxTCB = ( TCB_t *) xTaskToNotify`)。
进入临界区(`uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR()`)。
缓存当前的任务通知状态(`ucOriginalNotifyState = pxTCB->ucNotifyState`)。
设置任务通知状态为已接收(`pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED`)。
增加任务通知的值(`pxTCB->ulNotifiedValue++`)。
如果原本任务通知状态为等待(`ucOriginalNotifyState = taskWAITING_NOTIFICATION`):
{
确保任务不在事件队列中(`configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL`)。
如果调度器未挂起(`uxSchedulerSuspended == pdFALSE`):
{
将任务从Delayed任务队列中删除(`uxListRemove( &( pxTCB->xStateListItem ) )`)。
将任务加入到Ready任务队列(`prvAddTaskToReadyList( pxTCB )`)。
}
否则,即任务调度器处于挂起状态,则讲任务加入到PendingReady任务队列(`vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) )`)。
如果被唤醒的任务优先级较高(`pxTCB->uxPriority > pxCurrentTCB->uxPriority`):
{
如果希望获取更高优先级标志(`pxHigherPriorityTaskWoken != NULL`),设置标志(`*pxHigherPriorityTaskWoken = pdTRUE`)。
否则,直接设置调度挂起标志(`xYieldPending = pdTRUE`)。
}
}
恢复临界区(`portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus )`)。
}
而与之对应的获取信号量的函数是:
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait ) PRIVILEGED_FUNCTION;
该函数的后处理操作与xTaskNotifyWait()
不太一样。这里取决于使用者希望将notification当做二值信号量或者是计数信号量使用。当xClearCountOnExit
为pdTRUE
时,则作为二值信号量使用,因为后处理会将notification value清零;反之,作为计数信号量使用,后处理会将notification value减一。
返回值为notification value的值(执行后处理之前的值)。
下面看看其具体的实现:
{
进入临界区(`taskENTER_CRITICAL()`)。
如果当前的任务通知为0(`pxCurrentTCB->ulNotifiedValue == 0`):
{
设置任务通知的状态为等待(`pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION`)。
如果调用者愿意等待(`xTicksToWait > 0`):
{
将任务将入到Delayed任务队列(`prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE )`)。
发起一次调度(`portYIELD_WITHIN_API()`)。
}
}
退出临界区(`taskEXIT_CRITICAL()`)。
进入临界区(`taskENTER_CRITICAL()`)。
暂存当前任务通知用于返回(`ulReturn = pxCurrentTCB->ulNotifiedValue`)。
如果当前任务通知值不为0(`ulReturn != 0`):
{
如果设置了退出清除标志(`xClearCountOnExit != pdFALSE`),则清除任务通知(`pxCurrentTCB->ulNotifiedValue = 0`);
否则,仅将任务通知减一(`pxCurrentTCB->ulNotifiedValue = ulReturn - 1`)。
重置任务通知状态(`pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION`)。
}
退出临界区(`taskEXIT_CRITICAL()`)。
返回通知信息`ulReturn`。
}
3.4. 复位通知信息
清理notification的接收状态的函数原型:
BaseType_t xTaskNotifyStateClear( TaskHandle_t xTask);
需要说明一下该函数的返回值:如果调用该函数时,任务已经接收到了notification,该函数会将任务的状态设置为默认状态,且该函数则返回pdTRUE
;否则函数将直接返回pdFALSE
。
其具体实现如下:
{
获取任务TCB(`pxTCB = prvGetTCBFromHandle( xTask )`)。
进入临界区(`taskENTER_CRITICAL()`)。
如果任务通知状态为已接收(`pxTCB->ucNotifyState == taskNOTIFICATION_RECEIVED`):
{
重置任务通知状态(`pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION`)。
设置返回值(`xReturn = pdPASS`)。
}
否则,设置返回值(`xReturn = pdFAIL`)。
退出临界区(`taskEXIT_CRITICAL()`)。
返回`xReturn`。
}