4. 调度器与任务管理:ArduPilot的“心脏”怎么调

调度器这东西,说白了就是飞控的操作系统内核。你写的所有控制算法、传感器读取、通信协议,最后都得靠它来分配CPU时间。我刚开始接触ArduPilot时,总觉得调度器是底层的事,跟我写应用层代码没关系。直到有一次,我加了个复杂的路径规划模块,结果飞机在空中莫名其妙地“抽搐”……嗯,从那以后,我再也不敢小看调度器了。

这一章,我们就来扒一扒ArduPilot调度器的底裤。看看它怎么跑任务的,怎么调优先级,怎么优化看门狗,以及怎么配高精度定时器。

4.1 调度器架构:一个“轮询+中断”的混合体

ArduPilot的调度器不是实时操作系统(RTOS),它是个跑在裸机上的协作式调度器。你想想看,飞控芯片资源有限,跑个完整的RTOS太奢侈了。所以ArduPilot用了种更轻量的方式:主循环轮询 + 定时器中断。

核心结构就三个东西:

  • 调度表(Scheduler Table):一个静态数组,定义了所有任务的执行频率和优先级。
  • 任务队列(Task List):运行时动态维护,记录哪些任务“到点”该跑了。
  • 看门狗(Watchdog):监控每个任务的执行时间,超时就报警或重启。

我画了张图,帮你理解这个流程:

ArduPilot调度器核心架构 ArduPilot 调度器核心架构 主循环 (loop()) 调度表 (Scheduler Table) 任务队列 (Task Queue) 看门狗 (Watchdog) 任务执行 (Task Execution) 下一轮循环 调度表决定“谁该跑”,任务队列决定“谁先跑”,看门狗决定“谁跑太久”

主循环每跑一圈,调度器就查一次调度表,看哪些任务到时间了。然后把它们塞进任务队列,按优先级排好序,一个个执行。看门狗在旁边盯着,哪个任务跑超时了,它就咬人。

4.2 任务优先级调整策略:别让“小事”耽误了“大事”

ArduPilot的任务优先级是静态的,写在调度表里。但你可以通过修改参数来调整。我个人习惯把任务分成三类:

优先级等级 典型任务 执行频率 调整建议
高(0-5) 姿态控制、传感器读取、安全检测 400Hz - 1000Hz 不要动,动了会炸机
中(6-10) 位置控制、导航、通信 50Hz - 200Hz 根据负载微调,别低于50Hz
低(11-15) 日志记录、参数存储、地面站心跳 1Hz - 10Hz 可以降频,但别完全关掉

调整优先级的核心原则就一条:让时间敏感的任务先跑,让计算量大的任务少跑。

我的小技巧:如果你发现飞控CPU占用率超过70%,先别急着换芯片。试试把日志记录频率从10Hz降到5Hz,或者把地面站心跳从50Hz降到10Hz。我有个项目就是这么干的,CPU占用率直接从85%降到了55%,而且飞控表现完全没受影响。

具体怎么调?看这个代码片段:

// 在 AP_Scheduler/AP_Scheduler.cpp 中
// 调度表定义示例
const AP_Scheduler::Task AP_Scheduler::scheduler_tasks[] = {
    // 高优先级任务
    { &AP_InertialSensor::update,          &_ins,      1, 400 },  // 400Hz
    { &AP_AttitudeControl::update,         &_att,      1, 400 },  // 400Hz
    // 中优先级任务
    { &AP_Navigation::update,              &_nav,      1, 100 },  // 100Hz
    { &GCS::update,                        &_gcs,      1,  50 },  // 50Hz
    // 低优先级任务
    { &AP_Logger::update,                  &_log,      1,  10 },  // 10Hz
    { &AP_Param::save_pending,             &_param,    1,   1 },  // 1Hz
};

每个任务有四个参数:函数指针、对象指针、是否启用、期望频率(Hz)。你改最后一个数字就行。

警告:千万不要把高优先级任务的频率调低!我曾经为了省CPU,把姿态控制从400Hz降到了200Hz,结果飞机在风大的时候直接翻了个跟头。姿态控制是飞控的命根子,动不得。

4.3 看门狗超时优化:别让“保安”乱咬人

