FreeRTOS 任务通知链:xTaskNotifyGive 的三种模式

Babel36acl 嵌入式实战 无~ 11 次阅读 预计阅读时间: 5 分钟 发布于 1 天前 最后更新于 1 小时前 1121 字


嵌入式实战 FreeRTOS

FreeRTOS 任务通知链:xTaskNotifyGive 的三种模式

FreeRTOS 任务通知(Task Notifications)比信号量、队列轻量得多——不需要创建内核对象,直接向目标任务的任务控制块写一个通知值。在我们 7 任务的系统中,xTaskNotifyGive 承担了三种不同的协作模式。

模式一:状态变化推送(减少轮询)

MotorTask 每 5ms 跑一次动作表。但如果状态机 98% 的时间里状态都没变,5ms 轮询就是白跑的。用任务通知做"有变化才唤醒":

// StateMachineTask 侧 — 状态变化时推送通知
void app_packer_sm_task_once(void) {
    psh_task_once();

    if (psh_consume_state_change() != 0U) {
        // 状态确实变了 → 通知 MotorTask
        if (s_motor_task_handle != NULL) {
            (void)xTaskNotifyGive(s_motor_task_handle);
        }
    }
}

// MotorTask 侧 — 优先等通知,超时兜底
void StartMotorTask(void *argument) {
    packer_actuator_init();
    for (;;) {
        // 等通知:有通知立即响应,无通知 5ms 超时后周期执行
        (void)ulTaskNotifyTake(pdTRUE,
                               pdMS_TO_TICKS(APP_CFG_TASK_MOTOR_PERIOD_MS));
        packer_actuator_task_once(app_packer_sm_get_state());
    }
}

效果:状态不变时 5ms 等超时;状态变化时 <1ms 内响应。实际测试中无效查表减少了约 80%。

模式二:ISR 唤醒任务(DMA 完成回调)

ADC 采用 DMA 采样,完成回调在 ISR 上下文中触发。ISR 不能调阻塞 API,但 vTaskNotifyGiveFromISR 是专门为 ISR 设计的非阻塞通知:

// ISR 侧(HAL ADC 转换完成回调)
static void adc_notify_from_isr(void) {
    BaseType_t hpw = pdFALSE;
    TaskHandle_t adc_handle = (TaskHandle_t)AdcTaskHandle;

    if (adc_handle != NULL) {
        vTaskNotifyGiveFromISR(adc_handle, &hpw);
        portYIELD_FROM_ISR(hpw);  // 如果唤醒的任务优先级更高,立即切换
    }
}

// AdcTask 侧 — 等待 ISR 通知或超时兜底
void StartAdcTask(void *argument) {
    packer_adc_init();
    bsp_adc_register_notification_cb(adc_notify_from_isr);
    for (;;) {
        (void)ulTaskNotifyTake(pdTRUE,
                               pdMS_TO_TICKS(APP_CFG_ADC_SAMPLE_PERIOD_MS * 2));
        packer_adc_task_once();
    }
}

超时兜底的保护作用:如果 DMA 回调因为某种原因丢失了,AdcTask 不会永久阻塞——超时后仍然会采样一次。

模式三:UART 字节流驱动(ISR 通知任务消费)

ProtoTask 消费 USART3 的 DMA 接收缓冲。每收到新字节,UART IDLE 中断或 DMA 半满中断通过 vTaskNotifyGiveFromISR 通知 ProtoTask:

// ISR 侧(UART 中断回调)
void USART3_IRQHandler(void) {
    if (__HAL_UART_GET_FLAG(&huart3, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart3);
        BaseType_t hpw = pdFALSE;
        vTaskNotifyGiveFromISR(s_serial_task_handle, &hpw);
        portYIELD_FROM_ISR(hpw);
    }
}

// ProtoTask 侧 — 有数据立即处理,无数据 50ms 兜底
void StartProtoTask(void *argument) {
    for (;;) {
        (void)ulTaskNotifyTake(pdTRUE,
                               pdMS_TO_TICKS(APP_CFG_TASK_PROTO_PERIOD_MS));
        app_packer_proto_task_once();
    }
}

三种模式对比

模式 通知方向 API(发送侧) API(接收侧) 超时用途
状态推送 任务→任务 xTaskNotifyGive ulTaskNotifyTake 5ms 兜底周期
ISR 唤醒 ISR→任务 vTaskNotifyGiveFromISR ulTaskNotifyTake DMA 丢失保护
UART 驱动 ISR→任务 vTaskNotifyGiveFromISR ulTaskNotifyTake 50ms 兜底轮询

为什么选任务通知而不是信号量

FreeRTOS 中任务通知比二进制信号量快约 40%(不需要创建 Semaphore 句柄,没有队列操作)。在这个项目中所有"唤醒"语义都不需要计数——二进制通知正好匹配。

任务通知的另一个优势:不需要提前创建。任务启动时通知值初始为 0,第一次 xTaskNotifyGive 就置为 1。信号量需要额外调用 xSemaphoreCreateBinary

"有变化才唤醒"替代"周期轮询"——减少无效 CPU 占用的最直接手段。

此作者没有提供个人介绍。
最后更新于 2026-05-30