3. PX4整体架构解析:uORB消息机制、模块与任务调度、参数系统、飞行模式框架
好,咱们进入第三章。这一章是PX4开发的基石,说白了就是你要先搞清楚PX4这架飞机是怎么“思考”和“说话”的。很多新手一上来就急着改代码,结果连消息怎么传递的都没弄明白,最后debug到崩溃。
我个人习惯,在动任何一个模块之前,先把它的骨架摸清楚。PX4的骨架就是四个东西:uORB消息机制、模块与任务调度、参数系统、飞行模式框架。今天咱们一个一个拆开看。
核心观点:PX4不是一个“大循环”跑到底的单片机程序,而是一个由多个独立模块通过uORB“聊天”协作的分布式系统。理解了这个,你才算真正入了门。
3.1 uORB消息机制:模块间的“快递员”
你想想看,PX4里有几十个模块在同时跑:传感器模块在读数据,姿态估计模块在算角度,位置估计模块在算坐标,控制模块在算油门……它们之间怎么交换数据?靠全局变量?那不乱套了。
PX4的答案是uORB(micro Object Request Broker)。它本质上是一个轻量级的发布-订阅消息总线。一个模块发布(publish)一个话题,其他模块订阅(subscribe)这个话题,就能拿到数据。
举个例子,传感器模块发布了sensor_accel话题,姿态估计模块订阅了它,就能拿到加速度计数据。整个过程是异步的、解耦的。
我记得有一次做测绘模式,需要把相机触发信号和GPS时间戳对齐。我直接在imu模块里加了一个uORB发布,然后在相机驱动里订阅,几行代码就搞定了。要是没有uORB,你得改一堆接口。
3.1.1 常用uORB话题一览
| 话题名称 | 数据结构 | 发布模块 | 订阅模块(典型) |
|---|---|---|---|
sensor_accel |
sensor_accel_s |
传感器驱动 | EKF2、姿态估计 |
vehicle_attitude |
vehicle_attitude_s |
姿态估计 | 导航、控制 |
vehicle_local_position |
vehicle_local_position_s |
位置估计 | 导航、任务规划 |
vehicle_command |
vehicle_command_s |
地面站、任务模块 | 指挥官、执行器 |
actuator_controls |
actuator_controls_s |
混控器 | 执行器驱动 |
3.1.2 发布与订阅的代码骨架
发布一个话题,代码大概长这样:
// 发布者示例
#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 = read_accel_x();
accel_data.y = read_accel_y();
accel_data.z = read_accel_z();
orb_publish(ORB_ID(sensor_accel), accel_pub, &accel_data);
订阅一个话题,代码长这样:
// 订阅者示例
#include <uORB/uORB.h>
#include <uORB/topics/sensor_accel.h>
// 在模块初始化时
int accel_sub = orb_subscribe(ORB_ID(sensor_accel));
// 设置订阅队列深度,避免丢消息
orb_set_interval(accel_sub, 10); // 10ms
// 在循环中检查并读取
struct sensor_accel_s accel_data;
bool updated;
orb_check(accel_sub, &updated);
if (updated) {
orb_copy(ORB_ID(sensor_accel), accel_sub, &accel_data);
// 处理数据...
}
避坑指南:我曾经在写一个高频控制模块时,忘了设置orb_set_interval,结果订阅者每次循环都去读,CPU占用率直接飙到80%。后来加上10ms的间隔,瞬间降到5%。记住,不是所有数据都需要每毫秒都处理。
3.2 模块与任务调度:谁在什么时候干活
PX4的模块分为两类:内置模块和独立任务。内置模块是编译进固件的,比如commander、navigator。独立任务则是通过px4_task_spawn_cmd创建的,有自己的栈空间和优先级。
调度策略很简单:优先级抢占式。高优先级的任务可以打断低优先级的任务。PX4的调度器基于NuttX的实时线程,每个模块本质上都是一个POSIX线程。
我建议你在写新模块时,先想清楚它的实时性要求。比如控制循环必须跑在最高优先级,而日志记录可以跑在低优先级。
3.2.1 模块生命周期
- 启动:通过
px4_task_spawn_cmd创建任务,进入run()函数。 - 运行:在
while (!should_exit())循环中处理业务。 - 停止:收到
task_exit信号,清理资源,退出循环。
3.2.2 模块启动命令示例
// 在模块的main函数中
extern "C" __EXPORT int my_module_main(int argc, char *argv[]);
int my_module_main(int argc, char *argv[]) {
if (argc < 2) {
PX4_ERR("usage: my_module {start|stop|status}");
return 1;
}
if (!strcmp(argv[1], "start")) {
// 创建任务
px4_task_spawn_cmd("my_module",
SCHED_DEFAULT,
SCHED_PRIORITY_DEFAULT,
2048, // 栈大小
my_module_thread,
(char *const *)argv);
return 0;
}
// ... stop, status 处理
}
注意:栈大小不要拍脑袋定。我见过一个同事把栈设成512字节,结果模块一跑就崩,debug了两天才发现是栈溢出。保守起见,先设2048,跑起来后用top命令看实际栈使用量,再调优。
3.3 参数系统:不改代码也能调飞机
参数系统是PX4最人性化的设计之一。你不需要为了改一个PID系数重新编译固件,只需要在QGroundControl里改参数,然后保存重启就行。
每个参数都有一个唯一ID、类型、默认值和描述。参数存储在param模块中,通过param_get和param_set接口访问。
3.3.1 定义一个新参数
在src/modules/parameters/目录下的.c文件中添加:
PARAM_DEFINE_FLOAT(MPC_XY_VEL_MAX, 8.0f); // 最大水平速度,默认8m/s
PARAM_DEFINE_INT32(MPC_XY_VEL_P, 100); // 水平速度比例系数,默认100
然后在代码中使用:
param_t _param_xy_vel_max;
float xy_vel_max;
// 初始化时
_param_xy_vel_max = param_find("MPC_XY_VEL_MAX");
// 运行时
param_get(_param_xy_vel_max, &xy_vel_max);
3.3.2 参数更新回调
有些参数需要实时生效,比如控制增益。你可以注册一个回调:
// 在模块的update_params()函数中
if (_param_xy_vel_max != PARAM_INVALID) {
param_get(_param_xy_vel_max, &_xy_vel_max);
// 更新控制器的内部状态
}
个人经验:我在做测绘航线优化时,加了一个MIS_LEG_TIMEOUT参数,用来控制航段超时时间。这样用户不用改代码,直接在QGC里调,省了我好多事。参数系统是PX4给开发者留的“后门”,一定要善用。
3.4 飞行模式框架:从Auto到Manual的切换逻辑
飞行模式是PX4最复杂的部分之一。它不是一个简单的switch-case,而是一个状态机+模块协作的架构。
核心模块是commander,它负责接收来自地面站或遥控器的模式切换指令,然后通过uORB发布vehicle_command话题。其他模块(如navigator、mc_pos_control)订阅这个指令,切换到对应的行为。
3.4.1 模式切换流程
- 用户在地面站点击“Auto”模式。
- 地面站发送
MAV_CMD_DO_SET_MODE指令。 commander模块解析指令,设置内部状态。commander发布vehicle_control_mode话题,包含模式信息。navigator模块检测到模式变化,开始执行任务。- 控制模块根据模式选择对应的控制器(位置控制、速度控制、姿态控制)。
3.4.2 模式框架图
下面这张图展示了PX4飞行模式的核心框架。我画的时候尽量简化了,实际代码里还有更多细节。
3.4.3 测绘模式在框架中的位置
测绘模式属于Auto模式下的一个子模式。它由navigator模块中的MissionBlock派生而来。具体来说,测绘模式会生成一系列航点,然后通过vehicle_command发布给位置控制器。
我建议你在开发测绘模式时,重点关注navigator模块下的mission.cpp和geofence.cpp。这两个文件是任务规划的核心。
关键点:飞行模式框架的核心是“分层解耦”。Commander只管模式切换,Navigator只管任务生成,控制器只管执行。你改测绘模式,基本只需要动Navigator层,其他层不用碰。这就是架构设计的好处。
3.5 本章小结
这一章我们拆解了PX4的四大骨架:
- uORB消息机制:模块间通信的快递员,发布-订阅模式。
- 模块与任务调度:优先级抢占式调度,每个模块是一个独立线程。
- 参数系统:不改代码调参数,通过
param_get/set访问。 - 飞行模式框架:Commander + Navigator + 控制器,分层协作。