3. 任务创建与删除:FreeRTOS任务创建API详解、任务参数传递、任务删除的注意事项
好,咱们进入实战环节的第一个硬核知识点——任务的创建与删除。
说实话,任务调度是RTOS的灵魂,而任务创建就是灵魂的“出生证明”。很多初学者觉得不就是调个API嘛,有什么难的?但我见过太多项目因为任务创建时埋下的坑,导致后期调试痛不欲生。今天我就把这块掰开了揉碎了讲清楚。
3.1 xTaskCreate:最常用的任务创建API
FreeRTOS里创建任务,最核心的就是 xTaskCreate() 这个函数。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(仅用于调试)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(word)
void *pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
返回值只有两种可能:pdPASS 表示成功,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 表示内存不够。嗯,说白了就是要么成了,要么堆空间不够。
重点提醒: 栈深度单位是“字”,不是字节!在32位MCU上,1个字 = 4字节。如果你要分配1024字节的栈,参数要填256。我见过有人直接填1024,结果栈空间浪费了4倍,还一脸懵。
3.2 任务参数传递的几种姿势
任务参数传递,说白了就是 pvParameters 这个 void* 指针怎么玩。我总结了几种常见用法:
3.2.1 传递整数值
最简单的方式,直接把整数强转成指针传进去:
void vTaskFunction(void *pvParameters) {
int task_id = (int)pvParameters;
// 使用task_id
}
// 创建时
xTaskCreate(vTaskFunction, "Task1", 256, (void*)1, 1, NULL);
xTaskCreate(vTaskFunction, "Task2", 256, (void*)2, 1, NULL);
注意:这种方式只适合传递小整数。在64位系统上要小心指针宽度问题。
3.2.2 传递结构体指针
这是实际项目中最常用的方式。我在做充电宝租借系统时,每个任务都需要知道自己的“身份信息”:
typedef struct {
uint8_t task_id;
uint8_t priority;
uint16_t timeout_ms;
void (*callback)(void);
} TaskConfig_t;
TaskConfig_t config1 = {1, 2, 100, led_blink_callback};
void vTaskFunction(void *pvParameters) {
TaskConfig_t *cfg = (TaskConfig_t *)pvParameters;
// 使用cfg->task_id, cfg->timeout_ms等
}
xTaskCreate(vTaskFunction, "LED_Task", 256, &config1, 2, NULL);
我的习惯: 结构体参数一定要保证生命周期。如果结构体是局部变量,任务还没开始跑它就销毁了,那任务拿到的就是野指针。我一般用静态分配或者动态分配并让任务自己释放。
3.2.3 传递字符串
字符串传递其实也是指针传递,但要小心字符串存储位置:
// 正确:字符串常量存储在Flash/ROM
xTaskCreate(vTask, "Print", 256, "Hello RTOS", 1, NULL);
// 错误:局部数组,任务启动时可能已失效
void create_task_bad(void) {
char msg[] = "Hello";
xTaskCreate(vTask, "Print", 256, msg, 1, NULL);
}
3.3 任务删除的注意事项
任务删除比创建更容易出问题。我早期做项目时,就因为任务删除不当导致系统死机,查了两天才找到原因。
3.3.1 谁可以删除任务?
FreeRTOS允许三种删除方式:
- 任务自己删除自己: 调用
vTaskDelete(NULL)或vTaskDelete(xTaskGetCurrentTaskHandle()) - 其他任务删除指定任务: 调用
vTaskDelete(handle) - 中断服务函数中删除: 不推荐!除非你非常清楚后果
3.3.2 删除后的内存回收问题
这是最大的坑。任务删除后,FreeRTOS会回收TCB(任务控制块)和栈空间,但前提是你用的是动态内存分配。如果你用了 pvPortMalloc 在任务里分配了内存,删除任务前必须手动释放!
我曾经踩过的坑: 有一个数据采集任务,每次运行都malloc一块缓冲区。任务结束时我直接调了vTaskDelete,没释放那块内存。结果系统跑了三天后,内存耗尽,所有任务都创建失败。从那以后,我养成了一个习惯:任务退出前,先清理所有动态资源,再调用vTaskDelete。
3.3.3 删除任务的正确姿势
我推荐的做法是:任务自己清理,自己删除。
void vDataTask(void *pvParameters) {
uint8_t *buffer = (uint8_t *)pvPortMalloc(1024);
// 任务主循环
for(;;) {
// 处理数据...
if(should_exit) {
break; // 跳出循环准备退出
}
}
// 清理工作
if(buffer != NULL) {
vPortFree(buffer);
buffer = NULL;
}
// 关闭硬件资源
close_sensor();
// 最后删除自己
vTaskDelete(NULL);
}
3.3.4 删除后句柄的处理
任务删除后,之前保存的 TaskHandle_t 就失效了。如果你还拿着这个句柄去操作,比如挂起、恢复、发通知,那系统行为是未定义的。我建议删除任务后立即将句柄置为NULL:
TaskHandle_t xHandle = NULL;
// 创建
xTaskCreate(vTask, "Demo", 256, NULL, 1, &xHandle);
// 删除
if(xHandle != NULL) {
vTaskDelete(xHandle);
xHandle = NULL; // 关键!防止悬空指针
}
3.4 实战中的经验总结
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 传递简单标识 | 用整数强转void* | 用全局变量传参 |
| 传递复杂配置 | 用静态结构体 | 用局部变量结构体 |
| 任务退出 | 任务自己清理并删除 | 外部强制删除 |
| 内存管理 | 谁分配谁释放 | 任务删除后依赖系统回收 |
最后说一句: 任务创建和删除看似简单,但它们是RTOS稳定性的基石。你想想看,一个系统里几十个任务,如果每个任务创建时都多分配100字节栈,那内存就白白浪费了几KB。如果删除时漏了一个内存释放,那系统迟早会崩。我的建议是:写任务代码时,先把退出逻辑想清楚,再写主循环逻辑。这个顺序反过来,往往就是bug的开始。
好了,任务创建与删除的核心内容就这些。下一章咱们聊聊任务调度与优先级,那才是真正体现RTOS魅力的地方。