在 FreeRTOS 中,任务的调度是通过优先级来决定的,而直达任务(direct-to-task notification)是 FreeRTOS 提供的一种高效的任务间通信机制,它允许中断或其他任务直接向某个任务发送通知。这种通知与信号量、消息队列等其他任务间通信方式相比,具有更低的开销和更高的效率。
任务通知
每个 RTOS 任务都有一个任务通知数组(简称通知)。每个任务_都有“挂起”或“非挂起”的通知状态和一个 32 位的通知值。
常量 configTASK_NOTIFICATION_ARRAY_ENTRIES 设置任务通知数组中的索引数量。在 FreeRTOS V10.4.0 版本前,任务只有单条任务通知, 而无通知数组。
直达任务通知是直接发送到任务的事件,而不是通过中间对象 (如队列、事件组或信号量)间接发送至任务的事件。向任务发送“直达任务通知” 会将目标任务通知设为“挂起”状态。正如任务可以阻塞中间对象 (如等待信号量可用的信号量),任务也可以阻塞任务通知, 以等待通知状态变为“挂起”。
向任务发送“直达任务通知”也可以 使用下列任一方法更新目标通知的值(可选):
- 覆盖原值,无论接收任务是否读取被覆盖的值。
- 覆盖原值,但前提是接收任务已读取被覆盖的值。
- 在值中设置一个或多个位。
- 对值进行增量(添加 1)。
调用 xTaskNotifyWait() / xTaskNotifyWaitIndexed() 读取通知值会将该通知的状态清除为“非挂起”。此外,也可以通过调用xTaskNotifyStateClear()/xTaskNotifyStateClearIndexed() 将通知状态明确设置为“未挂起”。
注意:数组中的每条通知均独立运行 ——任务一次只能阻塞数组中的一个通知,并且不会被发送到任何其他数组索引的通知解除阻塞。
- 在任务通知数组中,任务只能阻塞在某个特定通知值上,并且只会等待该通知索引的通知。
- 当任务处于阻塞状态时,其他通知索引的通知变化不会影响当前任务的阻塞状态,只有当前任务等待的通知发生变化,才会解除阻塞。
默认情况下,RTOS 任务通知功能处于启用状态,并且可以 通过将 configUSE_TASK_NOTIFICATIONS 在 FreeRTOSConfig.h 中设置为 0 从构建中排除(每个任务每个数组索引节省 8 个字节)。
重要提示:FreeRTOS 流和消息缓冲区在数值索引为 0 时使用任务通知。 如需在调用流或消息缓冲区 API 函数时保持任务通知的状态, 请使用数组索引大于 0 的任务通知。
性能优势和使用限制
任务通知具有高度灵活性,使得它们可以在必须要创建单独队列、二进制信号量、计数信号量或事件组的情况下进行使用。与通过诸如二进制信号量等中间对象来解除任务阻塞状态相比,通过直接通知解除 RTOS 任务阻塞状态的速度快 45%,使用的 RAM 也更少。不过这些性能优势也有一些意料之内的使用限制:
- RTOS 任务通知仅可在只有一个任务可以接收事件时使用。不过,这个条件在大多数真实世界情况下是满足的。比如,中断解除了一个任务的阻塞状态,该任务将处理由中断接收的数据。
- 仅可在使用 RTOS 任务通知代替队列的情况下:当某个接收任务可在阻塞状态下等待通知 (因而不花费任何 CPU 时间)时,发送任务不能 在阻塞状态下等待发送完成(在发送不能立刻完成的情况下)。
API
xTaskNotifyGive
每项任务都有一个“任务通知”数组,每条通知都包含一个状态和一个 32 位的值。直达任务通知是直接发送给任务的事件,可以解除接收任务的阻塞状态,还可以通过多种不同的方式更新接收任务的某个通知值。
例如,任务通知数组可覆盖接收任务的某个通知值,或仅设置接收任务某个通知值中的一个或多个位。
xTaskNotifyGive() 宏可在 将任务通知用作速度更快的轻量级二进制或计数信号量的替代方案时使用。
FreeRTOS 信号量通过 xSemaphoreGive() API 函数释放,而 xTaskNotifyGive() 与其等效,使用接收 RTOS 任务的某个通知值代替信号量。
xSemaphoreGive() 主要用于释放一个信号量,可以解除另一个正在等待任务的阻塞
xTaskNotifyGive() 用于向某个任务发送一个通知,它是通过修改任务的 “通知值” 来通知目标任务某个事件的发生。
xTaskNotifyGive() 与 xTaskNotifyGiveIndexed() 是等效宏,唯一区别在于 xTaskNotifyGiveIndexed() 可以操作数组中的任何任务通知,而 xTaskNotifyGive() 总是操作数组中索引为 0 的任务通知。
当任务通知值用作二进制或计数信号量的等效物时, 接收通知的任务应该使用 ulTaskNotifyTake() API 函数来等待通知, 而不是使用 xTaskNotifyWait() API 函数。
注意:数组中的所有通知均独立操作,即一项任务在同一时间只能在数组中的一条通知上处于阻塞状态,并且不会被发送到其他数组索引的通知解除阻塞状态。
xTaskNotifyGive() 不能在中断服务程序中调用。 请使用 vTaskNotifyGiveFromISR() 代替。
必须在 FreeRTOSConfig.h 中将 configUSE_TASK_NOTIFICATIONS 设置为 1 (或保留为未定义状态),才可使用这些宏。常量 configTASK_NOTIFICATION_ARRAY_ENTRIES 决定了 每项任务的任务通知数组中的索引数。
参数:
xTaskToNotify
接收通知的 RTOS 任务的句柄,通知值会递增。可通过以下方法获取任务句柄: 使用 xTaskCreate() 创建任务,并通过 pxCreatedTask 参数获取句柄; 使用 xTaskCreateStatic() 创建任务,并存储返回值作为句柄; 调用 xTaskGetHandle(),通过任务名称获取句柄。当前正在执行的 RTOS 任务的句柄由 xTaskGetCurrentTaskHandle() API 函数返回。
uxIndexToNotify
目标任务的通知值数组中要向其发送通知的索引。 uxIndexToNotify 必须小于 configTASK_NOTIFICATION_ARRAY_ENTRIES。 xTaskNotifyGive() 没有此参数,并且总是将通知发送到索引 0。
返回:
xTaskNotifyGiveIndexed() 是一个宏,调用 xTaskNotifyIndexed(), 并将 eAction 参数设置为 eIncrement,因此所有调用都返回 pdPASS。
ulTaskNotifyTake
ulTaskNotifyTake() 是一个宏, 用于将任务通知作为一种速度更快、重量更轻的二进制或计数信号量替代品。
ulTaskNotifyTake() 是用来接收任务通知并在接收到通知时继续执行的。它通常用于替代信号量或消息队列的一种机制,特别适合任务间的简单同步。
1 | uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait); |
ulTaskNotifyTake() 和 ulTaskNotifyTakeIndexed() 是等效的宏 - 唯一的区别 是 ulTaskNotifyTakeIndexed() 可以在数组内的任何任务通知上运行, 而 ulTaskNotifyTake() 始终在数组索引 0 处的任务通知上运行。
当任务使用通知值作为二进制或计数信号量时,其他任务和中断应使用 xTaskNotifyGive() 宏或 xTaskNotify() 函数将数据发给任务,其中函数的 eAction 参数设置为 eIncrement (这两者是等效的)。
ulTaskNotifyTake() 可以在退出时清除任务的通知值为 0,在这种情况下, 通知值起到二进制信号量的作用;或在退出时递减任务的通知值,在这种情况下, 通知值更像是计数信号量。
RTOS 任务可以使用 ulTaskNotifyTake() [可选]进入阻塞状态以等待任务通知指。 任务处于“阻塞”状态时不会占用任何 CPU 时间。
当通知被挂起时,xTaskNotifyWait() 将返回,ulTaskNotifyTake() 将在任务的通知值不为零时返回,并在返回之前递减任务通知值 。
必须在 FreeRTOSConfig.h 中将 configUSE_TASK_NOTIFICATIONS 设置为 1(或 保留为未定义) ,这些宏才能可用。常量 configTASK_NOTIFICATION_ARRAY_ENTRIES 设置每个任务的任务通知数组中的索引数。
参数:
uxIndexToWaitOn
调用任务的通知值数组中的索引, 调用任务将在该索引上等待非零通知。
uxIndexToWaitOn 必须小于 configTASK_NOTIFICATION_ARRAY_ENTRIES。
xTaskNotifyTake() 没有此参数,总是在索引 0 处等待通知。
xClearCountOnExit
如果收到 RTOS 任务通知,且 xClearCountOnExit 设置为 pdFALSE,那么 RTOS 任务的通知值将在 ulTaskNotifyTake() 退出前递减。这相当于 成功调用 xSemaphoreTake() 后,计数信号量的值被递减。
如果收到 RTOS 任务通知且 xClearCountOnExit 设置为 pdTRUE,则 RTOS 任务的通知值将在 ulTaskNotifyTake() 退出前重置为 0。这等同于 在成功调用 xSemaphoreTake() 后,将二进制信号量的值保留为 0(或空,或“不可用”)。
xTicksToWait
表示如果调用 ulTaskNotifyTake() 时尚未收到通知,在阻塞状态下等待收到通知的最长时间。
处于阻塞状态的 RTOS 任务不会消耗任何 CPU 时间。时间以 RTOS 滴答周期为单位。pdMS_TO_TICKS() 宏可用于 将以毫秒为单位的时间转换为以滴答为单位的时间。
返回:
- 被递减或清楚之前的任务通知值的值 (请参阅 xClearCountOnExit 的说明)。
xTaskNotify
xTaskNotify() 用于直接向 RTOS 任务发送事件,并且可能解除该任务的阻塞状态,同时还可以按照以下任一方式更新接收任务的某个通知值:
- 将一个 32 位数字写入通知值
- 将通知值加一(递增)
- 设置通知值中的一个或多个位
- 保持通知值不变
1 | BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction ); |
xTaskNotify() 和 xTaskNotifyIndexed() 是等效函数,唯一区别在于 xTaskNotifyIndexed() 可以操作数组中的任何任务通知,而 xTaskNotify() 总是操作 数组中索引为 0 的任务通知。
不得从中断服务程序 (ISR) 调用此函数。 请使用 xTaskNotifyFromISR() 代替。
必须在 FreeRTOSConfig.h 中将 configUSE_TASK_NOTIFICATIONS 设置为 1 (或保留为未定义状态),才可使用这些函数。常量 configTASK_NOTIFICATION_ARRAY_ENTRIES 决定了 每项任务的任务通知数组中的索引数。
参数:
xTaskToNotify
接收通知的 RTOS 任务(即目标任务)的句柄。可通过以下方法获取任务句柄: 使用 xTaskCreate() 创建任务,并通过 pxCreatedTask 参数获取句柄; 使用 xTaskCreateStatic() 创建任务,并存储返回值作为句柄; 调用 xTaskGetHandle(),通过任务名称获取句柄。当前正在执行的 RTOS 任务的句柄 由 xTaskGetCurrentTaskHandle() API 函数返回。
uxIndexToNotify
目标任务的通知值数组中要向其发送通知的索引。uxIndexToNotify 必须小于 configTASK_NOTIFICATION_ARRAY_ENTRIES。xTaskNotify() 没有此参数, 并且总是将通知发送到索引 0。
ulValue
用于更新目标任务的通知值。请参阅下文 eAction 参数的说明。
eAction
一种枚举类型,可以取下列任一值,以执行相关操作。
eNoAction
目标任务接收事件,但其通知值不会更新。在这种情况下, 不会使用 ulValue。
eSetBits
目标任务的通知值将与 ulValue 进行按位“或”操作。例如,如果 ulValue 设置为 0x01,则目标任务通知值中的第 0 位将被设置。同样,如果 ulValue 设置为 0x04,则目标任务通知值中的第 2 位将被设置。通过这种方式,RTOS 任务 通知机制可以作为 事件组的轻量级替代方案。
eIncrement
目标任务的通知值将增加 1,这样调用 xTaskNotify() 相当于调用 xTaskNotifyGive()。在这种情况下,不会使用 ulValue。
eSetValueWithOverwrite
目标任务的通知值无条件设置为 ulValue。通过这种方式,RTOS 任务 通知机制可以作为 xQueueOverwrite() 的轻量级替代方案。
eSetValueWithoutOrwrite
如果目标任务当前没有挂起的通知,则其通知值将设置为 ulValue。
如果目标任务已有挂起的通知,则其通知值不会更新, 以免之前的值在使用前被覆盖。在这种情况下,调用 xTaskNotify() 会失败, 返回 pdFALSE。
通过这种方式,RTOS 任务通知机制可以 在长度为 1 的队列上作为 xQueueSend() 的轻量级替代方案。
返回:
除了 eAction 设置为 eSetValueWithoutOverwrite 且目标任务的通知值无法更新(因为目标任务已有挂起的通知)时, 其他情况下均返回 pdPASS。
xTaskNotifyAndQuery
xTaskNotifyAndQueryIndexed() 执行的操作与 xTaskNotifyIndexed() 相同, 另外还可通过额外的 pulPreviousNotifyValue 参数返回目标任务之前的通知值 (函数被调用时的通知值,而不是函数返回时的通知值) 。
xTaskNotifyAndQuery() 执行的操作与 xTaskNotify() 相同, 另外还可通过额外的 pulPreviousNotifyValue 参数返回目标任务之前的通知值 (函数被调用时的通知值,而不是函数返回时的通知值) 。
1 | BaseType_t xTaskNotifyAndQuery( TaskHandle_t xTaskToNotify, |
不得从中断服务程序 (ISR) 调用此函数。 请使用 xTaskNotifyAndQueryFromISR() 代替。
参数:
xTaskToNotify
接收通知的 RTOS 任务(即目标任务)的句柄。可通过以下方法获取任务句柄:
- 使用 xTaskCreate() 创建任务,并通过 pxCreatedTask 参数获取句柄;
- 使用 xTaskCreateStatic() 创建任务,并存储返回值作为句柄;
- 调用 xTaskGetHandle(),并通过任务名称获取句柄。
- 当前正在执行的 RTOS 任务的句柄由 xTaskGetCurrentTaskHandle() API 函数返回。
uxIndexToNotify
目标任务的通知值数组中要向其发送通知的索引。uxIndexToNotify 必须小于 configTASK_NOTIFICATION_ARRAY_ENTRIES。
ulValue 用于更新目标任务的通知值。请参阅下文 eAction 参数的说明。
eAction
一种枚举类型,可以取下列任一值,以执行相关操作。
pulPreviousNotifyValue
可用于在 xTaskNotifyAndQuery() 修改任何位之前传出目标任务的通知值。 pulPreviousNotifyValue 是可选参数,如果不需要,可设置为 NULL。如果不使用 pulPreviousNotifyValue, 可以考虑使用 xTaskNotify() 替代 xTaskNotifyAndQuery()。
eAction 值和相关操作
eNoAction
目标任务接收事件,但其通知值不会更新。在这种情况下, 不会使用 ulValue。
eSetBits
目标任务的通知值将与 ulValue 进行按位“或”操作。例如,如果 ulValue 设置为 0x01,则目标任务通知值中的第 0 位将被设置。同样,如果 ulValue 设置为 0x04,则目标任务通知值中的第 2 位将被设置。通过这种方式,RTOS 任务 通知机制可以作为事件组的轻量级替代方案。
eIncrement
目标任务的通知值将增加 1,这样调用 xTaskNotify() 相当于调用 xTaskNotifyGive()。在这种情况下,不会使用 ulValue。
eSetValueWithOverwrite
目标任务的通知值无条件设置为 ulValue。通过这种方式,RTOS 任务 通知机制可以作为 xQueueOverwrite() 的轻量级替代方案。
eSetValueWithoutOrwrite
如果目标任务当前没有挂起的通知,则其通知值 将设置为 ulValue。如果目标任务已有挂起的通知,则其通知值 不会更新,以免之前的值在使用前被覆盖。在这种情况下, 调用 xTaskNotify() 会失败,返回 pdFALSE。通过这种方式,RTOS 任务通知机制可以 在长度为 1 的队列上作为 xQueueSend() 的轻量级替代方案。
返回:
除了 eAction 设置为 eSetValueWithoutOverwrite 且目标任务的通知值无法更新(因为目标任务已有挂起的通知)时, 其他情况下 均返回 pdPASS。
xTaskNotifyWait
xTaskNotifyWait() 用于使调用任务等待接收通知,可以为其设置一个可选的超时时间。如果接收 RTOS 任务在等待通知时已经处于阻塞状态,则在等待的通知到达时, 接收 RTOS 任务将解除阻塞状态,通知也将清除。
1 | BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, |
注意:数组中的所有通知均独立操作,即一项任务在同一时间只能在数组中的一条通知上处于阻塞状态, 并且不会被发送到其他数组索引的通知解除阻塞状态。
xTaskNotifyWait() 和 xTaskNotifyWaitIndexed() 是等效宏,唯一区别在于 xTaskNotifyWaitIndexed() 可以操作数组中的任何任务通知, 而 xTaskNotifyWait() 总是操作数组中索引为 0 的任务通知。
xTaskNotifyGive() 不能在中断服务程序中调用。 请使用 vTaskNotifyGiveFromISR() 代替。
必须在 FreeRTOSConfig.h 中将 configUSE_TASK_NOTIFICATIONS 设置为 1 (或保留为未定义状态),才可使用这些宏。常量 configTASK_NOTIFICATION_ARRAY_ENTRIES 决定了每项任务的任务通知数组中的索引数。
参数:
uxIndexToWaitOn
调用任务的通知值数组中用于等待接收通知的索引。 uxIndexToWaitOn 必须小于 configTASK_NOTIFICATION_ARRAY_ENTRIES。xTaskNotifyWait() 没有此参数, 总是在索引 0 的位置等待通知。
ulBitsToClearOnEntry
调用 xTaskNotifyWait() 时,如果通知已挂起,则在进入 xTaskNotifyWait() 函数(即任务等待新通知之前)时,ulBitsToClearOnEntry 中设置的任何位都会在调用 RTOS 任务的通知值中 被清除 。例如,如果 ulBitsToClearOnEntry 设为 0x01, 则任务通知值中的第 0 位将在进入函数时被清除。 如果 ulBitsToClearOnEntry 设为 0xffffffff (ULONG_MAX) 则将清除任务通知值中的所有位,相当于将值清零。
ulBitsToClearOnExit
如果在调用 xTaskNotifyWait() 函数时收到了通知,则在 xTaskNotifyWait() 函数退出之前,ulBitsToClearOnExit 中设置的任何位都会在调用 RTOS 任务的通知值中 被清除。RTOS 任务的通知值保存到 *pulNotificationValue(见下文对 pulNotificationValue 的介绍)之后, 这些位即被清除。例如,如果 ulBitsToClearOnExit 设为 0x03, 则在函数退出之前,任务通知值中的第 0 位和第 1 位将被清除。如果 ulBitsToClearOnExit 设为 0xffffffff(ULONG_MAX),则将清除任务通知值中的所有位, 相当于将值清零。
pulNotificationValue
用于传出 RTOS 任务的通知值。复制到 *pulNotificationValue 的值是 RTOS 任务的通知值,该值是在应用 ulBitsToClearOnExit 设置清除任何位 之前的值。如果无需通知值,可以将 pulNotificationValue 设置为 NULL。
xTicksToWait
调用 xTaskNotifyWait() 时没有挂起通知的情况下, 在阻塞状态下等待接收通知的最长时间。RTOS 任务在阻塞状态下 不会消耗 CPU 时间。时间以 RTOS 滴答周期为单位。可以使用 pdMS_TO_TICKS() 宏 将以毫秒为单位的时间转换为以滴答为单位的时间。
返回:
如果收到了通知,或者在调用 xTaskNotifyWait() 时通知已挂起, 则返回 pdTRUE。
如果调用 xTaskNotifyWait() 超时且在超时前没有收到通知, 则返回 pdFALSE。
xTaskNotifyStateClear
ulTaskNotifyValueClear
作为二进制信号量
与通过二进制信号量解除任务阻塞状态不同,通过直接通知解除 RTOS 任务阻塞状态的速度提高 45%, 而且使用的 RAM 减少。
二进制信号量是一种最大计数为 1 的信号量,因此称为“二进制”。 只有在信号量可用的情况下,任务才能“获取”信号量, 而只有在其计数为 1 的情况下,信号量才可用。
当使用任务通知代替二进制信号量时,接收任务的通知值(32位)会用于替代二进制信号量的计数值, 而且 ulTaskNotifyTake()(或 ulTaskNotifyTakeIndexed())API 函数 会用于代替信号量的 xSemaphoreTake() API 函数。ulTaskNotifyTake() 函数的 xClearOnExit 参数设置为 pdTRUE,这样每次获取通知时计数值均归零 ——模拟二进制信号量。
同样,xTaskNotifyGive()(或 xTaskNotifyGiveIndexed()) 或者 vTaskNotifyGiveFromISR()(或 vTaskNotifyGiveIndexedFromISR())函数用于代替信号量的 xSemaphoreGive() 和 xSemaphoreGiveFromISR() 函数。
示例
1 | // RTOS任务调用传输函数,然后在阻塞状态下等待(因此不使用CPU时间),直到收到传输完成的通知。 |