Task 笔记

目录

队列、信号量、互斥量都是多对多,我们不知道是谁在参与。如果我们想单独通知一个特定的任务怎么办呢?使用任务通知

任务通知的基本概念

什么是任务通知

任务通知是FreeRTOS中最高效的轻量级通信机制,可以理解为每个任务自带的"邮箱":

  • 每个任务都有一个内置的通知值:就像每个人都有一个专属收件箱
  • 直接通知目标任务:无需创建中间对象,直接向目标任务的"邮箱"投递消息
  • 多种通信模式:可以模拟二进制信号量、计数信号量、事件组、轻量级队列

任务通知的形象比喻

邮箱系统: - 每个任务就像有一个专属邮箱(通知值) - 其他任务可以直接向这个邮箱投递信件(发送通知) - 任务可以随时检查自己的邮箱(接收通知) - 邮箱可以设置不同的接收规则(通知动作)

任务通知的核心特点

性能优势

速度最快: - 比队列快45% - 比二进制信号量快45% - 比计数信号量快10%

内存开销最小: - 每个任务只需要额外8字节存储通知值 - 无需创建独立的消息对象 - 零内存分配开销

功能特性

灵活性高: - 可以模拟多种通信机制 - 支持数据传递和事件通知 - 提供丰富的通知动作选项

直接通信: - 一对一通信模式 - 无需中间通信对象 - 直接操作目标任务状态

局限性

  • 只能一对一通信:一个通知只能发给一个特定任务
  • 无存储队列:只能保存一个通知值,新通知可能覆盖旧值
  • 需要知道目标任务句柄:必须持有目标任务的TaskHandle_t

FreeRTOS任务通知操作函数

发送通知函数

xTaskNotify - 通用通知发送

BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, 
                      uint32_t ulValue, 
                      eNotifyAction eAction);

参数说明: - xTaskToNotify:目标任务句柄 - ulValue:通知值 - eAction:通知动作类型

通知动作类型

typedef enum {
    eNoAction = 0,           // 仅更新通知状态,不修改通知值
    eSetBits,                // 设置通知值的指定位
    eIncrement,              // 递增通知值
    eSetValueWithOverwrite,  // 直接覆盖通知值
    eSetValueWithoutOverwrite // 仅在通知未处理时覆盖
} eNotifyAction;

xTaskNotifyFromISR - 中断中发送通知

BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify,
                             uint32_t ulValue,
                             eNotifyAction eAction,
                             BaseType_t *pxHigherPriorityTaskWoken);

xTaskNotifyGive - 轻量级信号量发送

void xTaskNotifyGive(TaskHandle_t xTaskToNotify);

相当于xTaskNotify(xTaskToNotify, 0, eIncrement)

xTaskNotifyAndQuery - 带查询的发送

BaseType_t xTaskNotifyAndQuery(TaskHandle_t xTaskToNotify,
                              uint32_t ulValue,
                              eNotifyAction eAction,
                              uint32_t *pulPreviousNotifyValue);

接收通知函数

ulTaskNotifyTake - 轻量级信号量接收

uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit,
                         TickType_t xTicksToWait);

专为模拟信号量设计,返回通知值的当前值。

xTaskNotifyWait - 通用通知接收

BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
                          uint32_t ulBitsToClearOnExit,
                          uint32_t *pulNotificationValue,
                          TickType_t xTicksToWait);

功能最全面的通知接收函数,支持位操作和数据获取。

查询函数

uxTaskNotifyGetCounter - 获取通知计数器

uint32_t uxTaskNotifyGetCounter(TaskHandle_t xTask);

xTaskNotifyStateClear - 清除通知状态

BaseType_t xTaskNotifyStateClear(TaskHandle_t xTask);

FreeRTOS任务通知使用模式

模式1:模拟二进制信号量

发送方

