🦾 ROS2 实战进阶:URDF 与 MoveIt2 机械臂仿真

从"用 URDF 描述一个机器人"到"用 MoveIt2 让机械臂动起来" —— 打通模型 → 可视化 → 坐标变换 → 控制器 → 运动规划的完整链路,参考 ROS2 官方教程与 MoveIt2 官方文档整理。
URDF xacro RViz2 TF2 ros2_control MoveIt2
🎯 本页学习目标
1. 能读懂 URDF 的 link / joint 完整标签,区分可视化几何、碰撞几何、惯性三种几何的作用
2. 能独立从零写一个 2 连杆机械臂的 URDF,并用 xacro 宏把它参数化、复用
3. 能在 RViz2 里正确加载 URDF(含 Fixed Frame 设置),并用 TF2 查任意两个坐标系之间的变换
4. 能读懂一个 launch.py 多节点启动文件,说明参数 / 包含 / 分组三种写法
5. 能讲清 ros2_control 的 controller_manager / hardware_interface 架构,并配置 joint_state_broadcaster
6. 能用 MoveIt2 的 MoveGroup 接口完成一次"规划 + 执行",并在 RViz 里拖拽目标执行
建议用时:90-120 分钟(含动手)。前置:建议先读本站 04-09 ROS2 入门实战,本页默认你已跑通过第一个节点、会用 colcon 与 ros2 topic。

1 URDF 深入:link 与 joint 的完整语法

URDF(Unified Robot Description Format,统一机器人描述格式)是 ROS 描述机器人几何、运动学、动力学的 XML 文件。人形机器人的每一个关节模组(髋、膝、踝、肩、肘、腕)在 URDF 里最终都落成两样东西:link(连杆,刚体)joint(关节,连接两个连杆的约束)

1.1 link:一个刚体,三种几何

一个 <link> 通常包含三块内容,它们的几何可以各不相同,这是初学者最容易混的点:

子标签作用由谁使用典型差异
<visual>可视化几何:屏幕上长什么样RViz2、Gazebo 渲染可以用高精度 mesh(.stl/.dae),越精细越像真机
<collision>碰撞几何:碰撞检测用什么MoveIt2 自碰撞、Gazebo 物理、规划器尽量用凸体简化(box/cylinder/sphere 或简化 mesh),越简单算得越快
<inertial>惯性:质量与转动惯量Gazebo 物理、动力学、逆动力学必须给 mass(必须 > 0)和 3×3 惯量张量 inertia

三者的 <origin> 都相对该 link 自身坐标系描述;而 link 之间的相对位姿则由 <joint> 里的 <origin> 定义。

1.2 joint:六种类型与关键字段

joint 通过 type 指定约束类型,并通过 parent / child 确定父子关系(child 相对 parent 运动,这是 TF 树的根)。常见六种:

type自由度典型用途
revolute1(绕 axis 旋转,有上下限)人形机器人几乎全部旋转关节模组(髋/膝/肘)
continuous1(无限旋转)轮子、转台
prismatic1(沿 axis 平移)滑轨、直线执行器
fixed0(焊死)把相机、IMU 固定到某个 link 上
floating6整机浮动基座(如四足/人形整机的根,较复杂)
planar3(平面内移动+旋转)平面移动小车(较少用)

一个 revolute 关节的关键字段:<origin>(关节坐标系相对父连杆的位姿)、<axis>(旋转轴单位向量,如 0 0 1 表示绕 Z 轴)、<limit>(lower/upper 限位、effort 最大力矩、velocity 最大速度)。

💡 坐标系约定:URDF 与 ROS 的位姿都用 x y z(米)和 rpy(弧度,roll/pitch/yaw)表示;欧拉角是"先绕固定轴 roll→pitch→yaw"的 RPY 顺序。颜色用 rgba,四个值都是 0~1。

1.3 从零写一个 2 连杆机械臂(纯 URDF)

下面是一个 2 自由度的平面机械臂:基座 + 两节绕 Z 轴旋转的连杆。把下面内容存成 two_link_arm.urdf 即可用于 RViz2 可视化:

