3. 任务创建与管理:xTaskCreate API、任务栈分配、任务优先级设计
好,我们直接进入正题。任务,是RTOS里最核心的概念。说白了,你写的无人机飞控代码,最终都要拆成一个一个的任务,交给调度器去跑。这一节,我就带你手把手把任务创建、栈分配、优先级设计这几个硬骨头啃下来。
3.1 xTaskCreate API:任务的“出生证明”
在FreeRTOS里,创建一个任务,靠的就是 xTaskCreate() 这个函数。我刚开始用的时候,总觉得参数太多,记不住。后来发现,你只要理解每个参数是干嘛的,自然就记住了。
来看原型:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名字(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字(不是字节!)
void *pvParameters, // 传给任务函数的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 返回的任务句柄
);
嗯,这里要注意一个坑:usStackDepth 的单位是“字”。在32位MCU上,一个字是4字节。所以如果你写 128,实际分配的栈大小是 128 * 4 = 512 字节。我见过有人直接写 1024,以为就是1024字节,结果栈溢出查了半天。
返回值也很关键。返回 pdPASS 表示创建成功,返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 说明堆空间不够。我在项目中遇到过,堆设小了,任务创建失败,飞控直接死机。后来我养成了一个习惯:每次创建任务后,都检查返回值。
TaskHandle_t xTaskPidHandle = NULL;
if (xTaskCreate(vTaskPidControl, "PID", 256, NULL, 5, &xTaskPidHandle) != pdPASS) {
// 创建失败!必须处理
vAssertCalled("PID task create failed");
}
3.2 任务栈分配:给每个任务一个“安全屋”
任务栈,说白了就是每个任务私有的“内存空间”。局部变量、函数调用、中断嵌套,全都要用这个栈。栈给大了,浪费RAM;给小了,栈溢出,系统崩溃。
怎么估算栈大小?我个人习惯用三步法:
- 静态分析:数一下任务函数里最大的局部变量数组。比如你定义了一个
float imu_data[6],那就是 6 * 4 = 24 字节。 - 函数调用链:任务函数里调用了哪些函数?每个函数有自己的栈帧。比如你调用了
vTaskDelay(),它内部可能还会调用其他函数,这些都要算进去。 - 中断嵌套:如果任务执行中被中断打断,中断服务函数也会用栈。特别是嵌套中断,栈消耗会翻倍。
举个例子,一个典型的姿态解算任务:
void vTaskAttitude(void *pvParameters) {
float gyro[3], accel[3], mag[3]; // 3*4*3 = 36字节
float quat[4]; // 16字节
float temp; // 4字节
// 加上函数调用栈帧,大约需要 100-150字节
// 再加上中断嵌套预留,我一般给 256字 = 1024字节
}
uxTaskGetStackHighWaterMark() 在调试阶段查看栈实际使用峰值。我一般留出 20%-30% 的余量。比如峰值用了 180字,我就设 256字。
我曾经在一个项目中,把GPS解析任务的栈设成了128字,结果跑着跑着就死机。查了两天,发现是栈溢出把任务控制块给覆盖了。从那以后,我每个任务都加上了栈水印检测。
3.3 任务优先级设计:谁先跑,谁后跑?
优先级设计,是飞控实时性的灵魂。优先级设错了,该先跑的任务跑不了,飞控就会抖、会炸机。
FreeRTOS的优先级规则很简单:数字越大,优先级越高。0是空闲任务,一般不用。最高优先级取决于 configMAX_PRIORITIES,我通常设32。
对于无人机飞控,我建议这样分配:
| 优先级 | 任务名称 | 说明 |
|---|---|---|
| 30-31 | 紧急保护任务 | 比如电池低压保护、遥控器信号丢失处理 |
| 20-25 | 传感器读取任务 | IMU、GPS、气压计,要求高实时性 |
| 15-19 | 姿态解算任务 | 融合传感器数据,输出姿态 |
| 10-14 | 控制任务 | PID计算,输出PWM |
| 5-9 | 通信任务 | 数传、遥控器接收、日志输出 |
| 1-4 | 低优先级任务 | LED闪烁、状态显示、参数保存 |
你想想看,如果传感器读取任务优先级比控制任务低,那控制任务拿到的就是过时的数据,飞控反应就会慢半拍。我见过一个案例,有人把GPS解析任务优先级设得比姿态解算还高,结果GPS数据更新慢,姿态解算被频繁打断,飞控直接震荡。
还有一个容易忽略的点:优先级反转。比如低优先级任务拿了互斥锁,高优先级任务等锁,结果中优先级任务抢占了CPU,导致高优先级任务迟迟拿不到锁。解决办法是用 vTaskPrioritySet() 临时提升优先级,或者用 xSemaphoreCreateMutex() 自带优先级继承机制。
嗯,这里要注意:不要把所有任务都设成同一个优先级。那样就退化成轮询了,实时性全无。我一般保持优先级差至少2-3级,给调度器留出切换空间。
3.4 实战:创建一个飞控任务
最后,我们结合一个实际例子,把上面三个知识点串起来。假设我们要创建一个姿态控制任务:
// 任务函数
void vTaskAttitudeControl(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = 5; // 200Hz
for (;;) {
// 等待到下一个周期
vTaskDelayUntil(&xLastWakeTime, xFrequency);
// 读取传感器数据(假设已经由更高优先级任务更新)
// 执行姿态解算
// 输出控制量
}
}
// 创建任务
void vCreateAttitudeTask(void) {
TaskHandle_t xAttitudeHandle = NULL;
// 栈大小:256字 = 1024字节,足够容纳局部变量和函数调用
// 优先级:16,介于传感器读取(20)和控制输出(12)之间
if (xTaskCreate(
vTaskAttitudeControl,
"Attitude",
256,
NULL,
16,
&xAttitudeHandle) != pdPASS) {
// 处理错误
vErrorHandler();
}
}
这个例子中,我用了 vTaskDelayUntil() 实现固定周期执行,而不是 vTaskDelay()。为什么?因为 vTaskDelayUntil() 能保证任务按固定频率执行,不会因为任务执行时间波动而累积误差。这在飞控里至关重要。
好了,任务创建与管理就讲到这里。记住三个关键点:栈大小要留余量、优先级要按实时性排序、创建后一定要检查返回值。下一节,我们会深入任务调度机制,看看FreeRTOS到底是怎么决定下一个该跑哪个任务的。