4. 任务创建与删除:任务创建API(xTaskCreate)、任务删除API(vTaskDelete)、任务句柄与优先级
好,咱们进入正题。任务创建和删除,这是FreeRTOS里最基础的操作。说白了,你写嵌入式程序,就是创建一堆任务,让它们跑起来,然后根据需要再干掉它们。我刚开始接触RTOS时,觉得这玩意儿挺玄乎,后来发现,其实就是几个API函数的事。
4.1 任务创建API:xTaskCreate
xTaskCreate 这个函数,可以说是整个系统的起点。没有它,你的任务就只是个函数,没法被调度器管理。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(word)
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
嗯,参数不少,但别慌。我一个个说。
- pvTaskCode:就是你的任务函数名。比如你写了个
void vLEDTask(void *pvParameters),这里就填vLEDTask。 - pcName:给任务起个名字。这名字只在调试时有用,比如用
vTaskList()查看任务状态时能看到。我习惯用英文,比如 "LED_Task"。 - usStackDepth:栈大小。这个很关键!我踩过坑,栈设小了,任务跑着跑着就崩了。单位是字(word),在32位MCU上,1个字 = 4字节。比如你设100,那就是400字节。
- pvParameters:传给任务函数的参数。可以是数字、结构体指针,啥都行。不需要就填NULL。
- uxPriority:优先级。数值越大,优先级越高。0是最低优先级。
- pxCreatedTask:任务句柄的指针。后面删除任务时要用到。不需要可以填NULL。
返回值:成功返回 pdPASS(其实就是1),失败返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(-1)。
举个例子,创建一个LED闪烁任务:
TaskHandle_t xLEDTaskHandle = NULL;
void vLEDTask(void *pvParameters)
{
(void)pvParameters; // 防止编译器警告
while(1)
{
GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void main(void)
{
// 硬件初始化...
xTaskCreate(
vLEDTask, // 任务函数
"LED_Task", // 任务名称
128, // 栈大小:128字 = 512字节
NULL, // 无参数
2, // 优先级设为2
&xLEDTaskHandle // 保存句柄
);
vTaskStartScheduler(); // 启动调度器
}
你看,就这么简单。任务函数里是个死循环,调度器会不断切换任务,让它们轮流执行。
4.2 任务删除API:vTaskDelete
有创建就有删除。vTaskDelete 用来干掉一个任务。原型更简单:
void vTaskDelete(TaskHandle_t xTaskToDelete);
参数就一个:要删除的任务句柄。如果传NULL,表示删除自己(当前任务)。
注意:删除任务后,任务占用的栈空间和TCB(任务控制块)会被释放回堆内存。但任务自己申请的动态内存(比如malloc出来的)不会自动释放!你得在任务退出前自己清理。
我曾经犯过一个错:在任务里malloc了一块缓冲区,然后直接调 vTaskDelete(NULL) 把自己删了。结果内存泄漏,系统跑了几天就挂了。嗯,血的教训。
正确的做法:
void vSomeTask(void *pvParameters)
{
uint8_t *pBuffer = (uint8_t *)pvPortMalloc(256);
if(pBuffer != NULL)
{
// 使用缓冲区...
// 删除前释放
vPortFree(pBuffer);
}
// 然后删除自己
vTaskDelete(NULL);
}
4.3 任务句柄与优先级
任务句柄,说白了就是任务的身份证。它是一个 TaskHandle_t 类型的变量,指向任务控制块。有了它,你才能对任务进行操作,比如删除、挂起、恢复、改变优先级等。
我习惯在创建任务时就把句柄保存好,像这样:
TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;
void CreateAllTasks(void)
{
xTaskCreate(vTask1, "Task1", 128, NULL, 1, &xTask1Handle);
xTaskCreate(vTask2, "Task2", 128, NULL, 2, &xTask2Handle);
}
这样,后面想删除Task1,直接 vTaskDelete(xTask1Handle) 就行。
优先级 是调度器决定谁先跑的依据。FreeRTOS支持抢占式调度,高优先级任务就绪时,会立即抢占低优先级任务。
| 优先级值 | 说明 |
|---|---|
| 0 | 最低优先级,空闲任务(Idle Task)就运行在这个优先级 |
| 1 ~ (configMAX_PRIORITIES - 1) | 用户任务可用优先级,数值越大优先级越高 |
| configMAX_PRIORITIES - 1 | 最高优先级 |
小技巧:configMAX_PRIORITIES 在 FreeRTOSConfig.h 中定义。别设太大,一般5~32就够用。设大了会浪费RAM,因为每个优先级都需要一个就绪列表。
优先级设计是个学问。我一般遵循几个原则:
- 实时性要求高的任务(比如按键响应、通信接收)给高优先级。
- 计算量大但不紧急的任务(比如数据记录、界面刷新)给低优先级。
- 避免两个任务优先级相同,除非你明确知道它们要轮转调度。
你想想看,如果两个高优先级任务互相抢CPU,低优先级任务可能永远得不到执行。这就是所谓的「优先级反转」或「饥饿」问题。嗯,后面章节会细讲。
4.4 避坑指南
我曾经踩过的坑:
- 栈溢出:任务栈设太小,函数调用层级深一点就崩。建议用
uxTaskGetStackHighWaterMark()检查栈使用情况。 - 句柄未初始化:创建任务时忘了传句柄地址,或者句柄变量是局部变量,任务还没删句柄就没了。
- 删除自己后还访问局部变量:任务函数里定义了局部变量,然后
vTaskDelete(NULL)把自己删了。但函数还没返回,局部变量还在栈上。这时候如果别的任务访问这些变量,会出问题。 - 优先级设太高:把某个任务优先级设成最高,结果它一直占着CPU,其他任务饿死。我见过有人把轮询按键的任务设成最高优先级,结果系统卡死。
好了,任务创建和删除就这些。记住:创建时想好栈大小,删除时记得清理资源,优先级别乱设。下一节咱们聊聊任务的状态切换,看看任务到底是怎么「活」起来的。