3. 任务创建与删除:FreeRTOS任务创建API、任务参数传递、任务删除与清理、任务句柄管理
好,咱们直接进入正题。任务创建与删除,这是RTOS里最基础的操作,但也是坑最多的地方。我见过不少新手,任务创建得飞起,删除时却留下一堆烂摊子。今天我就把这块掰开了揉碎了讲清楚。
3.1 任务创建API:xTaskCreate 到底怎么用?
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 才算创建成功。我见过有人直接忽略返回值,结果任务没创建成功,系统跑飞了都不知道为什么。
举个例子,创建一个简单的LED闪烁任务:
void vLEDTask(void *pvParameters)
{
// 参数就是LED的GPIO引脚号
uint8_t ucLEDPin = (uint8_t)pvParameters;
for(;;)
{
GPIO_Toggle(ucLEDPin);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 在main函数中创建
TaskHandle_t xLEDTaskHandle = NULL;
void main(void)
{
// 创建任务,传递LED引脚号作为参数
if(xTaskCreate(
vLEDTask, // 任务函数
"LED Task", // 任务名
128, // 栈大小(128个字 = 512字节)
(void*)GPIO_PIN_5, // 参数:LED引脚号
1, // 优先级
&xLEDTaskHandle // 句柄
) != pdPASS)
{
// 创建失败,处理错误
Error_Handler();
}
vTaskStartScheduler();
}
这里有个细节:栈大小单位是字(word)。在32位处理器上,1个字 = 4字节。所以128字 = 512字节。我建议新手至少给256字,别太抠门。栈溢出是RTOS里最难排查的问题之一。
3.2 任务参数传递:不只是传个整数那么简单
任务参数 pvParameters 是 void* 类型,这意味着你可以传任何东西。我个人习惯传结构体指针,这样能传递多个参数。
举个例子,控制多个电机时:
typedef struct
{
uint8_t ucMotorID;
uint16_t usTargetSpeed;
uint16_t usAccelTime;
} MotorConfig_t;
void vMotorControlTask(void *pvParameters)
{
MotorConfig_t *pxConfig = (MotorConfig_t*)pvParameters;
for(;;)
{
// 使用 pxConfig->ucMotorID, pxConfig->usTargetSpeed 等
Motor_SetSpeed(pxConfig->ucMotorID, pxConfig->usTargetSpeed);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 创建多个电机任务
MotorConfig_t xMotor1Config = {1, 1000, 500};
MotorConfig_t xMotor2Config = {2, 2000, 300};
xTaskCreate(vMotorControlTask, "Motor1", 256, &xMotor1Config, 2, NULL);
xTaskCreate(vMotorControlTask, "Motor2", 256, &xMotor2Config, 2, NULL);
注意:传结构体指针时,结构体变量必须在任务运行期间一直有效。如果你传的是局部变量,任务还没跑完,变量就被销毁了,那任务拿到的就是垃圾数据。我曾经在项目里犯过这个错,排查了整整两天。
解决办法有两个:
- 用全局变量或静态变量
- 用
pvPortMalloc动态分配内存,任务结束时释放
3.3 任务删除与清理:别直接跑路
任务删除用 vTaskDelete()。但注意,删除不是立即销毁,而是把任务从调度队列里移除,然后由空闲任务回收资源。
// 删除自己
vTaskDelete(NULL);
// 删除其他任务
vTaskDelete(xTaskHandle);
这里有个大坑:任务删除后,它占用的栈空间不会自动释放。FreeRTOS的空闲任务会帮你回收,但前提是——你得让空闲任务有机会运行。
我的经验:如果你在某个高优先级任务里删除了另一个任务,然后这个高优先级任务一直占着CPU不放,空闲任务就永远没机会运行。被删除任务的栈空间就永远无法回收。时间长了,内存就泄漏了。
正确的做法是:删除任务后,主动让出CPU:
void vSomeTask(void *pvParameters)
{
// ... 做一些工作 ...
// 删除另一个任务
vTaskDelete(xOtherTaskHandle);
// 主动让出CPU,让空闲任务有机会回收资源
taskYIELD();
// 继续做其他事
}
3.4 任务句柄管理:你的任务身份证
TaskHandle_t 就是任务的身份证。有了它,你可以:
- 删除任务:
vTaskDelete(xHandle) - 挂起/恢复任务:
vTaskSuspend/vTaskResume - 获取任务优先级:
uxTaskPriorityGet - 向任务发送通知:
xTaskNotifyGive
我建议把所有任务句柄集中管理,比如放在一个结构体里:
typedef struct
{
TaskHandle_t xLEDTask;
TaskHandle_t xMotorTask;
TaskHandle_t xCommTask;
TaskHandle_t xSensorTask;
} TaskHandles_t;
TaskHandles_t g_xTaskHandles = {0};
这样调试时一目了然。而且,如果你想在系统异常时挂起所有任务,直接遍历这个结构体就行。
重要:任务句柄在任务创建时赋值,但任务删除后,句柄就变成悬空指针了。如果你还拿着这个句柄去操作任务,系统会崩溃。我建议在删除任务后,把对应的句柄置为NULL:
void vDeleteAndCleanup(TaskHandle_t *pxHandle)
{
if(*pxHandle != NULL)
{
vTaskDelete(*pxHandle);
*pxHandle = NULL; // 清空句柄,防止误用
}
}
3.5 实战中的避坑指南
嗯,说了这么多,总结几个我踩过的坑:
- 栈大小估算:别太自信。我一般给任务分配栈后,用
uxTaskGetStackHighWaterMark()查看实际使用量,然后加50%余量。 - 参数生命周期:传指针时,确保指针指向的内存一直有效。传数值时,注意类型转换。
- 删除任务后:记得清空句柄,让出CPU。
- 不要在中断里创建或删除任务:FreeRTOS不允许。如果非要在中断里操作,用消息队列通知一个任务去做。
我曾经在一个电机控制项目里,因为忘记检查 xTaskCreate 的返回值,导致任务没创建成功,系统直接卡死。从那以后,我写任何创建任务的代码,第一件事就是检查返回值。你想想看,一个简单的 if 判断,能省下多少调试时间?
好了,任务创建与删除就讲到这里。下一章咱们聊聊任务状态与调度,看看任务到底是怎么在运行、就绪、阻塞、挂起之间切换的。