<?xml version="1.0"?>
<robot name="two_link_arm">

  <!-- ===== 基座:静止连杆 ===== -->
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="0.06" length="0.12"/>
      </geometry>
      <origin xyz="0 0 0.06" rpy="0 0 0"/>
      <material name="grey">
        <color rgba="0.4 0.4 0.4 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.06" length="0.12"/>
      </geometry>
      <origin xyz="0 0 0.06" rpy="0 0 0"/>
    </collision>
    <inertial>
      <origin xyz="0 0 0.06" rpy="0 0 0"/>
      <mass value="1.0"/>
      <inertia ixx="0.01" ixy="0.0" ixz="0.0" iyy="0.01" iyz="0.0" izz="0.01"/>
    </inertial>
  </link>

  <!-- ===== 关节 1:base_link --> link1,绕 Z 轴 -->
  <joint name="joint1" type="revolute">
    <parent link="base_link"/>
    <child link="link1"/>
    <origin xyz="0 0 0.12" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
    <limit lower="-3.14" upper="3.14" effort="10" velocity="3.14"/>
  </joint>

  <link name="link1">
    <visual>
      <geometry>
        <box size="0.08 0.08 0.3"/>
      </geometry>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <material name="blue">
        <color rgba="0.2 0.5 1.0 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.08 0.08 0.3"/>
      </geometry>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
    </collision>
    <inertial>
      <origin xyz="0 0 0.15" rpy="0 0 0"/>
      <mass value="1.0"/>
      <inertia ixx="0.01" ixy="0.0" ixz="0.0" iyy="0.01" iyz="0.0" izz="0.01"/>
    </inertial>
  </link>

  <!-- ===== 关节 2:link1 --> link2 -->
  <joint name="joint2" type="revolute">
    <parent link="link1"/>
    <child link="link2"/>
    <origin xyz="0 0 0.3" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
    <limit lower="-3.14" upper="3.14" effort="10" velocity="3.14"/>
  </joint>

  <link name="link2">
    <visual>
      <geometry>
        <box size="0.06 0.06 0.25"/>
      </geometry>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <material name="red">
        <color rgba="1.0 0.3 0.3 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.06 0.06 0.25"/>
      </geometry>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
    </collision>
    <inertial>
      <origin xyz="0 0 0.125" rpy="0 0 0"/>
      <mass value="0.6"/>
      <inertia ixx="0.005" ixy="0.0" ixz="0.0" iyy="0.005" iyz="0.0" izz="0.005"/>
    </inertial>
  </link>

</robot>
⚠️ 注意:上面的惯量值是教学用近似值(转动惯量取近似)。真实项目里惯量张量应按质量分布计算(如对长方体,Ixx = m/12·(h²+d²));MoveIt2 与 Gazebo 都强制要求 mass > 0 且提供了 <inertial>,否则加载直接报错。

1.4 xacro:把 URDF 参数化、复用

URDF 写长了会又臭又长:xacro(XML 宏)给它加上了常量、变量、数学运算、宏函数、文件包含。核心用法:

把上面的 2 连杆臂改写成 xacro(节选核心部分):

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

  <!-- 常量:连杆长度,改这里即可整体改变臂长 -->
  <xacro:property name="link1_len" value="0.3"/>
  <xacro:property name="link2_len" value="0.25"/>
  <xacro:property name="radius" value="0.04"/>

  <!-- 宏:生成一节连杆(参数化) -->
  <xacro:macro name="arm_link" params="name length color">
    <link name="${name}">
      <visual>
        <geometry>
          <box size="${2*radius} ${2*radius} ${length}"/>
        </geometry>
        <origin xyz="0 0 ${length/2}" rpy="0 0 0"/>
        <material name="m_${name}">
          <color rgba="${color}"/>
        </material>
      </visual>
      <collision>
        <geometry>
          <box size="${2*radius} ${2*radius} ${length}"/>
        </geometry>
        <origin xyz="0 0 ${length/2}" rpy="0 0 0"/>
      </collision>
      <inertial>
        <origin xyz="0 0 ${length/2}" rpy="0 0 0"/>
        <mass value="${length}"/>
        <inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/>
      </inertial>
    </link>
  </xacro:macro>

  <!-- 宏:生成一个 revolute 关节 -->
  <xacro:macro name="arm_joint" params="name parent child z">
    <joint name="${name}" type="revolute">
      <parent link="${parent}"/>
      <child link="${child}"/>
      <origin xyz="0 0 ${z}" rpy="0 0 0"/>
      <axis xyz="0 0 1"/>
      <limit lower="-3.14" upper="3.14" effort="10" velocity="3.14"/>
    </joint>
  </xacro:macro>

  <!-- 调用宏:三行搞定三节 + 两个关节 -->
  <arm_link name="link1" length="${link1_len}" color="0.2 0.5 1.0 1.0"/>
  <arm_link name="link2" length="${link2_len}" color="1.0 0.3 0.3 1.0"/>
  <arm_joint name="joint1" parent="base_link" child="link1" z="${0.12}"/>
  <arm_joint name="joint2" parent="link1" child="link2" z="${link1_len}"/>

