第4章 任务创建与删除:动态创建任务(xTaskCreate)、静态创建任务(xTaskCreateStatic)、任务删除(vTaskDelete)

好,咱们今天聊聊RTOS里最基础、也最核心的操作——任务的创建与删除。

你想想看,一个实时系统跑起来,总得有几个任务在干活吧?有的负责采集传感器数据,有的负责刷新屏幕,有的负责处理按键。这些任务怎么来的?又怎么没的?这就是本章要讲清楚的事。

4.1 动态创建任务:xTaskCreate

动态创建任务,说白了就是让RTOS内核帮你分配任务所需的栈空间和任务控制块(TCB)。你只管告诉内核“我要一个什么样的任务”,剩下的内存分配、初始化工作,内核全包了。

我个人习惯在项目初期大量使用动态创建。为什么呢?因为省事。你不需要提前算好每个任务要多少栈,内核会根据你的配置自动从堆里切一块出来。

核心函数原型:

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

返回值是pdPASS表示创建成功,否则是errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。嗯,这里要注意,如果堆不够了,创建就会失败。

一个实际例子:

void vTaskLED(void *pvParameters)
{
    while(1)
    {
        GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void main(void)
{
    TaskHandle_t xLEDHandle = NULL;
    
    if(xTaskCreate(
        vTaskLED,
        "LED Task",
        128,        // 128个字,约512字节
        NULL,
        2,          // 优先级2
        &xLEDHandle
    ) != pdPASS)
    {
        // 创建失败,处理错误
        Error_Handler();
    }
    
    vTaskStartScheduler();
}

我在项目中遇到过一个问题:某个任务创建后一直不运行,查了半天才发现是栈深度给少了。任务里有个局部数组,直接就把栈撑爆了。所以我的建议是——栈深度至少给任务实际需求的1.5倍,留点余量。

小技巧:FreeRTOS提供了uxTaskGetStackHighWaterMark()函数,可以查看任务栈还剩多少空间。调试阶段多调用几次,能帮你找到最合适的栈大小。

4.2 静态创建任务:xTaskCreateStatic

动态创建虽然方便,但有个问题——它依赖堆。如果你的系统对实时性要求极高,或者你压根不想用堆(比如安全关键系统),那就得用静态创建。

静态创建,说白了就是你自己准备好栈空间和TCB的内存,然后告诉内核“用这块内存来创建任务”。内核不会去碰堆,所有内存都是你提前分配好的。

核心函数原型:

TaskHandle_t xTaskCreateStatic(
    TaskFunction_t pvTaskCode,
    const char * const pcName,
    configSTACK_DEPTH_TYPE usStackDepth,
    void *pvParameters,
    UBaseType_t uxPriority,
    StackType_t *puxStackBuffer,    // 你提供的栈缓冲区
    StaticTask_t *pxTaskBuffer      // 你提供的TCB缓冲区
);

注意看,这里多了两个参数:栈缓冲区和TCB缓冲区。这两个都得你自己定义。

实际例子:

// 提前分配好内存
static StackType_t xLEDStack[128];
static StaticTask_t xLEDTaskBuffer;

void main(void)
{
    TaskHandle_t xLEDHandle = NULL;
    
    xLEDHandle = xTaskCreateStatic(
        vTaskLED,
        "LED Static",
        128,
        NULL,
        2,
        xLEDStack,
        &xLEDTaskBuffer
    );
    
    if(xLEDHandle == NULL)
    {
        // 创建失败
        Error_Handler();
    }
    
    vTaskStartScheduler();
}

我曾经在一个航空电子项目中被迫全部使用静态创建。原因很简单——适航认证要求不能有动态内存分配。那时候每个任务的栈大小都是反复测算、写死在代码里的。虽然麻烦,但心里踏实。

避坑指南:静态创建时,你提供的栈缓冲区必须保证生命周期足够长。如果你在函数内部定义一个局部数组作为栈缓冲区,函数返回后这块内存就失效了——任务跑着跑着就崩了。我曾经见过一个同事犯过这个错,排查了整整两天。

4.3 动态 vs 静态:怎么选?

对比项 动态创建 静态创建
内存来源 堆(heap) 你指定的缓冲区
灵活性 高,运行时按需创建 低,编译时就得定好
安全性 可能因堆碎片而失败 无碎片问题
代码量 少,内核帮你管理 多,你得自己准备内存
适用场景 原型开发、资源充足的系统 安全关键、资源受限的系统

我的建议是:项目初期用动态创建快速验证功能,后期优化时再考虑是否换成静态。当然,如果你的目标芯片RAM只有几KB,那从一开始就得精打细算,静态创建可能是唯一选择。

4.4 任务删除:vTaskDelete

任务创建了,总有不需要的时候。比如某个初始化任务,干完活就该把自己清理掉。这时候就用vTaskDelete。

函数原型:

void vTaskDelete(TaskHandle_t xTaskToDelete);

参数是任务句柄。如果传入NULL,表示删除当前正在运行的任务自己。

一个典型用法:

void vInitTask(void *pvParameters)
{
    // 做初始化工作
    InitializeHardware();
    LoadCalibrationData();
    
    // 初始化完成,删除自己
    vTaskDelete(NULL);
}

void main(void)
{
    xTaskCreate(vInitTask, "Init", 256, NULL, 3, NULL);
    vTaskStartScheduler();
}

这里有个关键点:任务被删除后,它占用的栈空间和TCB会被释放回堆(如果是动态创建)。但任务自己申请的其他资源——比如动态分配的内存、打开的文件句柄——不会自动释放。你得在任务退出前自己清理干净。

我曾经踩过的坑:有个任务里用pvPortMalloc分配了一块缓冲区,任务结束时只调了vTaskDelete(NULL),没调用vPortFree。结果系统跑了几天后,堆被耗尽,所有动态创建都失败了。从那以后,我养成了一个习惯——每个任务退出前,必须检查有没有未释放的资源。

另外,删除任务时要注意优先级反转的问题。如果被删除的任务正持有某个互斥量,而其他任务在等这个互斥量——嗯,那就会出大问题。所以我的建议是:确保任务在删除前已经释放了所有持有的同步资源。

4.5 综合示例:一个完整的创建-运行-删除流程

static TaskHandle_t xWorkerHandle = NULL;

void vWorkerTask(void *pvParameters)
{
    int *pData = (int *)pvParameters;
    
    for(int i = 0; i < 10; i++)
    {
        (*pData)++;
        vTaskDelay(pdMS_TO_TICKS(100));
    }
    
    // 任务结束,删除自己
    vTaskDelete(NULL);
}

void vSupervisorTask(void *pvParameters)
{
    int sharedData = 0;
    
    // 动态创建工作任务
    xTaskCreate(
        vWorkerTask,
        "Worker",
        128,
        &sharedData,
        1,
        &xWorkerHandle
    );
    
    // 等待工作任务完成
    vTaskDelay(pdMS_TO_TICKS(1500));
    
    // 如果工作任务还没自己删除,强制删除
    if(xWorkerHandle != NULL)
    {
        vTaskDelete(xWorkerHandle);
        xWorkerHandle = NULL;
    }
    
    // 监督任务继续做其他事
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

这个例子展示了几个要点:

  • 任务之间可以通过参数传递数据指针
  • 任务可以自己删除自己,也可以被其他任务删除
  • 删除后记得把句柄置NULL,避免悬空指针

个人经验:我习惯在任务函数开头加一个断言,检查传入的参数是否有效。比如检查pvParameters是否为NULL。这样一旦传参出错,调试阶段就能立刻发现,而不是等到任务跑飞了才去查。

好了,关于任务的创建与删除,核心内容就这些。动态创建灵活方便,静态创建安全可控,删除时记得清理资源。下一章咱们聊聊任务的挂起和恢复,那又是另一番天地了。