任务管理基础:任务状态机、任务优先级、任务创建与删除、任务堆栈配置

好,咱们今天聊聊任务管理。这部分是 FreeRTOS 的根基,说白了,你写的所有应用代码,最后都得靠任务来承载。我刚开始用 FreeRTOS 的时候,觉得任务不就是个死循环嘛,有啥好讲的?结果踩了不少坑,才明白这里面的门道有多深。

任务状态机:任务到底在干嘛?

一个任务,在它的生命周期里,不可能一直在跑。它得歇着、得等着、甚至可能被“雪藏”。FreeRTOS 把任务的状态分成了四种:运行态就绪态阻塞态挂起态

  • 运行态:CPU 正在执行这个任务的代码。单核 MCU 上,同一时刻只有一个任务能处于运行态。
  • 就绪态:任务能跑,但 CPU 正忙着,轮不到它。说白了,就是“排队等 CPU”。
  • 阻塞态:任务在等某个事件。比如等延时结束、等信号量、等队列消息。这时候任务不消耗 CPU。
  • 挂起态:任务被“雪藏”了。调用 vTaskSuspend() 就能让它进去,只有调用 vTaskResume() 才能出来。

嗯,这里要注意,任务不是想进哪个态就进哪个态的。比如,从运行态可以直接进阻塞态(调用延时函数),但就绪态不能直接进挂起态。你得先让它跑起来,才能挂起它。

核心要点:任务状态切换是由内核控制的,但触发条件是你写的代码。比如你调了 vTaskDelay(),任务就从运行态切到了阻塞态。

我在项目中遇到过一个问题:一个任务明明调了延时,但系统还是卡死。后来发现,这个任务在延时期间被其他任务频繁唤醒,根本没机会进阻塞态。你想想看,这就像你刚要睡觉,就被人叫醒,能不崩溃吗?

任务优先级:谁先跑?

FreeRTOS 支持抢占式调度。优先级高的任务,只要就绪了,就能把正在跑的低优先级任务踢出去。优先级数值越小,优先级越低(0 是最低)。

我个人习惯,把关键任务(比如通信、控制)设高优先级,把后台任务(比如日志、显示)设低优先级。但这里有个坑:优先级反转

避坑指南:我曾经在一个项目中,把三个任务设成了不同的优先级,结果低优先级任务占着共享资源不放,中优先级任务一直跑,高优先级任务等资源等到死。这就是典型的优先级反转。解决办法?用互斥量(Mutex),它自带优先级继承机制。

还有一点,优先级不是越多越好。FreeRTOS 支持的最大优先级数可以配置,但实际用 5-8 个就足够了。优先级太多,调度开销会变大,而且逻辑容易乱。

任务创建与删除:生与死的艺术

创建任务用 xTaskCreate()xTaskCreateStatic()。前者动态分配堆栈,后者静态分配。我建议新手先用动态的,省事。

// 动态创建任务
TaskHandle_t myTaskHandle = NULL;
xTaskCreate(
    vTaskFunction,    // 任务函数
    "My Task",        // 任务名字(调试用)
    256,              // 堆栈深度(单位:字,不是字节)
    NULL,             // 参数
    1,                // 优先级
    &myTaskHandle     // 任务句柄
);

删除任务用 vTaskDelete()。注意,任务可以自己删自己:vTaskDelete(NULL)。但删别人时,得确保那个任务不是正在运行的关键任务,否则系统可能崩溃。

个人经验:我一般不在任务运行中动态删除任务,而是用挂起 + 恢复来控制。因为频繁创建删除会导致堆碎片,时间长了内存就乱了。你想想看,内存碎片多了,新任务可能创建失败,这 bug 可不好查。

任务堆栈配置:给任务一个“家”

每个任务都有自己的堆栈,用来存局部变量、函数调用信息等。堆栈大小直接决定了任务能跑多“深”。

堆栈大小怎么定?我有个土办法:先给一个较大的值(比如 512 字),跑起来后用 uxTaskGetStackHighWaterMark() 查看剩余堆栈,然后逐步调小。这样既省内存,又不会溢出。

// 查看堆栈剩余空间
UBaseType_t stackHighWaterMark = uxTaskGetStackHighWaterMark(myTaskHandle);
printf("剩余堆栈: %d 字\n", stackHighWaterMark);

嗯,这里要注意,堆栈溢出是 FreeRTOS 最常见的 bug 之一。我曾经在一个项目中,任务堆栈设小了,结果跑着跑着就死机。查了两天才发现是堆栈溢出把任务控制块(TCB)给覆盖了。从那以后,我每个任务都会加堆栈溢出检测。

警告:堆栈溢出检测有两种方式:方法 1(堆栈增长检测)和方法 2(尾部标记检测)。我建议用方法 2,更可靠。在 FreeRTOSConfig.h 中设置 configCHECK_FOR_STACK_OVERFLOW 为 2 即可。

堆栈大小还跟函数调用深度有关。如果你的任务里用了递归,或者调了很多层函数,堆栈就得大一些。我一般给简单任务 128 字,复杂任务 256-512 字。但具体多少,还是得实测。

总结一下

任务管理这块,说白了就是三件事:状态机让你知道任务在干嘛,优先级决定谁先干,堆栈保证能干好。我建议你写代码前,先画个任务状态图,把每个任务的优先级和堆栈大小标出来。别嫌麻烦,这能省下后面调试的无数时间。

好,今天就聊到这儿。下一节咱们讲讲任务间的通信,那又是另一片天地了。