跳到主要内容

使用Xacro简化URDF文件

如果我们直接使用前面介绍的方法编写复杂的机器人模型,会面临两个问题:

  1. 冗余代码多:相同的参数(如轮子尺寸)在多处重复出现,修改时极易遗漏。

  2. 计算繁琐:需要手动计算各种几何参数(如惯性矩阵),一旦设计调整,所有数据都要重新计算。

为了解决这些问题,ROS 提供了 Xacro (XML Macros) 功能包。你可以把 Xacro 看作是 URDF 的“升级版”或“模版语言”。它主要提供三大核心功能来简化设计流程:

  • 常量 (Properties):定义变量,一处修改,全局生效。

  • 数学计算 (Math):在 XML 中直接进行加减乘除。

  • 宏 (Macros):类似于编程中的“函数”,用于封装重复的模块(如封装一个通用的“轮子”模块)。

我们首先学习 Xacro 的基础用法:基础与文件转换、常量与数学计算。

Xacro 基础与文件转换

Xacro 文件本质上是 XML 文件,通常以 .xacro 为后缀。在使用前,必须确保文件头部包含 Xacro 的命名空间声明。

1. 必要的头部声明

每一个可运行的 Xacro 文件,其顶部的 <robot> 标签必须包含 xmlns:xacro 声明。 示例:

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

注意:如果没有这一行,系统将无法识别 Xacro 语法,解析会报错。

2. 将 Xacro 转换为 URDF

计算机和仿真器最终只认识 .urdf 文件。因此,我们需要将编写好的 .xacro 文件“翻译”成 .urdf 文件。

方法一:手动转换(命令行) 在终端中使用以下命令,用于检查生成的 URDF 是否正确:

xacro model.xacro > model.urdf

方法二:自动转换(在 Launch 文件中) 在 ROS2 的启动文件中,不需要手动生成中间文件,而是使用 Command 指令在运行时动态生成 URDF。这种做法能保证模型永远是最新的,且不占用硬盘空间。

Launch 文件编写示例 (display.launch.py):

import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.substitutions import Command, LaunchConfiguration
from launch_ros.parameter_descriptions import ParameterValue

def generate_launch_description():
# 1. 获取 xacro 文件路径
pkg_share = get_package_share_directory('turtlebot3_description')
xacro_file = os.path.join(pkg_share, 'urdf', 'turtlebot3_burger.urdf.xacro')

# 2. 核心步骤:使用 Command 指令运行 xacro
# 这相当于在终端执行 "xacro path/to/file.xacro"
robot_description_config = Command(['xacro ', xacro_file])

# 3. 创建 robot_state_publisher 节点
params = {'robot_description': ParameterValue(robot_description_config, value_type=str)}

node_robot_state_publisher = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
output='screen',
parameters=[params]
)

return LaunchDescription([
node_robot_state_publisher
])

启动文件中必不可少的就是 robot_state_publisher 节点,该节点位于robot_state_publisher 功能包中。它的核心作用是将静态的模型文件与动态的关节数据结合起来:

  • 输入:它读取 URDF 文件(知道骨架长什么样)和 /joint_states 话题(知道当前关节转了多少度)。

  • 处理:利用正向运动学,实时计算出机器人每一个部件(Link)在三维空间中的确切位置。

  • 输出:将计算结果发布到 TF(坐标变换树) 中,告诉 Rviz、导航等组件机器人现在的具体姿态。

URDF 好比图纸,关节角度是数据,robot_state_publisher 负责把它们变成机器人在空间中的实时姿态。

使用常量 (Property)

在设计机器人底盘时,假设有一个圆柱体 base_link。按照之前介绍的方法,我们需要在 <visual><collision>中分别写一次尺寸,为了简化这个过程,我们可以使用 Xacro 常量:

使用之前的方法

<link name="base_link">
<visual>
<geometry>
<cylinder length="0.6" radius="0.2"/>
</geometry>
<material name="blue"/>
</visual>
<collision>
<geometry>
<cylinder length="0.6" radius="0.2"/>
</geometry>
</collision>
</link>

使用 Xacro 常量

我们可以使用 <xacro:property> 标签定义常量,然后使用 ${名称} 的语法来调用它。

<xacro:property name="width" value="0.2" />
<xacro:property name="bodylen" value="0.6" />

<link name="base_link">
<visual>
<geometry>
<cylinder radius="${width}" length="${bodylen}"/>
</geometry>
<material name="blue"/>
</visual>
<collision>
<geometry>
<cylinder radius="${width}" length="${bodylen}"/>
</geometry>
</collision>
</link>

${} 内部的值不仅可以是数字,也可以用于字符串拼接:

<xacro:property name="robotname" value="marvin" />

<link name="${robotname}s_leg" />

使用数学运算 (Math)

Xacro 允许在 ${} 内部直接进行数学运算,这意味着我们只需定义基础参数(如直径),其他参数(如半径)可以让系统自动计算。

支持的运算:

  • 基本运算:加 (+)、减 (-)、乘 (*)、除 (/)、取负 (-)

  • 常用函数sin, cos, sqrt (开方), pi (圆周率) 等

