FreeRTOS任务通知替代二值信号量:中断服务程序中的正确写法与陷阱

1. 任务通知与二值信号量的对比

FreeRTOS从V8.2.0开始引入任务通知(Task Notification)机制,每个任务都有一个32位的通知值和一个通知状态。任务通知可以替代二值信号量、计数信号量、事件组甚至邮箱,但具有更高的效率和更少的内存占用。

  • 二值信号量:独立的内核对象,需要动态或静态分配内存,操作涉及队列管理,开销较大。
  • 任务通知:直接内嵌于任务控制块(TCB),无需额外内存,操作仅涉及简单的变量读写,速度更快。

在中断服务程序(ISR)中,任务通知的发送使用vTaskNotifyGiveFromISR(),而二值信号量的释放使用xSemaphoreGiveFromISR()。两者在用法上相似,但任务通知存在一些特有的陷阱。

2. 在ISR中使用任务通知的正确步骤

2.1 创建任务并获取任务句柄

首先创建接收通知的任务,并保存其句柄:

TaskHandle_t xReceiverTaskHandle = NULL;

void vReceiverTask(void *pvParameters) {
    for (;;) {
        // 等待通知,无限期阻塞
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        // 处理事件
        process_event();
    }
}

void create_receiver_task(void) {
    xTaskCreate(vReceiverTask, "Receiver", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, &xReceiverTaskHandle);
}

2.2 在ISR中发送通知

在中断服务程序中,使用vTaskNotifyGiveFromISR()发送通知,并处理任务切换:

void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 清除中断标志
    EXTI->PR = EXTI_PR_PR0;
    // 发送通知
    vTaskNotifyGiveFromISR(xReceiverTaskHandle, &xHigherPriorityTaskWoken);
    // 如果有更高优先级任务就绪,则请求切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

2.3 关键配置

  • 确保在FreeRTOSConfig.h中启用任务通知:configUSE_TASK_NOTIFICATIONS 设置为1(默认启用)。
  • 中断优先级配置:调用FreeRTOS API的中断优先级必须低于configMAX_SYSCALL_INTERRUPT_PRIORITY(数值上更高),否则会触发断言。

3. 完整代码示例

以下示例展示使用任务通知替代二值信号量,在GPIO中断中通知任务处理按键事件。

#include "FreeRTOS.h"
#include "task.h"
#include "stm32f4xx_hal.h"

TaskHandle_t xButtonTaskHandle = NULL;

// 任务函数
void vButtonTask(void *pvParameters) {
    for (;;) {
        // 等待通知,清除通知值
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        // 简单消抖
        vTaskDelay(pdMS_TO_TICKS(20));
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
            // 处理按键事件
            HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
        }
    }
}

// 初始化函数
void button_init(void) {
    // 创建任务
    xTaskCreate(vButtonTask, "Button", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 2, &xButtonTaskHandle);
    // 配置GPIO和中断(略)
}

// 中断服务程序
void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    // 清除中断标志
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
    // 发送通知
    vTaskNotifyGiveFromISR(xButtonTaskHandle, &xHigherPriorityTaskWoken);
    // 必要时切换任务
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

4. 常见陷阱与注意事项

4.1 通知值覆盖

任务通知的值是32位,如果多次调用vTaskNotifyGiveFromISR(),通知值会累加(默认行为),但ulTaskNotifyTake(pdTRUE, ...)会一次性清除所有通知。若希望每次通知只对应一次处理,应使用ulTaskNotifyTake(pdFALSE, ...),但这样只减少计数而不清零。陷阱:若使用pdTRUE,多次中断可能合并为一次处理,导致事件丢失。

4.2 中断优先级配置错误

调用vTaskNotifyGiveFromISR()的中断优先级必须不高于configMAX_SYSCALL_INTERRUPT_PRIORITY(即优先级数值大于等于该值)。否则会触发configASSERT断言。注意:Cortex-M中优先级数值越低优先级越高,务必在CubeMX或启动文件中正确设置。

4.3 未处理任务切换

vTaskNotifyGiveFromISR()的第二个参数pxHigherPriorityTaskWoken必须初始化为pdFALSE,并在中断结束前调用portYIELD_FROM_ISR()。否则,即使唤醒了更高优先级任务,也不会立即切换,导致实时性下降。

4.4 任务句柄无效

在任务创建之前或任务删除之后,任务句柄可能为NULL,此时调用vTaskNotifyGiveFromISR()会导致未定义行为。建议:在发送通知前检查句柄是否有效,或确保任务已创建。

4.5 与二值信号量的行为差异

二值信号量是独立对象,可以被多个任务等待;而任务通知直接关联到特定任务,只能通知一个任务。若需要多个任务同步,应使用信号量或事件组。

4.6 中断延迟与通知合并

如果中断频率很高,任务可能来不及处理,通知值会累积(如果使用pdFALSE)或合并(如果使用pdTRUE)。根据应用需求选择合适的方式,必要时使用队列缓冲事件。

5. 总结

任务通知是FreeRTOS中高效的同步机制,在ISR中使用vTaskNotifyGiveFromISR()替代二值信号量可以节省内存并提升性能。但必须注意中断优先级配置、任务切换处理、通知值语义等陷阱。正确使用后,它能成为嵌入式系统中轻量级同步的利器。