4. 任务创建与管理:任务函数、任务句柄、任务堆栈、动态与静态创建

好,咱们今天聊聊任务创建。这是RTOS里最基础、也最核心的操作。你想想看,整个系统就是由一个个任务撑起来的,任务怎么生出来、怎么管好,直接决定了系统稳不稳。

我个人习惯把任务创建比作「招人干活」。你得先写好岗位职责(任务函数),再给员工配个工位(任务堆栈),最后发个工牌(任务句柄)方便管理。至于招人是走劳务派遣(动态创建)还是正式编制(静态创建),那就看项目需求了。

4.1 任务函数——任务的灵魂

任务函数说白了就是任务要执行的代码。它长什么样?我直接给你看个最典型的模板:

void vTaskFunction(void *pvParameters)
{
    // 初始化工作
    for(;;)
    {
        // 任务主体代码
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

注意几个关键点:

  • 无限循环:任务函数几乎都是死循环。为什么?因为任务一旦返回,就被系统回收了。除非你故意设计成一次性任务。
  • 参数指针:void *pvParameters 可以传任何东西。我做过一个项目,用这个参数传结构体指针,一个函数体跑了5个不同任务,省了不少代码量。
  • 不能阻塞:千万别在任务函数里用 while(1) 空转,那是裸机思维。要用 vTaskDelay 或信号量让出CPU。
小技巧:任务函数名最好带个「v」前缀,表示返回void。这是FreeRTOS的命名习惯,团队协作时一眼就能认出是任务函数。

4.2 任务句柄——任务的身份证

任务句柄是什么?就是一个指向任务控制块(TCB)的指针。你有了它,就能对任务为所欲为——挂起、恢复、删除、改优先级。

声明方式很简单:

TaskHandle_t xTaskHandle = NULL;

创建任务时,把句柄地址传进去:

xTaskCreate(vTaskFunction, "Task1", 256, NULL, 1, &xTaskHandle);

之后想操作这个任务,直接拿句柄说话:

vTaskSuspend(xTaskHandle);   // 挂起
vTaskResume(xTaskHandle);    // 恢复
vTaskDelete(xTaskHandle);    // 删除
避坑指南:我曾经在一个产品里忘了初始化句柄为NULL,结果任务创建失败后,后续代码拿着野指针去操作任务,直接硬故障。记住:句柄用前先清零,这是保命习惯。

4.3 任务堆栈——任务的工位

每个任务都有自己的堆栈,用来存局部变量、函数调用现场、中断返回地址。堆栈大小直接决定了任务能玩多大。

堆栈设多大?我一般这么估算:

  • 任务里调用的函数嵌套深度 × 每个函数的局部变量大小
  • 中断嵌套可能占用的栈空间
  • 加上20%的安全余量

举个例子,一个简单的传感器读取任务:

// 堆栈大小单位是字(4字节),不是字节!
#define TASK_STACK_SIZE  256

xTaskCreate(vSensorTask, "Sensor", TASK_STACK_SIZE, NULL, 2, NULL);

256个字 = 1024字节。够用吗?我实测过,如果任务里只调两三层函数,128个字都够。但如果你用 printf、sprintf 这种重型函数,堆栈分分钟爆掉。

经验之谈:调试阶段把堆栈设大两倍,稳定后再慢慢缩小。FreeRTOS 提供了 uxTaskGetStackHighWaterMark() 函数,可以查看任务实际用了多少堆栈。我每次做项目都会在调试串口打印这个值,心里踏实。

4.4 动态创建 vs 静态创建

这是新手最容易纠结的地方。我直接给你对比表:

对比项 动态创建 (xTaskCreate) 静态创建 (xTaskCreateStatic)
堆栈来源 从堆(heap)分配 用户自己提供数组
TCB来源 从堆分配 用户自己提供 StaticTask_t 结构体
灵活性 高,运行时动态创建/删除 低,编译时就定死了
安全性 可能内存碎片或分配失败 绝对可靠,不会分配失败
适用场景 原型开发、任务数量动态变化 安全关键系统、资源受限设备

动态创建代码:

if(xTaskCreate(vTask, "Task", 256, NULL, 1, &xHandle) != pdPASS)
{
    // 创建失败!处理错误
    printf("Task creation failed!\n");
}

静态创建代码:

StackType_t xTaskStack[256];
StaticTask_t xTaskTCB;

TaskHandle_t xHandle = xTaskCreateStatic(
    vTask,          // 任务函数
    "Task",         // 任务名
    256,            // 堆栈大小
    NULL,           // 参数
    1,              // 优先级
    xTaskStack,     // 堆栈数组
    &xTaskTCB       // TCB结构体
);

if(xHandle == NULL)
{
    // 创建失败(理论上不会,但还是要检查)
}
我的建议:商业产品我推荐静态创建。为什么?因为动态分配可能失败,而洪水预警系统这种7×24小时运行的关键设备,最怕的就是「跑着跑着突然创建不了任务」。静态创建虽然代码啰嗦点,但心里踏实。

4.5 创建任务的完整流程

最后,我给你总结一个实战流程。这是我做洪水预警数据采集节点时的标准做法:

  1. 定义任务函数:写好无限循环,里面放具体业务逻辑
  2. 声明句柄和堆栈:全局变量,方便其他任务操作
  3. 调用创建函数:在 main 或启动任务里创建
  4. 检查返回值:创建失败要记录日志并尝试恢复
  5. 启动调度器:vTaskStartScheduler()

嗯,这里要注意:创建任务本身也是在任务里执行的。我见过有人把 xTaskCreate 放在中断里,结果系统直接崩了。记住,RTOS API 大部分不能在中断服务函数里调用。

好了,任务创建这块就聊到这儿。下一节咱们讲任务状态与切换,到时候你会明白为什么任务有时候「睡着了叫不醒」。