3. 系统启动流程:从Bootloader到主程序入口,AP_HAL层初始化、调度器启动、任务创建的全过程

飞控上电后,到底发生了什么?

很多人拿到ArduPilot源码,直接跳到main()函数开始读。结果发现一堆宏定义、条件编译、HAL抽象层调用,看得云里雾里。我当年也是这么过来的,折腾了两周才理清脉络。

这一章,咱们就把启动流程从头到尾捋一遍。你跟着我走一遍,以后看任何飞控代码都不会迷路。

3.1 第一行代码:Bootloader在干什么?

飞控芯片上电后,CPU从复位向量指向的地址开始执行。这个地址里放的,通常是Bootloader。

Bootloader的任务很简单:

  • 初始化最基本的硬件(时钟、内存、串口)
  • 检查是否有固件更新请求(比如通过USB或UART)
  • 如果没有更新请求,跳转到应用程序入口

在ArduPilot的Pixhawk系列硬件上,Bootloader用的是PX4官方的那个。它会在启动后等待约1秒,看有没有NOP指令流进来。如果有,就进入固件烧录模式;没有,就跳转到Flash中的应用程序起始地址。

小技巧: 如果你发现飞控怎么都连不上地面站,先按住Bootloader按钮再上电,通常能强制进入固件更新模式。我遇到过好几次USB驱动冲突导致识别失败,用这招都救回来了。

3.2 进入主程序:main()函数入口

Bootloader跳转后,执行的是main()函数。但ArduPilot的main()不在你想象的ArduCopter.cpp里,而是在AP_HAL层中。

以STM32F4/F7平台为例,main()在libraries/AP_HAL_ChibiOS/hwdef/下的启动文件里。它长这样:

int main(void)
{
    // 1. 硬件初始化
    hal.init(OS_STARTUP);

    // 2. 调度器初始化
    hal.scheduler->init();

    // 3. 创建主任务
    hal.scheduler->register_timer_process(AP_HAL::Scheduler::timer_process);
    hal.scheduler->register_io_process(AP_HAL::Scheduler::io_process);

    // 4. 进入主循环
    hal.scheduler->system_initialized();
    while (true) {
        hal.scheduler->delay(1);
    }
}

嗯,这里要注意:你看到的hal是一个全局对象,它在AP_HAL命名空间里定义。不同的硬件平台,hal的实现完全不同。这就是HAL层的意义——上层代码不用关心你用的是STM32还是K210。

3.3 AP_HAL层初始化:把硬件抽象成接口

hal.init()干了什么?

说白了,就是把芯片上的外设全部初始化好,然后包装成统一的接口。以ChibiOS版本为例:

  • 时钟系统:配置PLL,让CPU跑在最高频率(比如216MHz)
  • GPIO:初始化所有引脚,设置复用功能
  • UART/串口:打开调试串口和GPS串口
  • I2C/SPI:初始化总线,准备挂载传感器
  • 定时器:配置系统滴答定时器,提供1ms心跳
  • ADC:初始化模数转换器,用于读取电池电压等

你想想看,如果没有这层抽象,你写一个Copter固件,换块飞控板就得重写一半代码。HAL层就是帮你挡掉这些脏活累活。

核心概念: AP_HAL层是ArduPilot跨平台能力的基石。它定义了hal全局对象,包含scheduler、uart、i2c、spi、analog、gpio等子模块。每个硬件平台只需要实现这些接口,上层代码完全不用改。

3.4 调度器启动:飞控的“心脏”

hal.scheduler->init()启动的是整个飞控的任务调度系统。

ArduPilot的调度器不是实时操作系统(RTOS)那种抢占式调度,而是一个协作式调度器。它维护一个任务列表,每个任务有一个期望执行频率(比如400Hz、50Hz、10Hz)。调度器每次循环,检查哪些任务该执行了,就调用对应的函数。

调度器的核心数据结构:

// 任务结构体
struct AP_Scheduler::Task {
    const char *name;       // 任务名称
    bool (*function)(void); // 任务函数指针
    float rate_hz;          // 期望执行频率
    uint16_t max_time_micros; // 最大执行时间(微秒)
};

调度器的主循环逻辑:

