第4章:实时操作系统(RTOS)入门:FreeRTOS在飞控中的角色,任务调度与优先级,为什么裸机不够用了?

说实话,我见过不少做飞控的初学者,上来就问我:「老师,飞控代码这么复杂,用裸机大循环不行吗?」

我的回答很直接:不行,绝对不行。

你想想看,一架四轴飞行器在空中,每秒要处理传感器数据、运行姿态解算、输出控制信号、还要响应遥控器指令。如果这些事都挤在一个while(1)大循环里,任何一个任务卡住几毫秒,飞机可能就翻跟头了。我早期做第一版飞控原型时,就吃过这个亏——裸机跑着跑着,电机突然停转,飞机直接砸地。后来一查,是某个传感器读取函数占用了太长时间。

4.1 为什么裸机不够用了?

裸机编程,说白了就是一个无限循环,加上中断服务函数。结构大概是这样:

void main(void)
{
    while(1)
    {
        read_sensor();      // 读取传感器
        attitude_estimate(); // 姿态解算
        control_output();   // 输出控制
        communication();    // 通信处理
    }
}

这种结构有什么问题?我总结了几点:

  • 任务互相阻塞:如果read_sensor()因为I2C总线忙而卡了5ms,后面所有任务都得等着。飞控里最怕的就是这种不确定性。
  • 实时性无法保证:姿态解算要求1ms内完成,但通信任务可能占用2ms。你没法精确控制每个任务的执行时间。
  • 代码耦合严重:所有功能揉在一起,想加个新功能?改一处可能影响全局。我见过一个裸机飞控项目,后期维护成本高得吓人。
  • 中断滥用:有人把所有事情都塞进中断里。嗯,这更危险——中断里做浮点运算、调用printf,分分钟堆栈溢出。
⚠️ 避坑指南
我曾经接手过一个裸机飞控项目,代码里用了5个定时器中断,每个中断里都做大量计算。结果飞机在高机动飞行时,中断嵌套导致堆栈溢出,直接失控炸机。从那以后,我坚决不在飞控里用裸机大循环。

4.2 FreeRTOS在飞控中的角色

FreeRTOS是什么?它就是一个轻量级的实时操作系统内核。在飞控里,它的角色可以概括为:任务调度器 + 资源管理者

我个人习惯把飞控任务拆成几个独立线程:

  • 传感器采集任务:以1kHz频率读取IMU数据
  • 姿态解算任务:以500Hz频率运行互补滤波或卡尔曼滤波
  • 控制输出任务:以1kHz频率输出PWM信号
  • 通信任务:处理遥控器SBUS信号和遥测数据
  • 日志任务:记录飞行数据到SD卡

每个任务都有自己的栈空间,互不干扰。FreeRTOS负责决定「现在该运行哪个任务」。这就好比一个交响乐团的指挥,让每个乐器在正确的时间演奏。

💡 核心观点
在飞控中,FreeRTOS不是「锦上添花」,而是「雪中送炭」。它把复杂的时序管理从你的大脑中解放出来,让你专注于算法本身。

4.3 任务调度与优先级

FreeRTOS支持两种调度方式:抢占式调度协作式调度。飞控里几乎只用抢占式。

抢占式调度的核心逻辑是:高优先级任务随时可以打断低优先级任务。举个例子:

// 任务优先级定义
#define PRIO_SENSOR     5   // 最高
#define PRIO_ATTITUDE   4
#define PRIO_CONTROL    3
#define PRIO_COMM       2
#define PRIO_LOG        1   // 最低

void vTaskSensor(void *pvParameters)
{
    while(1)
    {
        read_imu();
        // 读取完成后,主动让出CPU给姿态解算任务
        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

void vTaskAttitude(void *pvParameters)
{
    while(1)
    {
        estimate_attitude();
        vTaskDelay(pdMS_TO_TICKS(2));
    }
}

这里要注意:优先级不是越高越好。我见过有人把所有任务都设成最高优先级,结果系统一直在切换任务,CPU全浪费在上下文切换上了。

优先级 典型任务 周期要求 说明
最高 传感器读取 1ms 必须准时,否则姿态解算会漂
姿态解算 2ms 计算量大,但可容忍少量抖动
控制输出 1ms 与传感器同步,优先级可略低
通信/日志 10-100ms 偶尔延迟没关系
🔧 个人经验
我建议在飞控中保留一个空闲任务,用来统计CPU使用率。如果空闲任务运行时间少于20%,说明CPU快满了,需要优化。我曾经在一个项目中,通过空闲任务发现CPU占用率高达95%,后来发现是某个任务里用了vTaskDelay(0)——这会导致任务一直让出CPU,造成频繁切换。

4.4 任务间通信:队列与信号量

任务之间怎么传数据?裸机里用全局变量,但在RTOS里,千万别直接用全局变量。为什么?因为任务切换可能发生在任何时刻,你读一半数据,另一个任务把数据改了,这就出问题了。

FreeRTOS提供了几种机制:

  • 队列(Queue):用于传递数据块,比如把IMU原始数据从传感器任务传给姿态解算任务
  • 信号量(Semaphore):用于同步,比如通知控制任务「传感器数据已就绪」
  • 事件组(Event Group):用于多条件触发,比如「GPS定位完成且遥控器信号正常」才允许解锁

举个例子,传感器任务和姿态解算任务之间的典型通信:

// 定义队列句柄
QueueHandle_t xImuQueue;

// 传感器任务:发送数据
void vTaskSensor(void *pvParameters)
{
    ImuData_t xData;
    while(1)
    {
        read_imu(&xData);
        // 发送到队列,如果队列满则等待10ms
        xQueueSend(xImuQueue, &xData, pdMS_TO_TICKS(10));
        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

// 姿态解算任务:接收数据
void vTaskAttitude(void *pvParameters)
{
    ImuData_t xData;
    while(1)
    {
        // 从队列接收,如果队列空则阻塞等待
        if(xQueueReceive(xImuQueue, &xData, portMAX_DELAY) == pdPASS)
        {
            estimate_attitude(&xData);
        }
    }
}
⚠️ 注意
队列长度要合理设置。太短会导致数据丢失,太长会浪费内存。我一般根据任务频率来算:如果传感器任务1ms发一次,姿态解算2ms收一次,队列长度设3就够用了。设成100纯粹是浪费RAM。

4.5 飞控中的典型任务划分

最后,我分享一下自己常用的飞控任务架构。这是一个经过多次迭代的方案,你可以参考:

  1. 高优先级任务(1ms周期):读取IMU、磁力计、气压计。这些传感器数据是飞控的「眼睛」,必须最新。
  2. 中优先级任务(2-5ms周期):姿态解算、控制律计算。这是飞控的「大脑」,计算量大但可以容忍少量延迟。
  3. 低优先级任务(10-100ms周期):遥控器信号解析、遥测数据发送、SD卡写入。这些任务偶尔被抢占也没关系。
  4. 后台任务:系统监控、故障检测、LED指示。优先级最低,有空就跑一下。

嗯,这里要注意:任务栈大小要合理分配。我见过有人给每个任务分配512字节,结果姿态解算任务里用了递归函数,栈溢出导致系统崩溃。我一般给高优先级任务分配256-512字节,给计算密集型任务分配1024-2048字节。

📌 总结
裸机编程在飞控领域已经不够用了。FreeRTOS提供了任务调度、优先级管理、任务间通信等机制,让飞控代码更清晰、更可靠、更易维护。从裸机到RTOS,不是技术炫技,而是工程实践的必然选择。下一章,我会带你深入FreeRTOS的源码,看看任务切换到底是怎么实现的。