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. 总结
在只需一对一同步的场景中,任务通知是二值信号量的高效替代方案,可节省内存并提升性能。但在需要多任务等待或复杂同步时,二值信号量仍是更合适的选择。开发者应根据实际需求权衡。
暂无评论,来抢沙发~