3. 创建任务(上):xTaskCreate() API函数深度解析,参数详解与返回值处理

好,咱们进入正题。上一章我们聊了任务是什么,这一章直接上手——怎么把任务“生”出来。

在FreeRTOS里,创建任务最常用的就是 xTaskCreate() 这个函数。说实话,我刚开始用的时候,看着那一堆参数也有点懵。但别怕,咱们一个一个拆开看,你会发现其实没那么复杂。

3.1 函数原型长什么样?

先看一眼它的“长相”:

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

返回值是 BaseType_t 类型。成功返回 pdPASS,失败返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY

嗯,这里要注意:pdPASS 其实就是 1,失败是 0。但别偷懒写数字,用宏定义更安全。

3.2 参数逐个拆解

3.2.1 pvTaskCode —— 任务函数

这个参数就是任务要执行的函数指针。说白了,你写一个 void vTaskFunction(void *pvParameters) 这样的函数,然后把函数名传进去。

任务函数有个硬性要求:绝对不能返回。为什么?因为FreeRTOS的任务调度器会认为任务结束了,然后调用 vTaskDelete(NULL) 来清理它。但如果你在函数里写了 return,系统就会崩溃。

警告:任务函数必须是一个无限循环,或者调用 vTaskDelete() 来结束自己。千万不要让任务函数“跑到底”然后返回!

我遇到过一个小白同事,写了个任务函数,里面就一条打印语句,然后函数就结束了。结果系统跑起来直接死机。排查了半天才发现是任务函数返回了。

3.2.2 pcName —— 任务名字

这个参数就是个字符串,纯调试用的。比如 "LED_Blink""UART_Tx" 之类的。它不会影响任务的行为,但建议起个有意义的名字。

我个人习惯用 "T_模块名_功能" 这种格式,比如 "T_LED_Blink"。这样在调试器里一眼就能看出是哪个任务。

注意:名字长度不要超过 configMAX_TASK_NAME_LEN,默认是16个字符。超了会被截断。

3.2.3 usStackDepth —— 栈深度

这个参数特别容易让人误解。它单位是 字(word),不是字节!

在32位处理器上,1个字 = 4字节。所以如果你写 usStackDepth = 128,实际分配的栈大小是 128 × 4 = 512 字节。

栈大小怎么定?说实话,这没有标准答案。我一般这样估算:

  • 简单任务(就几个局部变量):64 ~ 128 字
  • 中等任务(有函数调用、局部数组):256 ~ 512 字
  • 复杂任务(有递归、大数组):1024 字以上
小技巧:先用 uxTaskGetStackHighWaterMark() 函数查看任务栈的“水位线”,看看实际用了多少。然后根据这个数据调整栈大小。我每次写完新任务都会跑一遍这个函数,确保栈不会溢出。

3.2.4 pvParameters —— 任务参数

这个参数是传给任务函数的。类型是 void *,所以你可以传任何东西:整数、结构体指针、字符串等等。

举个例子:

void vLED_Task(void *pvParameters) {
    int led_pin = (int)pvParameters;  // 把参数转成整数
    // 然后控制对应的LED
}

// 创建任务时传参数
xTaskCreate(vLED_Task, "LED1", 128, (void *)1, 1, NULL);
xTaskCreate(vLED_Task, "LED2", 128, (void *)2, 1, NULL);

你看,同一个任务函数,传不同的参数就能控制不同的LED。是不是很灵活?

但要注意:如果你传的是指针,一定要确保指针指向的内存是有效的。我曾经犯过一个错,传了一个局部变量的地址,结果任务还没开始跑,那个局部变量就已经被释放了。任务拿到的就是个野指针,程序直接跑飞。

3.2.5 uxPriority —— 任务优先级

优先级范围是 0 到 configMAX_PRIORITIES - 1。数字越大,优先级越高。

一般建议:

  • 0:空闲任务(系统保留)
  • 1~2:后台任务(比如LED闪烁、按键扫描)
  • 3~4:普通任务(比如数据处理、通信)
  • 5及以上:实时性要求高的任务(比如电机控制、传感器采集)

我个人习惯把优先级分成几个档次,不要用太多不同的优先级。因为优先级越多,调度器的开销越大。而且优先级反转的问题也会更复杂。

3.2.6 pxCreatedTask —— 任务句柄

这个参数是输出参数。如果你传一个 TaskHandle_t 类型的指针进去,函数会把创建好的任务句柄写进去。

任务句柄有什么用?

  • 删除任务:vTaskDelete(handle)
  • 挂起/恢复任务:vTaskSuspend(handle) / vTaskResume(handle)
  • 改变优先级:vTaskPrioritySet(handle, newPriority)

如果你不需要操作这个任务,可以传 NULL。但建议还是传一个变量,万一以后要用呢?

3.3 返回值处理

很多新手会忽略返回值检查。但我觉得这是最不该省的一步。

返回值只有两种可能:

返回值 含义 常见原因
pdPASS 任务创建成功 一切正常
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 内存不足,创建失败 堆空间不够、栈大小设置过大

我建议的写法:

TaskHandle_t xLEDTaskHandle = NULL;
BaseType_t xResult;

xResult = xTaskCreate(vLED_Task, "LED", 128, NULL, 1, &xLEDTaskHandle);

if (xResult != pdPASS) {
    // 创建失败,打印错误信息
    printf("Error: Failed to create LED task!\n");
    // 可以在这里做错误处理,比如重启系统
    while(1);
}

你看,就多几行代码,但能帮你省下大量调试时间。我见过太多人因为没检查返回值,任务没创建成功,系统却一直在跑,结果各种诡异现象。

3.4 一个完整的例子

最后,咱们看一个完整的创建任务的例子:

#include "FreeRTOS.h"
#include "task.h"

// 任务函数
void vBlinkTask(void *pvParameters) {
    int led_pin = (int)pvParameters;
    
    for (;;) {
        // 点亮LED
        GPIO_SetBits(led_pin);
        vTaskDelay(pdMS_TO_TICKS(500));
        
        // 熄灭LED
        GPIO_ResetBits(led_pin);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void main(void) {
    TaskHandle_t xTask1Handle = NULL;
    TaskHandle_t xTask2Handle = NULL;
    
    // 创建两个LED闪烁任务
    if (xTaskCreate(vBlinkTask, "LED1", 128, (void *)1, 1, &xTask1Handle) != pdPASS) {
        // 错误处理
        while(1);
    }
    
    if (xTaskCreate(vBlinkTask, "LED2", 128, (void *)2, 1, &xTask2Handle) != pdPASS) {
        // 错误处理
        while(1);
    }
    
    // 启动调度器
    vTaskStartScheduler();
    
    // 正常情况下不会执行到这里
    while(1);
}

这个例子很简单,但包含了所有关键点:任务函数、参数传递、返回值检查。你想想看,如果我不检查返回值,万一内存不够,任务没创建成功,系统启动后LED不亮,你查半天都查不出原因。

好了,这一章就到这里。下一章我们继续聊 xTaskCreateStatic() 和任务删除。记住,创建任务只是第一步,怎么管理任务才是真正的挑战。