任务通知是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()

下面看个栗子:

  1. /* 这个例子展示了一个使用外设进行数据传输的任务。RTOS中的某个任务
  2. 负责将数据通过DMA发送出去,在数据发送完成之前,任务会进入阻塞态,直
  3. 到DMA中断发送完成后使用任务通知通知任务,从阻塞态中恢复。*/
  4. /* 保存任务的通知句柄,用来在传输完成后通知任务*/
  5. static TaskHandle_t xTaskToNotify = NULL;
  6. /* T外设的数据传输函数. */
  7. void StartTransmission( uint8_t *pcData, size_t xDataLength )
  8. {
  9. /* 在没有传输任务执行的时候这里的任务句柄应该是NULL,必要时
  10. 可以使用互斥量实现 */
  11. configASSERT( xTaskToNotify == NULL );
  12. /* 保存任务句柄*/
  13. xTaskToNotify = xTaskGetCurrentTaskHandle();
  14. /* 开始发送,发送完成后,DMA会提起中断. */
  15. vStartTransmit( pcData, xDatalength );
  16. }
  17. /*-----------------------------------------------------------*/
  18. /* DMA中断. */
  19. void vTransmitEndISR( void )
  20. {
  21. BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  22. /* 传输任务句柄应该不是NULL,因为是任务发起的*/
  23. configASSERT( xTaskToNotify != NULL );
  24. /* 通知任务发送完成 */
  25. vTaskNotifyGiveFromISR( xTaskToNotify, &xHigherPriorityTaskWoken );
  26. /* 复位任务句柄 */
  27. xTaskToNotify = NULL;
  28. /* 如果xHigherPriorityTaskWoken被设置成pdTRUE意味着需要进行上下文
  29. 切换,则调用下面的宏来完成上下文切换,在中断退出后能及时切换到高优先级的任务中*/
  30. portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
  31. }
  32. /*-----------------------------------------------------------*/
  33. /* 任务发起传输,然后进入阻塞态,此时不占用CPU时间*/
  34. void vAFunctionCalledFromATask( uint8_t ucDataToTransmit, size_t xDataLength )
  35. {
  36. uint32_t ulNotificationValue;
  37. const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 200 );
  38. /* 开始传输 */
  39. StartTransmission( ucDataToTransmit, xDataLength );
  40. /* 等待任务传输完成发出通知,进入阻塞态。注意这里第一个参数被
  41. 设置为 pdTRUE ,意味着通知在被获取之后会被清0,使得与二值信号量
  42. 有相同的特性。*/
  43. ulNotificationValue = ulTaskNotifyTake( pdTRUE,
  44. xMaxBlockTime );
  45. if( ulNotificationValue == 1 )
  46. {
  47. /* 传输完成. */
  48. }
  49. else
  50. {
  51. /* 等待通知超时. */
  52. }
  53. }

替代计数信号量(counting semaphore)

计数信号量当某个信号触发的时候其值可以从0开始累加到一个最大值,当二值信号量的值大于0时,对于想要获取它的任务,它是有效的。

与替代二值信号量类似,当使用任务通知来代替计数信号量的时候,接收任务的通知值被用来代替计数信号量的计数值,此时ulTaskNotifyTake()API函数可以替代xSemaphoreTake()ulTaskNotifyTake()xClearOnExit参数被设置成pdFALSE,在每次通知被获取后。计数值会递减而不是清0,以此来模拟计数信号量。

同理,xTaskNotifyGive()vTaskNotifyGiveFromISR()用来代替xSemaphoreGive()xSemaphoreGiveFromISR()

下面要看两个栗子…