</robot>

xacro 文件要用 xacro 工具转成 URDF 才能被下游读取:

xacro two_link_arm.urdf.xacro > two_link_arm.urdf
# 也可以直接转成可视化 PDF 检查(部分发行版需装 graphviz/urdfdom)
check_urdf two_link_arm.urdf        # 校验结构是否合法
urdf_to_graphiz two_link_arm.urdf   # 生成 tf 结构图(可选)

2 RViz2 实操:把 URDF 显示出来

RViz2 只负责"显示",它自己不解析 URDF —— 解析和发布 TF 靠 robot_state_publisher,关节角度靠 joint_state_publisher。完整流程四步:

1转 xacro
xacro 文件先转成 URDF(或让 launch 用 Command 自动转)
2robot_state_publisher
读 URDF,发布 /robot_description 与静态 TF
3joint_state_publisher
发布关节角度(/joint_states)
4RViz2
订阅 TF 与 robot_description 渲染模型
# ① 转 URDF(可选,便于 check_urdf 校验)
xacro two_link_arm.urdf.xacro > two_link_arm.urdf

# ② 启动 robot_state_publisher,发布模型与 TF
ros2 run robot_state_publisher robot_state_publisher --ros-args \
  -p robot_description:="$(xacro /path/to/two_link_arm.urdf.xacro)"

# ③ 启动 joint_state_publisher_gui,可拖动滑杆改关节角
ros2 run joint_state_publisher_gui joint_state_publisher_gui

# ④ 新开终端,启动 RViz2
rviz2

RViz2 打开后按下面顺序配置(每一步都有坑):

  1. 设 Fixed Frame:左侧 Global Options → Fixed Framebase_link(注意大小写、不能有前导 /)。设错会整屏报红或模型消失。
  2. 添加 RobotModel:左下角 Add → 按主题(RobotModel)。它默认订阅 /robot_description 话题;若模型不出现,检查话题名是否一致。
  3. 添加 TF:Add → TF,可显示所有坐标系与连线,直观看到 base_link → link1 → link2 的树。
  4. 添加 Grid:Add → Grid,给一个地面参考,方便看方向和尺度。
  5. 保存配置:File → Save Config As,存成 display.rviz,下次用 rviz2 -d display.rviz 直接加载。
💡 拖拽验证:打开 joint_state_publisher_gui 的滑杆,拖动 joint1/joint2 的角度,RobotModel 应该跟着转。若模型是"散架/错位"的,通常是某个 <origin> 写错或 Fixed Frame 没设对。

3 TF2:机器人的"坐标变换总线"

TF2 维护一棵有向树上所有坐标系之间的相对位姿,并缓存一段时间让异步消息也能对齐时间戳。人形机器人典型的 tf 树:

map ──> odom ──> base_link ──> torso ──> l_arm ──> l_hand ──> l_gripper
                                        └────> r_arm ──> r_hand ──> r_gripper

3.1 三个根级坐标系:map / odom / base_link

坐标系含义典型发布者
base_link机器人本体基准系(通常位于底盘/躯干)robot_state_publisher(由 URDF 决定)
odom里程计系:随运动漂移的"世界系"近似轮式/腿式里程计节点、IMU 融合
map地图系:全局固定、不漂移的参考系SLAM/定位节点(如 AMCL)

为什么要分三层?因为里程计会累积误差(odom → base_link 随运动漂移),而地图需要稳定;定位节点定期发布 map → odom 来"纠正"漂移,从而隔离两种误差源。这是导航栈的标准设计。

