4. PX4固件架构:从整体到细节

PX4 这套固件,我接触了快十年。说实话,刚上手时也被它的模块数量吓到过。但摸清它的骨架之后,你会发现它其实很清晰。今天我们就从四个维度把它拆开看看:整体架构、消息通信、任务调度、参数系统。

4.1 整体架构概览

先看一张我画的架构图。它把 PX4 的核心模块和它们之间的关系都串起来了。

PX4 固件整体架构 应用层(Application Layer) Commander | Navigator | Position Controller | Attitude Controller GPS | Optical Flow | Distance Sensor | Battery Monitor ... 约 80+ 个独立模块 uORB 消息总线(uORB Message Bus) 发布/订阅 | 异步通信 | 零拷贝(共享内存) 中间件层(Middleware Layer) 参数系统 | 日志系统 | 时间管理 | 工作队列 MAVLink 通信 | EKF 融合框架 硬件抽象层(Hardware Abstraction Layer) PWM | I2C | SPI | UART | CAN | ADC | GPIO 说明: 应用模块通过 uORB 发布/订阅消息,不直接调用其他模块。中间件提供公共服务,HAL 屏蔽硬件差异。

你看,PX4 是典型的分层架构。从上到下依次是:应用层、uORB 消息总线、中间件层、硬件抽象层。每一层只跟相邻层打交道,模块之间不直接耦合。

我个人觉得,这种设计最大的好处是——你想换一个传感器驱动,完全不影响上层的控制逻辑。我在一个项目中换过 IMU 芯片,只改了 HAL 层的驱动代码,上层模块一个没动。

4.2 uORB 消息机制

uORB 是 PX4 的通信核心。说白了,它就是一套「发布-订阅」的消息总线。模块 A 发布一个话题,模块 B 订阅它,两者完全解耦。

4.2.1 核心概念

概念 说明 类比
话题(Topic) 消息的唯一标识,如 sensor_accel 电台频道
发布者(Publisher) 发送消息的模块,一个话题可以有多个发布者 广播员
订阅者(Subscriber) 接收消息的模块,一个话题可以有多个订阅者 听众
消息定义(Message Definition) 使用 .msg 文件定义数据结构 节目单

4.2.2 消息定义示例

我们来看一个实际的 .msg 文件。这是 sensor_accel.msg 的简化版:

# sensor_accel.msg
uint64 timestamp          # 时间戳(微秒)
uint64 timestamp_sample   # 采样时间

float32[3] x              # X 轴加速度(m/s²)
float32[3] y              # Y 轴加速度(m/s²)
float32[3] z              # Z 轴加速度(m/s²)

uint32 device_id          # 设备 ID
uint32 error_count        # 错误计数
uint8[4] _padding         # 对齐填充
小技巧: 我习惯在自定义消息时,timestamp 和 timestamp_sample 都带上。前者是消息生成时间,后者是数据采样时间。在 SITL 仿真中,这两个时间戳能帮你快速定位延迟问题。

4.2.3 发布与订阅的代码模式

发布消息的典型写法:

// 发布者示例
#include <uORB/uORB.h>
#include <uORB/topics/sensor_accel.h>

// 在模块初始化时
struct sensor_accel_s accel_data{};
orb_advert_t accel_pub = orb_advertise(ORB_ID(sensor_accel), &accel_data);

// 在循环中填充并发布
accel_data.timestamp = hrt_absolute_time();
accel_data.x[0] = imu_readings[0];
// ... 填充其他字段
orb_publish(ORB_ID(sensor_accel), accel_pub, &accel_data);

订阅消息的典型写法:

// 订阅者示例
int accel_sub = orb_subscribe(ORB_ID(sensor_accel));
// 设置订阅队列深度(可选)
orb_set_interval(accel_sub, 10); // 10ms 更新一次

struct sensor_accel_s accel_data{};
// 在循环中轮询
orb_check(accel_sub, &updated);
if (updated) {
    orb_copy(ORB_ID(sensor_accel), accel_sub, &accel_data);
    // 处理数据
}
注意: 我曾经在调试一个 SITL 仿真时,发现姿态估计一直不准。查了两天才发现——订阅者没有设置 orb_set_interval,导致消息队列被旧数据塞满。订阅者读到的永远是 5 秒前的数据。嗯,这个坑我踩过,你们别踩。

