1. 为什么需要自定义uORB话题?从传感器集成说起

如果你玩过PX4飞控,肯定知道它内部跑着一堆独立的模块:姿态解算、位置估计、传感器驱动、控制器……这些模块就像一个个小车间,各自埋头干活。但它们之间总得“通个气”吧?比如,超声波传感器测出了高度,这个数据得赶紧告诉姿态控制器和位置估计器,不然飞机怎么知道自己在哪儿、该往哪儿飞?

这就是uORB(Micro Object Request Broker)大显身手的地方。你可以把它想象成飞控系统内部的“信息广播站”或者“数据总线”。所有模块都通过它来发布自己的数据(比如“我这儿有最新的高度信息”),或者订阅自己需要的数据(比如“我需要最新的高度信息”)。这种设计让整个系统变得非常模块化,增删功能就像插拔积木一样方便。

那么,当我们自己给无人机加装一个新传感器,比如一个额外的激光雷达用于精准避障,或者一个自定义的空气质量传感器,该怎么办?系统里可没有现成的“激光雷达数据”或者“PM2.5浓度”这个话题。这时候,我们就得自己动手,从零构建一个自定义的uORB话题。这个过程,本质上就是教会PX4系统认识并传递一种新的数据“方言”。

我刚开始做这个的时候,也觉得有点头大,文档看起来一堆术语。但实际摸一遍就会发现,流程非常清晰,就像给乐高套装新造一种形状的积木块。一旦你成功一次,以后再集成任何传感器都会变得轻而易举。这篇文章,我就带你完整走一遍这个流程,从定义数据格式、创建话题,到写一个独立的应用程序来发布和订阅数据,最后把它集成到固件里。我会把每一步的细节和容易踩的坑都讲清楚,保证你跟着做就能成功。

2. 第一步:定义你的数据“合同”——创建.msg消息文件

在uORB的世界里,所有流通的数据都有严格定义的格式。这个格式就写在.msg文件里。你可以把它理解为数据交换的“合同”或“模板”,规定了话题里包含哪些数据、每个数据是什么类型。发布方必须按这个格式填充数据,订阅方也按这个格式来解读数据,这样大家才能对上暗号。

PX4系统自带的话题定义文件都放在Firmware/msg/目录下。比如vehicle_attitude.msg定义了姿态话题,sensor_combined.msg定义了原始传感器数据。我们要做的第一件事,就是在这里为自己新传感器创建一个.msg文件。

假设我们新增了一个简单的超声波传感器,它主要提供距离数据。我们给它起个名字叫custom_distance_sensor.msg

  1. 进入msg目录并创建文件

    cd /path/to/PX4-Autopilot/msg
    touch custom_distance_sensor.msg
    
  2. 编辑.msg文件内容: 用你喜欢的文本编辑器打开这个文件,定义你的数据结构。一个典型的传感器消息通常包含时间戳和测量值。例如:

    uint64 timestamp        # 数据产生时的系统时间(微秒)
    uint32 device_id        # 传感器设备ID,用于区分多个同类传感器
    float32 current_distance    # 当前测得的距离,单位:米
    float32 signal_quality     # 信号质量,范围0.0-1.0
    uint8   sensor_type        # 传感器类型,例如:0=未知,1=超声波,2=激光雷达
    

    我来解释一下这些字段

    • timestamp: 这是PX4 uORB消息的“标配”,几乎每个消息都有。它记录了数据产生的时刻(从系统启动开始计算的微秒数),对于数据同步和融合至关重要。这个字段强烈建议保留
    • device_id: 如果你的无人机上挂了两个同款超声波,可以用这个ID来区分它们的数据。
    • current_distance: 核心数据,就是我们最关心的测量距离。
    • signal_quality: 一个实用性很强的字段。传感器数据并非总是完美的,比如超声波可能因表面材质而失效。提供一个质量指标,下游模块(如滤波器)可以据此决定是否信任这个数据。
    • sensor_type: 枚举类型,方便以后扩展。也许你后来换成了激光雷达,但话题接口可以保持不变,只需改这个类型字段。

    定义时的注意事项

    • 基本类型:PX4的.msg支持uint8/16/32/64, int8/16/32/64, float32, float64, bool, char等。
    • 数组:可以定义数组,如float32[3] covariance表示一个3x3的协方差矩阵。
    • 注释:使用#号添加注释,说明字段含义和单位,这是个好习惯。
    • 命名风格:字段名通常使用snake_case(小写加下划线)。
  3. 注册你的消息文件: 创建好.msg文件后,必须告诉编译系统它的存在。打开msg/CMakeLists.txt文件,找到set(msg_files ...)这一大段。在这堆文件列表里,添加上你新创建的文件名:

    set(msg_files
        ...
        actuator_armed.msg
        actuator_controls.msg
        # ... 其他已有文件 ...
        custom_distance_sensor.msg    # 这是我们新添加的
        battery_status.msg
        ...
    )
    

    这一步千万别漏! 漏了的话,编译系统就不会为你的消息生成对应的C/C++头文件,后续代码里就没法用了。