栗子1:

  1. /*中断不直接处理,而是推迟到高优先级的任务中处理,任务通知此处负责
  2. 使任务从阻塞态中恢复和增加任务值。 */
  3. void vANInterruptHandler( void )
  4. {
  5. BaseType_t xHigherPriorityTaskWoken;
  6. /* 清中断 */
  7. prvClearInterruptSource();
  8. /* xHigherPriorityTaskWoken 必须初始化为 pdFALSE。
  9. 如果在调用 vTaskNotifyGiveFromISR()恢复阻塞的处理任务后,
  10. 并且这个处理任务比当前正在运行的任务优先级高,xHigherPriorityTaskWoken
  11. 会被自动设置为pdTRUE。*/
  12. xHigherPriorityTaskWoken = pdFALSE;
  13. /* 阻塞的处理任务在恢复后会做一些必要的中断处理, xHandlingTask在
  14. 任务创建的时候确定,vTaskNotifyGiveFromISR会增加处理任务的任务通知值*/
  15. vTaskNotifyGiveFromISR( xHandlingTask, &xHigherPriorityTaskWoken );
  16. /* 如果一个高优先级的任务就绪,则强制执行上下文切换 */
  17. portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
  18. }
  19. /*-----------------------------------------------------------*/
  20. /* 任务阻塞等待任务通知到来*/
  21. void vHandlingTask( void *pvParameters )
  22. {
  23. BaseType_t xEvent;
  24. const TickType_t xBlockTime = pdMS_TO_TICS( 500 );
  25. uint32_t ulNotifiedValue;
  26. for( ;; )
  27. {
  28. /* 阻塞等待通知到来,第一个参数设置成pdFALSE,这样在获得通知
  29. 之后,通知值会减少,而不是清0*/
  30. ulNotifiedValue = ulTaskNotifyTake( pdFALSE,
  31. xBlockTime );
  32. if( ulNotifiedValue > 0 )
  33. {
  34. /* 进行中断中未作的一些必要处理 */
  35. xEvent = xQueryPeripheral();
  36. if( xEvent != NO_MORE_EVENTS )
  37. {
  38. vProcessPeripheralEvent( xEvent );
  39. }
  40. }
  41. else
  42. {
  43. /* 等待通知超时 */
  44. vCheckForErrorConditions();
  45. }
  46. }
  47. }

栗子2:

这个例子展示了一个更实用的应用,这种类型的应用可以应用在如串口接收中,
通知值就是接收到的数据个数,任务在被任务通知唤醒后,将通知值代表的所
有事件一次性处理完成。中断部分和例子1相同,下面不重复了。

  1. void vHandlingTask( void *pvParameters )
  2. {
  3. BaseType_t xEvent;
  4. const TickType_t xBlockTime = pdMS_TO_TICS( 500 );
  5. uint32_t ulNotifiedValue;
  6. for( ;; )
  7. {
  8. /* 与例子1不同的是,这里将第一个参数设置成pdTRUE,因此通知值在
  9. 获取之后会被清0 */
  10. ulNotifiedValue = ulTaskNotifyTake( pdTRUE,
  11. xBlockTime );
  12. if( ulNotifiedValue == 0 )
  13. {
  14. /* 等待任务超时*/
  15. vCheckForErrorConditions();
  16. }
  17. else
  18. {
  19. /* 重复处理所有的中断事件 */
  20. while( ulNotifiedValue > 0 )
  21. {
  22. xEvent = xQueryPeripheral();
  23. if( xEvent != NO_MORE_EVENTS )
  24. {
  25. vProcessPeripheralEvent( xEvent );
  26. ulNotifiedValue--;
  27. }
  28. else
  29. {
  30. break;
  31. }
  32. }
  33. }
  34. }
  35. }

替代事件组(event group)

事件组是一个二进制标志集合,每个位用户都可以用来代表某个含义。RTOS任务在等待一个或者多个标志有效的时候会进入阻塞态,此时不占用CPU时间。

当任务通知用来代替时间组的时候,任务值被用来代替事件组的值,通知值的每一位被用来代表某个标志。xTaskNotifyWait()被用来代替xEventGroupWaitBits()

同理 xTaskNotify()xTaskNotifyFromISR()(eAction被替换成eSetBits)用来代替xEventGroupSetBits()xEventGroupSetBitsFromISR()

