第4章 任务创建与管理:从零开始搭建你的RTOS应用

好,咱们进入正题。任务,是RTOS里最核心的概念。说白了,整个系统就是一堆任务在轮流干活。这一章,我就带你手把手把任务创建、参数传递、优先级设置、堆栈分配这些基本功练扎实。

我记得刚接触FreeRTOS那会儿,总觉得任务创建就是个函数调用,没什么大不了的。结果第一次做项目,就因为堆栈设小了,程序跑着跑着就莫名其妙死机。嗯,这种坑,咱们今天一并填上。

4.1 任务创建函数 xTaskCreate 深度解析

任务创建的入口函数就是 xTaskCreate。它的原型长这样:

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

返回值只有两种可能:pdPASS 表示创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 表示内存不够。我建议你每次调用后都检查返回值,别偷懒。

关键点:任务函数必须是一个永不返回的死循环。如果函数执行到了末尾,FreeRTOS 会调用 vTaskDelete(NULL) 自动删除该任务。但我不推荐依赖这个行为——显式控制更安全。

4.2 任务参数传递:不只是传个数字那么简单

任务参数 pvParameters 是一个 void * 指针。这意味着你可以传任何东西进去:整数、结构体指针、字符串,甚至另一个任务的句柄。

我常用的几种传参方式:

  • 传整数:直接把整数强转为 void *,任务里再转回来。适合简单的ID或标志位。
  • 传结构体指针:多个参数打包成一个结构体,传指针。注意结构体生命周期必须覆盖任务运行期间。
  • 传字符串:传 char *,任务里直接使用。小心字符串存储位置——别传局部变量。

来看个实际例子:

typedef struct {
    uint8_t  led_id;
    uint16_t blink_rate_ms;
} led_param_t;

led_param_t led1_param = { .led_id = 1, .blink_rate_ms = 500 };

void vLEDTask(void *pvParameters) {
    led_param_t *param = (led_param_t *)pvParameters;
    while(1) {
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
        vTaskDelay(pdMS_TO_TICKS(param->blink_rate_ms));
    }
}

xTaskCreate(vLEDTask, "LED1", 128, &led1_param, 2, NULL);

我曾经踩过的坑:传局部变量地址给任务。任务还没开始跑,局部变量已经出栈了。结果任务拿到的全是垃圾数据。解决办法:要么用全局变量,要么用动态分配的堆内存,要么用静态分配的结构体。

4.3 任务优先级与堆栈设置:平衡的艺术

优先级决定了任务什么时候能抢到CPU。FreeRTOS 里,数值越大优先级越高。0 是最低优先级,configMAX_PRIORITIES - 1 是最高。

优先级范围 典型用途 注意事项
0 ~ 2 后台任务、空闲任务 空闲任务优先级固定为0
3 ~ 5 普通业务任务 大多数任务放这个区间
6 ~ 7 实时性要求高的任务 小心优先级反转

堆栈大小怎么定?这是新手最容易懵的地方。我一般按这个经验来:

  • 简单任务(只调几个函数,无局部大数组):128 ~ 256 字
  • 中等任务(有局部数组、调用printf等):256 ~ 512 字
  • 复杂任务(有递归、大结构体):512 ~ 1024 字

STM32L4 是 Cortex-M4 内核,一个字是 4 字节。所以 128 字 = 512 字节。你想想看,如果设成 64 字,才 256 字节,稍微嵌套几层函数调用就溢出了。

我的调试习惯:先用 uxTaskGetStackHighWaterMark() 查看任务堆栈剩余量。如果剩余量小于堆栈总量的 20%,我就加大堆栈。别靠猜,用数据说话。

4.4 任务删除与任务句柄:善始善终

任务删除用 vTaskDelete()。参数传任务句柄,传 NULL 表示删除自己。

TaskHandle_t xTask1Handle = NULL;

void vTask1(void *pvParameters) {
    while(1) {
        // 干点活...
        if(should_exit) {
            vTaskDelete(NULL);  // 删除自己
        }
    }
}

// 在其他任务中删除 task1
vTaskDelete(xTask1Handle);

任务句柄 TaskHandle_t 是个好东西。有了它,你可以在任务外部控制这个任务:挂起、恢复、删除、获取状态。创建任务时把句柄存起来,就像给任务办了个身份证。

注意:任务删除后,它占用的堆栈和TCB内存并不会自动释放回堆——除非你用的是动态内存管理并且开启了 configUSE_TRASH_FREE。在 STM32L4 这种资源受限的芯片上,频繁创建删除任务可能导致内存碎片。我建议:能复用就别删。

4.5 知识体系总览

下面这张图把任务创建与管理的核心流程串起来了。你可以把它当作本章的思维导图:

任务创建与管理核心流程 xTaskCreate 任务函数 + 参数 优先级 + 堆栈 任务句柄 参数传递方式 整数 / 结构体 / 字符串 堆栈大小估算 简单128 / 中等256 / 复杂512+ 任务控制 挂起 / 恢复 / 删除 避坑:生命周期 别传局部变量地址 调试手段 uxTaskGetStackHighWaterMark 删除注意事项 内存碎片 / 句柄失效 核心原则:先测堆栈,再调优先级,最后管好句柄

4.6 实战建议:从简单开始

如果你刚开始用 FreeRTOS,我建议你这样做:

  1. 先创建两个任务,一个闪灯,一个打印串口。优先级都设成 1。
  2. 用 uxTaskGetStackHighWaterMark() 看堆栈余量,调整到合理值。
  3. 试着改优先级,观察任务执行顺序的变化。
  4. 最后试试用句柄删除和恢复任务。