好了,“数据合同”已经拟好。接下来,系统(在编译时)会根据这份合同,自动生成一份“标准格式说明书”——也就是C语言的结构体头文件,供我们编程时使用。

3. 第二步:理解自动生成的代码与uORB核心API

当你完成上一步并开始编译PX4固件(哪怕只是编译一部分)后,魔法就发生了。编译系统(具体是px_generate_uorb_topic_files.py这个脚本)会读取所有的.msg文件,并在构建目录中自动生成对应的C/C++头文件。

对于我们的custom_distance_sensor.msg,生成的头文件路径通常类似于: build/px4_fmu-v5_default/uORB/topics/custom_distance_sensor.h (注意:px4_fmu-v5_default会根据你编译的目标板不同而变化)。

让我们打开这个生成的文件(或者提前了解它会有什么),看看里面有什么宝贝:

// 这是自动生成的文件,请勿手动编辑!
#pragma once
#include <uORB/uORB.h>

#ifdef __cplusplus
extern "C" {
#endif

// 我们的核心数据结构体
struct custom_distance_sensor_s {
    uint64_t timestamp; // 系统时间戳
    uint32_t device_id; // 设备ID
    float current_distance; // 当前距离
    float signal_quality; // 信号质量
    uint8_t sensor_type; // 传感器类型
    uint8_t _padding0[7]; // 自动填充,用于内存对齐
};

// 声明此话题的元数据(metadata)
ORB_DECLARE(custom_distance_sensor);

#ifdef __cplusplus
}
#endif

