4. 任务创建与管理:TCB、xTaskCreate() 与堆栈配置

任务,是 RTOS 世界里最核心的执行单元。你写的每个功能模块,最终都要包装成一个任务交给调度器去跑。SafeRTOS 里怎么创建任务?任务控制块里藏了哪些秘密?堆栈配多大才合适?

这一节,咱们把这些事彻底聊透。

4.1 任务控制块(TCB)——任务的“身份证”

每个任务在 SafeRTOS 内部都有一个对应的结构体,叫 任务控制块(Task Control Block,TCB)。说白了,它就是任务的“档案袋”。调度器切换任务时,靠的就是 TCB 里保存的上下文信息。

TCB 的核心字段

字段类型说明
pxTopOfStackStackType_t *当前栈顶指针,任务切换时保存/恢复寄存器用
pxStackStackType_t *堆栈起始地址,创建任务时分配
uxPriorityUBaseType_t任务优先级,数值越大优先级越高
ucStateuint8_t任务状态:就绪、运行、阻塞、挂起
pcTaskNamechar[configMAX_TASK_NAME_LEN]任务名称,调试时一眼认出是谁
xGenericListItemListItem_t就绪/阻塞链表节点,调度器靠它排队
xEventListItemListItem_t事件链表节点,用于同步与通信

我刚开始用 SafeRTOS 时,总觉得 TCB 就是个“黑盒子”。直到有一次调试一个任务莫名其妙不跑了,我 dump 出 TCB 一看——pxTopOfStack 指向了堆栈外的地址。嗯,堆栈溢出了。从那以后,我每次创建任务都会先看一眼 TCB 里的栈指针。

小技巧:调试时可以在断点处查看 pxCurrentTCB,它指向当前正在运行的任务的 TCB。通过它你能直接看到任务的栈使用情况、优先级和状态。

4.2 xTaskCreate() API 详解——创建任务的“标准动作”

SafeRTOS 里创建任务,最常用的就是 xTaskCreate()。它的原型长这样:

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

返回值:pdPASS 表示创建成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 表示堆内存不够。

每个参数我都踩过坑,一个一个说:

  • pvTaskCode:任务函数必须是一个永不返回的死循环。我见过有人把任务函数写成只跑一次就退出的——结果系统直接崩了。SafeRTOS 里任务函数返回会触发 vApplicationTaskDeleteHook(),如果你没实现这个钩子,系统就挂了。
  • pcName:名字长度受 configMAX_TASK_NAME_LEN 限制,默认 16 字节。别写太长,截断了你也看不全。
  • usStackDepth:这个最要命。单位是 字(word),不是字节。在 32 位处理器上,1 字 = 4 字节。所以 usStackDepth = 128 实际分配了 512 字节。
  • pvParameters:传参时建议传结构体指针,别传值。尤其是传大结构体,栈上拷贝一次就够你受的。
  • uxPriority:0 是最低优先级,configMAX_PRIORITIES - 1 是最高。别设成 configMAX_PRIORITIES 或更大,SafeRTOS 不会帮你检查越界。
  • pxCreatedTask:如果你不需要操作这个任务(比如删除、改变优先级),可以传 NULL。但我建议你始终保留句柄,调试时有用。

注意:SafeRTOS 的任务创建是动态分配内存的。如果你的系统对实时性要求极高,或者不允许动态分配,可以考虑使用 xTaskCreateStatic() 静态创建任务。静态创建需要你提前准备好 TCB 和堆栈缓冲区。

4.3 任务优先级与调度策略

SafeRTOS 是一个 抢占式优先级调度 的 RTOS。什么意思?高优先级的任务一旦就绪,会立刻抢占低优先级任务的 CPU 使用权。

优先级数值从 0 到 configMAX_PRIORITIES - 1。数值越大,优先级越高。我一般把关键任务(比如安全监控、心跳检测)放在最高优先级,普通业务逻辑放在中低优先级。

优先级设计原则(我自己的经验):

  • 最高优先级(3~5 个):留给时间敏感型任务,比如中断处理、安全监控、看门狗喂狗。
  • 中优先级(5~10 个):留给常规业务逻辑,比如通信协议处理、数据采集。
  • 低优先级(剩余):留给后台任务,比如日志记录、统计上报、自检。

优先级相同的情况下,SafeRTOS 使用 时间片轮转 调度。每个任务运行一个时间片(由 configTICK_RATE_HZ 决定),然后切换到下一个同优先级任务。

我曾经在一个项目里把 8 个任务都设成同一个优先级,结果系统响应变得很“肉”。后来我把关键任务提了一级,问题立刻解决了。你想想看,优先级不是越多越好,关键是分清楚哪些任务等不起。

