3. 任务创建与删除:动态创建任务、静态创建任务、任务删除、任务句柄与栈空间
好,咱们进入正题。任务创建与删除,这是RTOS里最基础、也最核心的操作。说白了,你写嵌入式程序,就是在跟任务打交道。任务怎么生出来?怎么让它消失?它的“身份证”和“房子”是怎么回事?今天咱们一次性讲透。
3.1 动态创建任务:最常用的方式
动态创建任务,就是用 xTaskCreate() 这个API。我个人习惯在项目初期大量使用动态创建,因为灵活。你想想看,任务栈空间是从堆里动态分配的,你不需要提前规划好每个任务用多少栈,系统帮你搞定。
来看一个典型的例子:
TaskHandle_t myTaskHandle = NULL;
void vTaskFunction(void *pvParameters) {
while(1) {
// 任务代码
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void main() {
BaseType_t xReturned;
xReturned = xTaskCreate(
vTaskFunction, // 任务函数
"My Task", // 任务名字(调试用)
256, // 栈深度,单位是字(Word)
NULL, // 参数
1, // 优先级
&myTaskHandle // 任务句柄
);
if (xReturned == pdPASS) {
// 创建成功
} else {
// 创建失败,通常是堆空间不足
}
vTaskStartScheduler();
}
这里有个坑,我踩过。栈深度单位是“字”,不是字节。在32位处理器上,一个字是4字节。所以256字 = 1024字节。我曾经在项目里写了个512,以为够用了,结果任务跑起来就崩。后来一查,任务里有个局部数组,栈溢出了。嗯,这里要注意,栈大小一定要算清楚。
3.2 静态创建任务:资源可控,但麻烦
静态创建任务,用的是 xTaskCreateStatic()。它不会从堆里分配内存,而是用你预先定义好的栈空间和任务控制块。说白了,就是你自己给任务准备好“房子”和“身份证”。
为什么要用静态创建?
- 安全性高:不会因为堆碎片导致创建失败。我在医疗设备项目里,就强制要求用静态创建,因为系统不能容忍任何动态分配失败的风险。
- 确定性好:内存占用在编译时就确定了,适合对实时性要求极高的场景。
代码长这样:
static StackType_t taskStack[256]; // 静态分配的栈
static StaticTask_t taskControlBlock; // 静态分配的任务控制块
TaskHandle_t xHandle = NULL;
void main() {
xHandle = xTaskCreateStatic(
vTaskFunction,
"Static Task",
256,
NULL,
1,
taskStack,
&taskControlBlock
);
vTaskStartScheduler();
}
你可能会问,那到底用动态还是静态?我的建议是:原型阶段用动态,产品阶段用静态。动态方便调试,静态可靠稳定。
3.3 任务删除:让任务“寿终正寝”
任务删除用 vTaskDelete()。这个API很简单,但有几个细节你必须知道。
删除任务有两种方式:
- 自删除:任务自己调用
vTaskDelete(NULL),把自己干掉。注意,传NULL表示删除当前任务。 - 他删除:其他任务调用
vTaskDelete(taskHandle),删除指定任务。
举个例子:
void vTaskFunction(void *pvParameters) {
// 做一些初始化工作
// 然后自删除
vTaskDelete(NULL); // 任务执行到这里就结束了
}
// 或者在其他任务里删除它
void vOtherTask(void *pvParameters) {
vTaskDelay(pdMS_TO_TICKS(1000));
vTaskDelete(myTaskHandle); // 1秒后删除myTask
}
这里有个重要概念:任务删除后,它占用的栈空间和任务控制块会被释放。但如果是静态创建的任务,这些资源不会被释放,因为它们本来就不是从堆里分配的。所以静态任务删除后,你不能再重新创建它,除非你手动重置那些静态变量。
3.4 任务句柄:任务的“身份证”
任务句柄,就是 TaskHandle_t 类型的变量。它本质上是一个指针,指向任务控制块(TCB)。你可以把它理解为任务的“身份证号”。
任务句柄有什么用?
- 删除任务:
vTaskDelete(taskHandle) - 挂起/恢复任务:
vTaskSuspend(taskHandle)/vTaskResume(taskHandle) - 获取任务信息:
uxTaskPriorityGet(taskHandle) - 通知任务:
xTaskNotifyGive(taskHandle)
我个人习惯在创建任务时,把句柄保存为全局变量或者结构体成员。这样方便其他任务引用。但要注意,任务删除后,句柄就失效了,不能再使用。否则会引发不可预知的行为。
3.5 栈空间:任务的“房子”
每个任务都有自己的栈,用来保存局部变量、函数调用信息、中断上下文等。栈空间的大小,直接决定了任务能跑多复杂的代码。
栈空间怎么算?我一般用这个经验公式:
- 简单任务(只有几个局部变量,没有深层函数调用):128字(512字节)
- 中等任务(有数组、结构体,函数调用深度3-5层):256字(1024字节)
- 复杂任务(有大量局部数据,或者调用了printf等库函数):512字(2048字节)以上
uxTaskGetStackHighWaterMark() 来查看任务栈的剩余空间。这个API会返回任务运行以来,栈空间最少剩余了多少。如果这个值接近0,说明栈空间不够,需要加大。
我记得有一次,客户反馈设备偶尔死机。我查了三天,最后用这个API一看,有个任务的栈高水位只有4个字了。嗯,就差那么一点点就溢出了。把栈从256改成384,问题解决。
3.6 避坑指南:我踩过的那些坑
最后,分享几个实战中容易犯的错误:
- 栈溢出检测:一定要开启FreeRTOS的栈溢出检测功能(
configCHECK_FOR_STACK_OVERFLOW)。我曾经在一个项目里没开这个,结果任务栈溢出后,把其他任务的数据给覆盖了,现象极其诡异。 - 任务优先级:创建任务时,优先级不要设得太高。我建议从1开始,逐步调高。否则高优先级任务一直占着CPU,低优先级任务永远得不到执行。
- 任务名字:名字长度不要超过
configMAX_TASK_NAME_LEN,否则会被截断。调试时看到一堆乱码名字,你会很崩溃。 - 静态任务的重复创建:静态任务删除后,不能直接重新创建。你需要先重置栈空间和TCB,或者干脆重新定义新的静态变量。
好了,任务创建与删除就讲到这里。下一节咱们聊聊任务状态与切换,这是理解RTOS调度的关键。到时候见。