从ROS2到自动驾驶:手把手教你用FastDDS搭建第一个机器人通信节点(附避坑指南)

在机器人开发领域,通信中间件的选择往往决定了整个系统的实时性和可靠性。当ROS2决定采用DDS作为其默认通信框架时,FastDDS凭借其开源特性和高性能表现,迅速成为开发者关注的焦点。本文将带你从零开始,在Ubuntu系统中搭建一个基于FastDDS的简单发布/订阅通信节点,并分享实际项目中积累的配置技巧和常见问题解决方案。

1. 环境准备与FastDDS安装

在开始编码之前,我们需要确保开发环境正确配置。FastDDS作为ROS2的默认中间件之一,其安装过程需要特别注意版本兼容性。

1.1 系统要求与依赖安装

推荐使用Ubuntu 20.04或22.04 LTS版本,这些版本被主流ROS2发行版官方支持。首先安装基础编译工具和依赖项:

sudo apt update
sudo apt install -y build-essential cmake git wget
sudo apt install -y libssl-dev libasio-dev libtinyxml2-dev

FastDDS需要Java环境支持其IDL编译器,建议安装OpenJDK 11:

sudo apt install -y openjdk-11-jdk

1.2 源码编译安装FastDDS

从GitHub获取最新稳定版源码(本文以2.10.1为例):

git clone --recursive https://github.com/eProsima/Fast-DDS.git
cd Fast-DDS
git checkout v2.10.1
mkdir build && cd build

配置编译选项时,建议启用性能优化和安全特性:

cmake -DCMAKE_BUILD_TYPE=Release \
      -DTHIRDPARTY=ON \
      -DSECURITY=ON ..
make -j$(nproc)
sudo make install

注意:编译过程可能耗时较长,-j参数可根据CPU核心数调整。安装完成后建议执行 ldconfig 更新动态链接库缓存。

1.3 验证安装

创建简单的测试程序验证安装是否成功:

// test_fastdds.cpp
#include <fastdds/dds/domain/DomainParticipant.hpp>

int main()
{
    eprosima::fastdds::dds::DomainParticipant* participant =
        eprosima::fastdds::dds::DomainParticipantFactory::get_instance()->create_participant(0);
    if(participant != nullptr) {
        std::cout << "FastDDS安装成功!" << std::endl;
        participant->delete_contained_entities();
    }
    return 0;
}

编译并运行测试程序:

g++ test_fastdds.cpp -o test_fastdds -lfastcdr -lfastrtps
./test_fastdds

2. 第一个FastDDS通信示例

理解了基础概念后,让我们实现一个完整的发布/订阅示例。这个例子将展示如何创建DomainParticipant、定义Topic、配置Publisher和Subscriber。

2.1 定义数据类型

首先需要定义通信中使用的数据结构。FastDDS使用IDL(接口定义语言)来描述数据类型:

// HelloWorld.idl
module hello {
    struct HelloWorld {
        unsigned long index;
        string message;
    };
};

使用FastDDS提供的IDL编译器生成C++代码:

fastddsgen HelloWorld.idl

这将生成HelloWorld.h和HelloWorld.cxx文件,包含序列化和反序列化所需的代码。

2.2 发布者实现

发布者代码需要完成以下步骤:

  1. 创建DomainParticipant
  2. 注册数据类型
  3. 创建Publisher
  4. 创建Topic
  5. 创建DataWriter
  6. 定期发布数据
// publisher.cpp
#include "HelloWorld.h"
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/publisher/Publisher.hpp>
#include <fastdds/dds/publisher/DataWriter.hpp>
#include <fastdds/dds/topic/Topic.hpp>

class HelloWorldPublisher {
public:
    HelloWorldPublisher() : participant_(nullptr), publisher_(nullptr), 
                          topic_(nullptr), writer_(nullptr), sample_(new HelloWorld()) {
        sample_->index(0);
        sample_->message("Hello from FastDDS!");
    }
    