3.2 发布静态变换

两坐标系若刚性固定(如给机器人装一个固定位置的激光雷达),用 static_transform_publisher 一次发布即可:

# 形式:xyz + 欧拉角(rpy),再跟 父系 子系
ros2 run tf2_ros static_transform_publisher 0.1 0 0.2 0 0 0 base_link laser

# 或使用命名参数(更不易写错顺序)
ros2 run tf2_ros static_transform_publisher --x 0.1 --y 0 --z 0.2 \
  --roll 0 --pitch 0 --yaw 0 --frame-id base_link --child-frame-id laser

3.3 lookupTransform:查任意两系变换

程序里查变换的核心是 Buffer + TransformListener(负责接收),再调 lookup_transform:

import rclpy
from rclpy.node import Node
from tf2_ros.buffer import Buffer
from tf2_ros.transform_listener import TransformListener

class TfLookup(Node):
    def __init__(self):
        super().__init__('tf_lookup')
        self.buffer = Buffer()
        self.listener = TransformListener(self.buffer, self)
        self.timer = self.create_timer(1.0, self.lookup)

    def lookup(self):
        try:
            # 查 base_link 到 link2 的变换,time 用 Time() 表示"取最新"
            t = self.buffer.lookup_transform('base_link', 'link2', rclpy.time.Time())
            pos = t.transform.translation
            self.get_logger().info('link2 相对 base_link: x=%.3f y=%.3f z=%.3f'
                                   % (pos.x, pos.y, pos.z))
        except Exception as e:
            self.get_logger().warn('查询失败(通常系还没发布): %s' % e)

def main(args=None):
    rclpy.init(args=args)
    node = TfLookup()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

调试命令:ros2 run tf2_ros tf2_echo base_link link2 持续打印变换;view_frames(生成 frames.pdf)可视化整棵树。

4 launch 文件:一个文件拉起一堆节点

真实机器人启动时少则几个、多则几十个节点,不可能手动敲。ROS2 推荐 Python 版 launch 文件(*.launch.py),它其实是普通 Python 脚本,入口函数返回一个 LaunchDescription。核心构件:

构件导入来源作用
LaunchDescriptionlaunch描述要启动什么(所有 Action 的列表)
Nodelaunch_ros.actions启动一个 ROS2 节点(package/executable/name/parameters)
DeclareLaunchArgumentlaunch.actions声明可覆盖参数,如 use_sim_time
LaunchConfigurationlaunch.substitutions读取参数当前值
IncludeLaunchDescriptionlaunch.actions嵌套包含另一个 launch 文件
GroupActionlaunch.actions分组,可统一加命名空间/条件
FindPackageSharelaunch_ros.substitutions定位包的 share 目录(找 URDF/配置)

一个把 URDF 显示起来的完整多节点示例(display.launch.py):

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import Command, LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare

def generate_launch_description():
    # 参数:运行时可用 use_sim_time:=true 覆盖
    use_sim_time = LaunchConfiguration('use_sim_time', default='false')

    robot_description = Command([
        'xacro ',
        PathJoinSubstitution([
            FindPackageShare('my_arm'),
            'urdf', 'two_link_arm.urdf.xacro',
        ]),
    ])

    return LaunchDescription([
        DeclareLaunchArgument('use_sim_time', default_value='false',
                              description='是否使用 /clock 仿真时间'),

        # 节点 1:发布模型与 TF
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            name='robot_state_publisher',
            parameters=[{'robot_description': robot_description,
                         'use_sim_time': use_sim_time}],
            output='screen',
        ),

        # 节点 2:关节角度(带 GUI 滑杆)
        Node(
            package='joint_state_publisher_gui',
            executable='joint_state_publisher_gui',
            name='joint_state_publisher_gui',
        ),

        # 节点 3:可视化
        Node(
            package='rviz2',
            executable='rviz2',
            name='rviz2',
            arguments=['-d', PathJoinSubstitution([
                FindPackageShare('my_arm'), 'rviz', 'display.rviz'])],
        ),
    ])

