2、ArduPilot代码架构解析:代码目录结构、核心库与任务调度器

好,咱们直接进入正题。上一章我们把ArduPilot的编译环境搭起来了,也跑了个简单的自定义模式。但说实话,那只是「知其然」。要想真正玩转自定义飞控模式,你必须得「知其所以然」。这一章,我就带你拆开ArduPilot的「五脏六腑」,看看它的代码到底是怎么组织的。

我个人习惯,拿到一个开源项目,第一件事不是看代码,而是看目录结构。这就像你去一个陌生的城市,先看地图一样。ArduPilot的代码量非常大,但它的目录设计其实很有章法。

2.1 代码目录结构:一张地图走天下

你从GitHub上拉下来的ArduPilot主目录,大概长这样。我挑几个最核心的目录讲:

ardupilot/
├── libraries/          # 核心库,所有固件共享
├── ArduCopter/         # 四旋翼/多旋翼固件
├── ArduPlane/          # 固定翼固件
├── ArduRover/          # 地面车辆/船固件
├── ArduSub/            # 水下机器人固件
├── AntennaTracker/     # 天线跟踪器固件
├── Tools/              # 工具脚本、地面站相关
├── mk/                 # 编译系统文件
└── modules/            # 第三方模块(如ChibiOS、uavcan)

嗯,这里要注意:libraries 目录是重中之重。所有飞控模式、传感器驱动、数学运算、通信协议,底层实现都在这里。而 ArduCopter 这些目录,其实是「应用层」——它们调用 libraries 里的功能,组合成具体的飞行逻辑。

核心观点: 你自定义的飞控模式,本质上就是在 ArduCopter 或 ArduPlane 的目录里,写一个继承自基础类的模式文件,然后调用 libraries 里的 API。

举个例子,你在 ArduCopter 目录下会看到 mode.hmode.cpp,里面定义了所有模式的基类。然后每个具体模式,比如 mode_stabilize.cppmode_loiter.cpp,都是从这个基类派生出来的。我们后面要做的自定义模式,也是同样的套路。

2.2 核心库(AP_HAL、AP_Math等):你的工具箱

核心库是ArduPilot的「地基」。我挑几个你一定会用到的讲:

库名 作用 你会在什么时候用到
AP_HAL 硬件抽象层,屏蔽不同硬件差异 读写GPIO、I2C、SPI、串口时
AP_Math 数学运算库,向量、矩阵、四元数 做姿态控制、坐标变换时
AP_InertialSensor IMU传感器驱动 获取加速度、角速度数据时
AP_GPS GPS驱动与定位解算 需要位置信息时
AP_Baro 气压计驱动 获取高度信息时
AP_AHRS 姿态航向参考系统 获取当前姿态、航向时
AP_NavEKF 扩展卡尔曼滤波器 融合多传感器数据时
AP_Motors 电机混控输出 控制电机转速时

这里我特别想说说 AP_HAL。为什么它重要?因为ArduPilot要跑在各种各样的硬件上——Pixhawk、Cube、甚至Linux板子。AP_HAL就是一层「翻译官」。你在写飞控模式时,永远不要直接操作寄存器,而是通过AP_HAL的接口。这样你的代码才能跨平台。

我的经验: 我在项目中遇到过有人直接操作STM32的GPIO寄存器,结果换了个飞控板,代码就炸了。记住:永远用AP_HAL::GPIO,别用HAL_GPIO_WritePin。这是血的教训。

再说 AP_Math。你想想看,飞控本质上就是在做数学运算。从传感器数据到姿态解算,从位置控制到电机输出,全是数学。AP_Math里封装了向量、矩阵、四元数、各种滤波器。你写自定义模式时,大概率会用到 Vector3f 来表示位置或速度,用 Quaternion 来做旋转。

2.3 任务调度器(Scheduler):飞控的「心脏」

好,目录结构和核心库都看完了。但有个问题:这么多任务——读取传感器、运行EKF、执行控制律、输出PWM——它们是怎么有序运行的?

答案就是 Scheduler(任务调度器)。它位于 libraries/AP_Scheduler 目录下。

