4. ArduPilot源码结构解析
ArduPilot 的源码并不是一个“大而全”的单体程序,而是由顶层飞行器工程(如 ArduCopter、ArduPlane、Rover 等)与大量可复用库共同组成。理解源码结构的关键,是先分清“谁负责调度、谁负责抽象、谁负责具体设备驱动”。其中 libraries/ 目录是外设驱动与设备接入的核心区域,而 AP_HAL、AP_InertialSensor、AP_GPS、AP_Baro 则是这条链路上最常被引用的几个模块。
一、libraries 目录:外设能力的“仓库”
libraries/ 下按功能划分出大量子目录,每个子目录通常对应一个可独立编译、可被不同飞行器工程共享的库。它们大致可以分为四类:
- 硬件抽象层:
AP_HAL、AP_HAL_ChibiOS、AP_HAL_Linux、AP_HAL_SITL等,负责屏蔽底层芯片与操作系统差异。 - 传感器驱动库:
AP_InertialSensor、AP_Baro、AP_Compass、AP_GPS、AP_RangeFinder、AP_Airspeed等,负责具体传感器的探测、初始化与数据读取。 - 估计与控制库:
AP_AHRS、AP_NavEKF2/3、AP_Motors、AP_Control等,负责姿态估计、导航与控制输出。 - 系统服务库:
AP_Scheduler、AP_Param、AP_Logger、AP_Notify、AP_SerialManager等,负责调度、参数、日志与串口管理。
这种组织方式的核心思想是“按能力分层、按设备分库”。飞行器工程只负责决定“用哪些库、以什么顺序初始化”,而库本身负责“如何与具体硬件打交道”。因此,当你要接入一个新外设时,通常不是去改飞行器主循环,而是新增或扩展 libraries/ 下的某个驱动库。
二、AP_HAL:一切硬件访问的入口
AP_HAL 是 ArduPilot Hardware Abstraction Layer 的缩写,它定义了统一的硬件访问接口,例如 AP_HAL::UARTDriver、AP_HAL::I2CDevice、AP_HAL::SPIDevice、AP_HAL::GPIO、AP_HAL::Scheduler 等。上层驱动库只依赖这些抽象接口,而不直接操作寄存器。
以串口为例,GPS 驱动不会直接调用 STM32 的 USART 寄存器,而是通过 AP_HAL::UARTDriver 的 read()、write()、available() 等方法收发数据。这样同一份 AP_GPS 代码可以运行在 ChibiOS、Linux 甚至 SITL 仿真环境中。AP_HAL 的具体实现由 AP_HAL_ChibiOS、AP_HAL_Linux、AP_HAL_SITL 等子目录提供,编译时通过 HAL_BOARD 等宏选择。
理解 AP_HAL 的一个实用判断:如果一段代码里出现了 hal.uartA、hal.i2c、hal.spi、hal.gpio,说明它正在通过 HAL 访问硬件;如果出现了具体芯片寄存器名,则说明它已经越过了抽象层,通常只应出现在 AP_HAL_xxx 内部。
三、AP_InertialSensor、AP_GPS、AP_Baro 的组织方式
这三个库是外设接入中最典型的“前端统一、后端多驱动”结构。它们通常都包含以下组成部分:
- 前端类:如
AP_InertialSensor、AP_GPS、AP_Baro,对外提供统一 API,例如get_gyro()、location()、get_pressure()。 - 后端驱动:如
AP_InertialSensor_Invensense、AP_GPS_UBLOX、AP_Baro_BMP280,分别对应具体芯片或协议。 - 探测与注册机制:通过
probe()或detect()在启动时扫描总线,把发现的设备注册到前端。 - 参数与状态:每个库通常带有自己的参数组,如
INS_、GPS_、BARO_前缀,用于配置设备类型、方向、偏移等。
以 AP_InertialSensor 为例,它管理多个 IMU 实例,负责采样、滤波、温度补偿和一致性检查;具体 SPI/I2C 读写则由后端驱动完成。GPS 库则更强调协议解析与状态机,不同厂商的报文格式被封装在各自后端中,前端统一输出位置、速度、时间与定位状态。气压计库相对简单,但同样遵循“前端统一、后端多样”的模式。
四、从源码到设备:一次典型接入流程
假设要接入一款新的 I2C 气压计,源码层面的工作通常按以下顺序展开:
- 在
libraries/AP_Baro/下新增后端文件,例如AP_Baro_MySensor.cpp/.h。 - 实现
probe(),通过hal.i2c读取 WHO_AM_I 寄存器确认设备存在。 - 实现
init()与read(),完成配置、采样与温度补偿。 - 在
AP_Baro前端注册该后端,并分配BARO_参数类型编号。 - 在目标飞行器工程的
hwdef.dat或默认参数中启用对应总线与设备。 - 编译并查看启动日志,确认
probe成功、数据更新正常。
// 伪代码示意:AP_Baro 后端探测与注册
bool AP_Baro_MySensor::probe() {
return _dev->read_registers(WHO_AM_I, &who, 1) && who == 0x58;
}
void AP_Baro::init() {
_backends[_num_backends++] = new AP_Baro_MySensor(*this);
}
五、常见误区与复盘
误区一:把驱动逻辑写进飞行器工程。飞行器工程应只做“选择与调度”,设备细节应留在 libraries/ 对应库中,否则无法被其他机型复用。
误区二:绕过 AP_HAL 直接操作寄存器。这样会破坏可移植性,导致同一驱动无法在 SITL 或 Linux 上运行。
误区三:只改后端、不改前端注册。新驱动即使编译通过,也不会被 probe 到,表现为“设备无数据”。
| 模块 | 主要职责 | 典型文件/类 | 接入时关注点 |
|---|---|---|---|
| AP_HAL | 统一硬件访问接口 | UARTDriver、I2CDevice、SPIDevice | 总线句柄、线程与锁 |
| AP_InertialSensor | 多 IMU 采样与一致性 | AP_InertialSensor_Invensense | 采样率、方向、温度补偿 |
| AP_GPS | 定位协议解析与状态机 | AP_GPS_UBLOX、AP_GPS_NMEA | 波特率、协议自动识别 |
| AP_Baro | 气压与温度采集 | AP_Baro_BMP280、AP_Baro_MS5611 | I2C 地址、校准参数 |
掌握这套结构后,再阅读 ArduPilot 源码时就可以先定位“前端库”,再顺着 probe() 找到“后端驱动”,最后通过 AP_HAL 确认实际总线访问路径。这样既能快速理解现有驱动,也能按同样的组织方式接入新的外设。