void vSenderTask(void *pvParameters) {
    TaskHandle_t xReceiverHandle = (TaskHandle_t)pvParameters;

    while(1) {
        // 完成工作后通知接收方
        do_work();

        // 发送通知(相当于give信号量)
        xTaskNotifyGive(xReceiverHandle);

        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
}

接收方

void vReceiverTask(void *pvParameters) {
    uint32_t ulNotificationValue;

    while(1) {
        // 等待通知(相当于take信号量)
        ulNotificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

        if(ulNotificationValue > 0) {
            // 处理工作
            process_work();
        }
    }
}

模式2:模拟事件组

发送方

void vEventSenderTask(void *pvParameters) {
    TaskHandle_t xProcessorHandle = (TaskHandle_t)pvParameters;

    while(1) {
        // 设置不同的事件位
        if(sensor1_ready()) {
            xTaskNotify(xProcessorHandle, (1UL << 0), eSetBits);  // 事件位0
        }

        if(sensor2_ready()) {
            xTaskNotify(xProcessorHandle, (1UL << 1), eSetBits);  // 事件位1
        }

        vTaskDelay(50 / portTICK_PERIOD_MS);
    }
}

接收方

void vEventProcessorTask(void *pvParameters) {
    uint32_t ulNotifiedValue;
    const uint32_t ALL_EVENTS_MASK = (1UL << 0) | (1UL << 1);

    while(1) {
        // 等待特定事件位
        xTaskNotifyWait(0,                          // 进入时不清除位
                       ALL_EVENTS_MASK,            // 退出时清除所有事件位
                       &ulNotifiedValue,           // 获取通知值
                       portMAX_DELAY);

        if((ulNotifiedValue & ALL_EVENTS_MASK) == ALL_EVENTS_MASK) {
            printf("All events received!\n");
            process_all_events();
        } else if(ulNotifiedValue & (1UL << 0)) {
            printf("Event 0 received\n");
            process_event_0();
        } else if(ulNotifiedValue & (1UL << 1)) {
            printf("Event 1 received\n");
            process_event_1();
        }
    }
}

模式3:数据传递

发送方

void vDataSenderTask(void *pvParameters) {
    TaskHandle_t xReceiverHandle = (TaskHandle_t)pvParameters;
    uint32_t data_counter = 0;

    while(1) {
        // 准备数据
        uint32_t data_packet = (data_counter++ & 0xFFFFFF) | (1UL << 31);

        // 发送数据(覆盖模式)
        xTaskNotify(xReceiverHandle, data_packet, eSetValueWithOverwrite);

        vTaskDelay(200 / portTICK_PERIOD_MS);
    }
}

接收方

void vDataReceiverTask(void *pvParameters) {
    uint32_t received_data;

    while(1) {
        // 接收数据
        if(xTaskNotifyWait(0, 0, &received_data, 1000 / portTICK_PERIOD_MS) == pdTRUE) {
            if(received_data & (1UL << 31)) {
                uint32_t actual_data = received_data & 0xFFFFFF;
                printf("Received data: %lu\n", actual_data);
            }
        } else {
            printf("No data received within timeout\n");
        }
    }
}

模式4:计数信号量

发送方

void vResourceProducerTask(void *pvParameters) {
    TaskHandle_t xConsumerHandle = (TaskHandle_t)pvParameters;

    while(1) {
        // 生产多个资源
        for(int i = 0; i < 3; i++) {
            xTaskNotifyGive(xConsumerHandle);  // 增加计数
        }

        vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
}

接收方

void vResourceConsumerTask(void *pvParameters) {
    uint32_t available_resources;

    while(1) {
        // 获取可用资源数量(不清除计数)
        available_resources = ulTaskNotifyTake(pdFALSE, 0);

        if(available_resources > 0) {
            // 使用一个资源(减少计数)
            ulTaskNotifyTake(pdTRUE, 0);

            printf("Using resource, %lu remaining\n", available_resources - 1);
            use_resource();
        } else {
            // 等待资源可用
            available_resources = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
            printf("Resource acquired, now using\n");
            use_resource();
        }

        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

任务通知最佳实践

使用场景选择

适合使用任务通知的场景: - 一对一任务通信 - 轻量级同步需求 - 性能敏感的应用 - 内存受限的系统 - 简单的数据传递

不适合使用任务通知的场景: - 一对多通信(使用事件组或队列) - 需要存储多个消息(使用队列) - 多个任务等待同一资源(使用信号量/互斥量) - 复杂的数据结构传递(使用队列)

性能优化技巧

减少通知丢失

// 使用不覆盖模式保护重要数据
xTaskNotify(xTargetTask, important_data, eSetValueWithoutOverwrite);

// 使用位操作累加事件
xTaskNotify(xTargetTask, event_mask, eSetBits);

合理选择通知动作

// 同步场景 - 使用递增
xTaskNotifyGive(xTargetTask);

// 事件场景 - 使用位设置  
xTaskNotify(xTargetTask, EVENT_MASK, eSetBits);

// 数据传递 - 使用覆盖
xTaskNotify(xTargetTask, data_value, eSetValueWithOverwrite);

错误处理策略

检查发送结果

BaseType_t result = xTaskNotify(xTargetTask, value, action);
if(result != pdPASS) {
    // 处理发送失败
    handle_notification_failure();
}

处理接收超时

if(xTaskNotifyWait(ulBitsToClearOnEntry, ulBitsToClearOnExit, 
                   &ulNotifiedValue, reasonable_timeout) != pdTRUE) {
    // 处理超时情况
    handle_notification_timeout();
}

CMSIS-RTOS v2 任务通知等效机制

CMSIS-RTOS v2 中的任务通知替代方案

CMSIS-RTOS v2 没有直接等效的任务通知机制,但提供了线程标志(Thread Flags) 作为最接近的替代方案。线程标志提供了类似任务通知的功能,但实现方式有所不同。

线程标志(Thread Flags)

线程标志的基本概念

线程标志是CMSIS-RTOS v2中每个线程自带的32位标志寄存器,具有以下特点:

  • 每个线程都有32个标志位:每个线程拥有独立的32位标志寄存器
  • 直接线程间通信:任何线程都可以设置其他线程的标志位
  • 多种等待选项:支持等待任意标志、所有标志或特定组合
  • 无存储限制:不会像FreeRTOS任务通知那样被新值覆盖

线程标志 vs FreeRTOS任务通知

特性 FreeRTOS任务通知 CMSIS-RTOS v2线程标志
存储大小 32位值 32个独立标志位
数据传递 可以传递32位数据 只能传递标志位状态
覆盖行为 可能被新通知覆盖 标志位可以独立设置/清除
通知动作 多种动作类型 只有设置标志位
性能 非常高
灵活性 中等 高(32个独立标志)

CMSIS-RTOS v2 线程标志函数

osThreadFlagsSet - 设置线程标志

uint32_t osThreadFlagsSet(osThreadId_t thread_id, uint32_t flags);

参数: - thread_id:目标线程ID - flags:要设置的标志位掩码

返回值: - 成功:设置后线程的标志位状态 - 失败:最高位为1的错误代码

使用示例

// 设置线程的标志位0和2
uint32_t result = osThreadFlagsSet(target_thread, (1UL << 0) | (1UL << 2));
if((result & 0x80000000) == 0) {
    printf("Flags set successfully, current flags: 0x%08lX\n", result);
} else {
    printf("Failed to set flags: error 0x%08lX\n", result);
}

osThreadFlagsWait - 等待线程标志

uint32_t osThreadFlagsWait(uint32_t flags, uint32_t options, uint32_t timeout);

参数: - flags:要等待的标志位掩码 - options:等待选项 - timeout:超时时间(毫秒)

等待选项

osFlagsWaitAny    // 等待任意指定标志位
osFlagsWaitAll    // 等待所有指定标志位
osFlagsNoClear    // 等待后不清除标志位

使用示例

// 等待标志位0或1(任意一个)
uint32_t received_flags = osThreadFlagsWait((1UL << 0) | (1UL << 1), 
                                           osFlagsWaitAny, 
                                           osWaitForever);

// 等待标志位2和3(两个都必须设置)
uint32_t received_flags = osThreadFlagsWait((1UL << 2) | (1UL << 3),
                                           osFlagsWaitAll,
                                           5000);  // 5秒超时

osThreadFlagsClear - 清除线程标志

uint32_t osThreadFlagsClear(uint32_t flags);

osThreadFlagsGet - 获取当前线程标志状态

uint32_t osThreadFlagsGet(void);

辅助函数

osThreadGetId - 获取当前线程ID

osThreadId_t osThreadGetId(void);

线程标志使用模式

模式1:模拟二进制信号量

FreeRTOS任务通知方式

// 发送方
xTaskNotifyGive(xReceiverTask);

// 接收方
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

CMSIS-RTOS v2线程标志方式

// 发送方
osThreadFlagsSet(receiver_thread, 0x01);  // 使用标志位0作为信号量

// 接收方
osThreadFlagsWait(0x01, osFlagsWaitAny, osWaitForever);  // 等待后自动清除

模式2:模拟事件组

FreeRTOS任务通知方式

// 发送方
xTaskNotify(xTargetTask, (1UL << 0) | (1UL << 2), eSetBits);

// 接收方
xTaskNotifyWait(0, (1UL << 0) | (1UL << 2), &value, portMAX_DELAY);

CMSIS-RTOS v2线程标志方式

// 发送方
osThreadFlagsSet(target_thread, (1UL << 0) | (1UL << 2));

// 接收方
uint32_t flags = osThreadFlagsWait((1UL << 0) | (1UL << 2), 
                                  osFlagsWaitAny, 
                                  osWaitForever);

模式3:多事件分离处理

CMSIS-RTOS v2特有优势

void event_handler_thread(void *argument) {
    uint32_t flags;

    while(1) {
        // 等待多种类型的事件
        flags = osThreadFlagsWait(0x000000FF, osFlagsWaitAny, osWaitForever);

        // 根据具体标志位处理不同事件
        if(flags & 0x01) {
            handle_temperature_event();
            osThreadFlagsClear(0x01);  // 明确清除已处理的事件
        }
        if(flags & 0x02) {
            handle_humidity_event();
            osThreadFlagsClear(0x02);
        }
        if(flags & 0x04) {
            handle_pressure_event();
            osThreadFlagsClear(0x04);
        }
        if(flags & 0x08) {
            handle_system_event();
            osThreadFlagsClear(0x08);
        }
        // ... 可以处理最多32种独立事件
    }
}

线程标志高级用法

标志位管理策略

按功能分组标志位

// 传感器相关标志(低8位)
#define SENSOR_MASK          0x000000FF
#define TEMPERATURE_EVENT    (1UL << 0)
#define HUMIDITY_EVENT       (1UL << 1)
#define PRESSURE_EVENT       (1UL << 2)

// 系统事件标志(中8位)  
#define SYSTEM_MASK          0x0000FF00
#define SYSTEM_ALERT         (1UL << 8)
#define CONFIG_CHANGE        (1UL << 9)
#define DATA_BACKUP          (1UL << 10)

// 用户事件标志(高8位)
#define USER_MASK           0x00FF0000
#define USER_INPUT           (1UL << 16)
#define DISPLAY_UPDATE       (1UL << 17)

多条件等待

void advanced_wait_example(void) {
    uint32_t flags;

    // 方案1:等待任意传感器事件或系统警报
    flags = osThreadFlagsWait(SENSOR_MASK | SYSTEM_ALERT, 
                             osFlagsWaitAny, 
                             osWaitForever);

    // 方案2:等待温度+湿度事件同时发生
    flags = osThreadFlagsWait(TEMPERATURE_EVENT | HUMIDITY_EVENT,
                             osFlagsWaitAll,
                             1000);  // 1秒超时

    // 方案3:等待但不清除标志(用于监控)
    flags = osThreadFlagsWait(USER_INPUT, 
                             osFlagsWaitAny | osFlagsNoClear,
                             500);
}

错误处理最佳实践

检查函数返回值

uint32_t result = osThreadFlagsSet(target_thread, flags);
if(result & 0x80000000) {
    // 处理错误
    switch(result) {
        case osErrorParameter:
            printf("Error: Invalid thread ID or flags\n");
            break;
        case osErrorResource:
            printf("Error: Thread flags resource not available\n");
            break;
        case osErrorISR:
            printf("Error: Cannot call from ISR context\n");
            break;
        default:
            printf("Error: Unknown error (0x%08lX)\n", result);
    }
}

超时处理策略

uint32_t flags = osThreadFlagsWait(EVENT_MASK, osFlagsWaitAny, timeout_ms);
if((flags & 0x80000000) == 0) {
    // 成功接收到标志
    process_events(flags);
} else {
    // 超时或其他错误
    if(flags == osErrorTimeout) {
        handle_timeout();
    } else {
        handle_error(flags);
    }
}

与FreeRTOS任务通知的迁移指南

函数映射表

FreeRTOS任务通知 CMSIS-RTOS v2线程标志 说明
xTaskNotifyGive() osThreadFlagsSet(thread, 0x01) 简单信号量模拟
ulTaskNotifyTake(pdTRUE, timeout) osThreadFlagsWait(0x01, osFlagsWaitAny, timeout) 接收并清除
xTaskNotify(task, value, eSetBits) osThreadFlagsSet(thread, flags) 设置标志位
xTaskNotifyWait(0, mask, &value, timeout) osThreadFlagsWait(mask, osFlagsWaitAny, timeout) 等待标志位
uxTaskNotifyGetCounter() osThreadFlagsGet() 获取当前标志状态

迁移注意事项

数据传递差异

// FreeRTOS:可以传递32位数据
uint32_t sensor_data = 0x12345678;
xTaskNotify(xProcessor, sensor_data, eSetValueWithOverwrite);

// CMSIS-RTOS v2:只能传递标志位状态,需要其他方式传递数据
// 方案1:使用消息队列传递数据
osMessageQueuePut(data_queue, &sensor_data, 0, 0);
osThreadFlagsSet(processor_thread, DATA_READY_FLAG);

// 方案2:使用全局变量+标志位
g_sensor_data = sensor_data;
osThreadFlagsSet(processor_thread, DATA_READY_FLAG);

覆盖行为差异

// FreeRTOS:新通知可能覆盖旧值
xTaskNotify(xTask, value1, eSetValueWithOverwrite);  // 可能丢失value1
xTaskNotify(xTask, value2, eSetValueWithOverwrite);

// CMSIS-RTOS v2:标志位可以累积,不会被覆盖
osThreadFlagsSet(thread, FLAG1);  // FLAG1保持设置
osThreadFlagsSet(thread, FLAG2);  // FLAG1和FLAG2都保持设置

性能考虑

CMSIS-RTOS v2线程标志的优势: - 32个独立标志位,不会相互覆盖 - 标志位状态持久化,直到显式清除 - 支持复杂的等待条件组合

FreeRTOS任务通知的优势: - 可以传递32位任意数据 - 更灵活的通知动作类型 - 略微更好的性能