3. PX4系统架构:uORB消息机制、工作队列与任务调度、设备驱动框架

好,咱们进入第三章。这一章可以说是PX4开发的“内功心法”。你写驱动也好,写应用也好,天天都在跟这三样东西打交道:uORB工作队列设备驱动框架。我当年刚接触PX4时,最头疼的就是搞不清它们之间的关系。今天咱们就把这层窗户纸捅破。

3.1 uORB消息机制:模块间的“快递员”

先问个问题:PX4里那么多模块——传感器、姿态估计、位置控制、电机输出——它们之间怎么通信?

答案就是uORB。全称是micro Object Request Broker,说白了就是一个轻量级的发布-订阅消息总线

3.1.1 发布与订阅模型

uORB的核心思想很简单:

  • 发布者:把数据塞进一个“话题”(topic)里,比如传感器数据话题 sensor_accel
  • 订阅者:去订阅这个话题,有新数据时就能收到通知

打个比方:就像你订阅了一个公众号。作者(发布者)更新文章,你(订阅者)就能收到推送。发布者不需要知道谁在看,订阅者也不需要知道作者是谁。这就是解耦。

关键点:uORB是进程间通信(IPC)机制。不同模块可以跑在不同的线程甚至不同的CPU核心上,通过uORB交换数据。

3.1.2 常用API

写代码时,你主要会用这几个函数:

// 发布数据
orb_advert_t orb_advertise(const struct orb_metadata *meta, const void *data);
int orb_publish(const struct orb_metadata *meta, orb_advert_t handle, const void *data);

// 订阅数据
int orb_subscribe(const struct orb_metadata *meta);
int orb_copy(const struct orb_metadata *meta, int handle, void *buffer);

// 检查更新
int orb_check(int handle, bool *updated);
int orb_poll(struct pollfd *fds, int nfds, int timeout);

我个人习惯用 orb_poll 配合工作队列,这样不会阻塞主循环。后面会讲到。

3.1.3 自定义话题

你写驱动时,很可能需要定义自己的话题。比如我做过一个激光雷达驱动,就定义了一个 lidar_distance 话题。

步骤很简单:

  1. msg/ 目录下新建一个 .msg 文件,定义数据结构
  2. 运行 make,自动生成C/C++头文件
  3. 在代码里包含头文件,用 ORB_DECLARE 声明话题
// 示例:lidar_distance.msg
uint64_t timestamp    # 时间戳
float32 distance_m    # 距离,单位米
uint8_t quality       # 信号质量 0-100

小技巧:话题名字不要超过32个字符,否则会被截断。我曾经因为这个查了半天bug……

3.2 工作队列与任务调度:别让CPU闲着

PX4跑在实时操作系统(RTOS)上,通常是NuttX。任务调度是RTOS的核心能力。但PX4在RTOS之上又封装了一层——工作队列(Work Queue)。

3.2.1 为什么需要工作队列?

你想想看,如果每个驱动都开一个独立线程,系统里可能有几十个线程在跑。线程切换开销大,而且容易出优先级反转之类的问题。

工作队列的思路是:把任务排队,让一个或几个工作线程去处理。就像你去银行办事,不用每个人都找一个柜员,而是排队等柜员叫号。

3.2.2 工作队列的类型

PX4里主要有两个工作队列:

队列名称 优先级 典型用途
wq_configurations::HPWorkQueue 高优先级 传感器数据读取、控制输出
wq_configurations::LPWorkQueue 低优先级 日志记录、参数更新、非实时任务

嗯,这里要注意:高优先级队列里的任务不能阻塞太久,否则会影响飞行控制。我见过有人把文件写入操作放在高优先级队列里,结果导致控制循环抖动……

3.2.3 如何创建工作项?

写驱动时,你通常会继承 ScheduledWorkItem 类:

class MyDriver : public ModuleBase<MyDriver>, public ScheduledWorkItem
{
public:
    MyDriver() : ScheduledWorkItem("my_driver", wq_configurations::HPWorkQueue) {}

    void Run() override {
        if (should_exit()) return;

        // 这里写你的周期性任务
        read_sensor_data();
        publish_data();

        // 重新调度自己,比如每10ms执行一次
        ScheduleDelayed(10_ms);
    }

private:
    void read_sensor_data() { /* ... */ }
    void publish_data() { /* ... */ }
};

关键点:Run() 函数会被工作队列的线程调用。你不需要自己创建线程,工作队列会帮你调度。

