跳转至

ROS2 系统集成

概述

ROS2(Robot Operating System 2)是机器人软件集成的事实标准中间件。它不是操作系统,而是提供通信、工具链和生态系统的框架,让各子系统的软件能够协同工作。


节点(Node)架构

每个子系统一个节点

ROS2 的核心思想:将机器人系统分解为独立的节点(Node),每个节点负责一个子系统。

典型机器人的节点图

graph TB
    subgraph 感知
        CAM[camera_node<br/>图像采集]
        LIDAR[lidar_node<br/>点云采集]
        IMU_N[imu_node<br/>姿态数据]
        DET[detection_node<br/>目标检测]
    end

    subgraph 控制
        CTRL[controller_node<br/>运动控制]
        PLAN[planner_node<br/>路径规划]
        LOC[localization_node<br/>定位]
    end

    subgraph 执行
        MOT[motor_driver_node<br/>电机驱动]
        GRIP[gripper_node<br/>夹爪控制]
    end

    subgraph 系统
        MON[monitor_node<br/>系统监控]
        LOG[logger_node<br/>数据记录]
    end

    CAM --> DET
    LIDAR --> LOC
    IMU_N --> LOC
    DET --> PLAN
    LOC --> PLAN
    PLAN --> CTRL
    CTRL --> MOT
    CTRL --> GRIP
    MOT --> MON

节点间通信模式

模式 用途 特点
Topic(话题) 持续数据流 发布-订阅,异步
Service(服务) 请求-响应 同步调用
Action(动作) 长时间任务 带反馈和可取消
Parameter(参数) 配置 运行时可修改

话题示例

# camera_node: 发布图像
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2

class CameraNode(Node):
    def __init__(self):
        super().__init__('camera_node')
        self.publisher = self.create_publisher(Image, '/camera/image_raw', 10)
        self.timer = self.create_timer(0.033, self.timer_callback)  # 30Hz
        self.cap = cv2.VideoCapture(0)
        self.bridge = CvBridge()

    def timer_callback(self):
        ret, frame = self.cap.read()
        if ret:
            msg = self.bridge.cv2_to_imgmsg(frame, 'bgr8')
            msg.header.stamp = self.get_clock().now().to_msg()
            msg.header.frame_id = 'camera_link'
            self.publisher.publish(msg)

Launch 文件

Launch 文件用于启动多个节点、设置参数、配置重映射。

Python Launch 文件

# launch/robot_bringup.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
import os

def generate_launch_description():
    pkg_dir = get_package_share_directory('my_robot')

    return LaunchDescription([
        # 相机节点
        Node(
            package='usb_cam',
            executable='usb_cam_node_exe',
            name='camera_node',
            parameters=[{
                'video_device': '/dev/video0',
                'image_width': 640,
                'image_height': 480,
                'framerate': 30.0,
            }],
            remappings=[
                ('/image_raw', '/camera/image_raw'),
            ],
        ),

        # LiDAR 节点
        Node(
            package='rplidar_ros',
            executable='rplidar_node',
            name='lidar_node',
            parameters=[{
                'serial_port': '/dev/ttyUSB0',
                'frame_id': 'lidar_link',
            }],
        ),

        # IMU 节点
        Node(
            package='imu_driver',
            executable='imu_node',
            name='imu_node',
            parameters=[os.path.join(pkg_dir, 'config', 'imu.yaml')],
        ),

        # 电机驱动节点
        Node(
            package='motor_driver',
            executable='motor_node',
            name='motor_driver_node',
            parameters=[os.path.join(pkg_dir, 'config', 'motors.yaml')],
        ),

        # 控制器节点
        Node(
            package='my_robot_control',
            executable='controller_node',
            name='controller_node',
            parameters=[os.path.join(pkg_dir, 'config', 'controller.yaml')],
        ),
    ])

XML Launch 文件

