3. 创建任务(上):xTaskCreate() API函数深度解析,参数详解与返回值处理
好,咱们进入正题。上一章我们聊了任务是什么,这一章直接上手——怎么把任务“生”出来。
在FreeRTOS里,创建任务最常用的就是 xTaskCreate() 这个函数。说实话,我刚开始用的时候,看着那一堆参数也有点懵。但别怕,咱们一个一个拆开看,你会发现其实没那么复杂。
3.1 函数原型长什么样?
先看一眼它的“长相”:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(纯调试用)
const uint16_t usStackDepth, // 任务栈大小,单位是字(word)
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄(输出参数)
);
返回值是 BaseType_t 类型。成功返回 pdPASS,失败返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。
嗯,这里要注意:pdPASS 其实就是 1,失败是 0。但别偷懒写数字,用宏定义更安全。
3.2 参数逐个拆解
3.2.1 pvTaskCode —— 任务函数
这个参数就是任务要执行的函数指针。说白了,你写一个 void vTaskFunction(void *pvParameters) 这样的函数,然后把函数名传进去。
任务函数有个硬性要求:绝对不能返回。为什么?因为FreeRTOS的任务调度器会认为任务结束了,然后调用 vTaskDelete(NULL) 来清理它。但如果你在函数里写了 return,系统就会崩溃。
vTaskDelete() 来结束自己。千万不要让任务函数“跑到底”然后返回!
我遇到过一个小白同事,写了个任务函数,里面就一条打印语句,然后函数就结束了。结果系统跑起来直接死机。排查了半天才发现是任务函数返回了。
3.2.2 pcName —— 任务名字
这个参数就是个字符串,纯调试用的。比如 "LED_Blink"、"UART_Tx" 之类的。它不会影响任务的行为,但建议起个有意义的名字。
我个人习惯用 "T_模块名_功能" 这种格式,比如 "T_LED_Blink"。这样在调试器里一眼就能看出是哪个任务。
注意:名字长度不要超过 configMAX_TASK_NAME_LEN,默认是16个字符。超了会被截断。
3.2.3 usStackDepth —— 栈深度
这个参数特别容易让人误解。它单位是 字(word),不是字节!
在32位处理器上,1个字 = 4字节。所以如果你写 usStackDepth = 128,实际分配的栈大小是 128 × 4 = 512 字节。
栈大小怎么定?说实话,这没有标准答案。我一般这样估算:
- 简单任务(就几个局部变量):64 ~ 128 字
- 中等任务(有函数调用、局部数组):256 ~ 512 字
- 复杂任务(有递归、大数组):1024 字以上
uxTaskGetStackHighWaterMark() 函数查看任务栈的“水位线”,看看实际用了多少。然后根据这个数据调整栈大小。我每次写完新任务都会跑一遍这个函数,确保栈不会溢出。
3.2.4 pvParameters —— 任务参数
这个参数是传给任务函数的。类型是 void *,所以你可以传任何东西:整数、结构体指针、字符串等等。
举个例子:
void vLED_Task(void *pvParameters) {
int led_pin = (int)pvParameters; // 把参数转成整数
// 然后控制对应的LED
}
// 创建任务时传参数
xTaskCreate(vLED_Task, "LED1", 128, (void *)1, 1, NULL);
xTaskCreate(vLED_Task, "LED2", 128, (void *)2, 1, NULL);
你看,同一个任务函数,传不同的参数就能控制不同的LED。是不是很灵活?
但要注意:如果你传的是指针,一定要确保指针指向的内存是有效的。我曾经犯过一个错,传了一个局部变量的地址,结果任务还没开始跑,那个局部变量就已经被释放了。任务拿到的就是个野指针,程序直接跑飞。
3.2.5 uxPriority —— 任务优先级
优先级范围是 0 到 configMAX_PRIORITIES - 1。数字越大,优先级越高。
一般建议:
- 0:空闲任务(系统保留)
- 1~2:后台任务(比如LED闪烁、按键扫描)
- 3~4:普通任务(比如数据处理、通信)
- 5及以上:实时性要求高的任务(比如电机控制、传感器采集)
我个人习惯把优先级分成几个档次,不要用太多不同的优先级。因为优先级越多,调度器的开销越大。而且优先级反转的问题也会更复杂。
3.2.6 pxCreatedTask —— 任务句柄
这个参数是输出参数。如果你传一个 TaskHandle_t 类型的指针进去,函数会把创建好的任务句柄写进去。
任务句柄有什么用?
- 删除任务:
vTaskDelete(handle) - 挂起/恢复任务:
vTaskSuspend(handle)/vTaskResume(handle) - 改变优先级:
vTaskPrioritySet(handle, newPriority)
如果你不需要操作这个任务,可以传 NULL。但建议还是传一个变量,万一以后要用呢?
3.3 返回值处理
很多新手会忽略返回值检查。但我觉得这是最不该省的一步。
返回值只有两种可能:
| 返回值 | 含义 | 常见原因 |
|---|---|---|
pdPASS |
任务创建成功 | 一切正常 |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY |
内存不足,创建失败 | 堆空间不够、栈大小设置过大 |
我建议的写法:
TaskHandle_t xLEDTaskHandle = NULL;
BaseType_t xResult;
xResult = xTaskCreate(vLED_Task, "LED", 128, NULL, 1, &xLEDTaskHandle);
if (xResult != pdPASS) {
// 创建失败,打印错误信息
printf("Error: Failed to create LED task!\n");
// 可以在这里做错误处理,比如重启系统
while(1);
}
你看,就多几行代码,但能帮你省下大量调试时间。我见过太多人因为没检查返回值,任务没创建成功,系统却一直在跑,结果各种诡异现象。
3.4 一个完整的例子
最后,咱们看一个完整的创建任务的例子:
#include "FreeRTOS.h"
#include "task.h"
// 任务函数
void vBlinkTask(void *pvParameters) {
int led_pin = (int)pvParameters;
for (;;) {
// 点亮LED
GPIO_SetBits(led_pin);
vTaskDelay(pdMS_TO_TICKS(500));
// 熄灭LED
GPIO_ResetBits(led_pin);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void main(void) {
TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;
// 创建两个LED闪烁任务
if (xTaskCreate(vBlinkTask, "LED1", 128, (void *)1, 1, &xTask1Handle) != pdPASS) {
// 错误处理
while(1);
}
if (xTaskCreate(vBlinkTask, "LED2", 128, (void *)2, 1, &xTask2Handle) != pdPASS) {
// 错误处理
while(1);
}
// 启动调度器
vTaskStartScheduler();
// 正常情况下不会执行到这里
while(1);
}
这个例子很简单,但包含了所有关键点:任务函数、参数传递、返回值检查。你想想看,如果我不检查返回值,万一内存不够,任务没创建成功,系统启动后LED不亮,你查半天都查不出原因。
好了,这一章就到这里。下一章我们继续聊 xTaskCreateStatic() 和任务删除。记住,创建任务只是第一步,怎么管理任务才是真正的挑战。