关键点解析

  1. 结构体:生成了一个名为custom_distance_sensor_s的结构体(后缀_s是PX4的命名惯例)。里面的字段和我们在.msg文件中定义的顺序完全一致,只是C语言化了。
  2. 内存对齐填充(_padding0:你可能会看到一个奇怪的_padding0数组。这是编译器为了优化内存访问效率(确保数据地址是某些字节的整数倍,比如8字节对齐)而自动添加的填充字节。我们写代码时完全不用管它,就当它不存在。这是uORB机制为了保证跨平台、跨进程数据拷贝正确性而做的底层工作。
  3. 元数据声明ORB_DECLARE(custom_distance_sensor)这行宏非常重要。它为custom_distance_sensor这个话题创建了一个全局唯一的标识符(orb_metadata),uORB的发布和订阅函数都需要通过这个标识符来找到正确的话题。

有了这个自动生成的结构体,我们就可以在C/C++代码中方便地操作数据了。接下来,我们看看操作uORB话题的几个最核心的API函数,我把它们分成“发布者套餐”和“订阅者套餐”。

发布者套餐(你先说,我听着)

  • orb_advertise(): 公告。相当于你在广播站注册一个频道,告诉大家“我要开始广播custom_distance_sensor这个话题了!”这是发布数据前的必须步骤
  • orb_publish(): 发布。注册成功后,每次你有新数据了,就调用这个函数把数据“喊”出去。

订阅者套餐(我想听,你说吧)

  • orb_subscribe(): 订阅。告诉系统“我想收听custom_distance_sensor这个频道”。即使这个频道还没人注册(没人公告),你也可以先订阅,只是暂时收不到数据。
  • orb_copy(): 拷贝数据。当你感觉到频道有更新了(通过pollorb_check),就用这个函数把最新的数据复制到自己的变量里。
  • orb_check(): 检查更新。快速检查一下自从我上次拷贝数据后,这个话题有没有新的内容发布。非阻塞,立即返回。
  • poll(): 等待更新。这是一个更通用的系统调用,可以同时等待多个话题(文件描述符)。设置一个超时时间,在数据到来前,它可以挂起当前任务,节省CPU资源。

理解了这些“工具”,我们就可以开始动手写代码,让一个独立的应用程序来扮演传感器数据发布者的角色了。

4. 第三步:实战!编写独立应用程序发布传感器数据

现在,我们假设你已经有了一个超声波传感器的驱动,它能通过I2C或UART读取到距离值。我们的目标是创建一个独立的PX4应用程序模块,定期读取这个传感器,并通过我们自定义的uORB话题把数据发布出去。

PX4的应用程序通常放在src/examples/src/modules/目录下。我们以src/examples/为例,创建一个新的应用目录。

  1. 创建应用程序目录和文件

    cd /path/to/PX4-Autopilot/src/examples/
    mkdir custom_sensor_publisher
    cd custom_sensor_publisher
    touch custom_sensor_publisher.c
    touch CMakeLists.txt
    
  2. 编写C源代码(custom_sensor_publisher.c): 这是最核心的部分,我会逐段解释。

    /**
     * @file custom_sensor_publisher.c
     * 一个简单的示例,演示如何读取自定义传感器并发布到uORB话题。
     */
    #include <px4_platform_common/px4_config.h>
    #include <px4_platform_common/module.h>
    #include <px4_platform_common/tasks.h>
    #include <px4_platform_common/posix.h>
    #include <px4_platform_common/log.h>
    #include <uORB/uORB.h>
    #include <uORB/topics/custom_distance_sensor.h> // 包含自动生成的头文件
    #include <drivers/drv_hrt.h> // 高分辨率定时器,用于获取时间戳
    #include <stdio.h>
    #include <string.h>
    
    // 模块的入口函数声明。__EXPORT宏确保这个函数能被系统命令表找到。
    __EXPORT int custom_sensor_publisher_main(int argc, char *argv[]);
    
    // 假设的传感器读取函数(你需要根据实际硬件驱动实现)
    static bool read_ultrasonic_sensor(uint32_t *device_id, float *distance, float *quality);
    
    int custom_sensor_publisher_main(int argc, char *argv[])
    {
        PX4_INFO("自定义传感器发布器启动!");
    
        // 1. 准备我们的数据容器
        struct custom_distance_sensor_s distance_data;
        memset(&distance_data, 0, sizeof(distance_data)); // 初始化为零是个好习惯
    
        // 2. 公告我们要发布的话题
        // 注意:orb_advertise的第二个参数需要传入一个初始数据结构的指针
        orb_advert_t distance_pub = orb_advertise(ORB_ID(custom_distance_sensor), &distance_data);
        if (distance_pub == nullptr) {
            PX4_ERR("公告 custom_distance_sensor 话题失败!");
            return -1;
        }
        PX4_INFO("成功公告 custom_distance_sensor 话题。");
    
        // 3. 主循环:定期读取并发布数据
        while (!px4_thread_should_exit()) { // 检查是否有退出命令(如`stop`)
            // 模拟或实际读取传感器数据
            uint32_t dev_id;
            float dist, qual;
            bool read_success = read_ultrasonic_sensor(&dev_id, &dist, &qual);
    
            if (read_success) {
                // 填充数据结构
                distance_data.timestamp = hrt_absolute_time(); // 获取当前系统时间戳(微秒)
                distance_data.device_id = dev_id;
                distance_data.current_distance = dist;
                distance_data.signal_quality = qual;
                distance_data.sensor_type = 1; // 假设1代表超声波
    
                // 4. 发布数据到话题
                int ret = orb_publish(ORB_ID(custom_distance_sensor), distance_pub, &distance_data);
                if (ret != PX4_OK) {
                    PX4_WARN("发布数据失败,错误码:%d", ret);
                } else {
                    // PX4_INFO("发布距离数据:%.2f米,质量:%.2f", (double)dist, (double)qual);
                    // 实际应用中,INFO日志太频繁,建议用DEBUG或减少打印频率
                }
            } else {
                PX4_WARN("读取传感器失败!");
            }
    
            // 5. 控制发布频率,例如50Hz (20ms周期)
            px4_usleep(20000); // 休眠20毫秒
        }
    
        PX4_INFO("自定义传感器发布器退出。");
        return 0;
    }
    
    // 传感器读取函数的简单模拟实现
    static bool read_ultrasonic_sensor(uint32_t *device_id, float *distance, float *quality)
    {
        // 这里是硬件操作部分,需要根据你的具体传感器来写
        // 例如,通过I2C读取特定寄存器。
        // 此处我们模拟一个稳定的读数。
        static float simulated_distance = 1.5f;
        simulated_distance += 0.01f;
        if (simulated_distance > 5.0f) {
            simulated_distance = 0.5f;
        }
    
        *device_id = 0x55; // 假设的设备ID
        *distance = simulated_distance;
        *quality = 0.95f; // 模拟高质量信号
    
        return true; // 读取成功
    }
    

    代码要点与避坑指南

    • __EXPORT:这确保了custom_sensor_publisher_main这个函数能被PX4的模块系统发现和调用。没有它,你的应用就无法通过命令行启动。
    • orb_advertise:必须在发布数据前调用,且只需调用一次。它的第二个参数需要传入一个已初始化的数据结构指针。这里我们传入了&distance_data
    • 时间戳hrt_absolute_time()是获取PX4高精度时钟的标准方法,单位是微秒。务必为每条数据赋予准确的时间戳,这是多传感器数据融合的基础。
    • 发布频率:通过px4_usleep()控制循环速度。频率选择要合理,既要满足下游模块需求,又不能过度消耗CPU。对于超声波,20-50Hz通常足够。
    • 错误处理:对orb_advertiseorb_publish的返回值进行检查是个好习惯。
    • 日志输出:在调试阶段可以使用PX4_INFO打印信息,但在最终产品中,高频发布的循环内应避免使用,以免拖慢系统。可以用PX4_DEBUG或条件编译来控制。
  3. 编写CMakeLists.txt: 这个文件告诉编译系统如何构建你的模块。

    px4_add_module(
        MODULE examples__custom_sensor_publisher
        MAIN custom_sensor_publisher
        SRCS
            custom_sensor_publisher.c
        DEPENDS
            platforms__common
        )
    
    • MODULE:定义模块的路径和名称,格式通常是目录__子目录__模块名
    • MAIN:指定包含入口函数*_main的源文件(不含.c后缀)。
    • SRCS:列出所有需要编译的源文件。
    • DEPENDS:声明模块依赖,这里依赖基础平台。

5. 第四步:让系统认识你的应用——编译与集成

代码写好了,但PX4在编译时还不知道它的存在。我们需要把它“注册”到目标板的构建列表中。

  1. 找到目标板的配置文件: 配置文件通常位于boards/目录下,根据你的飞控硬件选择。例如,常用的Pixhawk 4对应boards/px4/fmu-v5/default.px4board。如果你用的是其他飞控(如FMU-v2, FMU-v3),请找到对应的文件。

  2. 启用你的模块: 用文本编辑器打开这个.px4board文件,寻找与examples相关的配置行。通常会在文件靠后的位置,有一系列CONFIG_EXAMPLES_*的设置。在其中添加一行:

    CONFIG_EXAMPLES_CUSTOM_SENSOR_PUBLISHER=y
    

    注意:这里的宏名称CUSTOM_SENSOR_PUBLISHER必须与你CMakeLists.txtMODULE定义的最后一部分(在examples__之后)完全一致,并且全部大写。系统就是通过这个宏来决定是否编译该模块。

  3. 编译固件: 在PX4-Autopilot根目录下,执行针对你硬件的编译命令。例如,为Pixhawk 4编译:

    make px4_fmu-v5_default
    

    如果一切顺利,编译输出中应该能看到你的模块正在被编译和链接。如果出现“未定义的引用”等错误,请检查:

    • .msg文件是否已正确添加到msg/CMakeLists.txt
    • 源代码中#include <uORB/topics/custom_distance_sensor.h>路径是否正确。
    • .px4board文件中的配置宏名称是否正确。
  4. 烧录固件: 编译成功后,将飞控通过USB连接到电脑,执行上传命令:

    make px4_fmu-v5_default upload
    

    等待烧录完成,飞控会自动重启。

6. 第五步:验证与调试——订阅话题并查看数据

固件烧录进去了,怎么知道我们的发布器是否在正常工作,数据是否正确呢?最好的方法就是写一个简单的订阅程序来监听,或者使用现有的地面站工具。

方法一:使用QGroundControl (QGC) 的MAVLink控制台 这是最快捷的验证方式。

  1. 连接飞控和QGC。
  2. 在QGC顶部工具栏进入“工具” -> “MAVLink控制台”。
  3. 在控制台中输入list_tasksps命令,查找custom_sensor_publisher是否在运行列表中。如果没有,需要手动启动它。
  4. 启动我们的模块:在控制台输入 custom_sensor_publisher start
  5. 使用uorb top命令。这是一个强大的内置工具,可以实时显示所有活跃的uORB话题及其更新频率、数据大小。你应该能在列表中看到custom_distance_sensor,并观察其更新频率是否接近你设置的20ms(50Hz)。
  6. 使用listener命令监听具体话题。输入 listener custom_distance_sensor。如果一切正常,你将看到这个话题的数据流在屏幕上滚动刷新,包括时间戳、距离值、信号质量等。

方法二:编写一个配套的订阅者测试程序(进阶) 为了更深入地理解,我们可以仿照发布器,写一个简单的订阅者程序。这个程序可以放在另一个模块里,或者为了测试,甚至可以和发布器放在同一个应用里(先发布,再自己订阅回来检查)。

下面是一个极简的订阅者示例片段,展示了如何订阅并打印我们自定义的话题:

// ... 包含必要的头文件,同上 ...
#include <poll.h>

int test_subscriber_main(int argc, char *argv[])
{
    PX4_INFO("启动自定义话题订阅测试...");

    // 1. 订阅话题
    int distance_sub_fd = orb_subscribe(ORB_ID(custom_distance_sensor));
    if (distance_sub_fd < 0) {
        PX4_ERR("订阅 custom_distance_sensor 失败!");
        return -1;
    }

    // 2. (可选)设置更新频率限制,避免处理过快
    orb_set_interval(distance_sub_fd, 100); // 最少100ms更新一次

    // 3. 准备poll结构,用于等待数据
    struct pollfd fds[1];
    fds[0].fd = distance_sub_fd;
    fds[0].events = POLLIN; // 我们关心数据可读事件

    struct custom_distance_sensor_s distance_data;

    for (int i = 0; i < 10; i++) { // 只读10次作为演示
        // 4. 等待数据到来,超时设为1000毫秒
        int poll_ret = poll(fds, 1, 1000);

        if (poll_ret == 0) {
            PX4_WARN("等待数据超时!");
            continue;
        } else if (poll_ret < 0) {
            PX4_ERR("poll调用出错!");
            break;
        }

        // 5. 检查是否是我们订阅的话题有数据可读
        if (fds[0].revents & POLLIN) {
            // 6. 拷贝数据到本地变量
            if (orb_copy(ORB_ID(custom_distance_sensor), distance_sub_fd, &distance_data) == PX4_OK) {
                PX4_INFO("[%llu] 设备ID:%u, 距离:%.3f米, 质量:%.2f",
                         (unsigned long long)distance_data.timestamp,
                         (unsigned)distance_data.device_id,
                         (double)distance_data.current_distance,
                         (double)distance_data.signal_quality);
            }
        }
    }

    // 7. 清理:取消订阅
    orb_unsubscribe(distance_sub_fd);
    PX4_INFO("订阅测试结束。");
    return 0;
}

将这个订阅者同样编译集成到固件中,然后通过QGC控制台启动它,就能看到它打印出发布器发送的数据了。通过对比时间戳和数据内容,可以完美验证整个uORB通信链路是否畅通、数据是否正确。

7. 深入理解:uORB如何实现高效的进程间通信(IPC)

走通了整个流程,你可能觉得uORB用起来挺简单。但它在底层是如何做到让不同进程(模块)安全、高效地共享数据的呢?理解这一点,能帮助你在更复杂的场景下(比如高频率数据、多发布者)做出正确设计。

uORB的核心机制

  1. 基于设备文件的共享内存:在PX4运行的NuttX或Linux等RTOS上,uORB在底层创建了一个虚拟字符设备(比如/obj/sensor_combined)。当进程通过orb_advertiseorb_subscribe操作某个话题时,实际上是在打开这个对应的设备文件。
  2. 数据队列与锁:对于每个话题,uORB在内存中维护了一个环形缓冲区(通常只有最新的一到几条消息)。发布者写入新数据时,会更新这个缓冲区。订阅者读取时,是读取缓冲区中最新的有效数据。这个过程通过信号量互斥锁进行保护,确保在读写关键数据时不会被其他进程打断,避免了数据撕裂(data tearing)。
  3. 通知机制:当发布者写入新数据后,uORB会通知所有订阅了该话题的进程。这是通过poll/select机制或更底层的信号来实现的。订阅进程不必忙等待(busy-waiting),可以在数据未就绪时休眠,让出CPU,等数据到来时再被唤醒,这极大地提高了系统效率。
  4. 零拷贝或最小拷贝:在设计上,uORB致力于减少不必要的数据拷贝。理想情况下,数据从传感器驱动读出后,直接放入uORB的共享缓冲区,消费者模块直接从该缓冲区读取,避免了在进程地址空间之间来回拷贝大块数据(如图像、点云)的开销。

给你的实践建议

  • 数据频率与大小:对于高频小数据(如IMU,几百Hz),uORB非常高效。对于大数据(如相机图像),要谨慎设计,可以考虑使用共享内存结合uORB发送“数据就绪”通知的方式。
  • 多发布者:uORB支持同一个话题有多个发布者(例如两个GPS模块)。订阅者可以通过orb_priority()或检查数据中的device_id来选择或融合数据。
  • 实时性orb_publishorb_copy都是非阻塞调用,执行时间可预测,适合实时系统。但poll的等待时间会影响响应延迟。
  • 调试利器:除了uorb toplisteneruorb status命令可以查看uORB守护进程的整体状态,uorb bench可以进行简单的性能测试。

当你成功运行起自定义的发布器和订阅器,并在listener中看到数据流畅滚动时,那种成就感是非常棒的。你不仅仅是为PX4添加了一个传感器,更是深入理解了其模块化通信的基石。这套方法几乎是通用的:无论是超声波、激光雷达、红外传感器,还是其他任何能产生数据的设备,你都可以通过“定义消息 -> 编写发布器 -> 集成编译”这个流程,将其数据接入PX4庞大的生态系统,供导航、控制、日志等任何需要的模块使用。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