看门狗(Watchdog)是个好东西,也是个坏东西。好的是它能防止程序卡死,坏的是它有时候会误报。我遇到过最离谱的一次:飞控在正常飞行,突然看门狗超时重启了,飞机直接掉下来。查了半天,发现是SD卡写入卡了一下,导致日志任务超时。

看门狗的核心参数就两个:

  • 超时时间(Timeout):任务允许的最大执行时间,默认通常是 20ms。
  • 喂狗间隔(Feed Interval):主循环喂狗的频率,默认是 1ms。

优化策略其实很简单:

  1. 先分析,再调参:用 SCHED_DEBUG 参数打开调度器调试日志,看看哪个任务经常超时。
  2. 区分“真超时”和“假超时”:真超时是任务本身太慢,假超时是中断或DMA占用了太多时间。
  3. 适当放宽超时阈值:对于日志、参数存储这类非关键任务,可以把超时时间从20ms放宽到50ms。

看门狗配置示例:

// 在 hwdef.dat 中配置看门狗
// STM32H7 系列
WATCHDOG_TIMEOUT_MS 50    // 超时时间 50ms
WATCHDOG_FEED_INTERVAL_US 1000  // 喂狗间隔 1ms

// 或者在运行时通过参数调整
// 参数名:SCHED_WDOG_TIMEOUT
// 范围:10 - 100 (ms)
// 默认:20

核心观点:看门狗不是越严格越好。太严格了,正常操作都会被误杀;太宽松了,程序卡死半天才发现。我个人习惯把超时时间设为任务期望执行时间的3-5倍。比如一个10Hz的任务(100ms周期),超时时间设300ms比较合理。

4.4 高精度定时器配置:微秒级的“节拍器”

ArduPilot的定时器系统,说白了就是芯片上的硬件定时器。不同的芯片,定时器精度不一样。STM32F4系列是1us,STM32H7系列可以到0.1us。但精度越高,中断开销也越大。

配置高精度定时器,主要关注三个东西:

配置项 说明 推荐值
定时器时钟源 选择内部高速时钟还是外部晶振 外部晶振(精度更高)
预分频器 决定定时器的计数频率 根据芯片主频计算,目标1us
自动重装载值 决定定时器的溢出周期 根据任务最大周期设置

配置代码示例(以STM32H7为例):

// 在 hwdef.dat 中配置定时器
// 使用 TIM5 作为调度器定时器
TIM5_SOURCE 100000000  // 100MHz 时钟源
TIM5_PRESCALER 100     // 预分频 100,得到 1MHz 计数频率
TIM5_PERIOD 1000       // 自动重装载 1000,得到 1ms 中断周期

// 这样配置后,定时器每 1ms 触发一次中断
// 精度可以达到 1us

避坑指南:我曾经在STM32F405上把定时器精度配到了0.1us,结果中断太频繁,CPU全花在上下文切换上了,飞控反而变卡了。高精度不是越高越好,够用就行。对于大多数飞控应用,1us精度完全够用。

4.5 实战:一个完整的调度器优化案例

说个我实际处理过的案例。有个客户做农业无人机,喷洒系统需要精确控制泵的转速。他们发现喷洒量忽大忽小,怀疑是调度器的问题。

排查过程:

  1. 打开 SCHED_DEBUG 日志,发现喷洒控制任务(50Hz)经常被日志任务(10Hz)打断。
  2. 日志任务每次写入SD卡要花15ms,而喷洒控制任务周期只有20ms。
  3. 解决方案:把日志任务的优先级从10降到14,同时把喷洒控制任务的优先级从6升到4。

调整后的效果:喷洒控制任务再也没被中断过,喷洒量稳定了。代价是日志写入偶尔会丢几帧,但对飞行安全没影响。

这个案例告诉我们:调度器优化的本质,就是在“实时性”和“完整性”之间找平衡。没有完美的配置,只有最适合你应用的配置。

最后说一句:调度器优化是个细活,别指望一次调好。先飞个悬停,看看CPU占用率和任务超时情况,然后一点点调。我每次调完都会飞个10分钟,看看日志里有没有异常。稳扎稳打,比什么都强。