3. FreeRTOS 基础回顾:任务创建与删除,任务优先级,任务状态

好,咱们正式开始动手之前,得先把地基夯实了。这一节,我带你快速过一遍 FreeRTOS 里最核心的几个概念。别嫌啰嗦,这些是后面做灯光调度的命根子。

3.1 任务创建与删除:让代码跑起来

任务是什么?说白了,就是一段独立的、可以无限循环的函数。你想想看,一个 MCU 只有一个核,怎么同时干好几件事?就是靠这个任务切换。

创建任务,最常用的就是 xTaskCreate() 这个 API。我刚开始用的时候,总觉得参数太多,记不住。后来我总结了一个口诀:「函数名、栈大小、参数、优先级、句柄」。嗯,就这么简单。

// 任务函数原型
void vTaskFunction(void *pvParameters);

// 创建任务
BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,        // 任务函数指针
    const char * const pcName,        // 任务名字(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字
    void *pvParameters,               // 传给任务的参数
    UBaseType_t uxPriority,           // 任务优先级
    TaskHandle_t *pxCreatedTask       // 任务句柄,用来删除或挂起
);

这里有个坑,我必须要说。栈大小!栈给小了,任务跑着跑着就崩了。我在项目中遇到过,一个任务里用了局部大数组,栈直接爆了,系统死机。查了两天才找到原因。所以,我建议你刚开始给栈大一点,比如 256 或 512,稳定后再优化。

删除任务用 vTaskDelete()。注意,不能删除自己正在运行的任务以外的其他任务吗?其实可以,但你要保证那个任务已经不再使用任何资源。我个人习惯是,任务自己退出时,在函数末尾调用 vTaskDelete(NULL),把自个儿清理掉。

避坑指南: 我曾经在删除任务后,忘记释放动态分配的内存,导致内存泄漏。记住,vTaskDelete() 只删除任务本身,不会帮你清理任务里 malloc 出来的内存。

3.2 任务优先级:谁先跑,谁后跑

优先级,就是任务的「地位」。数字越大,优先级越高。高优先级的任务,会抢占低优先级的任务。你想想看,如果两个任务同时就绪,CPU 肯定先伺候优先级高的那个。

FreeRTOS 支持抢占式调度。什么意思?就是高优先级任务一旦就绪,立刻打断当前正在运行的低优先级任务。我刚开始做多任务时,总觉得这很酷,后来发现,优先级设不好,系统会出大问题。

举个例子,你有一个 LED 闪烁任务(优先级 1),一个按键扫描任务(优先级 2)。按键扫描任务里有个 vTaskDelay(10),延时期间 LED 任务能跑。但一旦按键按下,按键任务立刻抢回来。这没问题。

但如果你把 LED 任务优先级设成 3,按键任务优先级设成 2,那按键按下去后,LED 任务还在跑,按键响应就慢了。这就是典型的优先级反转的雏形。

我的经验: 对于灯光调度这种场景,我一般把控制逻辑(比如按键、传感器)的优先级设高,把显示效果(比如 LED 闪烁、呼吸灯)的优先级设低。因为控制需要实时响应,显示慢一点没关系。

3.3 任务状态:运行、就绪、阻塞、挂起

任务不是一直在跑的。它有四种状态,你必须要搞明白。

状态 含义 怎么进入
运行 正在使用 CPU 调度器选中它
就绪 能跑,但 CPU 被别的任务占着 创建后、从阻塞/挂起恢复后
阻塞 在等某个事件(延时、信号量、队列) 调用 vTaskDelay()、等待队列等
挂起 被强制暂停,不参与调度 调用 vTaskSuspend()

这里我重点说一下阻塞和挂起的区别。阻塞是任务自己主动去等的,比如 vTaskDelay(100),等 100 毫秒后自动回到就绪态。挂起是被别人强制暂停的,比如另一个任务调用了 vTaskSuspend(),除非有人调用 vTaskResume(),否则它永远不回来。

为什么会这样?因为挂起常用于调试或低功耗场景。我记得有一次做产品,需要进入低功耗模式,我就把所有任务挂起,只留一个唤醒任务。等唤醒条件满足后,再恢复所有任务。这样既省电,又不会丢数据。

核心要点: 在灯光调度中,你大部分时间看到的是「阻塞」状态。因为 LED 闪烁需要延时,按键扫描需要等待。如果某个任务一直处于「运行」状态,那说明它可能死循环了,或者优先级太高,把别的任务饿死了。

3.4 实战小贴士:任务状态转换图

我画个简单的状态转换图,你记在脑子里:

创建 → 就绪 → 运行 → 阻塞 → 就绪
                ↓
              挂起 → 就绪(恢复后)

注意,从「运行」到「就绪」,可能是时间片到了,也可能是被更高优先级的任务抢占了。从「运行」到「阻塞」,一定是任务自己调用了阻塞 API。

嗯,这里要注意:挂起状态不能直接到阻塞状态。必须先恢复成就绪,再运行,再阻塞。这个顺序别搞反了。

好了,这一节的内容就这些。任务创建、优先级、状态,这三个东西是 FreeRTOS 的骨架。你把它搞透了,后面写灯光调度代码,就像搭积木一样简单。