示例:

  1. 自动计算半径

    如果我们得到了轮子的直径,但 编写 URDF 需要半径,可以通过Xacro自动计算得到:

    <xacro:property name="wheel_diameter" value="0.1" />
    <cylinder radius="${wheel_diameter / 2}" length="0.05"/>
  2. 几何位置计算

    假设你需要将轮子安装在底盘边缘,可以使用公式自动推算位置:

    <xacro:property name="width" value="0.2" />
    <origin xyz="${width / 2 + 0.02} 0 0.25" />

通过使用常量和数学运算, URDF 文件不仅代码量会显著减少,而且具备了“参数化设计”的能力,修改机器人尺寸将变得非常简单。

Xacro 宏

宏(Macro)是 Xacro 中最强大、最实用的功能。 如果你有编程基础,可以把理解为函数

  1. 它可以封装一段重复使用的代码(比如一个轮子的定义)。

  2. 它可以接收参数(比如轮子的名字、位置、左右方向)。

  3. 调用宏时,系统会自动展开成完整的 URDF 代码。

1. 简单宏 (无参数)

我们先看一个最简单的宏,它不接收任何参数,仅仅是封装了一段固定的 XML 代码:

<xacro:macro name="default_origin">
<origin xyz="0 0 0" rpy="0 0 0"/>
</xacro:macro>

调用宏:

<xacro:default_origin />

生成的 URDF 结果:

<origin rpy="0 0 0" xyz="0 0 0"/>

注意name 属性是必须的,它相当于函数名,调用时语法为 <xacro:宏名称 />

2. 参数化宏 (带参数)

宏的真正优势在于参数化,通过传入不同的参数,我们可以用同一个宏生成不同的部件(例如:不同质量的物体,或者不同名字的连杆)。

(1). 普通参数

这是一个封装了惯性矩阵(Inertial)的宏,它接收一个参数 mass(质量)。

定义宏:

<xacro:macro name="default_inertial" params="mass">
<inertial>
<mass value="${mass}" />
<inertia ixx="1e-3" ixy="0.0" ixz="0.0"
iyy="1e-3" iyz="0.0"
izz="1e-3" />
</inertial>
</xacro:macro>

调用宏:

<xacro:default_inertial mass="10"/>

(2). 块参数 (Block Parameters)

有时候我们不仅想传入数字或字符串,还想传入整段 XML 代码块(例如,传入一个几何形状 <cylinder ... /> 或者 <box ... />)。这时我们需要在参数名前加 * 号。

定义宏:

<xacro:macro name="blue_shape" params="name *shape">
<link name="${name}">
<visual>
<geometry>
<xacro:insert_block name="shape" />
</geometry>
<material name="blue"/>
</visual>
<collision>
<geometry>
<xacro:insert_block name="shape" />
</geometry>
</collision>
</link>
</xacro:macro>

调用宏:

<xacro:blue_shape name="base_link">
<cylinder radius="0.42" length="0.01" />
</xacro:blue_shape>

宏的实际应用示例

Xacro 在机器人建模中最经典的用法是通过数学计算和镜像参数,快速生成对称部件(如左腿和右腿,左轮和右轮)。

假设我们要创建机器人的两条腿。

  • 腿的形状是一样的(Box)。

  • 除了安装位置的 Y 轴坐标相反(一个在左,一个在右),其他属性都相同。

可以利用 prefix(前缀)来区分名字,利用 reflect(反射系数)来处理对称坐标。

<xacro:macro name="leg" params="prefix reflect">

<link name="${prefix}_leg">
<visual>
<geometry>
<box size="${leglen} 0.1 0.2"/>
</geometry>
<origin xyz="0 0 -${leglen/2}" rpy="0 ${pi/2} 0"/>
<material name="white"/>
</visual>
<collision>
<geometry>
<box size="${leglen} 0.1 0.2"/>
</geometry>
<origin xyz="0 0 -${leglen/2}" rpy="0 ${pi/2} 0"/>
</collision>
<xacro:default_inertial mass="10"/>
</link>

<joint name="base_to_${prefix}_leg" type="fixed">
<parent link="base_link"/>
<child link="${prefix}_leg"/>
<origin xyz="0 ${reflect * (width + 0.02)} 0.25" />
</joint>

</xacro:macro>

<xacro:leg prefix="right" reflect="1" />

<xacro:leg prefix="left" reflect="-1" />

关键技巧总结

  1. 名称前缀 (Prefix): 使用 ${prefix}_leg 这样的写法,可以避免为左腿和右腿分别写两套代码,保证了命名的一致性。

  2. 数学计算原点 (Math in Origin): 在 <origin> 标签中使用数学公式,当机器人整体尺寸(如 width)发生变化时,腿的安装位置会自动调整,无需手动重新计算坐标。

  3. 镜像反射 (Reflect): 这是 URDF/Xacro 中处理对称结构的“杀手锏”。

    • reflect 设置为 1-1

    • 在坐标计算中乘以该系数(例如 y="${reflect * offset}"),即可轻松把部件放置在身体的两侧。