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 库则更强调协议解析与状态机,不同厂商的报文格式被封装在各自后端中,前端统一输出位置、速度、时间与定位状态。气压计库相对简单,但同样遵循“前端统一、后端多样”的模式。

ArduPilot 外设驱动分层与关键模块 飞行器工程层:ArduCopter / ArduPlane / Rover 决定初始化顺序、调度周期与任务逻辑 传感器前端库:统一 API 与设备管理 AP_InertialSensor AP_GPS AP_Baro 后端驱动:芯片 / 协议专用实现 Invensense / Bosch u-blox / NMEA BMP280 / MS5611 probe / detect AP_HAL:硬件抽象接口 UARTDriver I2CDevice SPIDevice GPIO / Scheduler 具体实现:AP_HAL_ChibiOS / AP_HAL_Linux / AP_HAL_SITL

四、从源码到设备:一次典型接入流程

假设要接入一款新的 I2C 气压计,源码层面的工作通常按以下顺序展开:

  1. 在 libraries/AP_Baro/ 下新增后端文件,例如 AP_Baro_MySensor.cpp/.h。
  2. 实现 probe(),通过 hal.i2c 读取 WHO_AM_I 寄存器确认设备存在。
  3. 实现 init() 与 read(),完成配置、采样与温度补偿。
  4. 在 AP_Baro 前端注册该后端,并分配 BARO_ 参数类型编号。
  5. 在目标飞行器工程的 hwdef.dat 或默认参数中启用对应总线与设备。
  6. 编译并查看启动日志,确认 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 确认实际总线访问路径。这样既能快速理解现有驱动,也能按同样的组织方式接入新的外设。