3. 任务创建与删除:FreeRTOS任务创建API详解、任务参数传递、任务删除的注意事项

好,咱们进入实战环节的第一个硬核知识点——任务的创建与删除。

说实话,任务调度是RTOS的灵魂,而任务创建就是灵魂的“出生证明”。很多初学者觉得不就是调个API嘛,有什么难的?但我见过太多项目因为任务创建时埋下的坑,导致后期调试痛不欲生。今天我就把这块掰开了揉碎了讲清楚。

3.1 xTaskCreate:最常用的任务创建API

FreeRTOS里创建任务,最核心的就是 xTaskCreate() 这个函数。它的原型长这样:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(仅用于调试)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(word)
    void *pvParameters,             // 传递给任务函数的参数
    UBaseType_t uxPriority,         // 任务优先级
    TaskHandle_t *pxCreatedTask     // 返回的任务句柄
);

返回值只有两种可能:pdPASS 表示成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 表示内存不够。嗯,说白了就是要么成了,要么堆空间不够。

重点提醒: 栈深度单位是“字”,不是字节!在32位MCU上,1个字 = 4字节。如果你要分配1024字节的栈,参数要填256。我见过有人直接填1024,结果栈空间浪费了4倍,还一脸懵。

3.2 任务参数传递的几种姿势

任务参数传递,说白了就是 pvParameters 这个 void* 指针怎么玩。我总结了几种常见用法:

3.2.1 传递整数值

最简单的方式,直接把整数强转成指针传进去:

void vTaskFunction(void *pvParameters) {
    int task_id = (int)pvParameters;
    // 使用task_id
}

// 创建时
xTaskCreate(vTaskFunction, "Task1", 256, (void*)1, 1, NULL);
xTaskCreate(vTaskFunction, "Task2", 256, (void*)2, 1, NULL);

注意:这种方式只适合传递小整数。在64位系统上要小心指针宽度问题。

3.2.2 传递结构体指针

这是实际项目中最常用的方式。我在做充电宝租借系统时,每个任务都需要知道自己的“身份信息”:

typedef struct {
    uint8_t  task_id;
    uint8_t  priority;
    uint16_t timeout_ms;
    void     (*callback)(void);
} TaskConfig_t;

TaskConfig_t config1 = {1, 2, 100, led_blink_callback};

void vTaskFunction(void *pvParameters) {
    TaskConfig_t *cfg = (TaskConfig_t *)pvParameters;
    // 使用cfg->task_id, cfg->timeout_ms等
}

xTaskCreate(vTaskFunction, "LED_Task", 256, &config1, 2, NULL);

我的习惯: 结构体参数一定要保证生命周期。如果结构体是局部变量,任务还没开始跑它就销毁了,那任务拿到的就是野指针。我一般用静态分配或者动态分配并让任务自己释放。

3.2.3 传递字符串

字符串传递其实也是指针传递,但要小心字符串存储位置:

// 正确:字符串常量存储在Flash/ROM
xTaskCreate(vTask, "Print", 256, "Hello RTOS", 1, NULL);

// 错误:局部数组,任务启动时可能已失效
void create_task_bad(void) {
    char msg[] = "Hello";
    xTaskCreate(vTask, "Print", 256, msg, 1, NULL);
}

3.3 任务删除的注意事项

任务删除比创建更容易出问题。我早期做项目时,就因为任务删除不当导致系统死机,查了两天才找到原因。

3.3.1 谁可以删除任务?

FreeRTOS允许三种删除方式:

  • 任务自己删除自己: 调用 vTaskDelete(NULL)vTaskDelete(xTaskGetCurrentTaskHandle())
  • 其他任务删除指定任务: 调用 vTaskDelete(handle)
  • 中断服务函数中删除: 不推荐!除非你非常清楚后果

3.3.2 删除后的内存回收问题

这是最大的坑。任务删除后,FreeRTOS会回收TCB(任务控制块)和栈空间,但前提是你用的是动态内存分配。如果你用了 pvPortMalloc 在任务里分配了内存,删除任务前必须手动释放!

我曾经踩过的坑: 有一个数据采集任务,每次运行都malloc一块缓冲区。任务结束时我直接调了vTaskDelete,没释放那块内存。结果系统跑了三天后,内存耗尽,所有任务都创建失败。从那以后,我养成了一个习惯:任务退出前,先清理所有动态资源,再调用vTaskDelete。

3.3.3 删除任务的正确姿势

我推荐的做法是:任务自己清理,自己删除。

void vDataTask(void *pvParameters) {
    uint8_t *buffer = (uint8_t *)pvPortMalloc(1024);
    
    // 任务主循环
    for(;;) {
        // 处理数据...
        if(should_exit) {
            break;  // 跳出循环准备退出
        }
    }
    
    // 清理工作
    if(buffer != NULL) {
        vPortFree(buffer);
        buffer = NULL;
    }
    
    // 关闭硬件资源
    close_sensor();
    
    // 最后删除自己
    vTaskDelete(NULL);
}

3.3.4 删除后句柄的处理

任务删除后,之前保存的 TaskHandle_t 就失效了。如果你还拿着这个句柄去操作,比如挂起、恢复、发通知,那系统行为是未定义的。我建议删除任务后立即将句柄置为NULL:

TaskHandle_t xHandle = NULL;

// 创建
xTaskCreate(vTask, "Demo", 256, NULL, 1, &xHandle);

// 删除
if(xHandle != NULL) {
    vTaskDelete(xHandle);
    xHandle = NULL;  // 关键!防止悬空指针
}

3.4 实战中的经验总结

场景 推荐做法 不推荐做法
传递简单标识 用整数强转void* 用全局变量传参
传递复杂配置 用静态结构体 用局部变量结构体
任务退出 任务自己清理并删除 外部强制删除
内存管理 谁分配谁释放 任务删除后依赖系统回收

最后说一句: 任务创建和删除看似简单,但它们是RTOS稳定性的基石。你想想看,一个系统里几十个任务,如果每个任务创建时都多分配100字节栈,那内存就白白浪费了几KB。如果删除时漏了一个内存释放,那系统迟早会崩。我的建议是:写任务代码时,先把退出逻辑想清楚,再写主循环逻辑。这个顺序反过来,往往就是bug的开始。

好了,任务创建与删除的核心内容就这些。下一章咱们聊聊任务调度与优先级,那才是真正体现RTOS魅力的地方。