3. 任务创建与管理:任务函数原型、任务优先级、任务堆栈配置、任务删除与挂起

好,咱们进入第三章。任务创建与管理,这可以说是RTOS里最基础、也最核心的操作了。你想想看,整个无人机飞控系统,说白了就是一堆任务在跑来跑去。有的负责传感器读取,有的负责姿态解算,有的负责电机控制。怎么把这些任务组织好、管理好,直接决定了你的飞控稳不稳、会不会炸机。

我个人习惯,在讲具体API之前,先让大家理解一个概念:任务到底是什么?在RTOS眼里,任务就是一个无限循环的函数,加上它自己的栈空间、优先级和一些控制块信息。嗯,就这么简单。

3.1 任务函数原型

先看任务函数的写法。在FreeRTOS里,任务函数长这样:

void vTaskFunction(void *pvParameters)
{
    // 参数转换
    uint32_t ulParam = (uint32_t)pvParameters;
    
    // 任务初始化代码(只执行一次)
    sensor_init();
    
    // 无限循环
    for(;;)
    {
        // 任务主体代码
        read_sensor_data();
        vTaskDelay(pdMS_TO_TICKS(10));  // 10ms周期
    }
}

注意几个关键点:

  • 返回值必须是void——任务函数永远不会返回,返回就意味着任务结束了,系统会帮你清理,但一般不建议这么干
  • 参数是一个void指针——你可以传任何东西进去,整数、结构体指针都行。我在项目中经常传一个结构体指针,里面放任务需要的配置参数
  • 函数体必须是一个无限循环——这是RTOS任务的典型模式。如果任务只执行一次,那执行完就会被删除
小技巧:我习惯在任务函数开头加一个vTaskDelay(0),让任务主动让出CPU一次。这样能避免刚创建时和其他高优先级任务抢资源。

3.2 任务优先级

优先级是RTOS里最容易出问题的地方。我记得刚做无人机那会儿,就因为优先级配错了,导致飞控在急转弯时直接失控。后来查了三天才发现,是姿态解算任务的优先级比传感器读取任务低,导致数据更新不及时。

FreeRTOS的优先级规则很简单:数值越大,优先级越高。比如:

#define PRIORITY_IDLE      0   // 空闲任务
#define PRIORITY_LOW       1   // 低优先级
#define PRIORITY_MEDIUM    2   // 中优先级
#define PRIORITY_HIGH      3   // 高优先级
#define PRIORITY_CRITICAL  4   // 关键任务

在无人机系统里,我一般这样分配:

任务类型 优先级 说明
电机控制 最高(如5) 实时性要求最高,1ms以内必须响应
姿态解算 高(如4) 需要及时更新姿态数据
传感器读取 中高(如3) 周期性读取,可容忍少量延迟
遥控器接收 中(如2) 人操作频率有限,优先级不用太高
日志记录 低(如1) 后台任务,有空才做
避坑指南:我曾经犯过一个错误——把两个任务设成相同优先级,而且都没加阻塞。结果两个任务互相抢CPU,系统负载直接飙到100%。记住:同优先级任务要配合时间片轮转或主动让出CPU

3.3 任务堆栈配置

堆栈配置是个技术活。配大了浪费内存,配小了直接栈溢出炸机。我见过太多新手在这上面栽跟头了。

先看创建任务的API:

TaskHandle_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 堆栈深度,单位是字(word)
    void *pvParameters,             // 参数
    UBaseType_t uxPriority,         // 优先级
    TaskHandle_t *pxCreatedTask     // 任务句柄
);

堆栈大小怎么算?我给大家一个经验公式:

  • 简单任务(只调用几个函数):128-256字(512-1024字节)
  • 中等任务(有局部数组、调用库函数):256-512字
  • 复杂任务(有递归、大数组、浮点运算):512-1024字

重要:FreeRTOS的堆栈单位是字(word),不是字节!在32位MCU上,1个字=4字节。所以usStackDepth=128,实际分配的是512字节。

我常用的调试方法:先给一个较大的堆栈(比如512字),运行一段时间后用uxTaskGetStackHighWaterMark()查看实际使用量,然后根据结果调整。比如:

// 在任务循环中定期检查
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
if(uxHighWaterMark < 50)  // 剩余少于50字,危险!
{
    // 记录警告日志
    log_warning("Task stack low: %d", uxHighWaterMark);
}
个人经验:我一般留30%的余量。比如实际用了200字,我就配成300字。别抠那点内存,炸机一次损失更大。

3.4 任务删除与挂起

任务的生命周期管理,说白了就是什么时候让任务干活,什么时候让它歇着。

任务删除:

// 删除自己
vTaskDelete(NULL);

// 删除其他任务
vTaskDelete(xTaskHandle);

注意:被删除的任务的堆栈和TCB会被释放回堆。但如果你用了动态内存分配,记得在任务退出前自己释放。

避坑指南:我曾经在中断服务函数里调用vTaskDelete(),结果系统直接死机。记住:vTaskDelete()不能在中断里调用,要用的话得通过消息队列或信号量通知一个任务来处理。

任务挂起与恢复:

// 挂起任务
vTaskSuspend(xTaskHandle);

// 恢复任务
vTaskResume(xTaskHandle);

// 从中断中恢复任务
xTaskResumeFromISR(xTaskHandle);

挂起和删除的区别:

  • 挂起:任务还在内存里,只是不参与调度。可以随时恢复,开销小
  • 删除:任务彻底没了,资源释放。下次要用得重新创建

在无人机里,我经常用挂起来做省电模式。比如检测到无人机悬停时,把一些非关键任务挂起,只保留姿态控制和电机控制。等用户开始操作了,再恢复其他任务。

void vPowerSaveMode(void)
{
    // 挂起非关键任务
    vTaskSuspend(xLogTaskHandle);
    vTaskSuspend(xLEDTaskHandle);
    vTaskSuspend(xTelemetryTaskHandle);
    
    // 降低主控频率
    system_clock_set(48 MHz);
}

void vNormalMode(void)
{
    // 恢复所有任务
    vTaskResume(xLogTaskHandle);
    vTaskResume(xLEDTaskHandle);
    vTaskResume(xTelemetryTaskHandle);
    
    // 恢复主控频率
    system_clock_set(168 MHz);
}

核心要点总结:

  1. 任务函数必须是一个无限循环,参数用void指针传递
  2. 优先级数值越大越优先,关键任务(如电机控制)给最高优先级
  3. 堆栈大小先给大再调小,用HighWaterMark函数辅助测量
  4. 删除会释放资源,挂起只是暂停调度,按需选择

嗯,这一章的内容就这些。任务创建与管理是RTOS的基石,你把这些搞明白了,后面讲任务同步、消息队列什么的,就顺理成章了。下一章我们聊聊任务间通信,那可是无人机数据流的命脉。