运行:ros2 launch my_arm display.launch.py use_sim_time:=true。若要"分组",把一组 Node 包进 GroupAction([...]) 并可加 namespace;若要"包含",用 IncludeLaunchDescription(PythonLaunchDescriptionSource([...])) 复用别的 launch 文件。

5 ros2_control:让关节真正被"控制"

URDF 只描述"长什么样",真正给每个关节模组下发力矩/位置/速度、并读回编码器反馈的是 ros2_control。它的架构把"控制逻辑"和"硬件驱动"解耦:

┌───────────── controller_manager ─────────────┐
│  joint_state_broadcaster   joint_trajectory_controller │
└───────────────┬───────────────────────────────┘
                │ 通过 state/cmd 接口
┌───────────────▼───────────────────────────────┐
│      hardware_interface (SystemInterface)      │
│  真机驱动(CAN/EtherCAT) 或 仿真插件(ign_ros2_control) │
└───────────────────────────────────────────────┘

5.1 joint_state_broadcaster 配置要点

它是最基础的控制器:把硬件读到的关节角度/速度/力矩广播/joint_states,供 TF 和 MoveIt2 使用。配置文件(controllers.yaml):

controller_manager:
  ros__parameters:
    update_rate: 50   # 控制循环 Hz

joint_state_broadcaster:
  ros__parameters:
    type: joint_state_broadcaster/JointStateBroadcaster
    publish_rate: 50   # 发布 /joint_states 的频率
    extra_joints: []   # 可额外并入非受控关节

启动与激活(两种方式):

# 方式 A:spawner(推荐,自动处理 lifecycle)
ros2 run controller_manager spawner joint_state_broadcaster

# 方式 B:ros2 control 命令行手动管理
ros2 control load_controller joint_state_broadcaster
ros2 control set_controller_state joint_state_broadcaster active
ros2 control list_controllers   # 查看所有控制器状态
⚠️ 高频坑:ros2_control 的控制器是生命周期节点(lifecycle),加载后处于 inactive,必须 activate 才会真正发布/控制。URDF 里的关节名、控制器配置里的关节名、MoveIt2 里的关节名三者必须完全一致,否则 controller_manager 直接报"no matching joint"。

6 MoveIt2:机械臂运动规划与执行

MoveIt2 是 ROS2 生态里最主流的操作与运动规划框架:给定起始位姿和目标位姿,它在避开障碍和自碰撞的前提下算出一条轨迹,再交给控制器执行。核心对象是 MoveGroup(一组关节/规划组的封装)。

6.1 MoveIt Setup Assistant:从 URDF 到配置包

一个交互式 GUI 工具,把 URDF 变成 MoveIt2 可用的配置包。流程:

1启动
ros2 launch moveit_setup_assistant setup_assistant.launch.py
2导入模型
New MoveIt Configuration Package → 选 .urdf/.xacro
3自碰撞矩阵
采样生成"哪些连杆之间允许碰撞"表
4规划组
建"arm"组(把 joint1/joint2 加入)
5预设位姿
定义 home/up 等命名位姿(可选)
6生成包
输出 my_arm_moveit_config
# 生成后即可在 RViz 里做交互式规划(带 MotionPlanning 插件)
ros2 launch my_arm_moveit_config demo.launch.py

6.2 MoveGroup 接口:规划 + 执行(C++)

程序化规划的最小示例(以规划组名 arm 为例):

#include <rclcpp/rclcpp.hpp>
#include <moveit/move_group_interface/move_group_interface.h>
#include <moveit/planning_scene_interface/planning_scene_interface.h>

int main(int argc, char** argv)
{
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("move_group_demo");

  // 用默认规划组 "arm" 构造 MoveGroup
  moveit::planning_interface::MoveGroupInterface move_group(node, "arm");

  // 目标 1:关节空间目标(单位弧度)
  std::vector<double> joint_goal = {0.0, -0.785, 1.57};
  move_group.setJointValueTarget(joint_goal);

  // 规划
  moveit::planning_interface::MoveGroupInterface::Plan plan;
  bool success = (move_group.plan(plan) == moveit::core::MoveItErrorCode::SUCCESS);

  // 成功才执行
  if (success) {
    RCLCPP_INFO(node->get_logger(), "plan ok, executing...");
    move_group.execute(plan);
  } else {
    RCLCPP_WARN(node->get_logger(), "plan failed");
  }

  rclcpp::shutdown();
  return 0;
}