void AP_Scheduler::loop()
{
    uint32_t now = micros();
    for (uint8_t i = 0; i < _num_tasks; i++) {
        if (now - _last_run[i] >= (uint32_t)(1000000.0f / _tasks[i].rate_hz)) {
            _tasks[i].function();
            _last_run[i] = now;
        }
    }
}

为什么不用RTOS?我个人觉得,协作式调度器在飞控这种硬实时场景下反而更可控。你想想,如果每个任务都能被随时打断,那姿态控制这种高优先级任务反而可能因为中断延迟而出问题。协作式调度器让每个任务自己控制执行时间,只要你不写死循环,系统就不会卡死。

避坑指南: 我曾经在自定义任务里写了一个while(1)等待传感器数据,结果整个飞控卡死,飞机直接翻了个跟头。后来才意识到,协作式调度器里绝对不能有阻塞等待。所有等待都要用状态机或者回调来实现。

3.5 任务创建:飞控的“器官”们开始工作

调度器启动后,接下来就是创建各个核心任务。这些任务在ArduCopter.cpp的setup()函数中注册:

void Copter::setup()
{
    // 初始化传感器
    init_barometer();
    init_compass();
    init_gyro();
    init_accel();

    // 初始化GPS
    init_gps();

    // 初始化RC接收机
    init_rc_in();

    // 初始化电机输出
    init_rc_out();

    // 注册调度任务
    scheduler.init(&_tasks[0], ARRAY_SIZE(_tasks));
}

注册的任务列表(以Copter为例):

任务名称 频率 说明
fast_loop 400 Hz 姿态控制、角速度控制
medium_loop 50 Hz 位置控制、速度控制
slow_loop 10 Hz 导航、航点管理
gcs_update 50 Hz 地面站数据发送
gcs_rc_loop 50 Hz 遥控器信号处理
compass_cal 10 Hz 罗盘校准
barometer_read 25 Hz 气压计读取

这些任务按照频率分层,高频率的任务做最核心的控制,低频率的任务做辅助功能。这种设计保证了飞控的实时性——400Hz的fast_loop永远不会被50Hz的gcs_update阻塞。

3.6 启动流程全景图

下面这张图把整个启动流程串起来了。你可以对照着看,每一步都对应着代码里的哪个函数。

ArduPilot系统启动流程 ArduPilot 系统启动流程全景图 阶段1: Bootloader 上电 → 初始化时钟/内存/串口 → 检查固件更新 → 跳转到应用程序入口 阶段2: main() 函数入口 hal.init() → hal.scheduler->init() → 注册定时/IO处理 → 进入主循环 阶段3: AP_HAL层初始化 时钟 → GPIO → UART → I2C/SPI → 定时器 → ADC 将硬件外设包装成统一的 hal 接口 阶段4: 调度器启动 初始化任务列表 → 设置任务频率 → 启动协作式调度循环 阶段5: 任务创建与注册 传感器初始化 → GPS → RC → 电机 → 注册 fast_loop / medium_loop / slow_loop 主循环: 调度器循环执行任务 说明:整个启动流程从上到下依次执行,每个阶段都依赖前一阶段完成。主循环启动后,飞控进入正常运行状态。

3.7 启动过程中的几个关键点

最后,我总结几个实际项目中容易踩的坑:

  • Bootloader跳转地址不对:如果你自己画了飞控板,一定要确认Bootloader跳转的地址和应用程序的链接地址一致。差一个字节,系统就起不来。
  • HAL层初始化顺序:有些外设依赖其他外设(比如I2C传感器依赖I2C总线),初始化顺序错了会导致设备挂不上。我建议严格按照hal.init()里的顺序来,不要自己乱改。
  • 调度器任务超时:每个任务都有max_time_micros限制,如果某个任务执行时间超过了这个值,调度器会打印警告。我曾经在GPS解析任务里用了printf调试,结果每次打印都超时,导致姿态控制任务被延迟。后来改用hal.console->printf才解决。
  • 传感器自检失败:在setup()阶段,每个传感器都会做自检。如果某个传感器挂了,飞控会进入FAILSAFE模式,不会解锁。这是好事,别为了省事把自检关掉。