3、uORB通信机制:核心概念与实战技巧

uORB,全称是Micro Object Request Broker。说白了,它就是PX4里各个模块之间传递消息的“快递系统”。你想想看,飞控里有传感器模块、姿态估计模块、控制模块,它们之间怎么交换数据?总不能每个模块都自己写一套通信协议吧?uORB就是来解决这个问题的。

我个人习惯把uORB理解成一个“消息黑板报”。谁想发布消息,就往黑板上写;谁想读取消息,就来黑板上看。发布者和订阅者之间完全解耦,各干各的,互不干扰。

3.1 uORB核心概念

先搞清楚几个关键角色:

  • 消息(Message):就是数据结构体,定义了要传递的数据长什么样。比如传感器数据、控制指令等。
  • 主题(Topic):消息的“名字”。比如sensor_accelvehicle_attitude。同一个主题只能对应一种消息类型。
  • 发布者(Publisher):往某个主题上写数据的模块。
  • 订阅者(Subscriber):从某个主题上读数据的模块。

核心原则:一个主题可以有多个发布者和多个订阅者。发布者只管写,订阅者只管读,两者不需要知道对方的存在。

我在项目中遇到过一个问题:两个模块同时往同一个主题上发布数据,结果订阅者收到的数据时而来自A,时而来自B,导致控制逻辑混乱。后来才意识到,uORB虽然支持多发布者,但订阅者默认只读取最新的一份数据。如果你需要区分数据来源,就得用不同的主题。

3.2 消息发布与订阅

发布和订阅的API其实很简单。我直接上代码,你一看就明白。

3.2.1 发布消息

// 1. 先声明消息结构体
struct sensor_accel_s accel_data;

// 2. 填充数据
accel_data.timestamp = hrt_absolute_time();
accel_data.x = 0.1f;
accel_data.y = -0.2f;
accel_data.z = 9.8f;

// 3. 发布
orb_advert_t pub = orb_advertise(ORB_ID(sensor_accel), &accel_data);
// 或者用 orb_advertise_queue 创建队列
// orb_advertise_queue(ORB_ID(sensor_accel), &accel_data, 10);

// 后续更新数据
accel_data.x = 0.2f;
orb_publish(ORB_ID(sensor_accel), pub, &accel_data);

这里有个细节:orb_advertise只调用一次,后续更新用orb_publish。我曾经见过新手每次更新都调用orb_advertise,结果内存泄漏,飞控跑着跑着就挂了。

3.2.2 订阅消息

// 1. 订阅主题
int sub_fd = orb_subscribe(ORB_ID(sensor_accel));

// 2. 设置订阅参数(可选)
orb_set_interval(sub_fd, 10); // 10ms更新一次

// 3. 循环读取数据
struct sensor_accel_s accel_data;
while (true) {
    orb_copy(ORB_ID(sensor_accel), sub_fd, &accel_data);
    // 处理数据...
    px4_usleep(1000);
}

小技巧:如果你只需要最新数据,用orb_check检查是否有新数据,再决定是否orb_copy。这样可以避免不必要的拷贝开销。

3.3 自定义uORB消息

有时候系统自带的主题不够用,比如你要传递一个自定义的调试信息。这时候就需要自己定义消息了。

步骤很简单:

  1. msg/目录下新建一个.msg文件,比如my_debug.msg
  2. 在文件里定义数据结构
  3. 运行make,系统会自动生成对应的C/C++头文件

举个例子,my_debug.msg的内容:

# my_debug.msg
uint64 timestamp        # 时间戳,单位微秒
float32[3] position     # 位置信息
uint8 debug_level       # 调试等级
string[50] message      # 调试信息

嗯,这里要注意:string类型在uORB里是固定长度的,不能动态分配。我刚开始用的时候踩过这个坑,定义了一个可变长度的字符串,结果编译通过,运行时数据截断了。

生成的头文件会放在build/<target>/msg/目录下,比如my_debug.hpp。然后你就可以像使用系统消息一样,用ORB_ID(my_debug)来发布和订阅了。

避坑指南:我曾经在自定义消息里放了一个指针,想着指向一个动态数组。结果uORB拷贝数据时只拷贝了指针本身,没有拷贝指针指向的内容。订阅者拿到的指针指向的是野地址,一读就崩。记住:uORB消息里只能用基本类型和固定数组,不能用指针。

3.4 uORB工具使用

PX4提供了一些命令行工具,方便调试uORB通信。我个人觉得最常用的是uorb topuorb list

命令 功能 示例
uorb top 实时显示各主题的发布频率、订阅者数量 uorb top -n 5 显示前5个最活跃的主题
uorb list 列出所有已注册的主题 uorb list
uorb pub 手动发布一条消息(用于测试) uorb pub sensor_accel '{"x": 0.1, "y": 0.2, "z": 9.8}'
uorb sub 订阅一个主题并打印数据 uorb sub sensor_accel

我记得有一次调试姿态估计模块,发现输出数据抖动得很厉害。用uorb top一看,原来是传感器数据发布的频率忽高忽低,有时候1ms发一次,有时候10ms才发一次。后来发现是传感器驱动里有个定时器配置错了。如果没有这个工具,我可能得花半天时间加打印日志才能定位问题。

下面这张图展示了uORB的整体工作流程,我画了个简单的示意图:

uORB通信机制流程图 传感器驱动 (发布者) GPS驱动 (发布者) 遥控器输入 (发布者) uORB 消息黑板报 sensor_accel vehicle_gps_position rc_channels 姿态估计 (订阅者) 位置估计 (订阅者) 控制模块 (订阅者) 发布者 uORB核心 订阅者

从这张图可以看得很清楚:发布者只管往uORB里写数据,订阅者只管从uORB里读数据。它们之间没有直接耦合。这种设计的好处是,你想加一个新的传感器驱动,只要它往对应的主题上发布数据,所有订阅了这个主题的模块都能自动收到,不需要改任何代码。

最后说一句,uORB虽然简单,但用好它需要理解它的设计哲学。说白了就是“发布-订阅”模式在嵌入式系统里的一个优雅实现。你在写驱动或者应用模块时,多想想“我这个数据应该发布到哪个主题上?谁需要订阅它?”想清楚了,代码结构自然就清晰了。


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