3. PX4模块架构:uORB消息机制、工作队列与模块注册流程

做PX4二次开发,说白了就是跟这三个东西打交道:uORB、工作队列、模块注册。我刚开始接触的时候也觉得头大,但摸清楚之后就会发现,这套设计其实挺巧妙的。

3.1 uORB:模块间的“快递系统”

uORB是PX4内部的消息总线。各个模块之间不直接调用,而是通过uORB发布和订阅消息。打个比方,GPS驱动发布了一个位置数据,导航模块订阅它,飞控模块也订阅它——大家各取所需,互不干扰。

核心概念:uORB是一种发布/订阅(Pub/Sub)模型,支持多对多通信。

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);
避坑指南:我曾经把一个需要5ms才能跑完的任务挂到HP队列,周期设成2ms。结果任务还没跑完,下一次调度又来了——直接导致堆栈溢出。记住:任务执行时间必须小于调度周期。

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、工作队列和模块注册三者之间的关系。我画这张图的时候,特意把数据流向标清楚了——你一看就明白。

PX4模块架构关系图 PX4模块架构:uORB · 工作队列 · 模块注册 模块注册层 CMakeLists.txt → task_spawn → instantiate → 挂载到工作队列 工作队列层 HP高优先级队列 LP低优先级队列 SPI/I2C专用队列 uORB消息层 发布端 (Publisher) orb_advertise → orb_publish 消息主题 (Topic) vehicle_attitude, sensor_accel... 订阅端 (Subscriber) orb_subscribe → orb_copy 模块注册 → 挂载到工作队列 → 通过uORB发布/订阅数据

3.5 实战中的避坑指南

做测绘模式扩展时,这几个坑我踩过,你注意一下:

  • uORB消息频率不匹配:发布方100Hz,订阅方只10Hz,中间的数据就丢了。我建议订阅方用 orb_check 检查是否有新数据,而不是盲目拷贝。
  • 工作队列阻塞:不要在回调函数里做耗时操作,比如写文件、打印日志。我曾经把日志写入放到HP队列,结果飞控直接炸了——后来改到LP队列才解决。
  • 模块注册路径错误:CMakeLists.txt里的路径必须和实际目录一致,大小写敏感。我因为把 MyModule 写成 mymodule,编译报错查了半天。
我的习惯:每次新建模块,先在 msg/ 里定义好消息结构,再写模块代码。这样思路清晰,不会出现“写到一半发现缺字段”的尴尬。

好了,这一章的内容就这些。uORB、工作队列、模块注册——这三个东西搞明白了,PX4的模块架构你就掌握了八成。剩下的就是多写代码、多调试,慢慢就熟了。