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.h 和 mode.cpp,里面定义了所有模式的基类。然后每个具体模式,比如 mode_stabilize.cpp、mode_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一次)
它的核心机制其实很简单。我画个图你就明白了:
你看,主循环 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