FreeRTOS任务通知替代二值信号量:内存与性能实测对比

1. 原理讲解

1.1 二值信号量

二值信号量是FreeRTOS中用于任务同步或中断与任务同步的机制。它基于队列实现,包含一个等待任务列表和一个计数值(0或1)。获取信号量时,如果计数值为1则减为0并返回成功;否则任务阻塞。释放信号量时,如果有任务等待则唤醒最高优先级任务,否则计数值置1。

1.2 任务通知

任务通知是FreeRTOS 8.2版本引入的特性,每个任务有一个32位的通知值和一个通知状态。任务通知可以直接向指定任务发送数据或事件,无需创建额外的内核对象。当用作二值信号量时,发送方调用xTaskNotifyGive(),接收方调用ulTaskNotifyTake()。

1.3 关键差异

  • 内存占用:二值信号量需要动态分配队列结构体(约80字节),而任务通知仅使用任务控制块(TCB)中已有的字段,无额外内存。
  • 性能:任务通知直接操作TCB,无需遍历等待列表,操作更快;二值信号量涉及队列操作和列表管理,开销更大。
  • 功能限制:任务通知只能一对一通信,且无法在中断中阻塞等待;二值信号量支持多任务等待。

2. 配置步骤

2.1 硬件与软件环境

  • STM32F407VG(168MHz,1MB Flash,192KB RAM)
  • FreeRTOS v10.4.3(CMSIS-RTOS v2封装)
  • STM32CubeIDE 1.11.0
  • 使用系统节拍频率1000Hz,configUSE_TASK_NOTIFICATIONS设为1(默认)。

2.2 创建测试任务

创建两个任务:发送任务(高优先级)和接收任务(低优先级),分别使用二值信号量和任务通知实现同步。通过GPIO翻转和DWT周期计数器测量性能。

3. 完整代码示例

3.1 二值信号量版本

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

SemaphoreHandle_t xBinarySem;

void vSenderTask(void *pvParameters) {
    while (1) {
        // 模拟事件发生
        vTaskDelay(pdMS_TO_TICKS(100));
        xSemaphoreGive(xBinarySem);
    }
}

void vReceiverTask(void *pvParameters) {
    while (1) {
        if (xSemaphoreTake(xBinarySem, portMAX_DELAY) == pdTRUE) {
            // 处理事件
            GPIOA->ODR ^= GPIO_PIN_5; // 翻转LED
        }
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    
    xBinarySem = xSemaphoreCreateBinary();
    xTaskCreate(vSenderTask, "Sender", 128, NULL, 2, NULL);
    xTaskCreate(vReceiverTask, "Receiver", 128, NULL, 1, NULL);
    vTaskStartScheduler();
    while (1);
}

3.2 任务通知版本

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

TaskHandle_t xReceiverHandle;

void vSenderTask(void *pvParameters) {
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(100));
        xTaskNotifyGive(xReceiverHandle);
    }
}

void vReceiverTask(void *pvParameters) {
    while (1) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        GPIOA->ODR ^= GPIO_PIN_5;
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    
    xTaskCreate(vReceiverTask, "Receiver", 128, NULL, 1, &xReceiverHandle);
    xTaskCreate(vSenderTask, "Sender", 128, NULL, 2, NULL);
    vTaskStartScheduler();
    while (1);
}

3.3 性能测量代码(DWT)

// 在发送任务中测量释放到接收任务唤醒的时间
#define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004)

void vSenderTask(void *pvParameters) {
    uint32_t start, end;
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(100));
        start = DWT_CYCCNT;
        xSemaphoreGive(xBinarySem); // 或 xTaskNotifyGive
        end = DWT_CYCCNT;
        // 记录end-start到全局变量
    }
}

4. 实测结果

4.1 内存占用对比

  • 二值信号量:xSemaphoreCreateBinary()动态分配约80字节(队列结构体+等待列表)。
  • 任务通知:0字节额外内存(仅使用TCB中已有的ulNotifiedValue和ucNotifyState)。
  • 节省:约80字节/信号量,对于大量同步对象可显著降低RAM需求。

4.2 性能对比(168MHz,DWT周期计数)

  • 二值信号量释放(xSemaphoreGive):约120个周期(0.71μs)。
  • 任务通知释放(xTaskNotifyGive):约60个周期(0.36μs)。
  • 上下文切换时间:任务通知比信号量快约50%。

5. 注意事项

  • 一对一限制:任务通知只能向特定任务发送,无法实现多任务等待同一事件。
  • 中断中使用:任务通知可在中断中使用vTaskNotifyGiveFromISR(),但需注意中断优先级和portYIELD_FROM_ISR()。
  • 通知值覆盖:多次发送通知可能覆盖未处理的值,需根据场景选择eSetValueWithOverwrite或eSetValueWithoutOverwrite。
  • 阻塞等待:接收任务使用ulTaskNotifyTake(pdTRUE, timeout)时,pdTRUE表示清除通知值,pdFALSE表示递减计数。
  • 兼容性:确保configUSE_TASK_NOTIFICATIONS为1,且任务通知不支持超时等待多个事件。

6. 总结

在只需一对一同步的场景中,任务通知是二值信号量的高效替代方案,可节省内存并提升性能。但在需要多任务等待或复杂同步时,二值信号量仍是更合适的选择。开发者应根据实际需求权衡。