4. 任务创建与删除:任务创建API(xTaskCreate)、任务删除API(vTaskDelete)、任务句柄与优先级

好,咱们进入正题。任务创建和删除,这是FreeRTOS里最基础的操作。说白了,你写嵌入式程序,就是创建一堆任务,让它们跑起来,然后根据需要再干掉它们。我刚开始接触RTOS时,觉得这玩意儿挺玄乎,后来发现,其实就是几个API函数的事。

4.1 任务创建API:xTaskCreate

xTaskCreate 这个函数,可以说是整个系统的起点。没有它,你的任务就只是个函数,没法被调度器管理。它的原型长这样:

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

嗯,参数不少,但别慌。我一个个说。

  • pvTaskCode:就是你的任务函数名。比如你写了个 void vLEDTask(void *pvParameters),这里就填 vLEDTask
  • pcName:给任务起个名字。这名字只在调试时有用,比如用 vTaskList() 查看任务状态时能看到。我习惯用英文,比如 "LED_Task"。
  • usStackDepth:栈大小。这个很关键!我踩过坑,栈设小了,任务跑着跑着就崩了。单位是字(word),在32位MCU上,1个字 = 4字节。比如你设100,那就是400字节。
  • pvParameters:传给任务函数的参数。可以是数字、结构体指针,啥都行。不需要就填NULL。
  • uxPriority:优先级。数值越大,优先级越高。0是最低优先级。
  • pxCreatedTask:任务句柄的指针。后面删除任务时要用到。不需要可以填NULL。

返回值:成功返回 pdPASS(其实就是1),失败返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(-1)。

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

TaskHandle_t xLEDTaskHandle = NULL;

void vLEDTask(void *pvParameters)
{
    (void)pvParameters; // 防止编译器警告
    while(1)
    {
        GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void main(void)
{
    // 硬件初始化...
    
    xTaskCreate(
        vLEDTask,           // 任务函数
        "LED_Task",         // 任务名称
        128,                // 栈大小:128字 = 512字节
        NULL,               // 无参数
        2,                  // 优先级设为2
        &xLEDTaskHandle     // 保存句柄
    );
    
    vTaskStartScheduler();  // 启动调度器
}

你看,就这么简单。任务函数里是个死循环,调度器会不断切换任务,让它们轮流执行。

4.2 任务删除API:vTaskDelete

有创建就有删除。vTaskDelete 用来干掉一个任务。原型更简单:

void vTaskDelete(TaskHandle_t xTaskToDelete);

参数就一个:要删除的任务句柄。如果传NULL,表示删除自己(当前任务)。

注意:删除任务后,任务占用的栈空间和TCB(任务控制块)会被释放回堆内存。但任务自己申请的动态内存(比如malloc出来的)不会自动释放!你得在任务退出前自己清理。

我曾经犯过一个错:在任务里malloc了一块缓冲区,然后直接调 vTaskDelete(NULL) 把自己删了。结果内存泄漏,系统跑了几天就挂了。嗯,血的教训。

正确的做法:

void vSomeTask(void *pvParameters)
{
    uint8_t *pBuffer = (uint8_t *)pvPortMalloc(256);
    if(pBuffer != NULL)
    {
        // 使用缓冲区...
        
        // 删除前释放
        vPortFree(pBuffer);
    }
    
    // 然后删除自己
    vTaskDelete(NULL);
}

4.3 任务句柄与优先级

任务句柄,说白了就是任务的身份证。它是一个 TaskHandle_t 类型的变量,指向任务控制块。有了它,你才能对任务进行操作,比如删除、挂起、恢复、改变优先级等。

我习惯在创建任务时就把句柄保存好,像这样:

TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;

void CreateAllTasks(void)
{
    xTaskCreate(vTask1, "Task1", 128, NULL, 1, &xTask1Handle);
    xTaskCreate(vTask2, "Task2", 128, NULL, 2, &xTask2Handle);
}

这样,后面想删除Task1,直接 vTaskDelete(xTask1Handle) 就行。

优先级 是调度器决定谁先跑的依据。FreeRTOS支持抢占式调度,高优先级任务就绪时,会立即抢占低优先级任务。

优先级值 说明
0 最低优先级,空闲任务(Idle Task)就运行在这个优先级
1 ~ (configMAX_PRIORITIES - 1) 用户任务可用优先级,数值越大优先级越高
configMAX_PRIORITIES - 1 最高优先级

小技巧configMAX_PRIORITIES 在 FreeRTOSConfig.h 中定义。别设太大,一般5~32就够用。设大了会浪费RAM,因为每个优先级都需要一个就绪列表。

优先级设计是个学问。我一般遵循几个原则:

  • 实时性要求高的任务(比如按键响应、通信接收)给高优先级。
  • 计算量大但不紧急的任务(比如数据记录、界面刷新)给低优先级。
  • 避免两个任务优先级相同,除非你明确知道它们要轮转调度。

你想想看,如果两个高优先级任务互相抢CPU,低优先级任务可能永远得不到执行。这就是所谓的「优先级反转」或「饥饿」问题。嗯,后面章节会细讲。

4.4 避坑指南

我曾经踩过的坑:

  1. 栈溢出:任务栈设太小,函数调用层级深一点就崩。建议用 uxTaskGetStackHighWaterMark() 检查栈使用情况。
  2. 句柄未初始化:创建任务时忘了传句柄地址,或者句柄变量是局部变量,任务还没删句柄就没了。
  3. 删除自己后还访问局部变量:任务函数里定义了局部变量,然后 vTaskDelete(NULL) 把自己删了。但函数还没返回,局部变量还在栈上。这时候如果别的任务访问这些变量,会出问题。
  4. 优先级设太高:把某个任务优先级设成最高,结果它一直占着CPU,其他任务饿死。我见过有人把轮询按键的任务设成最高优先级,结果系统卡死。

好了,任务创建和删除就这些。记住:创建时想好栈大小,删除时记得清理资源,优先级别乱设。下一节咱们聊聊任务的状态切换,看看任务到底是怎么「活」起来的。