第4章 任务创建与删除:动态创建任务(xTaskCreate)、静态创建任务(xTaskCreateStatic)、任务删除(vTaskDelete)
好,咱们今天聊聊RTOS里最基础、也最核心的操作——任务的创建与删除。
你想想看,一个实时系统跑起来,总得有几个任务在干活吧?有的负责采集传感器数据,有的负责刷新屏幕,有的负责处理按键。这些任务怎么来的?又怎么没的?这就是本章要讲清楚的事。
4.1 动态创建任务:xTaskCreate
动态创建任务,说白了就是让RTOS内核帮你分配任务所需的栈空间和任务控制块(TCB)。你只管告诉内核“我要一个什么样的任务”,剩下的内存分配、初始化工作,内核全包了。
我个人习惯在项目初期大量使用动态创建。为什么呢?因为省事。你不需要提前算好每个任务要多少栈,内核会根据你的配置自动从堆里切一块出来。
核心函数原型:
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。嗯,这里要注意,如果堆不够了,创建就会失败。
一个实际例子:
void vTaskLED(void *pvParameters)
{
while(1)
{
GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void main(void)
{
TaskHandle_t xLEDHandle = NULL;
if(xTaskCreate(
vTaskLED,
"LED Task",
128, // 128个字,约512字节
NULL,
2, // 优先级2
&xLEDHandle
) != pdPASS)
{
// 创建失败,处理错误
Error_Handler();
}
vTaskStartScheduler();
}
我在项目中遇到过一个问题:某个任务创建后一直不运行,查了半天才发现是栈深度给少了。任务里有个局部数组,直接就把栈撑爆了。所以我的建议是——栈深度至少给任务实际需求的1.5倍,留点余量。
小技巧:FreeRTOS提供了uxTaskGetStackHighWaterMark()函数,可以查看任务栈还剩多少空间。调试阶段多调用几次,能帮你找到最合适的栈大小。
4.2 静态创建任务:xTaskCreateStatic
动态创建虽然方便,但有个问题——它依赖堆。如果你的系统对实时性要求极高,或者你压根不想用堆(比如安全关键系统),那就得用静态创建。
静态创建,说白了就是你自己准备好栈空间和TCB的内存,然后告诉内核“用这块内存来创建任务”。内核不会去碰堆,所有内存都是你提前分配好的。
核心函数原型:
TaskHandle_t xTaskCreateStatic(
TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
StackType_t *puxStackBuffer, // 你提供的栈缓冲区
StaticTask_t *pxTaskBuffer // 你提供的TCB缓冲区
);
注意看,这里多了两个参数:栈缓冲区和TCB缓冲区。这两个都得你自己定义。
实际例子:
// 提前分配好内存
static StackType_t xLEDStack[128];
static StaticTask_t xLEDTaskBuffer;
void main(void)
{
TaskHandle_t xLEDHandle = NULL;
xLEDHandle = xTaskCreateStatic(
vTaskLED,
"LED Static",
128,
NULL,
2,
xLEDStack,
&xLEDTaskBuffer
);
if(xLEDHandle == NULL)
{
// 创建失败
Error_Handler();
}
vTaskStartScheduler();
}
我曾经在一个航空电子项目中被迫全部使用静态创建。原因很简单——适航认证要求不能有动态内存分配。那时候每个任务的栈大小都是反复测算、写死在代码里的。虽然麻烦,但心里踏实。
避坑指南:静态创建时,你提供的栈缓冲区必须保证生命周期足够长。如果你在函数内部定义一个局部数组作为栈缓冲区,函数返回后这块内存就失效了——任务跑着跑着就崩了。我曾经见过一个同事犯过这个错,排查了整整两天。
4.3 动态 vs 静态:怎么选?
| 对比项 | 动态创建 | 静态创建 |
|---|---|---|
| 内存来源 | 堆(heap) | 你指定的缓冲区 |
| 灵活性 | 高,运行时按需创建 | 低,编译时就得定好 |
| 安全性 | 可能因堆碎片而失败 | 无碎片问题 |
| 代码量 | 少,内核帮你管理 | 多,你得自己准备内存 |
| 适用场景 | 原型开发、资源充足的系统 | 安全关键、资源受限的系统 |
我的建议是:项目初期用动态创建快速验证功能,后期优化时再考虑是否换成静态。当然,如果你的目标芯片RAM只有几KB,那从一开始就得精打细算,静态创建可能是唯一选择。
4.4 任务删除:vTaskDelete
任务创建了,总有不需要的时候。比如某个初始化任务,干完活就该把自己清理掉。这时候就用vTaskDelete。
函数原型:
void vTaskDelete(TaskHandle_t xTaskToDelete);
参数是任务句柄。如果传入NULL,表示删除当前正在运行的任务自己。
一个典型用法:
void vInitTask(void *pvParameters)
{
// 做初始化工作
InitializeHardware();
LoadCalibrationData();
// 初始化完成,删除自己
vTaskDelete(NULL);
}
void main(void)
{
xTaskCreate(vInitTask, "Init", 256, NULL, 3, NULL);
vTaskStartScheduler();
}
这里有个关键点:任务被删除后,它占用的栈空间和TCB会被释放回堆(如果是动态创建)。但任务自己申请的其他资源——比如动态分配的内存、打开的文件句柄——不会自动释放。你得在任务退出前自己清理干净。
我曾经踩过的坑:有个任务里用pvPortMalloc分配了一块缓冲区,任务结束时只调了vTaskDelete(NULL),没调用vPortFree。结果系统跑了几天后,堆被耗尽,所有动态创建都失败了。从那以后,我养成了一个习惯——每个任务退出前,必须检查有没有未释放的资源。
另外,删除任务时要注意优先级反转的问题。如果被删除的任务正持有某个互斥量,而其他任务在等这个互斥量——嗯,那就会出大问题。所以我的建议是:确保任务在删除前已经释放了所有持有的同步资源。
4.5 综合示例:一个完整的创建-运行-删除流程
static TaskHandle_t xWorkerHandle = NULL;
void vWorkerTask(void *pvParameters)
{
int *pData = (int *)pvParameters;
for(int i = 0; i < 10; i++)
{
(*pData)++;
vTaskDelay(pdMS_TO_TICKS(100));
}
// 任务结束,删除自己
vTaskDelete(NULL);
}
void vSupervisorTask(void *pvParameters)
{
int sharedData = 0;
// 动态创建工作任务
xTaskCreate(
vWorkerTask,
"Worker",
128,
&sharedData,
1,
&xWorkerHandle
);
// 等待工作任务完成
vTaskDelay(pdMS_TO_TICKS(1500));
// 如果工作任务还没自己删除,强制删除
if(xWorkerHandle != NULL)
{
vTaskDelete(xWorkerHandle);
xWorkerHandle = NULL;
}
// 监督任务继续做其他事
while(1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
这个例子展示了几个要点:
- 任务之间可以通过参数传递数据指针
- 任务可以自己删除自己,也可以被其他任务删除
- 删除后记得把句柄置NULL,避免悬空指针
个人经验:我习惯在任务函数开头加一个断言,检查传入的参数是否有效。比如检查pvParameters是否为NULL。这样一旦传参出错,调试阶段就能立刻发现,而不是等到任务跑飞了才去查。
好了,关于任务的创建与删除,核心内容就这些。动态创建灵活方便,静态创建安全可控,删除时记得清理资源。下一章咱们聊聊任务的挂起和恢复,那又是另一番天地了。