2. 任务创建与管理:xTaskCreate函数详解、任务优先级、任务堆栈、任务句柄
好,咱们今天来聊聊FreeRTOS里最核心的东西——任务创建与管理。说实话,我刚接触嵌入式实时系统那会儿,觉得任务不就是个函数嘛,有啥好研究的?结果第一次做项目就栽了跟头——任务跑着跑着就崩了,堆栈溢出,优先级搞反,各种问题。后来才明白,任务创建这事儿,看似简单,里面的门道可不少。
2.1 xTaskCreate函数——任务的“出生证明”
在FreeRTOS里,创建一个任务,最常用的就是xTaskCreate()这个函数。它的原型长这样:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度(单位是字,不是字节)
void *pvParameters, // 传递给任务函数的参数
UBaseType_t uxPriority, // 任务优先级(0最低,configMAX_PRIORITIES-1最高)
TaskHandle_t *pxCreatedTask // 任务句柄(输出参数)
);
返回值是pdPASS或errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。嗯,说白了就是成功或失败。我习惯每次创建任务后都检查一下返回值,别问为什么,问就是吃过亏——有一次堆内存不够,任务没创建成功,程序静悄悄地跑偏了,查了两天才找到原因。
关键点:堆栈深度单位是“字”,在ESP32上1个字=4字节。所以如果你写1024,实际分配的是4096字节的堆栈空间。这个很多人搞混,我刚开始也犯过这错。
2.2 任务优先级——谁先跑,谁后跑
任务优先级,说白了就是决定哪个任务先获得CPU使用权。数值越大,优先级越高。在ESP32的FreeRTOS里,默认configMAX_PRIORITIES是25,也就是优先级范围0~24。
这里有个坑:优先级0是空闲任务的优先级。你想想看,如果你把所有任务优先级都设成0,那空闲任务就没法运行了?其实不会,空闲任务优先级就是0,但它是特殊的。不过我个人建议,用户任务优先级从1开始用,留点余地。
我在项目中遇到过这样一个场景:一个数据采集任务和一个网络发送任务。采集任务要求实时性高,我给了优先级5;网络发送任务偶尔延迟没关系,给了优先级2。结果呢?采集任务一直占着CPU,网络发送任务饿死了。后来我把网络发送任务里加了个vTaskDelay(),让它在等待时主动让出CPU,问题就解决了。
避坑指南:我曾经把两个任务设成相同优先级,以为它们会轮流跑。实际上,相同优先级的任务确实会时间片轮转,但前提是configUSE_TIME_SLICING要开启(默认开启)。如果你关了时间片轮转,同优先级任务就得靠taskYIELD()手动让出CPU了。
2.3 任务堆栈——别让你的任务“爆栈”
任务堆栈,就是每个任务私有的“内存小仓库”。局部变量、函数调用、中断保存的上下文,全放这儿。堆栈大小设小了,任务跑着跑着就崩了;设大了,浪费宝贵的RAM。
那怎么确定堆栈大小呢?我一般用两种方法:
- 估算:看任务函数里用了多少局部变量、调用了多深的函数嵌套。比如一个任务里有个512字节的数组,再加上几层函数调用,我至少给1024字(4096字节)。
- 实测:FreeRTOS提供了
uxTaskGetStackHighWaterMark()函数,可以查看任务堆栈剩余的最小值。我习惯在任务稳定运行后,调用这个函数看看还剩多少,然后留出30%的余量。
// 获取任务堆栈剩余空间(单位:字)
UBaseType_t uxHighWaterMark;
uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL); // NULL表示当前任务
printf("任务堆栈剩余:%d 字\n", uxHighWaterMark);
注意:ESP32的WiFi和蓝牙协议栈会占用大量堆栈空间。如果你在任务里调用WiFi相关API,堆栈至少给4096字(16384字节)。我有个同事没注意这个,WiFi一连接任务就崩,查了一整天。
2.4 任务句柄——任务的“身份证”
任务句柄,本质上是一个指向任务控制块(TCB)的指针。有了它,你就可以对任务进行各种操作:删除、挂起、恢复、改变优先级等等。
创建任务时,如果你不需要操作这个任务,可以把最后一个参数传NULL。但我建议你永远不要传NULL。为什么?因为后面调试、维护的时候,你很可能需要操作这个任务。我习惯在全局或结构体里保存所有任务句柄,方便统一管理。
TaskHandle_t xTask1Handle = NULL;
TaskHandle_t xTask2Handle = NULL;
void create_tasks(void)
{
xTaskCreate(task1_func, "Task1", 2048, NULL, 5, &xTask1Handle);
xTaskCreate(task2_func, "Task2", 2048, NULL, 3, &xTask2Handle);
}
// 后面想删除任务1
void delete_task1(void)
{
if (xTask1Handle != NULL) {
vTaskDelete(xTask1Handle);
xTask1Handle = NULL; // 记得置空,防止野指针
}
}
嗯,这里有个小细节:删除任务后,句柄要置NULL。不然你后面不小心用了这个句柄,那就是野指针操作,后果嘛……你懂的。
2.5 完整示例——创建两个任务
说了这么多,咱们来个完整的例子。创建两个任务:一个LED闪烁任务(优先级5),一个串口打印任务(优先级3)。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define LED_GPIO 2
TaskHandle_t xLedTaskHandle = NULL;
TaskHandle_t xPrintTaskHandle = NULL;
// LED闪烁任务
void led_task(void *pvParameters)
{
gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
while (1) {
gpio_set_level(LED_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(LED_GPIO, 0);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 串口打印任务
void print_task(void *pvParameters)
{
int count = 0;
while (1) {
printf("系统运行中... 计数:%d\n", count++);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
// 创建LED任务,优先级5,堆栈2048字
BaseType_t ret1 = xTaskCreate(led_task, "LED", 2048, NULL, 5, &xLedTaskHandle);
if (ret1 != pdPASS) {
printf("LED任务创建失败!\n");
}
// 创建打印任务,优先级3,堆栈2048字
BaseType_t ret2 = xTaskCreate(print_task, "Print", 2048, NULL, 3, &xPrintTaskHandle);
if (ret2 != pdPASS) {
printf("打印任务创建失败!\n");
}
// 主任务可以删除了,或者让它干别的
vTaskDelete(NULL);
}
个人经验:我习惯在app_main()里只做任务创建和初始化工作,然后把自己删掉。这样系统就完全由FreeRTOS调度了,主任务不占任何资源。干净利落。
2.6 常见问题与避坑
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 任务创建返回失败 | 堆内存不足 | 增大configTOTAL_HEAP_SIZE,或减少任务堆栈大小 |
| 任务跑着跑着就崩了 | 堆栈溢出 | 用uxTaskGetStackHighWaterMark()检查,增大堆栈 |
| 低优先级任务一直不运行 | 高优先级任务没有主动让出CPU | 在高优先级任务里加vTaskDelay()或taskYIELD() |
| 任务句柄无效 | 句柄被覆盖或未初始化 | 创建时检查返回值,删除后置NULL |
好了,关于任务创建与管理,核心就是这些。记住:堆栈给够、优先级合理、句柄管好,你的任务系统就稳了。下一章咱们聊聊任务的状态与调度,看看任务到底是怎么在“运行、就绪、阻塞、挂起”之间切换的。