    bool init() {
        // 创建DomainParticipant
        participant_ = eprosima::fastdds::dds::DomainParticipantFactory::get_instance()
            ->create_participant(0);
        if (!participant_) return false;
        
        // 注册数据类型
        eprosima::fastdds::dds::TypeSupport type(new HelloWorldPubSubType());
        type.register_type(participant_);
        
        // 创建Publisher
        publisher_ = participant_->create_publisher(
            eprosima::fastdds::dds::PUBLISHER_QOS_DEFAULT);
        if (!publisher_) return false;
        
        // 创建Topic
        topic_ = participant_->create_topic(
            "HelloWorldTopic", type.get_type_name(), 
            eprosima::fastdds::dds::TOPIC_QOS_DEFAULT);
        if (!topic_) return false;
        
        // 创建DataWriter
        writer_ = publisher_->create_datawriter(
            topic_, eprosima::fastdds::dds::DATAWRITER_QOS_DEFAULT);
        return writer_ != nullptr;
    }
    
    void run(uint32_t samples) {
        for (uint32_t i = 0; i < samples; ++i) {
            sample_->index(i);
            writer_->write(sample_.get());
            std::cout << "发送: " << sample_->message() << " " << sample_->index() << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        }
    }
    
private:
    eprosima::fastdds::dds::DomainParticipant* participant_;
    eprosima::fastdds::dds::Publisher* publisher_;
    eprosima::fastdds::dds::Topic* topic_;
    eprosima::fastdds::dds::DataWriter* writer_;
    std::unique_ptr<HelloWorld> sample_;
};

2.3 订阅者实现

订阅者需要实现类似的初始化步骤,并添加数据到达时的回调处理:

// subscriber.cpp
#include "HelloWorld.h"
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/subscriber/Subscriber.hpp>
#include <fastdds/dds/subscriber/DataReader.hpp>
#include <fastdds/dds/topic/Topic.hpp>

class HelloWorldSubscriber {
public:
    HelloWorldSubscriber() : participant_(nullptr), subscriber_(nullptr),
                           topic_(nullptr), reader_(nullptr) {}
    
    ~HelloWorldSubscriber() {
        if (reader_ != nullptr) subscriber_->delete_datareader(reader_);
        if (topic_ != nullptr) participant_->delete_topic(topic_);
        if (subscriber_ != nullptr) participant_->delete_subscriber(subscriber_);
        if (participant_ != nullptr) 
            eprosima::fastdds::dds::DomainParticipantFactory::get_instance()
                ->delete_participant(participant_);
    }
    
    bool init() {
        // 创建DomainParticipant
        participant_ = eprosima::fastdds::dds::DomainParticipantFactory::get_instance()
            ->create_participant(0);
        if (!participant_) return false;
        
        // 注册数据类型
        eprosima::fastdds::dds::TypeSupport type(new HelloWorldPubSubType());
        type.register_type(participant_);
        
        // 创建Subscriber
        subscriber_ = participant_->create_subscriber(
            eprosima::fastdds::dds::SUBSCRIBER_QOS_DEFAULT);
        if (!subscriber_) return false;
        
        // 创建Topic
        topic_ = participant_->create_topic(
            "HelloWorldTopic", type.get_type_name(),
            eprosima::fastdds::dds::TOPIC_QOS_DEFAULT);
        if (!topic_) return false;
        
        // 创建DataReader
        eprosima::fastdds::dds::DataReaderQos reader_qos = 
            eprosima::fastdds::dds::DATAREADER_QOS_DEFAULT;
        reader_ = subscriber_->create_datareader(
            topic_, reader_qos, this);
        return reader_ != nullptr;
    }
    
    void on_data_available(
        eprosima::fastdds::dds::DataReader* reader) {
        HelloWorld msg;
        eprosima::fastdds::dds::SampleInfo info;
        if (reader->take_next_sample(&msg, &info) == ReturnCode_t::RETCODE_OK) {
            if (info.valid_data) {
                std::cout << "收到: " << msg.message() << " " << msg.index() << std::endl;
            }
        }
    }
    
    void run() {
        std::cout << "订阅者运行中,等待数据..." << std::endl;
        while (true) {
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        }
    }
    
private:
    eprosima::fastdds::dds::DomainParticipant* participant_;
    eprosima::fastdds::dds::Subscriber* subscriber_;
    eprosima::fastdds::dds::Topic* topic_;
    eprosima::fastdds::dds::DataReader* reader_;
};