<!-- launch/robot_bringup.launch.xml -->
<launch>
    <node pkg="usb_cam" exec="usb_cam_node_exe" name="camera_node">
        <param name="video_device" value="/dev/video0"/>
        <param name="framerate" value="30.0"/>
    </node>

    <node pkg="rplidar_ros" exec="rplidar_node" name="lidar_node">
        <param name="serial_port" value="/dev/ttyUSB0"/>
    </node>

    <include file="$(find-pkg-share my_robot)/launch/sensors.launch.py"/>
</launch>

参数管理

YAML 参数文件

# config/controller.yaml
controller_node:
  ros__parameters:
    # 控制频率
    control_rate: 200.0  # Hz

    # PID 参数
    pid:
      kp: [10.0, 10.0, 10.0, 5.0, 5.0, 5.0]
      ki: [0.1, 0.1, 0.1, 0.05, 0.05, 0.05]
      kd: [1.0, 1.0, 1.0, 0.5, 0.5, 0.5]

    # 安全限制
    safety:
      max_velocity: 1.5      # m/s
      max_acceleration: 3.0   # m/s^2
      max_torque: 50.0        # N·m
      collision_threshold: 10.0  # N

运行时修改参数

# 查看参数
ros2 param list /controller_node
ros2 param get /controller_node pid.kp

# 动态修改参数(无需重启节点)
ros2 param set /controller_node safety.max_velocity 2.0

tf2 坐标变换

坐标变换树

机器人的每个部件都有自己的坐标系,tf2 管理它们之间的变换关系。

典型的 tf 树

world
  └── odom
        └── base_link
              ├── imu_link
              ├── lidar_link
              ├── camera_link
              │     └── camera_optical_link
              ├── left_front_hip
              │     └── left_front_thigh
              │           └── left_front_calf
              │                 └── left_front_foot
              ├── right_front_hip
              │     └── ...
              └── ...

发布静态变换

传感器相对于 base_link 的位置(不变的变换):

# 静态变换发布器
from tf2_ros import StaticTransformBroadcaster
from geometry_msgs.msg import TransformStamped

def publish_static_transforms(self):
    t = TransformStamped()
    t.header.stamp = self.get_clock().now().to_msg()
    t.header.frame_id = 'base_link'
    t.child_frame_id = 'lidar_link'
    t.transform.translation.x = 0.1   # 前方 10cm
    t.transform.translation.y = 0.0
    t.transform.translation.z = 0.15  # 高 15cm
    t.transform.rotation.w = 1.0      # 无旋转

    self.static_broadcaster.sendTransform(t)

Launch 文件中的静态变换

Node(
    package='tf2_ros',
    executable='static_transform_publisher',
    arguments=['0.1', '0', '0.15', '0', '0', '0',
               'base_link', 'lidar_link'],
),

URDF 机器人模型

URDF 基础

URDF(Unified Robot Description Format)描述机器人的运动学结构:

<?xml version="1.0"?>
<robot name="my_quadruped" xmlns:xacro="http://www.ros.org/wiki/xacro">

    <!-- 基座 -->
    <link name="base_link">
        <visual>
            <geometry>
                <box size="0.4 0.2 0.1"/>
            </geometry>
            <material name="gray">
                <color rgba="0.5 0.5 0.5 1"/>
            </material>
        </visual>
        <collision>
            <geometry>
                <box size="0.4 0.2 0.1"/>
            </geometry>
        </collision>
        <inertial>
            <mass value="5.0"/>
            <inertia ixx="0.01" ixy="0" ixz="0"
                     iyy="0.02" iyz="0" izz="0.02"/>
        </inertial>
    </link>

    <!-- 左前髋关节 -->
    <joint name="lf_hip_joint" type="revolute">
        <parent link="base_link"/>
        <child link="lf_hip_link"/>
        <origin xyz="0.15 0.1 0" rpy="0 0 0"/>
        <axis xyz="1 0 0"/>
        <limit lower="-0.8" upper="0.8"
               velocity="10" effort="50"/>
    </joint>

    <link name="lf_hip_link">
        <visual>
            <geometry>
                <cylinder radius="0.02" length="0.08"/>
            </geometry>
        </visual>
        <inertial>
            <mass value="0.5"/>
            <inertia ixx="0.001" ixy="0" ixz="0"
                     iyy="0.001" iyz="0" izz="0.001"/>
        </inertial>
    </link>

    <!-- 更多关节和连杆... -->