4.4 堆栈配置——大了浪费,小了崩溃

堆栈大小是任务创建时最让人头疼的参数。配大了,RAM 浪费;配小了,任务跑着跑着就栈溢出,系统静默崩溃。

SafeRTOS 提供了 堆栈水印检测 功能。你可以在任务运行一段时间后调用:

UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark( taskHandle );

这个函数返回任务堆栈历史上 剩余的最小空间,单位是字。如果返回值接近 0,说明堆栈快爆了。

我一般这样估算堆栈大小:

  1. 任务函数本身的局部变量:比如一个 256 字节的数组,就需要 64 字(32 位机)。
  2. 函数调用嵌套深度:每层调用至少保存返回地址和几个寄存器,大概 10~20 字。
  3. 中断嵌套:如果任务可能被中断打断,中断服务程序也会用栈。我一般额外加 50~100 字。
  4. SafeRTOS 内部开销:任务切换时保存的上下文,大约 30~50 字。

我的习惯做法:先给一个“看起来够用”的值(比如 256 字),跑一遍全功能测试,然后用 uxTaskGetStackHighWaterMark() 看实际使用量。最后在这个值上加 20%~30% 的余量,作为最终配置。

千万别这样做:把堆栈配得特别大,觉得“反正 RAM 够用”。RAM 是嵌入式系统最贵的资源之一。一个任务多 100 字,10 个任务就多 1KB。在资源受限的 MCU 上,这可能是压垮骆驼的最后一根稻草。

4.5 知识结构总览

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

任务创建与管理核心逻辑 xTaskCreate() TCB 任务控制块 pxTopOfStack / pxStack uxPriority / ucState xGenericListItem 优先级与调度 抢占式:高优抢低优 同优先级:时间片轮转 优先级 0 ~ configMAX_PRIORITIES-1 堆栈配置 单位:字(word) 水印检测:uxHighWaterMark 估算:局部变量+嵌套+中断+余量 创建成功 → 任务进入就绪队列 → 调度器管理 避坑:堆栈溢出、优先级反转、任务函数返回

4.6 实战示例:创建一个安全监控任务

说了这么多理论,咱们来写一段实际代码。下面是一个安全监控任务的创建示例:

/* 安全监控任务函数 */
static void vSafetyMonitorTask(void *pvParameters)
{
    /* 参数是一个指向传感器结构体的指针 */
    SensorData_t *pxSensorData = (SensorData_t *)pvParameters;

    for (;;)
    {
        /* 读取传感器数据 */
        if (xSensorRead(pxSensorData) == pdTRUE)
        {
            /* 检查是否超过安全阈值 */
            if (pxSensorData->temperature > TEMP_CRITICAL_THRESHOLD)
            {
                /* 触发安全动作 */
                vActivateSafetyShutdown();
            }
        }

        /* 每 100 毫秒检查一次 */
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

/* 创建任务 */
void vCreateSafetyTasks(void)
{
    TaskHandle_t xSafetyTaskHandle = NULL;

    /* 静态分配传感器数据结构体 */
    static SensorData_t xSensorData;

    BaseType_t xResult = xTaskCreate(
        vSafetyMonitorTask,         /* 任务函数 */
        "SafetyMon",                /* 任务名称 */
        256,                        /* 堆栈深度:256 字 = 1024 字节(32位机) */
        (void *)&xSensorData,      /* 传参:传感器数据结构体指针 */
        5,                          /* 优先级:5(中等偏高) */
        &xSafetyTaskHandle          /* 任务句柄 */
    );

    if (xResult != pdPASS)
    {
        /* 创建失败,处理错误 */
        vErrorHandler(ERR_TASK_CREATE_FAILED);
    }
}

这段代码里,我用了 vTaskDelay(pdMS_TO_TICKS(100)) 让任务每 100ms 执行一次。注意 pdMS_TO_TICKS() 这个宏,它把毫秒转换成系统 Tick 数。如果你的系统 Tick 是 1000Hz,100ms 就是 100 个 Tick。

关于任务函数命名:我习惯在任务函数前加 v 前缀表示返回 void,加 Task 后缀表明这是一个 RTOS 任务。团队协作时,命名规范能省很多沟通成本。

任务创建完成后,SafeRTOS 会自动把它加入就绪队列。调度器会在下一个调度点检查优先级,如果这个任务优先级最高,它就会开始运行。

嗯,任务创建这块其实不难,但细节决定成败。堆栈大小、优先级分配、参数传递方式——每一个选择都会影响系统的稳定性和实时性。我见过太多因为堆栈溢出导致的神秘崩溃,也见过优先级设反了导致关键任务被饿死的情况。

记住一句话:任务创建不是写完代码就完事了,调试和验证才是重头戏。