常用接口速查:setPoseTarget()(笛卡尔位姿目标)、setJointValueTarget()(关节角目标)、setMaxVelocityScalingFactor()(限速,真机必设)、plan()(只规划)、move()(规划并执行一步到位)、execute(plan)(执行已有轨迹)。

6.3 RViz 交互:拖拽目标 + Execute

demo.launch.py 打开的 RViz 里:

  1. 左下角切换到 MotionPlanning 面板,确认 Planning Group 为 arm;
  2. 在 3D 视图里拖动机械臂末端的交互标记(interactive marker)到想要的位置/姿态;
  3. Plan(或 Plan & Execute)查看规划轨迹,绿色为通过;
  4. Execute 让机器人执行。真机务必先点 Plan 目视确认无碰撞再 Execute。
🔴 安全提醒:真机上执行前,务必设置速度缩放(setMaxVelocityScalingFactor(0.1) 之类)并站在急停开关旁。本页示例都是仿真,切勿直接把未经减速的轨迹下发到关节模组。

7 与仿真器桥接:Gazebo 与 Isaac Sim

URDF 描述模型、MoveIt2 负责规划,而"物理世界"由仿真器提供:重力、接触、传感器、电机。仿真器与 ROS2 之间靠"桥"交换消息(关节状态、力矩命令、TF、时钟等)。两大类桥接:

仿真器桥接方案说明
Gazebo Classic(gazebo11) gazebo_ros(gazebo_ros_pkgs) 插件式接入;可用 gazebo_ros2_control 把 ros2_control 的硬件接口接到仿真关节上,模型进仿真器即可受控。
Gazebo 新版(Harmonic/Ionic,旧名 Ignition) ros_gz_bridge 独立桥接节点,按需把 Gazebo 话题与 ROS2 话题双向映射(如 /joint_states、/cmd_vel)。
NVIDIA Isaac Sim Isaac Sim ROS2 Bridge(Omniverse 扩展) 启用后自动提供 ROS2 时钟/TF/话题;配合 Isaac Lab 可做大规模并行强化学习(详见本站 04-04)。

典型 Gazebo Classic 启动(URDF 经 gazebo_ros 插件带进仿真):

# 启动 Gazebo + 加载 URDF 模型(spawn 到仿真)
ros2 launch gazebo_ros gazebo.launch.py
ros2 run gazebo_ros spawn_entity.py -topic robot_description -entity my_arm

# 若用 ros2_control 驱动仿真关节(需在 URDF 里配置 gazebo_ros2_control 插件)
ros2 run controller_manager spawner joint_state_broadcaster
ros2 run controller_manager spawner joint_trajectory_controller
💡 选型建议:做整机 ROS2 联调、传感器仿真用 Gazebo;做大规模并行强化学习、需要 GPU 吞吐用 Isaac Sim / Isaac Lab。两者的仿真器选型、物理引擎差异与强化学习流水线,详见本站 04-04 仿真与强化学习:MuJoCo 与 Isaac Lab

8 常见坑(踩过才知道)

⚠️ 高频坑汇总:
1. Fixed Frame 设错 → 模型消失/满屏红色错误:RViz2 的 Fixed Frame 必须是 tf 树里真实存在的坐标系,且不能带前导 /、区分大小写(常见 base_linkbase_footprint)。
2. 忘 source install/setup.bash:每个新终端都要 source ~/ws/install/setup.bash,否则 ros2 launch/run 找不到你的包(详见 04-09 的八步流程)。
3. URDF 缺 <inertial> 或 mass=0:MoveIt2 与 Gazebo 加载直接失败;可视化可以没有,物理/规划必须有惯性。
4. 直接把 .xacro 当 URDF 用:robot_description 参数若直接指向 .xacro 而没经过 xacro 工具或 launch 的 Command(['xacro',...]) 处理,会解析失败。
5. 控制器没激活:ros2_control 加载后处于 inactive,不 activate 就没有 /joint_states,TF 树也不更新。
6. 三处关节名不一致:URDF、controller 配置、MoveIt2 配置的关节名必须逐字一致(大小写/下划线都要对齐)。