4.3 模块与任务调度

PX4 的模块分为两类:任务模块和库模块。任务模块有自己的线程或工作队列,库模块只是被其他模块调用。

4.3.1 模块生命周期

每个任务模块都遵循固定的生命周期:

  1. 注册:模块启动时,向调度器注册自己
  2. 初始化:分配资源、订阅话题、发布话题
  3. 运行:进入主循环,处理数据
  4. 停止:收到停止信号,清理资源

模块的启动命令格式是:

# 启动模块
module_name start

# 带参数启动
module_name start -p param1 -p param2

# 停止模块
module_name stop

# 查看模块状态
module_name status

4.3.2 工作队列(Work Queue)

PX4 使用工作队列来管理低优先级任务。我举个例子:

队列类型 优先级 典型用途
高优先级队列 最高 姿态控制、位置控制(1kHz 循环)
中优先级队列 中等 传感器数据读取、EKF 更新
低优先级队列 最低 日志写入、参数同步、MAVLink 心跳

你想想看,如果日志写入占用了 CPU,导致姿态控制延迟,飞机会怎样?嗯,会炸。所以 PX4 把关键任务放在高优先级队列,非关键任务放在低优先级队列。

核心原则: 控制循环的优先级永远高于 I/O 和日志。在 SITL 仿真中,如果你发现仿真跑得卡顿,先检查是不是日志级别开太高了。

4.4 参数系统

参数系统是 PX4 的「配置中心」。所有可调参数都通过它管理,不需要重新编译固件。

4.4.1 参数定义

参数在 src/modules/parameters 目录下定义,使用 XML 文件:

<parameters>
    <parameter name="MC_PITCHRATE_P" type="float" default="0.15">
        <description>多旋翼俯仰角速率 P 增益</description>
        <unit>1/s</unit>
        <min>0.0</min>
        <max>1.0</max>
    </parameter>

    <parameter name="SYS_AUTOSTART" type="int32" default="0">
        <description>自动启动配置索引</description>
        <min>0</min>
        <max>65535</max>
    </parameter>
</parameters>

4.4.2 参数操作命令

在 SITL 仿真中,你可以通过 param 命令操作参数:

# 查看参数
param show MC_PITCHRATE_P

# 设置参数
param set MC_PITCHRATE_P 0.20

# 保存参数到文件
param save /fs/microsd/params.txt

# 从文件加载参数
param load /fs/microsd/params.txt

# 重置所有参数为默认值
param reset_all
我的习惯: 每次调参前,先用 param save 备份当前参数。调崩了直接 param load 恢复。在 SITL 仿真中,我还会把不同机型的参数文件分开保存,比如 quad_params.txt、plane_params.txt。

4.4.3 参数在代码中的使用

// 在模块中获取参数
#include <parameters.h>

// 声明参数句柄
param_t _param_pitchrate_p;

// 在初始化时获取参数句柄
_param_pitchrate_p = param_find("MC_PITCHRATE_P");

// 在运行时读取参数
float pitchrate_p;
param_get(_param_pitchrate_p, &pitchrate_p);

// 监听参数更新
if (parameter_update_sub != nullptr) {
    parameter_update_s param_update;
    orb_copy(ORB_ID(parameter_update), parameter_update_sub, &param_update);
    // 重新读取所有参数
    param_get(_param_pitchrate_p, &pitchrate_p);
}
注意: 参数更新不是实时的。你通过 param set 修改参数后,模块需要等到下一次 parameter_update 话题发布时才会读取新值。这个间隔默认是 100ms。如果你在 SITL 仿真中改了参数没生效,别急,等一等。

4.5 本章小结

PX4 的架构设计,说白了就是「高内聚、低耦合」的工程实践。uORB 让模块之间不打架,工作队列让关键任务优先跑,参数系统让调参不用改代码。这三个机制配合起来,你才能在 SITL 仿真中快速迭代算法,然后无缝迁移到实飞。

我记得刚入行时,总想把所有代码写在一个文件里。后来被架构设计「教育」了几次,才明白模块化的重要性。嗯,希望你们能少走这些弯路。