任务通知 Task Notification
FreeRTOS 最轻量的任务间通信机制:无需额外内核对象,比二值信号量快 45%、省 RAM,是"一对一"通信的首选。
FreeRTOS 最轻量的任务间通信机制:无需额外内核对象,比二值信号量快 45%、省 RAM,是"一对一"通信的首选。
uint32_t 变量。它可以被当作事件位(按位或/清位)、计数值(自增)、或直接被覆盖成任意数据。这一个 32 位数字,就能替代二值信号量、计数信号量、事件组,甚至简单的邮箱。xTaskNotifyWait / ulTaskNotifyTake)、待处理通知(已收到但任务还没取走)。发送时若目标正在等待,会立即解除其阻塞。xTaskNotify 的动作参数:eNoAction(仅解阻塞,不改值,等价二值信号量)、eSetBits(按位或,等价事件组)、eIncrement(值加一,等价计数信号量)、eSetValueWithOverwrite(无条件覆盖,等价邮箱)、eSetValueWithoutOverwrite(有待处理通知时不覆盖并返回失败)。vTaskNotifyGive() 让通知值加一(相当于 Give 信号量);ulTaskNotifyTake() 取走通知,第一个参数决定取走后是清零(当二值用)还是减一(当计数用),并可带超时阻塞。这对组合是性能最高的用法。eSetBits 发送,能实现事件组式的多位标志等待。configTASK_NOTIFICATION_ARRAY_ENTRIES 配置数量。带 Indexed 后缀的 API(如 xTaskNotifyIndexed)指定操作第几个通知,让一个任务能并行接收多路不冲突的通知。传统信号量/队列需要先在堆里创建一个内核对象,收发时要操作对象内的等待列表、加解锁。任务通知直接改目标 TCB 里那个现成的 32 位变量,省去了对象查找和多余的列表操作:
中断里 Give,任务里 Take——这是"中断延迟处理"的经典范式,比 ISR 里直接干活好得多:
#include "FreeRTOS.h" #include "task.h" TaskHandle_t g_uartTask = NULL; /* 任务:等待"数据到达"通知 */ void UartRxTask(void *arg) { for (;;) { /* 阻塞等通知;参数1=pdTRUE 表示取走后清零(当二值用) */ uint32_t n = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if (n > 0) { process_uart_data(); /* 在任务上下文里从容处理 */ } } } /* 中断服务程序:收到一个字节就通知任务 */ void USART2_IRQHandler(void) { BaseType_t woken = pdFALSE; if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_RXNE)) { buffer_push(huart2.Instance->DR); /* ISR 版本:唤醒 g_uartTask */ vTaskNotifyGiveFromISR(g_uartTask, &woken); } /* 若唤醒了更高优先级任务,退出中断时立即切换 */ portYIELD_FROM_ISR(woken); }
所有 FromISR 的通知/信号量/队列 API 都带一个 pxHigherPriorityTaskWoken 出参。若它被置为 pdTRUE,说明这次操作唤醒了一个比当前被打断任务更高优先级的任务,必须在中断结束前调用 portYIELD_FROM_ISR(woken) 触发上下文切换,否则要等到下一个 Tick 才切换,白白损失实时性。这是第 9 章中断管理的核心细节。
#define EVT_WIFI_UP (1UL << 0) #define EVT_MQTT_UP (1UL << 1) #define EVT_TIME_SYNC (1UL << 2) void NetTask(void *arg) { uint32_t bits; for (;;) { /* 进入前不清位;退出时清全部;等到任意位被置 */ xTaskNotifyWait(0x00, 0xFFFFFFFF, &bits, portMAX_DELAY); if (bits & EVT_WIFI_UP) printf("wifi up\r\n"); if (bits & EVT_MQTT_UP) printf("mqtt up\r\n"); if (bits & EVT_TIME_SYNC) printf("time synced\r\n"); } } /* 其他任务/中断置位通知 */ void on_wifi_connected(void) { xTaskNotify(g_netTask, EVT_WIFI_UP, eSetBits); }
/* 发送方:把最新的 ADC 值直接覆盖进接收任务 */ xTaskNotify(g_ctrlTask, adc_value, eSetValueWithOverwrite); /* 接收方:取出最新值 */ void CtrlTask(void *arg) { uint32_t latest; for (;;) { if (xTaskNotifyWait(0, 0, &latest, pdMS_TO_TICKS(100)) == pdTRUE) { pid_update(latest); } else { /* 100ms 没新值:可视为超时/维持上次输出 */ } } }
| 要替代的对象 | 发送侧动作 | 接收侧 API | 是否推荐 |
|---|---|---|---|
| 二值信号量 | vTaskNotifyGive / eNoAction | ulTaskNotifyTake(pdTRUE, …) | 强烈推荐(最快) |
| 计数信号量 | vTaskNotifyGive / eIncrement | ulTaskNotifyTake(pdFALSE, …) | 推荐 |
| 事件组(单接收者) | eSetBits | xTaskNotifyWait(…) | 推荐 |
| 邮箱(1 个 32 位值) | eSetValueWithOverwrite | xTaskNotifyWait(…) | 推荐 |
| 队列(多数据/多接收者) | — | — | 不能替代,用队列 |
任务通知只能直达一个已知的接收任务。当出现以下需求时必须回退到队列/信号量/事件组:① 多个任务要等待同一事件(广播);② 需要缓冲多条消息(队列的深度);③ 接收者身份不确定(如生产者-消费者池);④ 需要传递大于 4 字节的结构体(应用队列拷贝)。用错会导致通知丢失或只有一个任务被唤醒。
任务通知是 FreeRTOS 最轻量的 IPC:每个任务 TCB 内自带一个 32 位通知值,发送方直接操作目标 TCB,无需创建额外内核对象,因此比二值信号量快约 45%、零额外 RAM。核心 API:vTaskNotifyGive/ulTaskNotifyTake 组合最快(当二值/计数信号量用),xTaskNotify+eNotifyAction(eNoAction/eSetBits/eIncrement/eSetValueWithOverwrite)配 xTaskNotifyWait 能模拟事件组和邮箱。ISR 里用 vTaskNotifyGiveFromISR/xTaskNotifyFromISR,并务必 portYIELD_FROM_ISR(woken)。V10.4 起支持带索引的多通道通知。最大限制是只能一对一——需要广播、多缓冲、多接收者或大数据时,必须用队列/信号量/事件组。中断→任务的"延迟处理"是它最经典、最推荐的用法。