9 本节自测

1. URDF 里 <visual>、<collision>、<inertial> 三个几何的主要区别是什么?
💡 visual 供 RViz/Gazebo 渲染;collision 供 MoveIt2 自碰撞与物理检测(应凸体简化);inertial 提供 mass 与惯量张量,物理与规划强制需要。
2. RViz2 里 RobotModel 不显示,首先应该检查哪一项?
💡 Fixed Frame 设错会导致模型消失或满屏红色错误;还应确认 robot_state_publisher 已发布 /robot_description。
3. map、odom、base_link 三个坐标系的关系,下列说法正确的是?
💡 odom 由里程计/IMU 融合发布且随运动漂移,map 由 SLAM/定位发布且全局固定,这是导航栈分层设计的关键。
4. 关于 ros2_control 的 controller_manager 与控制器,正确的是?
💡 控制器是 lifecycle 节点,需显式 activate;joint_state_broadcaster 是"广播关节状态"而非下发力矩;运动规划是 MoveIt2 的事,hardware_interface 才是硬件/仿真抽象。
5. 关于 xacro 的说法,下列哪一项是错误的?
💡 xacro 只是宏预处理语言,下游只认展开后的 URDF:必须经 xacro 工具或 launch 里 Command 包装的 xacro 命令转换(见 1.4 与常见坑 4),直接把 .xacro 当 URDF 用会解析失败。
6. 用 MoveGroup 接口完成一次「规划 + 执行」,下列说法正确的是?
💡 见 6.2:setJointValueTarget 设关节角、setPoseTarget 设笛卡尔位姿;plan() 只规划不执行,成功后才 execute(plan);真机务必用 setMaxVelocityScalingFactor 限速并站急停旁(6.3 安全提醒)。
7. 关于 URDF 中 joint 的 type,下列说法正确的是?
💡 见 1.2 表格:revolute = 1 自由度绕 axis 旋转(有上下限),continuous = 1 自由度无限旋转,fixed = 0 自由度(焊死),floating = 6 自由度(浮动基座)。这是初学最易混淆的一组。
📌 本节要点速查:①link 三种几何各司其职:visual 显示、collision 碰撞(应凸体简化)、inertial 质量/惯量(物理与规划必须);②joint 六类型,revolute 绕 axis 有上下限、continuous 无限旋转、fixed 焊死;③xacro 用 property/macro 参数化复用,须转成 URDF 才能被下游读取;④RViz2 显示靠 robot_state_publisher + joint_state_publisher,Fixed Frame 填 base_link;⑤TF2 分 map/odom/base_link 三层,定位节点发 map→odom 纠正漂移;⑥ros2_control 控制器是 lifecycle 节点需 activate,URDF/控制器/MoveIt2 三处关节名必须一致;⑦MoveIt2 用 MoveGroup:setJointValueTarget→plan→execute,真机先限速再执行。
📌 本节小结:URDF 用 link/joint 描述机器人,visual/collision/inertial 三种几何各司其职,xacro 让模型参数化可复用;RViz2 显示模型靠 robot_state_publisher + joint_state_publisher,关键是 Fixed Frame;TF2 维护坐标变换树,map/odom/base_link 三层设计隔离漂移;launch.py 用 LaunchDescription 批量起节点;ros2_control 用 controller_manager 管控制器、hardware_interface 接硬件;MoveIt2 经 Setup Assistant 生成配置包,再用 MoveGroup 完成规划+执行。整条链路是:URDF → TF2 → ros2_control → MoveIt2 → 仿真/真机
🤔 思考题: 1. 把 2 连杆臂的关节 axis 改成 0 1 0(绕 Y 轴),在 RViz2 里会有什么不同?这对"人形机器人肩关节俯仰"意味着什么?
2. 为什么 static_transform_publisher 只适合固定关系,而 robot_state_publisher 才能处理会转动的关节?两者发布 TF 的机制有何区别?
3. 若给 MoveGroup 设一个不可达的笛卡尔位姿目标,plan() 会返回什么?你会如何提示用户并重试?
4. 试着自己把 2 连杆臂加一个 prismatic 关节,做成"旋转+伸缩"的 2 自由度机构,并跑通 MoveIt2 规划。

10 参考来源