2. 任务创建与管理:任务函数原型、xTaskCreate() API详解、任务参数传递、任务句柄

好,咱们进入正题。任务创建,是FreeRTOS里最基础的操作。你想想看,整个系统跑起来,靠的就是一个个任务在调度器手里来回切换。这一节,我就带你手把手把任务“生”出来。

2.1 任务函数长什么样?

每个任务本质上就是一个永不返回的C函数。它的原型是固定的,我直接给你看代码:

void vTaskFunction(void *pvParameters)
{
    // 任务初始化代码
    for( ;; )
    {
        // 任务主体代码
        // 这里通常会调用 vTaskDelay() 或等待事件
    }
    // 理论上永远不会执行到这里
    // 如果真执行到了,必须调用 vTaskDelete(NULL)
}

注意几个关键点:

  • 返回值是void —— 任务函数没有返回值,它是个死循环。
  • 参数是void指针 —— 这是FreeRTOS留给你的“后门”,可以传任何东西进去。
  • 函数名随意 —— 但建议用有意义的命名,比如 vTaskLED、vTaskUART。
我的习惯: 任务函数名我一般加个前缀“vTask”,一看就知道这是个任务。团队协作时,这个习惯能省不少沟通成本。

2.2 xTaskCreate() —— 核心API详解

有了任务函数,怎么把它注册到系统里?靠的就是 xTaskCreate()。这个API我用了不下几百次,它的原型长这样:

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

我来逐个拆解,每个参数都有坑:

参数 说明 避坑指南
pvTaskCode 函数名即可,比如 vTaskLED 别加括号!传的是地址,不是调用
pcName 字符串,比如 "LED_Task" 长度不要超过 configMAX_TASK_NAME_LEN
usStackDepth 栈大小,单位是字 32位MCU上,1字=4字节。别搞混了!
pvParameters void* 类型,传啥都行 传结构体指针时,注意生命周期
uxPriority 0 ~ configMAX_PRIORITIES-1 0是最低优先级,数值越大优先级越高
pxCreatedTask TaskHandle_t 类型的指针 如果不需要句柄,传NULL即可
我曾经踩过的坑: 栈深度单位是“字”不是“字节”。有一次我传了128,以为够用,结果任务一跑就崩。后来一算,128字=512字节,其实栈溢出了。建议新手先给个256字起步,稳定后再优化。

2.3 任务参数传递 —— 别小看这个void指针

任务参数传递,说白了就是通过 pvParameters 给任务“喂数据”。我见过很多新手直接传一个局部变量的地址,结果任务还没启动,变量就销毁了。嗯,这里要注意。

正确的做法有几种:

  • 传整数值 —— 直接把整数强转为void*,比如 (void*)1。任务里再转回来。
  • 传全局变量地址 —— 全局变量生命周期是整个程序,安全。
  • 传动态分配的结构体指针 —— 记得在任务里释放内存。
  • 传静态结构体地址 —— 我比较推荐这种方式,简单可靠。

给你看个实际例子:

// 定义一个结构体,存放任务需要的参数
typedef struct {
    uint8_t ucLEDPin;
    uint16_t usDelayMs;
} LEDConfig_t;

// 静态分配,生命周期有保障
static LEDConfig_t xLED1Config = { .ucLEDPin = 13, .usDelayMs = 500 };
static LEDConfig_t xLED2Config = { .ucLEDPin = 14, .usDelayMs = 1000 };

void vTaskLED(void *pvParameters)
{
    LEDConfig_t *pxConfig = (LEDConfig_t *)pvParameters;
    
    for( ;; )
    {
        GPIO_Toggle(pxConfig->ucLEDPin);
        vTaskDelay(pdMS_TO_TICKS(pxConfig->usDelayMs));
    }
}

// 创建任务时传参
xTaskCreate(vTaskLED, "LED1", 256, &xLED1Config, 1, NULL);
xTaskCreate(vTaskLED, "LED2", 256, &xLED2Config, 1, NULL);
核心思想: 同一个任务函数,传不同的参数,就能实现不同的行为。这就是代码复用的精髓。我在项目中经常用这个技巧,一个UART处理函数,传不同的缓冲区地址,就能同时处理多个串口。

2.4 任务句柄 —— 任务的“身份证”

任务句柄,类型是 TaskHandle_t。它本质上是一个指针,指向任务控制块(TCB)。有了它,你就能在任务外部操作这个任务:

  • 删除任务 —— vTaskDelete(xTaskHandle)
  • 挂起/恢复任务 —— vTaskSuspend() / vTaskResume()
  • 获取任务优先级 —— uxTaskPriorityGet()
  • 通知任务 —— xTaskNotifyGive()

创建任务时,如果你传了 &xTaskHandle,系统会把任务句柄写进去。如果你不需要,传NULL就行。我个人习惯是:只要后续可能操作这个任务,就保留句柄。

TaskHandle_t xLEDTaskHandle = NULL;

void vCreateTasks(void)
{
    xTaskCreate(vTaskLED, "LED", 256, NULL, 1, &xLEDTaskHandle);
    
    // 后续可以通过句柄操作
    // vTaskSuspend(xLEDTaskHandle);
    // vTaskResume(xLEDTaskHandle);
}
小技巧: 在任务函数内部,可以通过 xTaskGetCurrentTaskHandle() 获取自己的句柄。这在任务需要删除自己时特别有用:vTaskDelete(xTaskGetCurrentTaskHandle())

2.5 知识体系总览

这一节的内容,我用一张图帮你理清逻辑:

任务创建与管理核心流程 任务函数 void vTask(void *pvParam) xTaskCreate() 注册任务到调度器 任务句柄 TaskHandle_t 参数传递方式 整数值强转 (void*)1 全局变量地址 &xGlobalVar 结构体指针 &xConfig 句柄操作 删除任务 挂起/恢复 任务函数 + 参数 + 优先级 + 栈大小 = 一个可被调度器管理的独立执行单元

这张图把整个流程串起来了。你从任务函数开始,通过 xTaskCreate() 注册,得到任务句柄,然后就可以通过句柄控制任务。参数传递是连接数据和任务的桥梁,别搞砸了。

嗯,这一节的内容就这些。任务创建是基本功,多写几遍就熟了。记住:栈大小给够,参数生命周期管好,句柄该留就留。做到这三点,任务创建这块基本不会出问题。