3. 任务创建与删除:xTaskCreate() API详解、任务参数传递、vTaskDelete() 使用

好,咱们今天来聊聊任务创建和删除。这是FreeRTOS里最基础,也是最常用的两个操作。你想想看,一个RTOS系统,核心就是跑任务。任务怎么来的?怎么没的?搞明白这两个API,你才算真正入了门。

我个人习惯,拿到一个新板子,第一件事就是写两个任务,一个闪灯,一个打印。先让系统转起来,心里才踏实。咱们就从xTaskCreate()开始。

3.1 xTaskCreate() — 任务的“出生证明”

这个函数,说白了就是给内核注册一个新任务。它告诉内核:“嘿,我这有个函数,你帮我调度它。”

原型长这样:

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表示内存不够。

嗯,这里要注意。很多新手会问:“pcName有什么用?” 它其实不参与调度,纯粹是为了调试。你用调试器看任务列表时,这个名字会显示出来。我建议名字起得有规律,比如“Task_LED”、“Task_UART”,别用“Task1”、“Task2”,否则项目大了你自己都分不清。

3.2 栈深度 — 最容易翻车的地方

usStackDepth这个参数,单位是“字”。在32位处理器上,1个字 = 4字节。所以如果你写128,实际分配的栈空间是512字节。

我在项目中遇到过一个问题:一个任务里用了sprintf(),结果栈设小了,系统跑着跑着就死机。查了两天才发现是栈溢出。后来我学乖了,每个任务先设大一点,比如512字(2KB),等调稳定了再用工具测量实际栈使用量,再优化。

避坑指南: 我曾经因为栈设得太小,导致任务切换时发生HardFault。调试器一看,栈指针都飞到天上了。记住:宁可多给,不要少给。栈溢出不会报错,只会莫名其妙死机。

3.3 任务参数传递 — 一个函数,多种玩法

pvParameters是一个void*指针。这意味着你可以传任何东西进去。整数、结构体、字符串,统统可以。

最常见的用法是传一个整数:

void vTaskFunction(void *pvParameters) {
    int task_id = (int)pvParameters;
    while(1) {
        printf("Task %d is running\n", task_id);
        vTaskDelay(1000);
    }
}

// 创建时传参
xTaskCreate(vTaskFunction, "Task1", 128, (void*)1, 1, NULL);
xTaskCreate(vTaskFunction, "Task2", 128, (void*)2, 1, NULL);

你看,同一个函数,传不同的参数,就变成了两个不同的任务实例。这在项目里非常实用。比如你有4个LED,每个LED一个任务,参数就是LED的GPIO引脚号。

传结构体也很常见:

typedef struct {
    uint8_t uart_port;
    uint32_t baudrate;
} UART_Config_t;

void vUARTTask(void *pvParameters) {
    UART_Config_t *config = (UART_Config_t *)pvParameters;
    // 使用 config->uart_port, config->baudrate
}

UART_Config_t config = {1, 115200};
xTaskCreate(vUARTTask, "UART", 256, &config, 2, NULL);
小技巧: 传结构体时,注意生命周期。如果结构体是局部变量,任务还没用完它就销毁了,那指针就变成野指针了。我一般用静态变量或者动态分配内存。

3.4 优先级与句柄 — 调度器的“指挥棒”

uxPriority决定了任务在就绪列表里的位置。数值越大,优先级越高。FreeRTOS支持0到configMAX_PRIORITIES-1。

pxCreatedTask是输出参数。如果你不需要操作这个任务(比如删除、挂起),可以传NULL。但如果你后面要删除它,就必须保存这个句柄。

TaskHandle_t xLEDTaskHandle = NULL;

xTaskCreate(vLEDTask, "LED", 128, NULL, 1, &xLEDTaskHandle);

// 后面想删除它
vTaskDelete(xLEDTaskHandle);

3.5 vTaskDelete() — 任务的“安乐死”

删除任务很简单,但有几个坑要注意。

void vTaskDelete(TaskHandle_t xTaskToDelete);

参数传NULL,表示删除自己。传句柄,删除别人。

删除自己时,任务不会立即消失。它会在退出临界区后,由空闲任务(Idle Task)负责清理栈空间。所以,如果你删除了自己,后面不要再执行任何代码了。

重要提醒: 删除任务前,确保它没有持有互斥量、信号量等资源。否则这些资源永远不会被释放,造成死锁。我曾经在项目里删了一个持有互斥量的任务,结果其他任务全部卡住。排查了整整一个下午。

另外,vTaskDelete()不会释放任务动态分配的内存。如果你在任务里malloc了内存,删除前记得free掉。否则就内存泄漏了。

3.6 一个完整的例子

咱们来个完整的,创建两个任务,一个打印,一个闪灯。5秒后删除打印任务。

TaskHandle_t xPrintTaskHandle = NULL;

void vPrintTask(void *pvParameters) {
    int count = 0;
    while(1) {
        printf("Count: %d\n", count++);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vLEDTask(void *pvParameters) {
    while(1) {
        // 假设有LED翻转函数
        LED_Toggle();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void main(void) {
    // 创建任务
    xTaskCreate(vPrintTask, "Print", 256, NULL, 1, &xPrintTaskHandle);
    xTaskCreate(vLEDTask, "LED", 128, NULL, 1, NULL);

    // 启动调度器
    vTaskStartScheduler();

    // 不会执行到这里
    while(1);
}

// 在某个地方,比如按键中断里
void vDeletePrintTask(void) {
    if(xPrintTaskHandle != NULL) {
        vTaskDelete(xPrintTaskHandle);
        xPrintTaskHandle = NULL;  // 防止野指针
        printf("Print task deleted\n");
    }
}

你看,删除后把句柄置NULL,这是个好习惯。否则你后面不小心又调用vTaskDelete(),传了个无效句柄,系统会崩溃。

3.7 总结一下

任务创建和删除,说白了就是动态管理你的系统资源。创建时注意栈大小,删除时注意资源释放。这两个API用好了,你的系统就活了。

我个人建议,每个任务都保存句柄,哪怕暂时用不到。因为你永远不知道后面会不会需要删除它。项目开发过程中,需求变更是常有的事。

下一章,咱们聊聊任务状态与状态切换。你会发现,任务其实一直在“就绪、运行、阻塞、挂起”这四个状态里转圈圈。很有意思。