说白了,Scheduler就是一个「时间管理者」。它把飞控需要做的事情,按照不同的频率,安排到每一个时间片里。比如:

  • IMU读取:1kHz(每1ms一次)
  • 姿态控制:400Hz(每2.5ms一次)
  • 位置控制:100Hz(每10ms一次)
  • GPS数据更新:10Hz(每100ms一次)
  • 地面站通信:50Hz(每20ms一次)

它的核心机制其实很简单。我画个图你就明白了:

ArduPilot 任务调度器工作机制 主循环 loop() Scheduler::run() 任务列表 (按频率排列) 检查每个任务:是否到了执行时间? 是 → 执行该任务的回调函数 否 → 跳过,检查下一个 每个任务定义时指定: • 函数指针 • 期望频率(Hz) • 最大执行时间(微秒)

你看,主循环 loop() 每跑一次,就调用一次 Scheduler::run()。然后调度器遍历任务列表,检查每个任务「该不该执行了」。判断依据就是:当前时间 - 上次执行时间 ≥ 任务周期。

举个例子,一个100Hz的任务,周期是10ms。如果上次执行是在时间戳1000ms,那么当前时间到了1010ms,就该再执行一次。如果当前才1005ms,那就跳过。

关键点: 调度器不是「抢占式」的,而是「协作式」的。每个任务必须尽快执行完,把CPU让出来。如果一个任务执行太久,后面的任务就会延迟。这就是为什么ArduPilot要求每个任务不能超过其时间片。

我曾经踩过一个坑:在自定义模式里,我在一个高频任务中做了大量的浮点运算,结果导致IMU读取延迟,飞控直接炸机。后来我加了个 AP_Scheduler::get_run_time() 来监控执行时间,才把问题定位到。

2.4 如何注册你自己的任务

好,理论讲完了。咱们来点实际的。如果你想在自定义模式里,添加一个自己的周期性任务,怎么做?

其实很简单。在 ArduCopter 目录下,找到 ArduCopter.cpp,里面有一个 const AP_Scheduler::Task Copter::scheduler_tasks[] 数组。这就是任务列表。你只需要在这里添加一行:

// 在 scheduler_tasks 数组中添加
SCHED_TASK(my_custom_task, 50, 200),  // 50Hz,最大执行时间200微秒

然后在你的模式类里,实现这个函数:

void Copter::my_custom_task(void)
{
    // 你的自定义逻辑
    // 比如:读取一个传感器,做点运算
    // 注意:不要在这里做耗时操作!
}

警告: 任务的最大执行时间(第三个参数)一定要保守估计。如果你不确定,先设大一点(比如500微秒),然后通过地面站日志观察实际执行时间,再逐步调小。我见过有人设了100微秒,结果实际跑了300微秒,导致整个调度器乱套。

嗯,这里还要注意:调度器的任务列表是全局的,不是某个模式私有的。也就是说,无论你切换到哪个模式,这些任务都会跑。所以你的自定义任务里,最好加个判断:只有当前模式是你的自定义模式时,才执行核心逻辑。

2.5 小结一下

这一章我们干了三件事:

  • 看了ArduPilot的目录结构,知道代码放哪
  • 认识了核心库,知道工具箱里有什么
  • 理解了调度器,知道飞控怎么「一心多用」

说实话,调度器这部分,我当年刚接触时也觉得有点绕。但后来我写了个小工具,把每个任务的执行时间打印出来,看着那些波形图,一下子就明白了。你如果也遇到困惑,不妨也试试这个方法——把调度器的工作过程可视化出来。

下一章,我们就开始真正动手,写一个最简单的自定义模式。但在此之前,确保你对本章的内容有感觉。尤其是调度器,它是你后面调试自定义模式时,最常打交道的模块。

我的建议: 打开你的ArduPilot代码,找到 libraries/AP_Scheduler/AP_Scheduler.cpp,把 run() 函数读一遍。代码不长,但看完之后,你对飞控的理解会上一个台阶。


公众号:蓝海资料掘金营,微信deep3321