4. 任务创建与管理:任务函数、任务句柄、任务堆栈、动态与静态创建
好,咱们今天聊聊任务创建。这是RTOS里最基础、也最核心的操作。你想想看,整个系统就是由一个个任务撑起来的,任务怎么生出来、怎么管好,直接决定了系统稳不稳。
我个人习惯把任务创建比作「招人干活」。你得先写好岗位职责(任务函数),再给员工配个工位(任务堆栈),最后发个工牌(任务句柄)方便管理。至于招人是走劳务派遣(动态创建)还是正式编制(静态创建),那就看项目需求了。
4.1 任务函数——任务的灵魂
任务函数说白了就是任务要执行的代码。它长什么样?我直接给你看个最典型的模板:
void vTaskFunction(void *pvParameters)
{
// 初始化工作
for(;;)
{
// 任务主体代码
vTaskDelay(pdMS_TO_TICKS(100));
}
}
注意几个关键点:
- 无限循环:任务函数几乎都是死循环。为什么?因为任务一旦返回,就被系统回收了。除非你故意设计成一次性任务。
- 参数指针:void *pvParameters 可以传任何东西。我做过一个项目,用这个参数传结构体指针,一个函数体跑了5个不同任务,省了不少代码量。
- 不能阻塞:千万别在任务函数里用 while(1) 空转,那是裸机思维。要用 vTaskDelay 或信号量让出CPU。
小技巧:任务函数名最好带个「v」前缀,表示返回void。这是FreeRTOS的命名习惯,团队协作时一眼就能认出是任务函数。
4.2 任务句柄——任务的身份证
任务句柄是什么?就是一个指向任务控制块(TCB)的指针。你有了它,就能对任务为所欲为——挂起、恢复、删除、改优先级。
声明方式很简单:
TaskHandle_t xTaskHandle = NULL;
创建任务时,把句柄地址传进去:
xTaskCreate(vTaskFunction, "Task1", 256, NULL, 1, &xTaskHandle);
之后想操作这个任务,直接拿句柄说话:
vTaskSuspend(xTaskHandle); // 挂起
vTaskResume(xTaskHandle); // 恢复
vTaskDelete(xTaskHandle); // 删除
避坑指南:我曾经在一个产品里忘了初始化句柄为NULL,结果任务创建失败后,后续代码拿着野指针去操作任务,直接硬故障。记住:句柄用前先清零,这是保命习惯。
4.3 任务堆栈——任务的工位
每个任务都有自己的堆栈,用来存局部变量、函数调用现场、中断返回地址。堆栈大小直接决定了任务能玩多大。
堆栈设多大?我一般这么估算:
- 任务里调用的函数嵌套深度 × 每个函数的局部变量大小
- 中断嵌套可能占用的栈空间
- 加上20%的安全余量
举个例子,一个简单的传感器读取任务:
// 堆栈大小单位是字(4字节),不是字节!
#define TASK_STACK_SIZE 256
xTaskCreate(vSensorTask, "Sensor", TASK_STACK_SIZE, NULL, 2, NULL);
256个字 = 1024字节。够用吗?我实测过,如果任务里只调两三层函数,128个字都够。但如果你用 printf、sprintf 这种重型函数,堆栈分分钟爆掉。
经验之谈:调试阶段把堆栈设大两倍,稳定后再慢慢缩小。FreeRTOS 提供了 uxTaskGetStackHighWaterMark() 函数,可以查看任务实际用了多少堆栈。我每次做项目都会在调试串口打印这个值,心里踏实。
4.4 动态创建 vs 静态创建
这是新手最容易纠结的地方。我直接给你对比表:
| 对比项 | 动态创建 (xTaskCreate) | 静态创建 (xTaskCreateStatic) |
|---|---|---|
| 堆栈来源 | 从堆(heap)分配 | 用户自己提供数组 |
| TCB来源 | 从堆分配 | 用户自己提供 StaticTask_t 结构体 |
| 灵活性 | 高,运行时动态创建/删除 | 低,编译时就定死了 |
| 安全性 | 可能内存碎片或分配失败 | 绝对可靠,不会分配失败 |
| 适用场景 | 原型开发、任务数量动态变化 | 安全关键系统、资源受限设备 |
动态创建代码:
if(xTaskCreate(vTask, "Task", 256, NULL, 1, &xHandle) != pdPASS)
{
// 创建失败!处理错误
printf("Task creation failed!\n");
}
静态创建代码:
StackType_t xTaskStack[256];
StaticTask_t xTaskTCB;
TaskHandle_t xHandle = xTaskCreateStatic(
vTask, // 任务函数
"Task", // 任务名
256, // 堆栈大小
NULL, // 参数
1, // 优先级
xTaskStack, // 堆栈数组
&xTaskTCB // TCB结构体
);
if(xHandle == NULL)
{
// 创建失败(理论上不会,但还是要检查)
}
我的建议:商业产品我推荐静态创建。为什么?因为动态分配可能失败,而洪水预警系统这种7×24小时运行的关键设备,最怕的就是「跑着跑着突然创建不了任务」。静态创建虽然代码啰嗦点,但心里踏实。
4.5 创建任务的完整流程
最后,我给你总结一个实战流程。这是我做洪水预警数据采集节点时的标准做法:
- 定义任务函数:写好无限循环,里面放具体业务逻辑
- 声明句柄和堆栈:全局变量,方便其他任务操作
- 调用创建函数:在 main 或启动任务里创建
- 检查返回值:创建失败要记录日志并尝试恢复
- 启动调度器:vTaskStartScheduler()
嗯,这里要注意:创建任务本身也是在任务里执行的。我见过有人把 xTaskCreate 放在中断里,结果系统直接崩了。记住,RTOS API 大部分不能在中断服务函数里调用。
好了,任务创建这块就聊到这儿。下一节咱们讲任务状态与切换,到时候你会明白为什么任务有时候「睡着了叫不醒」。