</robot>

Xacro 宏

避免重复——用宏定义参数化的腿:

<xacro:macro name="leg" params="prefix x_offset y_offset">
    <joint name="${prefix}_hip_joint" type="revolute">
        <parent link="base_link"/>
        <child link="${prefix}_hip_link"/>
        <origin xyz="${x_offset} ${y_offset} 0"/>
        <axis xyz="1 0 0"/>
        <limit lower="-0.8" upper="0.8" velocity="10" effort="50"/>
    </joint>
    <!-- ... -->
</xacro:macro>

<!-- 实例化四条腿 -->
<xacro:leg prefix="lf" x_offset="0.15" y_offset="0.1"/>
<xacro:leg prefix="rf" x_offset="0.15" y_offset="-0.1"/>
<xacro:leg prefix="lr" x_offset="-0.15" y_offset="0.1"/>
<xacro:leg prefix="rr" x_offset="-0.15" y_offset="-0.1"/>

robot_state_publisher

将 URDF + 关节状态 → 发布 tf 变换:

Node(
    package='robot_state_publisher',
    executable='robot_state_publisher',
    parameters=[{
        'robot_description': open('urdf/robot.urdf').read()
    }],
),

常用 ROS2 包

感知相关

功能
usb_cam USB 摄像头驱动
realsense2_camera Intel RealSense 驱动
rplidar_ros RPLiDAR 驱动
velodyne Velodyne LiDAR 驱动
imu_tools IMU 滤波/可视化

控制相关

功能
ros2_control 硬件抽象层 + 控制器框架
ros2_controllers PID/关节轨迹/差速控制器
moveit2 运动规划(机械臂)
nav2 自主导航栈

工具

功能
rviz2 3D 可视化
rqt GUI 工具集
rosbag2 数据录制回放
foxglove_bridge Web 可视化

QoS 配置

常用 QoS 策略

from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy

# 传感器数据(允许丢包,要最新的)
sensor_qos = QoSProfile(
    reliability=ReliabilityPolicy.BEST_EFFORT,
    history=HistoryPolicy.KEEP_LAST,
    depth=1
)

# 控制命令(必须可靠送达)
control_qos = QoSProfile(
    reliability=ReliabilityPolicy.RELIABLE,
    history=HistoryPolicy.KEEP_LAST,
    depth=10
)

实时性考虑

ROS2 默认使用 DDS 中间件。对于实时控制:

  • 控制回路频率:200-1000 Hz 典型
  • DDS 延迟:通常 < 1 ms(同一机器内)
  • 避免在控制回路中:动态内存分配、文件IO、网络等待
# 控制节点的定时器回调
def control_callback(self):
    # 1. 读取传感器数据(从缓存,非阻塞)
    imu_data = self.latest_imu
    joint_states = self.latest_joints

    # 2. 计算控制(纯计算,无IO)
    torques = self.compute_control(imu_data, joint_states)

    # 3. 发送控制命令
    self.publish_torques(torques)

    # 总耗时应 < 1/control_rate

多机协作

Namespace 隔离

多个相同机器人时使用 namespace:

ros2 launch my_robot bringup.launch.py namespace:=robot1
ros2 launch my_robot bringup.launch.py namespace:=robot2

话题自动变为 /robot1/cmd_vel, /robot2/cmd_vel 等。

DDS Discovery

同一网络内的 ROS2 节点自动发现。隔离方法:

# 不同域 ID 的节点互不可见
export ROS_DOMAIN_ID=42

相关内容


参考资源


评论 #