3. 任务创建与删除:FreeRTOS任务创建API、任务参数传递、任务删除与清理、任务句柄管理

好,咱们直接进入正题。任务创建与删除,这是RTOS里最基础的操作,但也是坑最多的地方。我见过不少新手,任务创建得飞起,删除时却留下一堆烂摊子。今天我就把这块掰开了揉碎了讲清楚。

3.1 任务创建API:xTaskCreate 到底怎么用?

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 才算创建成功。我见过有人直接忽略返回值,结果任务没创建成功,系统跑飞了都不知道为什么。

举个例子,创建一个简单的LED闪烁任务:

void vLEDTask(void *pvParameters)
{
    // 参数就是LED的GPIO引脚号
    uint8_t ucLEDPin = (uint8_t)pvParameters;
    
    for(;;)
    {
        GPIO_Toggle(ucLEDPin);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// 在main函数中创建
TaskHandle_t xLEDTaskHandle = NULL;

void main(void)
{
    // 创建任务,传递LED引脚号作为参数
    if(xTaskCreate(
        vLEDTask,           // 任务函数
        "LED Task",         // 任务名
        128,                // 栈大小(128个字 = 512字节)
        (void*)GPIO_PIN_5,  // 参数:LED引脚号
        1,                  // 优先级
        &xLEDTaskHandle     // 句柄
    ) != pdPASS)
    {
        // 创建失败,处理错误
        Error_Handler();
    }
    
    vTaskStartScheduler();
}

这里有个细节:栈大小单位是字(word)。在32位处理器上,1个字 = 4字节。所以128字 = 512字节。我建议新手至少给256字,别太抠门。栈溢出是RTOS里最难排查的问题之一。

3.2 任务参数传递:不只是传个整数那么简单

任务参数 pvParametersvoid* 类型,这意味着你可以传任何东西。我个人习惯传结构体指针,这样能传递多个参数。

举个例子,控制多个电机时:

typedef struct
{
    uint8_t ucMotorID;
    uint16_t usTargetSpeed;
    uint16_t usAccelTime;
} MotorConfig_t;

void vMotorControlTask(void *pvParameters)
{
    MotorConfig_t *pxConfig = (MotorConfig_t*)pvParameters;
    
    for(;;)
    {
        // 使用 pxConfig->ucMotorID, pxConfig->usTargetSpeed 等
        Motor_SetSpeed(pxConfig->ucMotorID, pxConfig->usTargetSpeed);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// 创建多个电机任务
MotorConfig_t xMotor1Config = {1, 1000, 500};
MotorConfig_t xMotor2Config = {2, 2000, 300};

xTaskCreate(vMotorControlTask, "Motor1", 256, &xMotor1Config, 2, NULL);
xTaskCreate(vMotorControlTask, "Motor2", 256, &xMotor2Config, 2, NULL);

注意:传结构体指针时,结构体变量必须在任务运行期间一直有效。如果你传的是局部变量,任务还没跑完,变量就被销毁了,那任务拿到的就是垃圾数据。我曾经在项目里犯过这个错,排查了整整两天。

解决办法有两个:

  • 用全局变量或静态变量
  • pvPortMalloc 动态分配内存,任务结束时释放

3.3 任务删除与清理:别直接跑路

任务删除用 vTaskDelete()。但注意,删除不是立即销毁,而是把任务从调度队列里移除,然后由空闲任务回收资源。

// 删除自己
vTaskDelete(NULL);

// 删除其他任务
vTaskDelete(xTaskHandle);

这里有个大坑:任务删除后,它占用的栈空间不会自动释放。FreeRTOS的空闲任务会帮你回收,但前提是——你得让空闲任务有机会运行。

我的经验:如果你在某个高优先级任务里删除了另一个任务,然后这个高优先级任务一直占着CPU不放,空闲任务就永远没机会运行。被删除任务的栈空间就永远无法回收。时间长了,内存就泄漏了。

正确的做法是:删除任务后,主动让出CPU:

void vSomeTask(void *pvParameters)
{
    // ... 做一些工作 ...
    
    // 删除另一个任务
    vTaskDelete(xOtherTaskHandle);
    
    // 主动让出CPU,让空闲任务有机会回收资源
    taskYIELD();
    
    // 继续做其他事
}

3.4 任务句柄管理:你的任务身份证

TaskHandle_t 就是任务的身份证。有了它,你可以:

  • 删除任务:vTaskDelete(xHandle)
  • 挂起/恢复任务:vTaskSuspend/vTaskResume
  • 获取任务优先级:uxTaskPriorityGet
  • 向任务发送通知:xTaskNotifyGive

我建议把所有任务句柄集中管理,比如放在一个结构体里:

typedef struct
{
    TaskHandle_t xLEDTask;
    TaskHandle_t xMotorTask;
    TaskHandle_t xCommTask;
    TaskHandle_t xSensorTask;
} TaskHandles_t;

TaskHandles_t g_xTaskHandles = {0};

这样调试时一目了然。而且,如果你想在系统异常时挂起所有任务,直接遍历这个结构体就行。

重要:任务句柄在任务创建时赋值,但任务删除后,句柄就变成悬空指针了。如果你还拿着这个句柄去操作任务,系统会崩溃。我建议在删除任务后,把对应的句柄置为NULL:

void vDeleteAndCleanup(TaskHandle_t *pxHandle)
{
    if(*pxHandle != NULL)
    {
        vTaskDelete(*pxHandle);
        *pxHandle = NULL;  // 清空句柄,防止误用
    }
}

3.5 实战中的避坑指南

嗯,说了这么多,总结几个我踩过的坑:

  1. 栈大小估算:别太自信。我一般给任务分配栈后,用 uxTaskGetStackHighWaterMark() 查看实际使用量,然后加50%余量。
  2. 参数生命周期:传指针时,确保指针指向的内存一直有效。传数值时,注意类型转换。
  3. 删除任务后:记得清空句柄,让出CPU。
  4. 不要在中断里创建或删除任务:FreeRTOS不允许。如果非要在中断里操作,用消息队列通知一个任务去做。

我曾经在一个电机控制项目里,因为忘记检查 xTaskCreate 的返回值,导致任务没创建成功,系统直接卡死。从那以后,我写任何创建任务的代码,第一件事就是检查返回值。你想想看,一个简单的 if 判断,能省下多少调试时间?

好了,任务创建与删除就讲到这里。下一章咱们聊聊任务状态与调度,看看任务到底是怎么在运行、就绪、阻塞、挂起之间切换的。