从 v8.2.0 版本开始,FreeRTOS 新增了任务通知(Task Notifictions)这个功能,可以使用任务通知来代替信号量、消息队列、事件标志组等这些东西。使用任务通知的话效率会更高。
有个疑惑:
队列是两个互通消息的任务之外的一个特性,而任务通知是任务本身的属性,如何合理地使用呢?比如任务A向任务B发消息,用队列的话,就是两个任务都可以操作这个队列;但如果是任务通知,消息传递时,用的是任务A的任务通知还是任务B的任务通知呢?
任务通知简介
任务通知在 FreeRTOS 中是一个可选的功能,要使用任务通知的话就需要将宏configUSE_TASK_NOTIFICATIONS 定义为 1。
FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞的话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态。也可以更新接收任务的任务通知值,任务通知更新时可以有如下几种选项:
● 更新时不覆盖接收任务的通知值(如果上次发送给接收任务的通知还没被处理)。
● 更新时覆盖接收任务的通知值。
● 更新时更新接收任务通知值的一个或多个 bit。
● 更新时增加接收任务的通知值。
合理、灵活的使用上面这些更改任务通知值的方法可以在一些场合中替代队列、二值信号量、计数型信号量和事件标志组。使用任务通知来实现二值信号量功能的时候,解除任务阻塞的时间比直接使用二值信号量要快 45%(FreeRTOS 官方测试结果,使用 v8.1.2 版本中的二值信号量,GCC 编译器,-O2 优化的条件下测试的,没有使能断言函数 configASSERT()),并且使用的 RAM 更少! 这是因为任务通知是任务控制块本身的特征,而不必额外使用队列,因此也就省去了创建队列时使用的空间。
任务通知的发送使用函数 xTaskNotify()或者 xTaskNotifyGive()(还有此函数的中断版本)来完成,这个通知值会一直被保存着,直到接收任务调用函数 xTaskNotifyWait()或者 ulTaskNotifyTake()来获取这个通知值。假如接收任务因为等待任务通知而阻塞的话那么在接收到任务通知以后就会解除阻塞态。
任务通知虽然可以提高速度,并且减少 RAM 的使用,但是任务通知也是有使用限制的:
● FreeRTOS 的任务通知只能有一个接收任务,其实大多数的应用都是这种情况。
● 接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞。
任务通知是任务控制块本身的内容,创建任务时已经有了,所以不必再次专门创建。
发送任务通知
任务通知发送函数有 6 个,如下表所示:
函数 xTaskNotify()
此函数用于发送任务通知,此函数发送任务通知的时候带有通知值,此函数是个宏,真正执行的函数 xTaskGenericNotify(),函数原型如下:
某个任务中,调用该函数,向某个指定的任务发送指定的任务通知值,并指定更新的方式。当通知的方式不同时,就可以模拟队列、二值信号量、计数型信号量和事件标志组。后续详细说明。
函数 xTaskNotifyGive()
发送任务通知,相对于函数 xTaskNotify(),此函数发送任务通知的时候不带有通知值。此函数只是将任务通知值简单的加一,此函数是个宏,真正执行的是函数 xTaskGenericNotify(),
此函数原型如下:
疑惑:
这个函数和xTaskNotify函数在选择“通知值+1”时有何区别?一个带通知值,一个不带通知值?既然xTaskNotify是“通知值+1”,那带的通知值有什么用?
函数 xTaskNotifyAndQuery()
此函数和 xTaskNotify()很类似,此函数比 xTaskNotify()多一个参数,此参数用来保存更新前的通知值。此函数是个宏,真正执行的是函数 xTaskGenericNotify(),此函数原型如下:
任务通知发送函数解析
我们学习了 3 个任务级任务通知发送函数:xTaskNotify()、xTaskNotifyGive() 和xTaskNotifyAndQuery(),这三个函数最终调用的都是函数 xTaskGenericNotify()!此函数在文件 tasks.c 中定义,可自行查阅源码。
大致实现流程如下所述:
(1)、判断参数 pulPreviousNotificationValue 是否有效,因为此参数用来保存更新前的任务通知值。
(2)、如果参数 pulPreviousNotificationValue 有效的话就用此参数保存更新前的任务通知值。
(3)、保存任务通知状态,因为下面会修改这个状态,后面我们要根据这个状态来确定是否将任务从阻塞态解除。
(4)、更新任务通知状态为 taskNOTIFICATION_RECEIVED。
(5)、根据不同的更新方式做不同的处理,如果为 eSetBits 的话就将指定的 bit 置 1。也就是更新接收任务通知值的一个或多个 bit。
(6)、如果更新方式为 eIncrement 的话就将任务通知值加一。
(7)、如果更新方式为 eSetValueWithOverwrite 的话就直接覆写原来的任务通知值。
(8)、如果更新方式为 eSetValueWithoutOverwrite 的话就需要判断原来的任务通知值是否被处理(保存起来),如果已经被处理(保存起来)了就更新为任务通知值。如果此前的任务通知值话没有被处理(保存起来)的话就标记 xReturn 为 pdFAIL,后面会返回这个值。
(9)、根据(3)中保存的接收任务之前的状态值来判断是否有任务需要解除阻塞,如果在任务通知值被更新前任务处于 taskWAITING_NOTIFICATION 状态的话就说明有任务因为等待任务通知值而进入了阻塞态。
(10)、将任务从状态列表中移除。
(11)、将任务重新添加到就绪列表中。
(12)、判断刚刚解除阻塞的任务优先级是否比当前正在运行的任务优先级高,如果是的话需要进行一次任务切换。
(13)、返回 xReturn 的值,pdFAIL 或 pdPASS。
发送通知后,接收任务就可以开始接收任务通知了。
获取任务通知
获取任务通知的函数有两个,如下表所示:
函数 ulTaskNotifyTake()
此函数为获取任务通知函数,当任务通知用作二值信号量或者计数型信号量的时候可以使用此函数来获取信号量,函数原型如下:
此函数在文件 tasks.c 中有定义,大致实现过程如下:
(1)、判断任务通知值是否为 0,如果为 0 的话说明还没有接收到任务通知。
(2)、修改任务通知状态为 taskWAITING_NOTIFICATION。
(3)、如果阻塞时间不为 0 的话就将任务添加到延时列表中,并且进行一次任务调度。
(4)、如果任务通知值不为 0 的话就先获取任务通知值。
(5)、任务通知值大于 0。
(6)、参数 xClearCountOnExit 不为 pdFALSE,那就将任务通知值清零。
(7)、如果参数 xClearCountOnExit 为 pdFALSE 的话那就将任务通知值减一。
(8)、更新任务通知状态为 taskNOT_WAITING_NOTIFICATION。
函数 xTaskNotifyWait()
此函数也是用来获取任务通知的,不过此函数比 ulTaskNotifyTake()更为强大,不管任务通知用作二值信号量、计数型信号量、队列和事件标志组中的哪一种,都可以使用此函数来获取任务通知。但是当任务通知用作二值信号量和计数型信号量的时候推荐使用函数 ulTaskNotifyTake()。此函数原型如下:
说实话,前两个参数没太明白啥意思。
待补充理解。
任务通知模拟二值信号量
前面说了,根据 FreeRTOS 官方的统计,使用任务通知替代二值信号量的时候任务解除阻塞的时间要快 45%,并且需要的 RAM 也更少。其实通过我们上面分析任务通知发送和获取函数的过程可以看出,任务通知的代码量很少,所以执行时间与所需的 RAM 也就相应的会减少。
二值信号量就是值最大为 1 的信号量,这也是名字中“二值”的来源。当任务通知用于替代二值信号量的时候任务通知值就会替代信号量值,函数 ulTaskNotifyTake()就可以替代信号量获取函数xSemaphoreTake(),函数 ulTaskNotifyTake()的参数 xClearCountOnExit 设置为 pdTRUE。这样在每次获取任务通知的时候模拟的信号量值就会清零。函数xTaskNotifyGive()和vTaskNotifyGiveFromISR()用于替代函数 xSemaphoreGive()和 xSemaphoreGiveFromISR()。
任务通知模拟计数型信号量
不同与二值信号量,计数型信号量值可以大 1,这个最大值在创建信号量的时候可以设置。当计数型信号量有效的时候任务可以获取计数型信号量,信号量值只要大于 0 就表示计数型信号量有效。
当任务通知用作计数型信号量的时候获取信号量相当于获取任务通知值,使用函数ulTaskNotifyTake()来替代函数 xSemaphoreTake()。函数 ulTaskNotifyTake()的参数xClearOnExit要设置为 pdFLASE,这样每次获取任务通知成功以后任务通知值就会减一。使用任务通知发送函数 xTaskNotifyGive() 和 vTaskNotifyGiveFromISR() 来替代计数型信号量释放函数xSemaphoreGive()和 xSemaphoreGiveFromISR()。
任务通知模拟消息邮箱
任务通知也可用来向任务发送数据,但是相对于用队列发送消息,任务通知向任务发送消息会受到很多限制!
1、只能发送 32 位的数据值。
2、消息被保存为任务的任务通知值,而且一次只能保存一个任务通知值,相当于队列长度为 1。
因此说任务通知可以模拟一个轻量级的消息邮箱而不是轻量级的消息队列。任务通知值就是消息邮箱的值。
发送数据可以使用函数 xTaskNotify()或者 xTaskNotifyFromISR(),函数的参数 eAction 设置eSetValueWithOverwrite 或 者 eSetValueWithoutOverwrite 。如果参数 eAction为 eSetValueWithOverwrite 的话不管接收任务的通知值是否已经被处理,这个通知值都会被更新。参数 eAction 为 eSetValueWithoutOverwrite 的话如果上一个任务通知值话还没有被处理,那么新的任务通知值就不会更新。如果要读取任务通知值的话就使用函数xTaskNotifyWait()。
任务通知模拟事件标志组
事件标志组其实就是一组二进制事件标志(位),每个事件标志位的具体意义由应用程序编写者来决定。当一个任务等待事件标志组中的某几个标志(位)的时候可以进入阻塞态,当任务因为等待事件标志(位)而进入阻塞态以后这个任务就不会消耗 CPU。
当任务通知用作事件标志组的话任务通知值就相当于事件组,这个时候任务通知值的每个 bit 用作事件标志 ( 位 ) 。函数 xTaskNotifyWait() 替代事件标志组中的 API 函数 xEventGroupWaitBits()。函数 xTaskNotify()和 xTaskNotifyFromISR()(函数的参数 eAction为eSetBits)替代事件标志组中的 API 函数 xEventGroupSetBits()和xEventGroupSetBitsFromISR()。
任务通知暂时就了解这么多,实际开发中先把队列、信号量这些用明白再说。