1.3.2.6 任务通知
任务通知是8.2.0
版本新增加的功能。每个任务都有一个32bit的"通知值"(notification value
)。RTOS的任务通知是一个事件,可以直接发送到一个任务,并且将该任务从阻塞态恢复。是否更新接收任务的任务通知值是可选的。
任务通知可以通过以下几种方式更新接收任务的通知值:
- 直接设置而不用覆写接收任务的通知值
- 覆写接收任务的通知值
- 设置接收任务通知值的一个或多个bit位
- 增加接收任务的通知值
这种灵活性可以使得任务间的通信更加灵活而不需要创建队列、信号量、互斥量、计数信号量或者事件组。使用任务通知恢复一个阻塞的任务比使用二值信号量要快45%而其占用的内存也更小。
通知使用API函数xTaskNotify()
或xTaskNotifyGive()
来发送,这个通知将一直挂起直到接收任务使用API函数xTaskNotifyWait()
或ulTaskNotifyTake()
来接收通知。如果接收任务在通知到来时已经被阻塞,则会从阻塞态恢复,同时通知被清除。
任务通知默认时可用的,如果出于节省空间的考虑(每个任务可以节省4个字节),可以设置configUSE_TASK_NOTIFICATIONS
为0来禁用。
优点和限制
在实现同样目标的情况下,任务通知占用的空间更小,速度更快。同样的,这些好处是有条件的:
- 任务通知只能使用在只用一个任务接收事件的场合。
- 只能在用来代替队列的情况下。当接收任务在等待通知的时候进入阻塞,发送任务如果在通知不能立即发送完成的时候也不能进入阻塞态。
替代二值信号量(binary semaphore)
任务通知比二值信号量快45%并且内存占用更小,这个文档将介绍这是如何实现的。
二值信号量是最大数量为1的信号量,正如其"二值"的意义,任务只有在二值信号量有效的时候才能获取,也就是二值信号量的非空。
当使用任务通知来代替二值信号量的时候,接收任务的通知值被用来代替二值信号量的计数值,此时ulTaskNotifyTake()
API函数可以替代xSemaphoreTake()
。ulTaskNotifyTake()
的xClearOnExit
参数被设置成pdTRUE
,在每次通知被获取后。计数值返回0,以此来模拟二值信号量。
同理,xTaskNotifyGive()
和vTaskNotifyGiveFromISR()
用来代替xSemaphoreGive()
和xSemaphoreGiveFromISR()
。
下面看个栗子:
/* 这个例子展示了一个使用外设进行数据传输的任务。RTOS中的某个任务
负责将数据通过DMA发送出去,在数据发送完成之前,任务会进入阻塞态,直
到DMA中断发送完成后使用任务通知通知任务,从阻塞态中恢复。*/
/* 保存任务的通知句柄,用来在传输完成后通知任务*/
static TaskHandle_t xTaskToNotify = NULL;
/* T外设的数据传输函数. */
void StartTransmission( uint8_t *pcData, size_t xDataLength )
{/* 在没有传输任务执行的时候这里的任务句柄应该是NULL,必要时可以使用互斥量实现 */configASSERT( xTaskToNotify == NULL );
/* 保存任务句柄*/xTaskToNotify = xTaskGetCurrentTaskHandle();
/* 开始发送,发送完成后,DMA会提起中断. */vStartTransmit( pcData, xDatalength );
}
/*-----------------------------------------------------------*/
/* DMA中断. */
void vTransmitEndISR( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 传输任务句柄应该不是NULL,因为是任务发起的*/configASSERT( xTaskToNotify != NULL );
/* 通知任务发送完成 */vTaskNotifyGiveFromISR( xTaskToNotify, &xHigherPriorityTaskWoken );
/* 复位任务句柄 */xTaskToNotify = NULL;
/* 如果xHigherPriorityTaskWoken被设置成pdTRUE意味着需要进行上下文切换,则调用下面的宏来完成上下文切换,在中断退出后能及时切换到高优先级的任务中*/portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/*-----------------------------------------------------------*/
/* 任务发起传输,然后进入阻塞态,此时不占用CPU时间*/
void vAFunctionCalledFromATask( uint8_t ucDataToTransmit, size_t xDataLength )
{
uint32_t ulNotificationValue;
const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 200 );
/* 开始传输 */StartTransmission( ucDataToTransmit, xDataLength );
/* 等待任务传输完成发出通知,进入阻塞态。注意这里第一个参数被设置为 pdTRUE ,意味着通知在被获取之后会被清0,使得与二值信号量有相同的特性。*/ulNotificationValue = ulTaskNotifyTake( pdTRUE,xMaxBlockTime );
if( ulNotificationValue == 1 ){
/* 传输完成. */}else{
/* 等待通知超时. */}
}
替代计数信号量(counting semaphore)
计数信号量当某个信号触发的时候其值可以从0开始累加到一个最大值,当二值信号量的值大于0时,对于想要获取它的任务,它是有效的。
与替代二值信号量类似,当使用任务通知来代替计数信号量的时候,接收任务的通知值被用来代替计数信号量的计数值,此时ulTaskNotifyTake()
API函数可以替代xSemaphoreTake()
。ulTaskNotifyTake()
的xClearOnExit
参数被设置成pdFALSE
,在每次通知被获取后。计数值会递减而不是清0,以此来模拟计数信号量。
同理,xTaskNotifyGive()
和vTaskNotifyGiveFromISR()
用来代替xSemaphoreGive()
和xSemaphoreGiveFromISR()
。
下面要看两个栗子...
栗子1:
/*中断不直接处理,而是推迟到高优先级的任务中处理,任务通知此处负责
使任务从阻塞态中恢复和增加任务值。 */
void vANInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
/* 清中断 */prvClearInterruptSource();
/* xHigherPriorityTaskWoken 必须初始化为 pdFALSE。如果在调用 vTaskNotifyGiveFromISR()恢复阻塞的处理任务后,并且这个处理任务比当前正在运行的任务优先级高,xHigherPriorityTaskWoken会被自动设置为pdTRUE。*/xHigherPriorityTaskWoken = pdFALSE;
/* 阻塞的处理任务在恢复后会做一些必要的中断处理, xHandlingTask在任务创建的时候确定,vTaskNotifyGiveFromISR会增加处理任务的任务通知值*/vTaskNotifyGiveFromISR( xHandlingTask, &xHigherPriorityTaskWoken );
/* 如果一个高优先级的任务就绪,则强制执行上下文切换 */portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/*-----------------------------------------------------------*/
/* 任务阻塞等待任务通知到来*/
void vHandlingTask( void *pvParameters )
{
BaseType_t xEvent;
const TickType_t xBlockTime = pdMS_TO_TICS( 500 );
uint32_t ulNotifiedValue;
for( ;; ){
/* 阻塞等待通知到来,第一个参数设置成pdFALSE,这样在获得通知
之后,通知值会减少,而不是清0*/
ulNotifiedValue = ulTaskNotifyTake( pdFALSE,xBlockTime );
if( ulNotifiedValue > 0 )
{/* 进行中断中未作的一些必要处理 */xEvent = xQueryPeripheral();
if( xEvent != NO_MORE_EVENTS ){ vProcessPeripheralEvent( xEvent );}
}
else
{/* 等待通知超时 */vCheckForErrorConditions();
}}
}
栗子2:
这个例子展示了一个更实用的应用,这种类型的应用可以应用在如串口接收中, 通知值就是接收到的数据个数,任务在被任务通知唤醒后,将通知值代表的所 有事件一次性处理完成。中断部分和例子1相同,下面不重复了。
void vHandlingTask( void *pvParameters )
{
BaseType_t xEvent;
const TickType_t xBlockTime = pdMS_TO_TICS( 500 );
uint32_t ulNotifiedValue;
for( ;; ){
/* 与例子1不同的是,这里将第一个参数设置成pdTRUE,因此通知值在
获取之后会被清0 */
ulNotifiedValue = ulTaskNotifyTake( pdTRUE,xBlockTime );
if( ulNotifiedValue == 0 )
{/* 等待任务超时*/vCheckForErrorConditions();
}
else
{/* 重复处理所有的中断事件 */while( ulNotifiedValue > 0 ){ xEvent = xQueryPeripheral();if( xEvent != NO_MORE_EVENTS ) {vProcessPeripheralEvent( xEvent );ulNotifiedValue--; } else {break; }}
}}
}
替代事件组(event group
)
事件组是一个二进制标志集合,每个位用户都可以用来代表某个含义。RTOS任务在等待一个或者多个标志有效的时候会进入阻塞态,此时不占用CPU时间。
当任务通知用来代替时间组的时候,任务值被用来代替事件组的值,通知值的每一位被用来代表某个标志。xTaskNotifyWait()
被用来代替xEventGroupWaitBits()
。
同理 xTaskNotify()
和 xTaskNotifyFromISR()
(eAction
被替换成eSetBits
)用来代替xEventGroupSetBits()
和 xEventGroupSetBitsFromISR()
。
xTaskNotifyFromISR()
相比xEventGroupSetBitsFromISR()
有着显著的性能优势,因为前者的所有操作都在中断中完成,而后者的部分操作需要在内核的守护进程(daemon task
)中完成。
与实用时间组不同的是任务无法指定某个标志将其从阻塞态恢复,任何bit变成有效都会将任务从阻塞态恢复,因此任务需要自己去确定是哪一个标志将其恢复。
老规矩,看栗子:
/* 这个栗子演示了实用同一个任务处理两个中断,一个接收中断一个发送中断,
很多外设同时使用着两个中断,外设的中断寄存器可以简单的与接收任务的通知
按位进行或运算
每一位代表的中断源定义 */
#define TX_BIT 0x01
#define RX_BIT 0x02
/* 这个句柄的任务会用来接收任务通知,句柄在任务创建时确立 */
static TaskHandle_t xHandlingTask;
/*-----------------------------------------------------------*/
/* 发送中断 */
void vTxISR( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 清中断 */
prvClearInterrupt();
/* 通过设置TX_BIT,通知任务发送完成 */
xTaskNotifyFromISR( xHandlingTask, TX_BIT, eSetBits, &xHigherPriorityTaskWoken );
/* 高优先级任务就绪,上下文切换 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/*-----------------------------------------------------------*/
/* 接收中断 */
void vRxISR( void )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 清中断. */
prvClearInterrupt();
/* 置位RX_BIT,通知接收任务接收中断发送 */
xTaskNotifyFromISR( xHandlingTask, RX_BIT, eSetBits, &xHigherPriorityTaskWoken );
/* 高优先级任务就绪,上下文切换 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/*-----------------------------------------------------------*/
/* 任务实现 */
static void prvHandlingTask( void *pvParameter )
{
const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 500 );
BaseType_t xResult;
for( ;; )
{ /* 等待中断通知. */ xResult = xTaskNotifyWait( pdFALSE, /* 输入不清除位. */ ULONG_MAX,/* 退出时清除位 */ &ulNotifiedValue, /* 通知值. */ xMaxBlockTime );
if( xResult == pdPASS ) {
/* 收到通知,检查置位情况. */
if( ( ulNotifiedValue & TX_BIT ) != 0 )
{/* 发送中断置位. */prvProcessTx();
}
if( ( ulNotifiedValue & RX_BIT ) != 0 )
{/* 接收中断置位 */prvProcessRx();
} } else {
/* 等待超时. */
prvCheckForErrors(); }
}
}
替代邮箱(mailbox)
RTOS任务通知只能用来发送数据到一个任务中,相比用队列实现,有一些限制:
- 只能发送32-bit数据
- 保存的是接收任务的值,因此同一个时刻只能存在一个接收任务
因此,使用"轻量邮箱"这个短语代替"轻量队列"。任务的通知值就是邮箱值。
数据通过使用xTaskNotify()
和xTaskNotifyFromISR()
发送至任务。其中,eAction
参数可以设置成eSetValueWithOverwrite
或者eSetValueWithoutOverwrite
,前者会直接更新通知值及时任务已经有一个通知挂起,后者会在任务没有通知挂起才会更新任务值-在接收任务处理之前更新通知值会覆写先前的值。
任务可以通过调用xTaskNotifyWait()
来获取自己的通知值。