在之前的文章中已经讲解了很多种用于任务件通信的机制,包括队列、事件组和各种不同类型的信号量。使用这些机制都需要创建一个通信对象。
事件和数据不会直接发送到接收任务或接收ISR,而是发送到通信对象(也就是发送到队列、事件组、信号量)。同样,任务和ISR从通信对象接收事件和数据,而不是直接从发送事件或数据的任务或ISR接收事件和数据。
任务通知允许任务与其他任务交互,并与ISR同步,而不需要单独的通信对象。通过使用任务通知,任务或ISR可以直接向接收任务发送事件。
原文链接:FreeRTOS全解析-11.任务通知(Task Notifications)
目录
1.任务通知的优缺点
1.1优点
1.2缺点
2.使用任务通知
2.1简易版
2.2复杂版
1.任务通知的优缺点
1.1优点
1.更快,使用任务通知将事件或数据发送到任务要比使用队列、信号量或事件组更快。
2.更节约内存,使用任务通知将事件或数据发送到任务所需的RAM比使用队列、信号量或事件组执行等效操作所需的RAM少得多。因为通信对象(队列、信号量或事件组),要先创建,才能使用,而启用任务通知功能每个任务只有8字节的RAM的固定开销。
1.2缺点
部分情况无法使用
(1)向ISR发送事件或数据
通信对象可用于将事件和数据从ISR发送到任务,并从任务发送到ISR。任务通知可用于将事件和数据从ISR发送到任务,但是它们不能用于将事件或数据从任务发送到ISR。
(2)有多个接收任务
通信对象可以被任何知道其句柄的任务或ISR访问。任意数量的任务和ISR都可以接收通信对象。任务通知直接发送到接收任务,因此只能由接收通知的任务处理。
(3)缓冲多个数据项
队列是一种通信对象,一次可以保存多个数据项。已发送到队列但尚未从队列接收的数据将在队列对象中进行缓冲。任务通知通过更新接收任务的通知值来向任务发送数据。任务的通知值一次只能保存一个值。
(3)发送到多个任务
事件组是一个通信对象,可用于一次向多个任务发送事件。任务通知直接发送给接收任务,因此只能由接收任务处理。
(3)在阻塞态下等待发送完成
如果一个通信对象暂时处于不能再写入数据或事件的状态(例如,当队列已满时,不能再向队列发送数据),那么尝试写入该对象的任务可以选择进入阻塞态,等待机会去完成写入操作。如果任务试图将任务通知发送给已经有通知挂起的任务,则发送任务不能在阻塞态下等待接收任务重置其通知状态。
2.使用任务通知
要开启任务通知功能,首先需要在FreeRTOSConfig.h中将configUSE_TASK_NOTIFICATIONS设置为1。
当configUSE_TASK_NOTIFICATIONS设置为1时,每个任务都有一个“通知状态”,可以是“Pending”或“Not-Pending”,以及一个“通知值”,这是一个32位无符号整数。当任务收到通知时,其通知状态设置为“Pending”。当任务读取其通知值时,其通知状态设置为“Not-Pending”。任务可以在Blocked状态下等待其通知状态变为“Pending”。
2.1简易版
xTaskNotifyGive() API函数
xTaskNotifyGive()直接向任务发送通知,并增加接收任务的通知值。调用xTaskNotifyGive()将把接收任务的通知状态设置为pending(如果它还没有挂起的话)。
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
ulTaskNotifyTake() API函数
ulTaskNotifyTake()允许任务在Blocked状态下等待其通知值大于零,并在返回之前减少(减去1)或清除任务的通知值。
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
例子1:延迟中断处理
const TickType_t xInterruptFrequency = pdMS_TO_TICKS( 500UL );
static void vHandlerTask( void *pvParameters )
{
const TickType_t xMaxExpectedBlockTime = xInterruptFrequency + pdMS_TO_TICKS( 10 );
uint32_t ulEventsToProcess;
for( ;; )
{
ulEventsToProcess = ulTaskNotifyTake( pdTRUE, xMaxExpectedBlockTime );
if( ulEventsToProcess != 0 ) {
while( ulEventsToProcess > 0 ) {
vPrintString( "Handler task - Processing event.\r\n" );
ulEventsToProcess--;
}
} else {
//如果运行到这,表示超时时间内,中断没有发生
}
}
}
static uint32_t ulExampleInterruptHandler( void )
{
BaseType_t xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR( xHandlerTask,&xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
#define mainINTERRUPT_NUMBER 3
static void vPeriodicTask( void *pvParameters )
{
const TickType_t xDelay500ms = pdMS_TO_TICKS( 500UL );
vTaskDelay( xDelay500ms );
vPrintString( "Periodic task - About to generate an interrupt.\r\n" );
vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER );
vPrintString( "Periodic task - Interrupt generated.\r\n\r\n\r\n" );
}
如上是一个利用任务通知来延迟中断处理的例子vPeriodicTask函数模拟周期性中断,在中断前会打印Periodic task - About to generate an interrupt.中断后会打印Periodic task - Interrupt generated.
ulExampleInterruptHandler函数为中断处理函数,但在这个函数中并不真正进行处理,只是采用vTaskNotifyGiveFromISR函数,给出任务通知,vHandlerTask函数,才是真正的处理函数,vHandlerTask函数作为一个任务来运行,调用ulTaskNotifyTake函数进入阻塞态,以等待任务通知的到来。
ulTaskNotifyTake()中xClearCountOnExit参数被设置为pdTRUE,这将导致在ulTaskNotifyTake()返回之前,接收任务的通知值被清除为零。也就是说必须处理所有的任务通知。
例子2:UART发送
外围设备上的一些操作需要较长的时间才能完成。比如高精度ADC转换,以及在UART上传输大数据包。可以通过轮询(重复读取)外设的状态寄存器,以确定操作何时完成。然而,轮询非常浪费CPU,因为它占用CPU,而没有执行任何实质性操作。
当然我们可以利用中断,操作处理任务take信号量进入阻塞,等待发生中断,在其对应中断处理函数中give信号量,来替代轮询。
比如uart发送函数,利用二进制信号量
BaseType_t xUART_Send( xUART *pxUARTInstance, uint8_t *pucDataSource, size_t uxLength )
{
BaseType_t xReturn;
/* 确保信号量为0 */
xSemaphoreTake( pxUARTInstance->xTxSemaphore, 0 );
/* 启动传输*/
UART_low_level_send( pxUARTInstance, pucDataSource, uxLength );
/* 等待传输完毕 */
xReturn = xSemaphoreTake( pxUARTInstance->xTxSemaphore, pxUARTInstance->xTxTimeout );
return xReturn;
}
/*发送完毕中断服务函数*/
void xUART_TransmitEndISR( xUART *pxUARTInstance )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 清除中断 */
UART_low_level_interrupt_clear( pxUARTInstance );
/* give信号量 */
xSemaphoreGiveFromISR( pxUARTInstance->xTxSemaphore, &xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
但是这种方式需要创建信号量才能使用,创建信号量就涉及占用RAM。可以用任务通知代替:
BaseType_t xUART_Send( xUART *pxUARTInstance, uint8_t *pucDataSource, size_t uxLength )
{
BaseType_t xReturn;
/* 保存调用该函数的任务句柄. */
pxUARTInstance->xTaskToNotify = xTaskGetCurrentTaskHandle();
/* 确保任务通知为0. */
ulTaskNotifyTake( pdTRUE, 0 );
/*启动传输. */
UART_low_level_send( pxUARTInstance, pucDataSource, uxLength );
/* 阻塞等待发送完成.*/
xReturn = ( BaseType_t ) ulTaskNotifyTake( pdTRUE, pxUARTInstance->xTxTimeout );
return xReturn;
}
/*-----------------------------------------------------------*/
/* 发送完成中断服务函数. */
void xUART_TransmitEndISR( xUART *pxUARTInstance )
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 清除中断. */
UART_low_level_interrupt_clear( pxUARTInstance );
/* 发送任务通知. */
vTaskNotifyGiveFromISR( pxUARTInstance->xTaskToNotify, &xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
2.2复杂版
xTaskNotify()API函数
xTaskNotify()比xTaskNotifyGive()更灵活、更强大,由于这种额外的灵活性和强大功能,使用起来也稍微复杂一些。
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 );
eAction是一个枚举型变量
eAction的值 | 作用 |
eNoAction | 接收任务的通知状态设置为挂起,而不更新其通知值。未使用xTaskNotify() ulValue参数。替代二进制信号量 |
eSetBits | 接收任务的通知值与xTaskNotify() ulValue参数中传递的值按位或匹配。例如,如果ulValue设置为0x01,则接收任务的通知值将设置0位。另一个例子,如果ulValue是0x06(二进制0110),那么接收任务的通知值将设置第1位和第2位。替代事件组 |
eIncrement | 接收任务的通知值递增。未使用xTaskNotify() ulValue参数。替代计数信号量 |
eSetValueWithoutOverwrite | 如果在调用xTaskNotify()之前接收任务有一个挂起的通知,则不采取任何操作,xTaskNotify()将返回pdFAIL。如果在调用xTaskNotify()之前接收任务没有挂起的通知,则将接收任务的通知值设置为xTaskNotify() ulValue参数中传递的值。 |
eSetValueWithOverwrite | 接收任务的通知值被设置为xTaskNotify() ulValue参数中传递的值,而不管在调用xTaskNotify()之前接收任务是否有挂起的通知。 |
xTaskNotifyWait()API函数
xTaskNotifyWait()是ulTaskNotifyTake()的一个更强大的版本。它允许任务使用可选的超时等待,以便调用任务的通知状态变为挂起(如果它尚未挂起)。xTaskNotifyWait()提供了在进入函数和退出函数时在调用任务的通知值中清除比特的选项。
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );
参数 | 作用 |
ulBitsToClearOnEntry | 如果调用任务在调用xTaskNotifyWait()之前没有通知挂起,那么在进入该函数时,在任务的通知值中清除在ulBitsToClearOnEntry中设置的任何位。例如,如果ulBitsToClearOnEntry是0x01,那么任务通知值的第0位将被清除。如果将ulBitsToClearOnEntry设置为0xffff (ULONG_MAX)将清除任务通知值中的所有位。 |
ulBitsToClearOnExit | 如果调用任务因为收到通知而退出xTaskNotifyWait(),或者因为在调用xTaskNotifyWait()时已经有通知挂起,那么在ulBitsToClearOnExit中设置的任何位将在任务退出xTaskNotifyWait()函数之前在任务的通知值中被清除。当任务的通知值保存在*pulNotificationValue(参见下面的pulNotificationValue的描述)中后,这些位将被清除。例如,如果ulBitsToClearOnExit是0x03,那么任务的通知值的第0位和第1位将在此之前被清除将ulBitsToClearOnExit设置为0xfffffff (ULONG_MAX)将清除任务通知值中的所有位。 |
pulNotificationValue | 用于传递出任务的通知值(被清除前的值)。pulNotificationValue是一个可选参数,如果不需要,可以设置为NULL。 |
xTicksToWait | 调用任务保持阻塞态以等待其通知状态变为挂起的最长时间。 |
返回值 | pdTRUE成功接到通知,pdFALSE未接收到通知 |
例子:ADC
void vADCTask( void *pvParameters )
{
uint32_t ulADCValue;
BaseType_t xResult;
const TickType_t xADCConversionFrequency = pdMS_TO_TICKS( 50 );
for( ;; ) {
xResult = xTaskNotifyWait(0,0,&ulADCValue,xADCConversionFrequency * 2 );
if( xResult == pdPASS ) {
ProcessADCResult( ulADCValue );
} else {
}
}
}
void ADC_ConversionEndISR( xADC *pxADCInstance )
{
uint32_t ulConversionResult;
BaseType_t xHigherPriorityTaskWoken = pdFALSE, xResult;
ulConversionResult = ADC_low_level_read( pxADCInstance );
xResult = xTaskNotifyFromISR( xADCTaskToNotify,ulConversionResult, eSetValueWithoutOverwrite,&xHigherPriorityTaskWoken );
configASSERT( xResult == pdPASS );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
ADC_ConversionEndISR函数是ADC转换结束的ISR,vADCTask函数是等待ADC值的任务。
ADC_low_level_read是读取ADC值函数,读取后调用xTaskNotifyFromISR函数发送任务通知,ulValue参数为ulConversionResult,即ADC转换的结果,eAction参数为eSetValueWithoutOverwrite,如果在调用xTaskNotify()之前接收任务有一个挂起的通知,则不采取任何操作,xTaskNotify()将返回pdFAIL。如果在调用xTaskNotify()之前接收任务没有挂起的通知,则将接收任务的通知值设置为xTaskNotify() ulValue参数中传递的值。
可见xTaskNotify()API函数和xTaskNotifyWait()API函数组合实现了传值的功能,这是简易版做不到的,不过大多数情况下,使用简易版就够了
往期精彩:
STM32F4+FreeRTOS+LVGL实现快速开发(缝合怪)
嵌入式C语言几个重点(const、static、voliatile、位运算)
嵌入式Linux驱动学习-5.驱动的分层分离思想
从Linux内核中学习高级C语言宏技巧
嵌入式Linux学习经典书籍-学完你就是大佬