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

好,咱们今天聊聊任务怎么生,怎么死。

在RTOS里,任务不是写死在代码里的。你可以随时创建它,也可以随时干掉它。这就像消防系统里的探测器——有的设备是固定焊死的,有的则是即插即用的。我个人习惯把动态创建比作「热插拔」,静态创建比作「预埋件」。

4.1 动态创建:xTaskCreate

这是最常用的方式。任务所需的堆栈和TCB(任务控制块)都由RTOS内核从堆里分配。说白了,你只管调用,内存的事交给系统。

函数原型:

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

返回值是pdPASS表示成功,pdFAIL表示失败——通常是堆内存不够了。

我实际项目中踩过的坑:

  • 堆栈深度给太小,任务一跑就崩。我曾经在调试一个多路报警采集任务时,堆栈给了128字,结果跑着跑着就HardFault。后来一查,任务里有个局部数组,直接爆栈了。嗯,后来我养成了习惯:堆栈至少给256字起步,复杂任务给512甚至1024。
  • 任务名别太长,FreeRTOS内部会拷贝,但长度有限制。我一般控制在16字符以内。
  • 优先级别乱给。我见过有人把所有任务都设成同一优先级,结果调度器直接懵了——轮询调度,谁也别想及时响应。

小技巧:如果你不确定堆栈大小,可以先给个大点的值,跑起来后用uxTaskGetStackHighWaterMark()查看实际使用量,再慢慢往下压。我每次做新项目都会这么干一遍。

4.2 静态创建:xTaskCreateStatic

静态创建,说白了就是你自己准备内存。TCB和堆栈都由你提供,内核只管用。这在什么场景下用?

  • 不能动态分配内存的场景——比如军工、医疗、某些安全苛求系统。我做过一个消防主机项目,客户明确要求:不许用malloc。那好,全上静态。
  • 想精确控制内存布局——比如把关键任务的堆栈放在内部SRAM,把普通任务的堆栈放在外部SDRAM。
  • 避免内存碎片——动态创建/删除多了,堆里会出碎片。静态创建没这问题。

函数原型:

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 myTaskStack[256];
static StaticTask_t myTaskTCB;

// 创建任务
TaskHandle_t xHandle = xTaskCreateStatic(
    vMyTask,
    "MyTask",
    256,
    NULL,
    3,
    myTaskStack,
    &myTaskTCB
);

if (xHandle != NULL) {
    // 创建成功
}

警告:静态创建的任务,你提供的缓冲区必须在整个任务生命周期内有效。别在函数里定义局部数组然后传给xTaskCreateStatic——函数返回后栈空间就释放了,任务必挂。我曾经见过一个同事这么干,查了两天才找到原因。

4.3 任务删除:vTaskDelete

任务不是永生的。当它完成了使命,或者系统需要回收资源时,就该删除了。

函数原型:

void vTaskDelete(TaskHandle_t xTaskToDelete);

参数传NULL表示删除自己。我经常在任务函数末尾这么写:

void vOneShotTask(void *pvParameters)
{
    // 做一次性的初始化工作
    InitSensor();
    CalibrateAlarm();
    
    // 干完了,自我了断
    vTaskDelete(NULL);
}

删除时要注意什么?

  • 动态创建的任务:删除后,内核会自动回收TCB和堆栈内存。但任务自己申请的其他资源(比如malloc出来的内存、打开的文件句柄)不会自动释放——你得自己在任务退出前清理干净。
  • 静态创建的任务:删除后,内核只是把任务从调度队列里移除。你提供的TCB和堆栈缓冲区还在原地,可以复用。我一般会在删除静态任务后,把缓冲区清零,然后重新创建。
  • 别删正在运行的任务:准确说,你可以删自己(传NULL),但别从别的任务里删一个正在运行的任务——除非你清楚后果。FreeRTOS里,vTaskDelete会先让目标任务退出运行态,再清理。但如果你删的是比自己优先级高的任务,可能会引发一次调度。

避坑指南:我曾经在消防报警主控任务里,收到一个「自检完成」信号后,直接vTaskDelete把自检任务给删了。结果自检任务里有个定时器还没关,定时器回调里访问了已删除任务的变量——直接崩了。所以,删除前一定要确保所有依赖都清理干净。

4.4 动态 vs 静态:怎么选?

对比项 动态创建 静态创建
内存来源 堆(heap) 你提供
使用难度 简单,一行搞定 需要提前准备缓冲区
内存碎片 频繁创建/删除会产生碎片 无碎片问题
安全性 可能因堆不足而失败 编译时就能确定内存
适用场景 原型开发、任务数量动态变化 安全苛求系统、内存受限系统

我个人习惯:原型阶段全用动态,方便调试。定型阶段,把关键任务(比如报警处理、通信管理)改成静态,确保可靠性。普通任务(比如日志记录、状态显示)继续用动态。

4.5 一个完整的例子

咱们写个消防报警系统的任务创建与删除示例:

// 报警采集任务
void vAlarmCollectTask(void *pvParameters)
{
    for (;;)
    {
        // 采集探测器状态
        uint8_t status = ReadDetector();
        
        if (status == ALARM)
        {
            // 创建报警处理任务
            xTaskCreate(
                vAlarmProcessTask,
                "AlarmProc",
                512,
                (void*)&status,
                5,  // 高优先级
                NULL
            );
        }
        
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 报警处理任务(一次性任务)
void vAlarmProcessTask(void *pvParameters)
{
    uint8_t alarmType = *(uint8_t*)pvParameters;
    
    // 处理报警
    ActivateSiren();
    SendToDisplay(alarmType);
    
    // 处理完毕,自我删除
    vTaskDelete(NULL);
}

void main(void)
{
    // 创建主采集任务
    xTaskCreate(
        vAlarmCollectTask,
        "AlarmCollect",
        256,
        NULL,
        3,
        NULL
    );
    
    vTaskStartScheduler();
}

你看,这里动态创建了报警处理任务。每次有报警就创建一个,处理完就删掉。如果报警频繁,就会频繁创建/删除——这时候就要考虑内存碎片问题了。实际项目中,我一般会用任务池+信号量的方式复用任务,而不是频繁创建删除。但那是后话了。

总结一下:

  • 动态创建:灵活,但要注意堆内存和碎片
  • 静态创建:可靠,但需要你多写几行代码
  • 删除任务:记得清理资源,别留尾巴

嗯,任务创建与删除就这些。下一章咱们聊聊任务的状态切换——你会发现,任务不是只有「运行」和「不运行」两种状态。