3. 与ROS2集成实战

FastDDS作为ROS2的默认中间件之一,了解如何直接使用FastDDS可以帮助我们更好地理解和优化ROS2系统。

3.1 配置ROS2使用特定RMW实现

ROS2支持多种DDS实现,可以通过环境变量指定使用的中间件:

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp

验证当前使用的RMW实现:

ros2 doctor --report | grep "RMW implementation"

3.2 自定义FastDDS XML配置

FastDDS支持通过XML文件进行详细配置,这对于性能调优特别有用。创建一个名为 fastdds_config.xml 的文件:

<?xml version="1.0" encoding="UTF-8" ?>
<dds>
    <profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
        <transport_descriptors>
            <transport_descriptor>
                <transport_id>udp_transport</transport_id>
                <type>UDPv4</type>
                <sendBufferSize>65536</sendBufferSize>
                <receiveBufferSize>65536</receiveBufferSize>
            </transport_descriptor>
        </transport_descriptors>
        
        <participant profile_name="custom_participant">
            <rtps>
                <userTransports>
                    <transport_id>udp_transport</transport_id>
                </userTransports>
                <useBuiltinTransports>false</useBuiltinTransports>
                <builtin>
                    <discovery_config>
                        <leaseDuration>
                            <sec>3</sec>
                        </leaseDuration>
                        <leaseAnnouncement>
                            <sec>1</sec>
                        </leaseAnnouncement>
                    </discovery_config>
                </builtin>
            </rtps>
        </participant>
    </profiles>
</dds>

通过环境变量指定配置文件:

export FASTRTPS_DEFAULT_PROFILES_FILE=fastdds_config.xml

3.3 性能优化技巧

  1. 共享内存传输 :对于同一主机上的通信,使用共享内存可以大幅提升性能
  2. 零拷贝优化 :利用FastDDS的零拷贝特性减少内存复制
  3. QoS策略调整 :根据应用场景调整可靠性、持久性等QoS参数

共享内存配置示例:

<transport_descriptor>
    <transport_id>shm_transport</transport_id>
    <type>SHM</type>
    <segment_size>16777216</segment_size>
</transport_descriptor>

4. 常见问题与解决方案

在实际项目中使用FastDDS时,开发者常会遇到一些典型问题。以下是经过验证的解决方案。

4.1 编译与链接问题

问题1:找不到FastDDS库

error while loading shared libraries: libfastrtps.so.2: cannot open shared object file

解决方案:

sudo ldconfig

如果问题依旧,检查库文件是否在标准路径中:

find /usr -name "libfastrtps*"

问题2:类型注册冲突

Type already registered

解决方案:确保类型名称唯一,或在程序退出时正确注销类型。

4.2 运行时问题

问题1:发布者与订阅者无法通信

检查步骤:

  1. 确认双方使用相同的Domain ID
  2. 验证网络连接(特别是多机通信时)
  3. 检查防火墙设置
  4. 确认双方使用相同的Topic名称和数据类型

问题2:高延迟或数据丢失

优化建议:

  1. 调整发送缓冲区大小
  2. 使用更高效的传输协议(如共享内存)
  3. 优化QoS策略,平衡可靠性与性能

4.3 ROS2集成问题

问题1:ROS2节点无法发现彼此

解决方案:

  1. 确认所有节点使用相同的DDS实现
  2. 检查ROS_DOMAIN_ID设置是否一致
  3. 验证网络组播是否正常工作
# 测试组播通信
ping 239.255.0.1

问题2:ROS2与原生FastDDS节点互通

实现要点:

  1. 确保使用相同的数据类型定义
  2. 配置兼容的QoS策略
  3. 使用相同的Domain ID

数据类型转换示例(ROS2 msg ↔ FastDDS):

// 将ROS2消息转换为FastDDS类型
void convert(const std_msgs::msg::String& ros_msg, HelloWorld& dds_msg) {
    dds_msg.message(ros_msg.data);
}

// 将FastDDS类型转换为ROS2消息
void convert(const HelloWorld& dds_msg, std_msgs::msg::String& ros_msg) {
    ros_msg.data = dds_msg.message();
}
Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