2. 任务创建与管理:任务函数原型、xTaskCreate() API详解、任务参数传递、任务句柄
好,咱们进入正题。任务创建,是FreeRTOS里最基础的操作。你想想看,整个系统跑起来,靠的就是一个个任务在调度器手里来回切换。这一节,我就带你手把手把任务“生”出来。
2.1 任务函数长什么样?
每个任务本质上就是一个永不返回的C函数。它的原型是固定的,我直接给你看代码:
void vTaskFunction(void *pvParameters)
{
// 任务初始化代码
for( ;; )
{
// 任务主体代码
// 这里通常会调用 vTaskDelay() 或等待事件
}
// 理论上永远不会执行到这里
// 如果真执行到了,必须调用 vTaskDelete(NULL)
}
注意几个关键点:
- 返回值是void —— 任务函数没有返回值,它是个死循环。
- 参数是void指针 —— 这是FreeRTOS留给你的“后门”,可以传任何东西进去。
- 函数名随意 —— 但建议用有意义的命名,比如 vTaskLED、vTaskUART。
我的习惯: 任务函数名我一般加个前缀“vTask”,一看就知道这是个任务。团队协作时,这个习惯能省不少沟通成本。
2.2 xTaskCreate() —— 核心API详解
有了任务函数,怎么把它注册到系统里?靠的就是 xTaskCreate()。这个API我用了不下几百次,它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(仅用于调试)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(不是字节!)
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄(输出参数)
);
我来逐个拆解,每个参数都有坑:
| 参数 | 说明 | 避坑指南 |
|---|---|---|
| pvTaskCode | 函数名即可,比如 vTaskLED | 别加括号!传的是地址,不是调用 |
| pcName | 字符串,比如 "LED_Task" | 长度不要超过 configMAX_TASK_NAME_LEN |
| usStackDepth | 栈大小,单位是字 | 32位MCU上,1字=4字节。别搞混了! |
| pvParameters | void* 类型,传啥都行 | 传结构体指针时,注意生命周期 |
| uxPriority | 0 ~ configMAX_PRIORITIES-1 | 0是最低优先级,数值越大优先级越高 |
| pxCreatedTask | TaskHandle_t 类型的指针 | 如果不需要句柄,传NULL即可 |
我曾经踩过的坑: 栈深度单位是“字”不是“字节”。有一次我传了128,以为够用,结果任务一跑就崩。后来一算,128字=512字节,其实栈溢出了。建议新手先给个256字起步,稳定后再优化。
2.3 任务参数传递 —— 别小看这个void指针
任务参数传递,说白了就是通过 pvParameters 给任务“喂数据”。我见过很多新手直接传一个局部变量的地址,结果任务还没启动,变量就销毁了。嗯,这里要注意。
正确的做法有几种:
- 传整数值 —— 直接把整数强转为void*,比如
(void*)1。任务里再转回来。 - 传全局变量地址 —— 全局变量生命周期是整个程序,安全。
- 传动态分配的结构体指针 —— 记得在任务里释放内存。
- 传静态结构体地址 —— 我比较推荐这种方式,简单可靠。
给你看个实际例子:
// 定义一个结构体,存放任务需要的参数
typedef struct {
uint8_t ucLEDPin;
uint16_t usDelayMs;
} LEDConfig_t;
// 静态分配,生命周期有保障
static LEDConfig_t xLED1Config = { .ucLEDPin = 13, .usDelayMs = 500 };
static LEDConfig_t xLED2Config = { .ucLEDPin = 14, .usDelayMs = 1000 };
void vTaskLED(void *pvParameters)
{
LEDConfig_t *pxConfig = (LEDConfig_t *)pvParameters;
for( ;; )
{
GPIO_Toggle(pxConfig->ucLEDPin);
vTaskDelay(pdMS_TO_TICKS(pxConfig->usDelayMs));
}
}
// 创建任务时传参
xTaskCreate(vTaskLED, "LED1", 256, &xLED1Config, 1, NULL);
xTaskCreate(vTaskLED, "LED2", 256, &xLED2Config, 1, NULL);
核心思想: 同一个任务函数,传不同的参数,就能实现不同的行为。这就是代码复用的精髓。我在项目中经常用这个技巧,一个UART处理函数,传不同的缓冲区地址,就能同时处理多个串口。
2.4 任务句柄 —— 任务的“身份证”
任务句柄,类型是 TaskHandle_t。它本质上是一个指针,指向任务控制块(TCB)。有了它,你就能在任务外部操作这个任务:
- 删除任务 ——
vTaskDelete(xTaskHandle) - 挂起/恢复任务 ——
vTaskSuspend()/vTaskResume() - 获取任务优先级 ——
uxTaskPriorityGet() - 通知任务 ——
xTaskNotifyGive()
创建任务时,如果你传了 &xTaskHandle,系统会把任务句柄写进去。如果你不需要,传NULL就行。我个人习惯是:只要后续可能操作这个任务,就保留句柄。
TaskHandle_t xLEDTaskHandle = NULL;
void vCreateTasks(void)
{
xTaskCreate(vTaskLED, "LED", 256, NULL, 1, &xLEDTaskHandle);
// 后续可以通过句柄操作
// vTaskSuspend(xLEDTaskHandle);
// vTaskResume(xLEDTaskHandle);
}
小技巧: 在任务函数内部,可以通过
xTaskGetCurrentTaskHandle() 获取自己的句柄。这在任务需要删除自己时特别有用:vTaskDelete(xTaskGetCurrentTaskHandle())。
2.5 知识体系总览
这一节的内容,我用一张图帮你理清逻辑:
这张图把整个流程串起来了。你从任务函数开始,通过 xTaskCreate() 注册,得到任务句柄,然后就可以通过句柄控制任务。参数传递是连接数据和任务的桥梁,别搞砸了。
嗯,这一节的内容就这些。任务创建是基本功,多写几遍就熟了。记住:栈大小给够,参数生命周期管好,句柄该留就留。做到这三点,任务创建这块基本不会出问题。