3. PX4模块架构:uORB消息机制、工作队列与模块注册流程
做PX4二次开发,说白了就是跟这三个东西打交道:uORB、工作队列、模块注册。我刚开始接触的时候也觉得头大,但摸清楚之后就会发现,这套设计其实挺巧妙的。
3.1 uORB:模块间的“快递系统”
uORB是PX4内部的消息总线。各个模块之间不直接调用,而是通过uORB发布和订阅消息。打个比方,GPS驱动发布了一个位置数据,导航模块订阅它,飞控模块也订阅它——大家各取所需,互不干扰。
3.1.1 消息定义与生成
消息定义文件放在 msg/ 目录下,后缀是 .msg。比如 vehicle_attitude.msg 定义了飞行姿态数据。编译时,系统会自动生成对应的C++结构体和发布/订阅函数。
# 一个典型的.msg文件示例
uint64 timestamp # 时间戳,微秒
float32[4] q # 四元数
float32 roll # 滚转角
float32 pitch # 俯仰角
float32 yaw # 偏航角
我个人习惯在定义新消息时,一定加上 timestamp 字段。为什么?因为我在项目中遇到过数据不同步的问题,后来发现就是少了时间戳,导致订阅方不知道数据有多新鲜。
3.1.2 发布与订阅接口
发布消息的流程很简单:先 orb_advertise 广告一个主题,然后循环调用 orb_publish 发送数据。订阅方则用 orb_subscribe 获取句柄,再用 orb_copy 把数据拷出来。
// 发布端示例
int pub_fd = orb_advertise(ORB_ID(vehicle_attitude), &att);
orb_publish(ORB_ID(vehicle_attitude), pub_fd, &att);
// 订阅端示例
int sub_fd = orb_subscribe(ORB_ID(vehicle_attitude));
orb_copy(ORB_ID(vehicle_attitude), sub_fd, &att);
orb_set_interval 设置最小更新间隔,避免高频轮询浪费CPU。我一般设成10ms,够用又不占资源。
3.2 工作队列:任务调度的“管家”
PX4不是每个模块都开一个独立线程,那样太浪费了。它用的是工作队列(Work Queue)机制——把任务排队,由专门的线程统一调度。
你想想看,如果每个传感器驱动都开一个线程,系统光切换上下文就忙不过来了。工作队列的好处就是:节省资源,避免竞态。
3.2.1 工作队列的类型
| 队列名称 | 优先级 | 典型用途 |
|---|---|---|
| wq_configurations::HP | 高 | 传感器数据读取、控制输出 |
| wq_configurations::LP | 低 | 日志记录、参数更新 |
| wq_configurations::SPI | 高 | SPI总线设备驱动 |
| wq_configurations::I2C | 中 | I2C总线设备驱动 |
嗯,这里要注意:高优先级队列里的任务不能阻塞太久,否则会影响飞控的实时性。我曾经见过一个同事把日志写入放到HP队列里,结果飞控偶尔出现抖舵——就是因为写SD卡太慢了。
3.2.2 如何将模块挂载到工作队列
在模块的启动代码中,通过 ScheduleNow() 或 ScheduleOnInterval() 把任务挂到指定队列。
// 将模块挂载到高优先级工作队列
ScheduleOnInterval(1000); // 每1ms执行一次
// 或者手动指定队列
ScheduleOnInterval(wq_configurations::HP, 1000);
3.3 模块注册流程:让系统认识你的模块
写了一个新模块,怎么让PX4知道它的存在?这就涉及到模块注册流程了。说白了,就是告诉系统:我叫什么名字、我属于哪个类别、我该怎么启动。
3.3.1 模块类定义
每个模块都是一个C++类,继承自 ModuleBase 或 ModuleParams。我一般用 ModuleBase,因为它提供了标准的生命周期管理。
class MyModule : public ModuleBase<MyModule>, public ModuleParams
{
public:
MyModule();
~MyModule();
// 必须实现的接口
int init() override;
void run() override;
// 命令行支持
static int task_spawn(int argc, char *argv[]);
static MyModule *instantiate(int argc, char *argv[]);
static int print_usage(const char *reason = nullptr);
};
3.3.2 注册到构建系统
在 CMakeLists.txt 里添加模块路径,编译系统会自动识别。
# 在对应的CMakeLists.txt中添加
px4_add_module(
MODULE modules__my_module
MAIN my_module
STACK_MAIN 2000
SRCS
my_module.cpp
DEPENDS
lib__parameters
lib__uorb
)
3.3.3 启动与命令行接口
注册完成后,就可以在nsh终端里用命令启动了:
# 启动模块
my_module start
# 查看状态
my_module status
# 停止模块
my_module stop
task_spawn 是模块的入口,它负责创建任务并挂载到工作队列。instantiate 则负责实例化模块对象。这两个函数缺一不可。
3.4 整体架构关系图
下面这张图展示了uORB、工作队列和模块注册三者之间的关系。我画这张图的时候,特意把数据流向标清楚了——你一看就明白。
3.5 实战中的避坑指南
做测绘模式扩展时,这几个坑我踩过,你注意一下:
- uORB消息频率不匹配:发布方100Hz,订阅方只10Hz,中间的数据就丢了。我建议订阅方用
orb_check检查是否有新数据,而不是盲目拷贝。 - 工作队列阻塞:不要在回调函数里做耗时操作,比如写文件、打印日志。我曾经把日志写入放到HP队列,结果飞控直接炸了——后来改到LP队列才解决。
- 模块注册路径错误:CMakeLists.txt里的路径必须和实际目录一致,大小写敏感。我因为把
MyModule写成mymodule,编译报错查了半天。
msg/ 里定义好消息结构,再写模块代码。这样思路清晰,不会出现“写到一半发现缺字段”的尴尬。
好了,这一章的内容就这些。uORB、工作队列、模块注册——这三个东西搞明白了,PX4的模块架构你就掌握了八成。剩下的就是多写代码、多调试,慢慢就熟了。