4. PX4固件架构:从整体到细节
PX4 这套固件,我接触了快十年。说实话,刚上手时也被它的模块数量吓到过。但摸清它的骨架之后,你会发现它其实很清晰。今天我们就从四个维度把它拆开看看:整体架构、消息通信、任务调度、参数系统。
4.1 整体架构概览
先看一张我画的架构图。它把 PX4 的核心模块和它们之间的关系都串起来了。
你看,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);
// 处理数据
}
orb_set_interval,导致消息队列被旧数据塞满。订阅者读到的永远是 5 秒前的数据。嗯,这个坑我踩过,你们别踩。
4.3 模块与任务调度
PX4 的模块分为两类:任务模块和库模块。任务模块有自己的线程或工作队列,库模块只是被其他模块调用。
4.3.1 模块生命周期
每个任务模块都遵循固定的生命周期:
- 注册:模块启动时,向调度器注册自己
- 初始化:分配资源、订阅话题、发布话题
- 运行:进入主循环,处理数据
- 停止:收到停止信号,清理资源
模块的启动命令格式是:
# 启动模块
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 把关键任务放在高优先级队列,非关键任务放在低优先级队列。
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, ¶m_update);
// 重新读取所有参数
param_get(_param_pitchrate_p, &pitchrate_p);
}
param set 修改参数后,模块需要等到下一次 parameter_update 话题发布时才会读取新值。这个间隔默认是 100ms。如果你在 SITL 仿真中改了参数没生效,别急,等一等。
4.5 本章小结
PX4 的架构设计,说白了就是「高内聚、低耦合」的工程实践。uORB 让模块之间不打架,工作队列让关键任务优先跑,参数系统让调参不用改代码。这三个机制配合起来,你才能在 SITL 仿真中快速迭代算法,然后无缝迁移到实飞。
我记得刚入行时,总想把所有代码写在一个文件里。后来被架构设计「教育」了几次,才明白模块化的重要性。嗯,希望你们能少走这些弯路。