3. uORB核心数据结构:uORB::Publication、uORB::Subscription、orb_id_t

好,咱们今天来啃uORB最核心的三个数据结构。说白了,你只要搞懂了PublicationSubscriptionorb_id_t,uORB通信机制在你眼里就没啥秘密了。

我刚开始接触PX4的时候,也被这些名字唬住了。什么发布者、订阅者、消息ID……感觉挺玄乎。后来真正上手写自定义消息,才发现其实没那么复杂。今天我就用最直白的方式,把这三个家伙彻底讲透。

3.1 orb_id_t:消息的“身份证”

orb_id_t是什么?它就是uORB里每条消息的唯一标识符。你可以把它理解成人的身份证号——每个消息类型都有一个独一无二的ID。

在源码里,orb_id_t其实是一个指向orb_metadata结构体的指针。这个结构体里存了消息的名字、大小、实例数量这些元信息。

// 这是orb_metadata结构体的简化版
struct orb_metadata {
    const char *o_name;    // 消息名字,比如"vehicle_attitude"
    const size_t o_size;   // 消息结构体的大小
    const size_t o_instances; // 最大实例数
};

嗯,这里要注意:orb_id_t本身不存数据,它只负责描述“这条消息长什么样”。数据是存在共享内存里的,orb_id_t就是那把钥匙,帮你找到对应的数据位置。

核心要点:

  • orb_id_t 是消息类型的标识符
  • 它指向 orb_metadata 结构体
  • 同一个消息类型的所有实例共享同一个 orb_id_t

我在项目中遇到过一个问题:两个模块用了同一个orb_id_t但结构体定义不一致,结果数据解析全乱了。所以记住——orb_id_t和消息结构体必须严格对应,这是铁律。

3.2 uORB::Publication:数据的“发布者”

Publication,顾名思义,就是负责把数据发出去的模块。你想想看,传感器数据采集完了,总得有人把它扔到uORB总线上吧?这个“扔”的动作,就是Publication干的。

在C++接口里,uORB::Publication是一个模板类。你只需要告诉它消息类型,它就能帮你完成注册、发布等一系列操作。

// 创建一个发布者,发布vehicle_attitude消息
uORB::Publication<vehicle_attitude_s> att_pub{ORB_ID(vehicle_attitude)};

// 填充数据
vehicle_attitude_s att{};
att.roll = 0.1f;
att.pitch = 0.2f;
att.yaw = 0.3f;

// 发布数据
att_pub.publish(att);

你看,代码就这么几行。但背后发生了什么?我简单说一下:

  1. 注册节点:Publication对象构造时,会在uORB内核里注册一个发布节点
  2. 分配共享内存:如果是第一次发布这个消息类型,uORB会分配一块共享内存
  3. 拷贝数据:调用publish()时,数据被拷贝到共享内存里
  4. 通知订阅者:uORB会通知所有订阅了这个消息的模块:“有新数据了!”

个人经验:我建议发布频率不要太快。曾经有个项目,我把IMU数据以1000Hz的频率发布,结果CPU负载直接飙到80%。后来降到400Hz,一切正常。发布频率要跟得上,但别浪费算力。

3.3 uORB::Subscription:数据的“订阅者”

有发布就有订阅。Subscription就是负责从uORB总线上拿数据的模块。你想想看,姿态估计模块需要IMU数据,它就得订阅IMU的消息。

uORB::Subscription也是个模板类,用法跟Publication对称:

// 创建一个订阅者,订阅vehicle_attitude消息
uORB::Subscription att_sub{ORB_ID(vehicle_attitude)};

// 获取最新数据
vehicle_attitude_s att{};
att_sub.copy(&att);

// 或者检查是否有新数据
if (att_sub.updated()) {
    att_sub.copy(&att);
    // 处理新数据...
}

这里有个细节:copy()是阻塞的还是非阻塞的?答案是——非阻塞的。如果当前没有新数据,它直接返回false,不会卡住你的线程。这一点在实时系统中非常重要。

避坑指南:我曾经犯过一个错误——在中断服务函数里直接调用copy()。结果呢?死锁了。因为uORB内部用了互斥锁,中断里不能加锁。记住:uORB的API不能在中断上下文调用。

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

光说理论不够直观,我画了一张图,帮你理清这三个核心数据结构的关系:

uORB核心数据结构关系图 orb_id_t 消息的“身份证” 指向 orb_metadata 引用 引用 uORB::Publication 数据的“发布者” publish() → 写入共享内存 通知所有订阅者 uORB::Subscription 数据的“订阅者” copy() → 读取共享内存 updated() → 检查新数据 共享内存(数据实际存储地) 写入 读取

这张图其实已经把整个流程说清楚了:

  • orb_id_t 是消息的“身份证”,Publication和Subscription都引用它
  • Publication 负责把数据写入共享内存
  • Subscription 负责从共享内存读取数据
  • 共享内存是数据实际存储的地方,所有模块都能访问

3.5 实战:如何选择发布者和订阅者模式

在实际项目中,你可能会遇到一个问题:什么时候用Publication,什么时候直接用orb_advertise

我个人习惯是:新代码一律用C++模板类。原因有三:

  1. 类型安全:模板类在编译期就检查类型,不会出现结构体不匹配的问题
  2. 代码简洁:不用手动管理句柄,RAII机制自动处理资源释放
  3. 性能更好:内联优化后,模板类的开销几乎为零

但如果你在维护老代码,或者需要跟C代码交互,那orb_advertiseorb_subscribe还是得用。毕竟PX4的底层API就是C写的。

总结一下:

数据结构 角色 核心方法 注意事项
orb_id_t 消息标识符 ORB_ID() 宏获取 必须与结构体严格对应
uORB::Publication 数据发布者 publish() 注意发布频率,别太高
uORB::Subscription 数据订阅者 copy(), updated() 不能在中断中调用

好了,这三个核心数据结构就讲到这里。你想想看,其实uORB的设计思想跟ROS的topic很像,都是发布-订阅模式。只不过uORB更轻量,更适合嵌入式实时系统。搞懂了这些,后面自定义消息就水到渠成了。