FreeRTOS中的任务通知是一种极高效率的进程间通信技术,它彻底摆脱了对独立中间对象的依赖。该机制在系统底层能够完美模拟二值信号量、计数信号量以及单元素消息队列的运作。
相较于传统的队列或信号量,任务通知在内存受限的微控制器环境中表现出显著优势。它不仅大幅缩减了内存开销,还提升了数据流转的整体性能。发送方可以向目标直接传递数值或触发数值的累加,而接收方则依托其内部状态机自动完成阻塞等待与唤醒。
标准的队列、信号量和事件组等通信技术在生效前,必须在系统内存中开辟并初始化一个专属的中间数据结构。所有的任务进程必须围绕这个公共实体来进行消息的读写与同步,这必然会带来额外的内存消耗与操作层面的间接性。
任务通知绕过了传统中间介质的创建过程,允许任务或中断服务直接向明确指派的目标任务发送信号。启用此特性需在系统配置中开启特定的宏定义,随后操作系统的任务控制块中便会自动绑定一个专属的32位无符号整型通知值变量。
目标任务在面对任务通知时具备挂起与非挂起两种状态标识。当任务控制块中存在尚未被处理的通知数据时,任务将被标记为挂起状态。若任务在未挂起状态下尝试去获取通知,则会自动切入阻塞模式,将CPU控制权交出,直到新的通知到达。
利用任务通知跨进程传递数据时,其代码执行路径远短于传统队列机制,处理速度得到大幅跃升。同时,由于仅在已有的任务控制块上附加极少量变量,其内存占用的经济性无可比拟。
基于其底层的灵活性设计,任务通知不仅能够直接传递数据字,更是替换系统内部二值信号量、计数型信号量以及事件组机制的理想选择,尤其适用于只需要单一存储深度的事件同步场景。
任务通知的通信路径具有严格的方向限制,它允许从任务发送到任务,也允许从中断发送到任务,但绝对禁止向中断服务例程(ISR)发送任何通知。此外,它采用点对点的强绑定模式,无法实现一个消息向全局多个任务同时广播的功能。
在单一数据吞吐能力上,任务通知被严格限制为每次仅能传输或接收一个32位的整型数据值。如果系统业务要求一次性连续搬运大块的缓冲结构体数据,任务通知便无法取代传统长队列的角色。
任务通知按用途分可以分为两组:通用版本和将任务通知当作信号量使用的版本。通用版本在传递任务通知时,可以将数据直接写入通知值里;信号量版本每次使用会使任务通知值增/减 1 。
xTaskNotify该函数对应的中断版本为
xTaskNotifyFromISR。它增加了一个专门处理上下文切换的指针参数,用于向系统内核报告本次在硬件中断中发送的通知是否唤醒了一个更高优先级的任务,从而确保退出中断时能够立刻执行任务调度。
#define xTaskNotify( xTaskToNotify, ulValue, eAction ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL )
//原函数参数
BaseType_t xTaskGenericNotify(
TaskHandle_t xTaskToNotify, // 目标任务的句柄(要发送通知给哪个任务)
uint32_t ulValue, // 通知值(具体的32位数据,结合 eAction 使用)
eNotifyAction eAction, // 通知动作(决定如何更新目标任务的通知值,例如覆盖、按位或、加1等)
uint32_t *pulPreviousNotificationValue // 保存旧值的指针(用于在更新前读出并保存目标任务原来的通知值;若不需要可传 NULL)
);
//eAction参数取值范围
typedef enum
{
eNoAction = 0, /* 仅发送通知,不更新任务的通知值。 */
eSetBits, /* 设置任务通知值中的特定位(按位或操作)。 */
eIncrement, /* 递增任务的通知值(加1)。 */
eSetValueWithOverwrite, /* 强制设置任务的通知值(即使任务尚未读取旧值,也会直接覆盖)。 */
eSetValueWithoutOverwrite /* 条件设置任务的通知值(仅当任务已经读取了旧值时,才会写入新值,不覆盖未读旧值)。 */
} eNotifyAction;
//xTaskNotify参数
xTaskNotify(
TaskHandle_t xTaskToNotify, // 目标任务的句柄(要通知哪个任务)
uint32_t ulValue, // 通知值(具体的32位数据)
eNotifyAction eAction // 通知动作(如何更新目标任务的通知值)
);
//ISR函数
xTaskNotifyFromISR(
TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t *pxHigherPriorityTaskWoken
);这是负责发送通知数值的通用基础函数。它不仅能向目标传递一个32位的数据,还允许开发者配置具体的动作策略,例如是用新数据覆盖旧数据、将新数据与旧数据进行按位或运算,还是仅仅触发数值的自增。调用后系统会返回接收者更新完毕的最新状态值。
xTaskNotifyAndQuery该函数对应的中断版本为
xTaskNotifyAndQueryFromISR。
#define xTaskNotifyAndQuery( xTaskToNotify, ulValue, eAction, pulPreviousNotifyValue ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), ( pulPreviousNotifyValue ) )
BaseType_t xTaskNotifyAndQuery(
TaskHandle_t xTaskToNotify, // 目标任务的句柄(要发送通知给哪个任务)
uint32_t ulValue, // 通知值(具体的32位数据,结合 eAction 使用)
eNotifyAction eAction, // 通知动作(决定如何更新目标任务的通知值,如覆盖、按位或、加1等)
uint32_t *pulPreviousNotifyValue // 保存旧值的指针(在执行 eAction 更新之前,将目标任务原先的通知值保存到这个地址)
);该函数在继承通用发送函数所有功能特性的同时,扩展了历史数据的溯源能力。开发者可以通过传入一个外部指针变量,将接收任务在本次通知状态被篡改之前的原始通知值完整捕获出来。
xTaskNotifyGive该函数对应的中断版本为
vTaskNotifyGiveFromISR。
xTaskNotifyGive(
TaskHandle_t xTaskToNotify // 目标任务的句柄
);这是一个适用信号量模拟场景的精简版发送函数。它剥离了具体的数值传递功能,在被调用时唯一的操作就是将指定接收任务的通知值自动执行加一运算,是释放底层同步信号的最快途径。
xTaskNotifyWait该函数没有对应的中断版本。
BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, // 进入函数前要清除的位(在开始等待通知之前,将当前通知值的哪些位清零)
uint32_t ulBitsToClearOnExit, // 退出函数前要清除的位(在成功接收到通知并准备返回前,将通知值的哪些位清零)
uint32_t *pulNotificationValue, // 保存接收到的通知值的指针(用来读取任务的通知值,注意:读出的是清零退出位之前的值;若不关心可传 NULL)
TickType_t xTicksToWait // 阻塞超时时间(如果当前没有通知,任务最多阻塞等待多少个时钟节拍;可用 portMAX_DELAY 死等)
);作为功能最完整的等待接收函数,它为任务在进入等待前和成功退出等待时提供了极其细致的位掩码清零控制。任务可在此函数中设定最长超时阻塞节拍数,最终函数会明确返回是否在指定期限内成功等到了预期的通知信号。
ulTaskNotifyTake该函数没有对应的中断版本。
uint32_t ulTaskNotifyTake(
BaseType_t xClearCountOnExit, //退出时如何处理计数值(pdTRUE:退出时清零;pdFALSE:退出时数值减 1)
TickType_t xTicksToWait //阻塞超时时间(如果当前通知值为0,任务最多阻塞等待多少个时钟节拍)
);该函数是为资源消耗模型设计的接收接口,通常与递增发送函数配合使用。任务获取到通知后,函数内部可配置为将通知值一键清零(用作二值状态机),或者仅仅将当前计数减去一(用作排队或计数池消费),并返回操作前的实际剩余值。
xTaskNotifyStateClear该函数没有对应的中断版本。
BaseType_t xTaskNotifyStateClear(
TaskHandle_t xTask // 目标任务的句柄(要清除哪个任务的通知状态;传入 NULL 表示清除当前调用任务自己的状态)
);此函数提供了一种独立干预任务挂起状态的手段。它能够强制抹除指定任务因为等待通知而产生的阻塞挂起标志,让任务回归正常的调度序列,且在整个干预过程中,任务内部锁存的32位通知数据本身不会被修改或销毁。
当对自己使用时,可以预防因意外产生的脏数据。
假设你的任务要等待一个传感器数据,它设置了 100ms 的超时时间。 如果 100ms 到了,数据没来,任务因为超时放弃了等待,去执行了其他错误处理代码。但是,就在任务刚离开等待状态时,传感器数据突然来了(发送了通知,状态变成了 Pending)。
如果任务循环回来,再次调用
xTaskNotifyWait时,它会立刻“秒收到”上一次迟到的过期数据。 为了防止这种情况,在重新开始等待之前,可以先“清空一下标志”。
// 1. 确保在开始新的等待前,没有任何残留的旧通知状态干扰我们
xTaskNotifyStateClear( NULL );
// 2. 此时再开始等待,保证等到的是全新的通知
if( xTaskNotifyWait( 0x00, 0xFFFFFFFF, &ulData, pdMS_TO_TICKS(100) ) == pdTRUE )
{
// 成功收到新鲜的数据
}ulTaskNotifyValueClear该函数没有对应的中断版本。
uint32_t ulTaskNotifyValueClear(
TaskHandle_t xTask, // 目标任务句柄(要操作哪个任务的通知值;传入 NULL 表示操作当前调用任务自己)
uint32_t ulBitsToClear // 要清除的位掩码(将通知值中对应的位清零)
);ulTaskNotifyValueClear 是在 FreeRTOS V10.4.0 中引入的一个相对较新的函数。它的主要作用是:清除某个任务通知值中的指定位(Bit),并在清除前将原来的旧值返回。
它是一个非阻塞、纯操作数据的函数。无论有没有通知,它立刻执行位清除并返回。 它只会对数据进行操作,而不影响目标任务的状态。
当传入的位掩码为 0 时,该函数可以用来查询目标任务当前任务通知值。