2. 任务创建与管理:任务状态机、优先级、句柄与API详解
各位同学,咱们今天聊聊FreeRTOS里最核心的东西——任务。说白了,整个系统就是靠任务在跑。你想想看,一个单片机要同时处理按键、显示、通信、控制,没有任务调度,那代码得写成什么样?
我个人习惯,拿到一个新项目,第一件事就是先把任务框架搭好。任务怎么创建、怎么管理、怎么切换,这些搞明白了,后面写代码就顺风顺水。
2.1 任务状态机:就绪、运行、阻塞、挂起
任务在FreeRTOS里不是一直傻跑的。它有四种状态,像个状态机一样来回切换。我刚开始学的时候,总觉得这玩意儿抽象,后来在项目里踩过坑才真正理解。
四种核心状态:
- 就绪态(Ready):任务已经准备好,随时可以运行。就等着调度器翻牌子。
- 运行态(Running):正在占用CPU。单核MCU同一时刻只有一个任务在跑。
- 阻塞态(Blocked):任务在等某个事件。比如等延时结束、等信号量、等队列消息。
- 挂起态(Suspended):任务被强制暂停。调用vTaskSuspend()就进去了,得用vTaskResume()才能唤醒。
这里有个关键点:阻塞态和挂起态的区别。阻塞是任务自己主动去等,比如调了vTaskDelay();挂起是被别人强行按住,比如别的任务调了vTaskSuspend()。我在项目中遇到过,有人把这两个搞混,结果任务一直不跑,查了半天才发现是挂起没恢复。
下面这张图,是我手绘的任务状态切换逻辑,你一看就明白:
嗯,这里要注意:任务创建后,默认进入就绪态。调度器一跑,就从就绪队列里挑一个优先级最高的去运行。运行中如果调了延时函数,就进阻塞态;如果被别的任务挂起,就进挂起态。
2.2 任务优先级:谁抢谁?
优先级这玩意儿,说白了就是给任务排个队。数值越大,优先级越高。FreeRTOS支持从0到(configMAX_PRIORITIES - 1),一般配置成5或32就够用了。
避坑指南:我曾经在一个项目里,把两个关键任务的优先级设成了相同值。结果一个任务跑着跑着,另一个老抢不到CPU,导致系统响应变慢。后来改成不同优先级,问题就解决了。
优先级的使用原则,我总结了几条:
- 实时性要求高的任务:给高优先级。比如电机控制、紧急停止。
- 周期性任务:中等优先级。比如数据采集、状态刷新。
- 后台任务:低优先级。比如日志打印、界面刷新。
你想想看,如果按键扫描和电机控制优先级一样高,那电机转的时候按键按了没反应,这能行吗?
2.3 任务句柄:任务的身份证
任务句柄,就是TaskHandle_t类型的变量。它指向任务控制块(TCB),里面存了任务的所有信息——栈指针、优先级、状态等等。
我习惯在创建任务时,把句柄保存成全局变量。这样后面想挂起、恢复、删除任务,直接拿句柄操作就行。
TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;
// 创建任务时传入句柄地址
xTaskCreate(vTask1, "Task1", 256, NULL, 1, &xTask1Handle);
xTaskCreate(vTask2, "Task2", 256, NULL, 2, &xTask2Handle);
// 后面用句柄操作
vTaskSuspend(xTask1Handle); // 挂起任务1
vTaskResume(xTask2Handle); // 恢复任务2
小技巧:句柄不要乱用。我见过有人把句柄当普通变量传来传去,结果任务删了句柄还留着,一操作就崩溃。记住:句柄有效的前提是任务还存在。
2.4 xTaskCreate() 与 xTaskCreateStatic() 详解
这两个函数,是创建任务的两种方式。一个动态分配内存,一个静态分配。说白了,就是栈空间谁来给的问题。
xTaskCreate():动态创建
这是最常用的方式。任务栈和TCB都由FreeRTOS内部用pvPortMalloc()分配。用起来简单,适合大多数场景。
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名字(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(不是字节)
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
返回值:pdPASS表示成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY表示内存不够。
实际案例:我在一个物联网项目里,用动态创建了5个任务。后来发现系统跑着跑着就卡死,查了半天,原来是某个任务栈设小了,导致栈溢出。从那以后,我每个任务都留够余量,至少256字起步。
xTaskCreateStatic():静态创建
这个函数需要你自己提供栈空间和TCB内存。好处是:
- 不用动态内存分配,适合对实时性要求极高的场景
- 内存占用完全可控,不会出现碎片
- 适合裸机转FreeRTOS的老项目
StackType_t xTaskStack[256]; // 自己定义的栈
StaticTask_t xTaskTCB; // 自己定义的TCB
TaskHandle_t xTaskHandle = xTaskCreateStatic(
vTaskFunction, // 任务函数
"StaticTask", // 任务名
256, // 栈深度
NULL, // 参数
1, // 优先级
xTaskStack, // 栈指针
&xTaskTCB // TCB指针
);
如果返回NULL,说明创建失败。检查一下你给的栈空间和TCB是不是有效的。
| 对比项 | xTaskCreate() | xTaskCreateStatic() |
|---|---|---|
| 内存来源 | FreeRTOS内部动态分配 | 用户自己提供 |
| 栈空间 | 自动分配 | 用户定义数组 |
| TCB空间 | 自动分配 | 用户定义StaticTask_t变量 |
| 适用场景 | 大多数项目,方便快捷 | 对实时性/内存有严格要求的场景 |
| 风险 | 可能内存不足或产生碎片 | 需要手动管理内存,代码稍多 |
我个人习惯,原型阶段用动态创建,快速验证功能。产品定型后,如果发现内存碎片问题,再改成静态创建。你想想看,一个跑了几万小时的产品,内存碎片积累多了,突然某天创建任务失败,那多尴尬。
重要提醒:静态创建时,栈数组和TCB变量必须保持有效,不能是局部变量。我见过有人把栈定义在函数内部,函数返回后栈被释放,任务直接跑飞。正确做法是定义成全局变量或静态变量。
好了,任务创建和管理这块,核心就是这些。状态机搞明白,优先级设对,句柄用好,创建函数选对,你的任务系统就稳了。下一节咱们聊聊任务间的通信,那又是另一番天地。
公众号:蓝海资料掘金营,微信deep3321