避坑指南:我曾经在 Run() 里调用了 usleep(),结果整个工作队列都被阻塞了。记住:工作项里不要用阻塞调用。如果需要延时,用 ScheduleDelayed() 代替。

3.3 设备驱动框架:写驱动的“标准模板”

PX4的设备驱动框架,说白了就是一套约定俗成的代码结构。你按照这个结构写,别人一看就懂,而且能自动适配PX4的启动、停止、参数管理等机制。

3.3.1 驱动的基本结构

一个典型的PX4驱动包含这几个部分:

  1. 类定义:继承 ModuleBaseScheduledWorkItem
  2. 启动/停止接口:实现 start()stop()status() 等静态方法
  3. 参数处理:使用 param_t 读取用户配置
  4. uORB发布:把数据发布到标准话题
  5. 硬件访问:通过I2C、SPI或UART与传感器通信

3.3.2 一个UART驱动的骨架

咱们这课程主要讲UART设备,所以我给个UART驱动的骨架:

class UartSensor : public ModuleBase<UartSensor>, public ScheduledWorkItem
{
public:
    UartSensor() :
        ScheduledWorkItem("uart_sensor", wq_configurations::HPWorkQueue)
    {
        // 打开UART设备
        _uart_fd = ::open(UART_DEVICE_PATH, O_RDWR | O_NONBLOCK);
        // 配置波特率、数据位等
        configure_uart();
    }

    ~UartSensor() {
        if (_uart_fd >= 0) ::close(_uart_fd);
    }

    // 模块接口
    static int start(int argc, char *argv[]);
    static int stop(int argc, char *argv[]);
    static int status(int argc, char *argv[]);

    void Run() override {
        if (should_exit()) return;

        // 读取UART数据
        uint8_t buf[64];
        int nread = ::read(_uart_fd, buf, sizeof(buf));

        if (nread > 0) {
            // 解析协议
            parse_data(buf, nread);
            // 发布到uORB
            publish_sensor_data();
        }

        // 继续调度
        ScheduleDelayed(10_ms);
    }

private:
    int _uart_fd{-1};

    void configure_uart() { /* 设置波特率、停止位等 */ }
    void parse_data(uint8_t *data, int len) { /* 协议解析 */ }
    void publish_sensor_data() { /* orb_publish */ }
};

3.3.3 模块注册与命令行

为了让你的驱动能通过 uart_sensor start 这样的命令启动,需要注册模块:

// 在文件末尾
static int uart_sensor_main(int argc, char *argv[])
{
    return UartSensor::main(argc, argv);
}

// 在CMakeLists.txt中注册
px4_add_module(
    MODULE drivers/uart_sensor
    MAIN uart_sensor
    STACK_MAIN 2048
    STACK_MAX 4096
    SRCS
        uart_sensor.cpp
    DEPENDS
        platform__uorb
    )

个人经验:我建议你在 status() 方法里打印一些调试信息,比如接收到的数据包数量、错误计数等。这在现场排查问题时特别有用。

3.4 三者关系:一张图说清楚

好,咱们用一张图来总结uORB、工作队列和设备驱动框架的关系:

PX4驱动架构:uORB、工作队列与设备驱动框架 硬件层 UART外设 / I2C总线 / SPI总线 / GPIO 设备驱动层(你的代码) 继承 ScheduledWorkItem,实现 Run() 方法 读取硬件 → 解析数据 → 发布到uORB 工作队列(Work Queue) HPWorkQueue(高优先级) / LPWorkQueue(低优先级) 发布数据 uORB消息总线 sensor_accel / sensor_gyro / vehicle_attitude / ... 应用层(订阅者) 姿态估计 / 位置控制 / 导航 / 日志记录

从这张图可以看得很清楚:

  • 硬件层在最底下,你的驱动通过UART/SPI/I2C去读写它
  • 驱动层跑在工作队列里,周期性执行 Run() 方法
  • 驱动解析完数据后,通过uORB发布出去
  • 上层的应用模块订阅这些话题,拿到数据做进一步处理

说白了,你的驱动就是硬件和上层应用之间的翻译官。工作队列决定了翻译官什么时候干活,uORB决定了翻译官把消息传给谁。

核心要点:写PX4驱动,你只需要关注三件事:

  1. 怎么读写硬件(UART/SPI/I2C)
  2. 怎么解析数据(协议)
  3. 怎么发布到uORB(话题定义)

剩下的——线程调度、任务优先级、模块生命周期——框架都帮你搞定了。

好,这一章就到这里。记住这张图,后面写驱动时你会反复用到它。


公众号:蓝海资料掘金营,微信deep3321