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 任务还在跑,按键响应就慢了。这就是典型的优先级反转的雏形。
3.3 任务状态:运行、就绪、阻塞、挂起
任务不是一直在跑的。它有四种状态,你必须要搞明白。
| 状态 | 含义 | 怎么进入 |
|---|---|---|
| 运行 | 正在使用 CPU | 调度器选中它 |
| 就绪 | 能跑,但 CPU 被别的任务占着 | 创建后、从阻塞/挂起恢复后 |
| 阻塞 | 在等某个事件(延时、信号量、队列) | 调用 vTaskDelay()、等待队列等 |
| 挂起 | 被强制暂停,不参与调度 | 调用 vTaskSuspend() |
这里我重点说一下阻塞和挂起的区别。阻塞是任务自己主动去等的,比如 vTaskDelay(100),等 100 毫秒后自动回到就绪态。挂起是被别人强制暂停的,比如另一个任务调用了 vTaskSuspend(),除非有人调用 vTaskResume(),否则它永远不回来。
为什么会这样?因为挂起常用于调试或低功耗场景。我记得有一次做产品,需要进入低功耗模式,我就把所有任务挂起,只留一个唤醒任务。等唤醒条件满足后,再恢复所有任务。这样既省电,又不会丢数据。
3.4 实战小贴士:任务状态转换图
我画个简单的状态转换图,你记在脑子里:
创建 → 就绪 → 运行 → 阻塞 → 就绪
↓
挂起 → 就绪(恢复后)
注意,从「运行」到「就绪」,可能是时间片到了,也可能是被更高优先级的任务抢占了。从「运行」到「阻塞」,一定是任务自己调用了阻塞 API。
嗯,这里要注意:挂起状态不能直接到阻塞状态。必须先恢复成就绪,再运行,再阻塞。这个顺序别搞反了。
好了,这一节的内容就这些。任务创建、优先级、状态,这三个东西是 FreeRTOS 的骨架。你把它搞透了,后面写灯光调度代码,就像搭积木一样简单。