当前位置: 首页 > 文档资料 > freeRTOS 使用教程 >

1.3.2.6 任务通知

优质
小牛编辑
127浏览
2023-12-01

任务通知是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()来获取自己的通知值。