xTaskNotifyFromISR()相比xEventGroupSetBitsFromISR()有着显著的性能优势,因为前者的所有操作都在中断中完成,而后者的部分操作需要在内核的守护进程(daemon task)中完成。

与实用时间组不同的是任务无法指定某个标志将其从阻塞态恢复,任何bit变成有效都会将任务从阻塞态恢复,因此任务需要自己去确定是哪一个标志将其恢复。

老规矩,看栗子:

  1. /* 这个栗子演示了实用同一个任务处理两个中断,一个接收中断一个发送中断,
  2. 很多外设同时使用着两个中断,外设的中断寄存器可以简单的与接收任务的通知
  3. 按位进行或运算
  4. 每一位代表的中断源定义 */
  5. #define TX_BIT 0x01
  6. #define RX_BIT 0x02
  7. /* 这个句柄的任务会用来接收任务通知,句柄在任务创建时确立 */
  8. static TaskHandle_t xHandlingTask;
  9. /*-----------------------------------------------------------*/
  10. /* 发送中断 */
  11. void vTxISR( void )
  12. {
  13. BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  14. /* 清中断 */
  15. prvClearInterrupt();
  16. /* 通过设置TX_BIT,通知任务发送完成 */
  17. xTaskNotifyFromISR( xHandlingTask,
  18. TX_BIT,
  19. eSetBits,
  20. &xHigherPriorityTaskWoken );
  21. /* 高优先级任务就绪,上下文切换 */
  22. portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
  23. }
  24. /*-----------------------------------------------------------*/
  25. /* 接收中断 */
  26. void vRxISR( void )
  27. {
  28. BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  29. /* 清中断. */
  30. prvClearInterrupt();
  31. /* 置位RX_BIT,通知接收任务接收中断发送 */
  32. xTaskNotifyFromISR( xHandlingTask,
  33. RX_BIT,
  34. eSetBits,
  35. &xHigherPriorityTaskWoken );
  36. /* 高优先级任务就绪,上下文切换 */
  37. portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
  38. }
  39. /*-----------------------------------------------------------*/
  40. /* 任务实现 */
  41. static void prvHandlingTask( void *pvParameter )
  42. {
  43. const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 500 );
  44. BaseType_t xResult;
  45. for( ;; )
  46. {
  47. /* 等待中断通知. */
  48. xResult = xTaskNotifyWait( pdFALSE, /* 输入不清除位. */
  49. ULONG_MAX, /* 退出时清除位 */
  50. &ulNotifiedValue, /* 通知值. */
  51. xMaxBlockTime );
  52. if( xResult == pdPASS )
  53. {
  54. /* 收到通知,检查置位情况. */
  55. if( ( ulNotifiedValue & TX_BIT ) != 0 )
  56. {
  57. /* 发送中断置位. */
  58. prvProcessTx();
  59. }
  60. if( ( ulNotifiedValue & RX_BIT ) != 0 )
  61. {
  62. /* 接收中断置位 */
  63. prvProcessRx();
  64. }
  65. }
  66. else
  67. {
  68. /* 等待超时. */
  69. prvCheckForErrors();
  70. }
  71. }
  72. }

替代邮箱(mailbox)

RTOS任务通知只能用来发送数据到一个任务中,相比用队列实现,有一些限制:

  • 只能发送32-bit数据
  • 保存的是接收任务的值,因此同一个时刻只能存在一个接收任务

因此,使用”轻量邮箱”这个短语代替”轻量队列”。任务的通知值就是邮箱值。

数据通过使用xTaskNotify()xTaskNotifyFromISR()发送至任务。其中,eAction参数可以设置成eSetValueWithOverwrite或者eSetValueWithoutOverwrite,前者会直接更新通知值及时任务已经有一个通知挂起,后者会在任务没有通知挂起才会更新任务值-在接收任务处理之前更新通知值会覆写先前的值。

任务可以通过调用xTaskNotifyWait()来获取自己的通知值。