一、ArduPilot整体架构:从宏观角度俯瞰飞控系统如何“活”起来

大家好,我是你们的讲师。今天咱们正式开篇,聊聊ArduPilot的整体架构。

很多新手拿到ArduPilot源码,第一反应往往是懵的——几万行代码,从传感器驱动到导航算法,从遥控器输入到电机输出,到底是怎么串起来的?说白了,飞控系统就像一个人体:传感器是眼睛耳朵,控制算法是大脑,执行机构是手脚。但要让这一切协调工作,你得先理解它的“神经系统”和“心跳节奏”。

我个人习惯把ArduPilot的架构拆成三层来看:硬件抽象层、核心调度层、应用逻辑层。今天咱们重点讲中间那层——调度机制,因为这是飞控“活”起来的根本。

1.1 代码结构:三个关键目录

打开ArduPilot源码,你首先会看到几个顶层目录。我建议你重点关注这三个:

  • libraries/:核心库,包含传感器驱动、数学工具、通信协议等。这里的东西是跨平台的,不管你是Pixhawk还是Cube,都能用。
  • ArduPlane/:固定翼专用逻辑。航线控制、姿态解算、任务管理都在这里。
  • AP_HAL/:硬件抽象层。这是ArduPilot的精髓——通过HAL把上层代码和具体硬件解耦。换一个MCU?只要实现HAL接口就行。

我记得刚接触ArduPilot时,总搞不清ArduPlane和libraries的区别。后来才明白:libraries是工具箱,ArduPlane是施工图纸。工具箱里的扳手、螺丝刀谁都能用,但图纸决定了怎么搭出一架固定翼。

1.2 调度机制:飞控的“心跳”

飞控系统本质上是一个实时嵌入式系统。它不能像PC那样“等用户点击”,而是必须按照固定节拍,周期性地完成传感器读取、姿态解算、控制输出。

ArduPilot的调度核心是AP_Scheduler库。它的工作方式很简单:

  • 定义一个任务列表,每个任务有固定的执行频率(比如400Hz、50Hz、10Hz)
  • 主循环不断轮询,检查当前时间该执行哪个任务
  • 如果某个任务超时,就跳过本次执行,保证不阻塞后续任务

来看一段简化后的调度代码:

// 任务表定义
static const AP_Scheduler::Task task_table[] = {
    { &read_sensors,      400, 100 },  // 400Hz,预留100微秒
    { &update_attitude,   400, 200 },  // 400Hz
    { &update_navigation, 50,  300 },  // 50Hz
    { &update_waypoints,  10,  500 },  // 10Hz
    { &send_telemetry,    5,   1000 }, // 5Hz
};

void AP_Scheduler::loop() {
    uint32_t now = micros();
    for (uint8_t i = 0; i < num_tasks; i++) {
        if (now - task_table[i].last_run >= task_table[i].interval) {
            task_table[i].function();
            task_table[i].last_run = now;
        }
    }
}

你可能会问:为什么不用RTOS的任务调度?嗯,这里有个历史原因。ArduPilot早期跑在8位AVR单片机上,资源极其有限,连RTOS都跑不起来。后来虽然移植到了STM32等32位平台,但这种协作式调度的架构一直保留了下来。好处是简单可靠,坏处是——如果一个任务卡死,整个系统就挂了。

避坑指南:我曾经在调试一个自定义传感器驱动时,不小心在读取函数里加了个while循环等待应答。结果飞控直接“死机”,飞机在天上失控了十几秒才切回手动模式。从那以后,我给自己定了个规矩:任何传感器读取都必须加超时保护,绝不能在主循环里阻塞

1.3 核心线程:谁在干活?

虽然ArduPilot没有用RTOS,但它确实有“线程”的概念——只不过是用定时器中断模拟出来的。固定翼飞控主要靠以下几个“隐形线程”工作:

线程/中断 频率 职责
主循环 (Main Loop) 400Hz 调度所有周期性任务
定时器中断 (Timer ISR) 1kHz PWM生成、RC输入捕获
I2C/SPI DMA 异步 传感器数据搬运,不占CPU
UART中断 异步 数传、GPS数据接收

你看,真正干活的主循环只有400Hz,但通过中断和DMA,系统能处理1kHz的PWM信号和异步的传感器数据。这就是嵌入式系统的典型设计——用中断处理实时性要求高的事件,用主循环处理复杂的逻辑计算

1.4 飞控如何“活”起来?

现在我们把所有拼图拼起来,看看一个典型的控制周期:

  1. 传感器读取(400Hz):从IMU读取加速度和角速度,从GPS读取位置和速度
  2. 姿态解算(400Hz):用EKF或互补滤波,融合IMU和GPS数据,得到当前姿态
  3. 导航计算(50Hz):根据当前姿态和航线目标,计算需要的控制量
  4. 控制输出(400Hz):把控制量转换成PWM信号,驱动舵机和电机
  5. 任务管理(10Hz):检查是否到达航点,是否需要切换飞行模式

整个过程就像工厂流水线——每个工位(任务)有自己的节拍,物料(数据)在工位间传递。如果某个工位慢了,后面的工位就得等,但绝不能堵死整条线。

核心要点:ArduPilot的“活”,靠的是固定频率的调度 + 非阻塞的任务设计 + 中断处理紧急事件。这三者缺一不可。

1.5 一张图看懂整体架构

下面这张SVG图,是我自己画的ArduPilot整体架构。它把刚才讲的三层结构、调度机制、数据流都串起来了。你仔细看看,应该能对飞控系统有个全局认识。

ArduPilot 固定翼飞控整体架构 应用逻辑层 (ArduPlane) 航线管理 | 姿态控制 | 任务调度 | 飞行模式切换 例:update_waypoints() 10Hz, update_navigation() 50Hz 例:set_servo() 400Hz, send_telemetry() 5Hz 核心调度层 (AP_Scheduler) 任务表管理 | 频率控制 | 超时检测 | 负载监控 主循环 400Hz 轮询任务表,非阻塞执行 定时器中断 1kHz 处理PWM/RC输入 硬件抽象层 (AP_HAL) I2C/SPI/UART驱动 | GPIO | PWM输出 | 定时器 传感器:IMU (400Hz), GPS (10Hz), 空速计 (50Hz) 执行器:舵机 (400Hz), 电机 (400Hz) 注册任务 调用HAL接口 中断信号 传感器数据 注:实线为控制流,虚线为数据流/中断

这张图里,从上到下是三层结构,从左到右是数据流。你注意看虚线箭头——硬件层通过中断把传感器数据“推”给调度层,调度层再“喂”给应用层。这种推拉结合的方式,既保证了实时性,又避免了轮询浪费CPU。

小技巧:如果你在调试时发现飞控响应变慢,可以打开AP_Scheduler的负载监控。它会告诉你每个任务实际占用了多少时间。我一般要求总负载不超过70%,留30%给突发情况。

1.6 总结

好了,这一章我们站在高处,把ArduPilot的骨架看了一遍。记住三个关键词:三层架构、协作式调度、中断驱动。下一章我们会深入ArduPlane的代码,看看固定翼的航线控制到底是怎么一步步实现的。

最后留个思考题:为什么ArduPilot不直接用FreeRTOS?如果你来设计,会怎么改进这个调度器?想清楚了,后面几章你会理解得更深。


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