2. 发布-订阅模型:理解核心设计模式,主题(Topic)、发布者(Publisher)、订阅者(Subscriber)

好,咱们今天来聊聊uORB最核心的设计——发布-订阅模型。

说白了,这就是一套“谁说话,谁听讲”的规则。你想想看,飞控系统里几十个模块同时跑,姿态估计要数据、GPS要数据、遥控器也要数据。如果每个模块都直接去拿,那代码就乱成一锅粥了。

uORB的解决方案很简单:数据放中间,谁用谁取

2.1 三个核心角色

这个模型里只有三个角色,我习惯叫它们“铁三角”:

  • 主题(Topic):数据的“名字”。比如 sensor_accel 就是加速度计数据的主题。它不存数据,只定义数据类型和ID。
  • 发布者(Publisher):数据的“生产者”。比如传感器驱动,它把原始数据打包,然后往主题上“贴”出去。
  • 订阅者(Subscriber):数据的“消费者”。比如姿态估计算法,它说“我要听 sensor_accel 这个频道”,然后就能拿到最新数据。

关键点:发布者和订阅者互相不知道对方的存在。发布者只管发,订阅者只管收。这就是解耦。

我在项目中遇到过一个问题:有个同事写了个模块,直接调用了另一个模块的全局变量。结果后来那个模块重构了,变量名一改,整个系统编译报错。改用uORB之后,这种问题再也没出现过。因为大家只认主题,不认人。

2.2 核心逻辑流程图

为了让你更直观地理解,我画了一张图。这张图展示了数据从传感器到应用层的完整路径。

uORB 发布-订阅模型核心流程 发布者 (Publisher) 传感器驱动 / 姿态模块 主题 (Topic) sensor_accel 订阅者 (Subscriber) 姿态估计 / 位置控制 数据流方向:发布者 → 主题(uORB内部缓存) → 订阅者 发布者和订阅者完全解耦,通过主题名称建立联系 发布者内部流程 1. 获取主题句柄:orb_advertise() 2. 填充数据结构体 3. 发布数据:orb_publish() 4. uORB更新缓存,通知订阅者 订阅者内部流程 1. 订阅主题:orb_subscribe() 2. 检查数据更新:orb_check() 3. 拷贝数据:orb_copy() 4. 处理数据,执行控制逻辑

嗯,这张图你看懂了吗?发布者把数据“扔”到主题上,订阅者自己去“拿”。中间那个主题,就像一个公告栏。谁贴通知,谁看通知,互不干扰。

2.3 为什么用这种模式?

我刚开始学PX4的时候,也觉得多此一举。直接函数调用多快啊?后来踩了坑才明白,嵌入式系统最怕的就是“耦合”。

  • 模块独立:每个模块只关心自己的事。GPS坏了,不影响姿态控制。因为姿态控制只从 vehicle_attitude 主题拿数据,不管数据是谁发的。
  • 易于扩展:想加一个新传感器?简单。写个驱动,往对应主题上发布数据就行。其他模块不用改一行代码。
  • 调试方便:我曾经调试一个炸机问题,用 listener 命令直接监听 sensor_accel 主题,发现数据跳变异常。5分钟定位到是传感器硬件虚焊。要是没有uORB,我得翻遍所有代码才能找到数据源头。

调试小技巧:在nsh终端输入 listener sensor_accel 5,可以实时打印最近5次加速度计数据。这是排查数据异常的利器。

2.4 避坑指南:我曾经犯过的错

这里分享几个我踩过的坑,你遇到了可以少走弯路。

  • 忘记调用 orb_advertise():发布者必须先“广告”一下,告诉uORB“我要发这个主题了”。我第一次写驱动时忘了这步,结果订阅者永远拿不到数据。查了半天才发现。
  • 订阅者数据陈旧:uORB默认只保留最新一份数据。如果你的订阅者处理太慢,可能会丢数据。我建议在实时性要求高的地方,用 orb_check() 配合轮询,而不是阻塞等待。
  • 多线程竞争:虽然uORB内部有锁,但如果你在中断服务函数里发布数据,还是可能出问题。我的习惯是:中断里只设标志位,在主循环里发布。

重要提醒:不要在orb_publish()和orb_copy()里做耗时操作。这两个函数虽然快,但毕竟涉及内核调用。我曾经在一个高频率发布循环里加了打印日志,结果导致系统调度抖动,飞机直接翻了个跟头。

2.5 代码实战:手写一个最小示例

光说不练假把式。咱们写一个最简单的发布-订阅对。假设我们要发布一个“自定义温度”主题。

// 发布者代码(temperature_publisher.c)
#include <uORB/uORB.h>
#include <uORB/topics/temperature.h>

void publish_temperature(float temp_celsius) {
    struct temperature_s temp_data;
    temp_data.temperature = temp_celsius;
    temp_data.timestamp = hrt_absolute_time();

    // 第一步:广告主题(只做一次)
    static orb_advert_t pub_handle = nullptr;
    if (pub_handle == nullptr) {
        pub_handle = orb_advertise(ORB_ID(temperature), &temp_data);
    }

    // 第二步:发布数据
    orb_publish(ORB_ID(temperature), pub_handle, &temp_data);
}
// 订阅者代码(temperature_subscriber.c)
#include <uORB/uORB.h>
#include <uORB/topics/temperature.h>

void check_temperature(void) {
    // 第一步:订阅主题(只做一次)
    static int sub_handle = -1;
    if (sub_handle == -1) {
        sub_handle = orb_subscribe(ORB_ID(temperature));
    }

    // 第二步:检查是否有新数据
    bool updated = false;
    orb_check(sub_handle, &updated);

    if (updated) {
        struct temperature_s temp_data;
        // 第三步:拷贝数据
        orb_copy(ORB_ID(temperature), sub_handle, &temp_data);
        printf("当前温度: %.2f °C\n", temp_data.temperature);
    }
}

你看,代码就这么几行。发布者只管 orb_advertiseorb_publish,订阅者只管 orb_subscribeorb_checkorb_copy。模式非常清晰。

2.6 性能与实时性

有人会问:这种“中间商”模式,会不会影响性能?

我实测过,在STM32F7上,一次 orb_publishorb_copy 的延迟大约在 1-3 微秒。对于飞控这种 250Hz 到 1kHz 的控制循环来说,完全够用。

操作 典型耗时(STM32F7, 216MHz) 说明
orb_advertise() ~5 µs 仅首次调用,分配句柄
orb_publish() ~1.5 µs 拷贝数据到内核缓存
orb_subscribe() ~3 µs 仅首次调用,注册订阅者
orb_check() ~0.5 µs 检查更新标志位
orb_copy() ~1.5 µs 从内核缓存拷贝到用户空间

嗯,数据摆在这里。uORB的发布-订阅模型,不是花架子,是真能打。

2.7 小结

发布-订阅模型,说白了就是“数据放中间,谁用谁取”。它让飞控系统里的几十个模块各司其职,互不干扰。你写驱动的时候,只管发布;你写算法的时候,只管订阅。中间的事,uORB帮你搞定。

我个人觉得,理解了这个模型,你就掌握了PX4软件架构的一半。剩下的,就是熟悉各个主题的数据结构了。

一句话记住:发布者不关心谁在听,订阅者不关心谁在说。他们只认主题名字。


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