xacro:讓 URDF 可以參數化與重複利用

2026-07-18
  • ros2
  • xacro
  • urdf
  • robot-model

問題定義

上一節手寫的兩節手臂 URDF,已經需要三個 link、兩個 joint、加起來快 80 行 XML。如果是一台四足機器人(4 條腿 × 每條腿 3 個關節)、或是想同時維護「小台」「大台」兩種尺寸的同款機器人,純手寫 URDF 會產生大量幾乎一樣、只有數字不同的重複區塊——改一個尺寸要同時改十幾個地方,非常容易漏改或改錯。xacro(XML Macro)就是為了解決這個維護問題而生。

核心概念說明

xacro 本質上是「展開前處理」,不是新的描述格式

理解 xacro 最關鍵的一點:它不是取代 URDF 的新格式,而是一層文字展開機制。你寫的 .xacro 檔案裡可以用變數、巨集、數學運算,但這些東西在真正被 robot_state_publisher 讀取之前,會先被 xacro 這個工具「展開」成一份純粹、不含任何 xacro 語法的標準 URDF。理解這一點之後,很多行為就變得直觀——例如 property 只是純文字替換,不是執行期間的變數。

三個最常用的機制

Property(屬性):類似變數,宣告一次、多處引用,避免同一個數字散落在檔案各處:

xml
<xacro:property name="arm_length" value="0.3"/>

Macro(巨集):類似函式,把一段重複的 link/joint 結構包成可以帶參數呼叫的模板,這是解決「四條腿長一樣」這類重複結構最核心的工具:

xml
<xacro:macro name="leg" params="prefix x_offset">
  ...
</xacro:macro>

Include(檔案引用):把大型機器人描述拆成多個檔案(例如感測器獨立一份、手臂獨立一份),再用 include 組合起來,避免單一檔案過度肥大:

xml
<xacro:include filename="$(find my_package)/urdf/sensors.xacro"/>

實作範例:用 macro 重寫手臂,並擴充成雙臂

延續上一節的兩節手臂,這裡示範如何用 macro 把「一條手臂」包裝成可重複呼叫的模板,一次生出左右兩條手臂。

檔案位置:~/ros2_ws/src/my_package/urdf/dual_arm.xacro

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

  <xacro:property name="upper_arm_length" value="0.3"/>
  <xacro:property name="forearm_length" value="0.24"/>

  <link name="base_link">
    <visual>
      <geometry>
        <cylinder radius="0.1" length="0.05"/>
      </geometry>
    </visual>
  </link>

  <xacro:macro name="arm" params="prefix reflect">
    <link name="${prefix}_upper_arm">
      <visual>
        <origin xyz="0 0 ${upper_arm_length/2}" rpy="0 0 0"/>
        <geometry>
          <box size="0.05 0.05 ${upper_arm_length}"/>
        </geometry>
      </visual>
      <collision>
        <origin xyz="0 0 ${upper_arm_length/2}" rpy="0 0 0"/>
        <geometry>
          <box size="0.05 0.05 ${upper_arm_length}"/>
        </geometry>
      </collision>
      <inertial>
        <mass value="0.5"/>
        <inertia ixx="0.005" ixy="0" ixz="0" iyy="0.005" iyz="0" izz="0.001"/>
      </inertial>
    </link>

    <link name="${prefix}_forearm">
      <visual>
        <origin xyz="0 0 ${forearm_length/2}" rpy="0 0 0"/>
        <geometry>
          <box size="0.04 0.04 ${forearm_length}"/>
        </geometry>
      </visual>
      <collision>
        <origin xyz="0 0 ${forearm_length/2}" rpy="0 0 0"/>
        <geometry>
          <box size="0.04 0.04 ${forearm_length}"/>
        </geometry>
      </collision>
      <inertial>
        <mass value="0.3"/>
        <inertia ixx="0.002" ixy="0" ixz="0" iyy="0.002" iyz="0" izz="0.0005"/>
      </inertial>
    </link>

    <joint name="${prefix}_shoulder_joint" type="revolute">
      <parent link="base_link"/>
      <child link="${prefix}_upper_arm"/>
      <origin xyz="0 ${reflect * 0.1} 0.025" rpy="0 0 0"/>
      <axis xyz="0 1 0"/>
      <limit lower="-1.57" upper="1.57" effort="20" velocity="1.0"/>
    </joint>

    <joint name="${prefix}_elbow_joint" type="revolute">
      <parent link="${prefix}_upper_arm"/>
      <child link="${prefix}_forearm"/>
      <origin xyz="0 0 ${upper_arm_length}" rpy="0 0 0"/>
      <axis xyz="0 1 0"/>
      <limit lower="-2.0" upper="2.0" effort="10" velocity="1.0"/>
    </joint>
  </xacro:macro>

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

</robot>

這個範例展示了幾個關鍵用法:

  • ${upper_arm_length/2}${} 裡可以直接寫數學運算,property 定義的長度在展開時會被算成實際數字。
  • ${prefix}_upper_arm:用字串接合產生不同名稱,這是 macro 能重複呼叫、卻不會產生重複 link 名稱(URDF 要求所有 link 名稱必須唯一)的關鍵。
  • reflect 參數傳入 1-1,讓左右手臂的 Y 軸偏移方向相反,一份 macro 定義同時服務左右兩側,不用複製貼上兩次幾乎一樣的區塊。

展開並檢查結果

bash
xacro ~/ros2_ws/src/my_package/urdf/dual_arm.xacro > /tmp/dual_arm_expanded.urdf
check_urdf /tmp/dual_arm_expanded.urdf

預期輸出

text
robot name is: dual_arm
---------- Successfully Parsed XML ---------------
root Link: base_link has 2 child(ren)
    child(1):  left_upper_arm
        child(1):  left_forearm
    child(2):  right_upper_arm
        child(2):  right_forearm

可以看到一份 30 幾行的 macro 定義加上兩行呼叫,展開後產生了完整的左右對稱雙臂結構——如果不用 macro,手寫這份結構需要把上一節的手臂區塊完整複製一份,並手動改掉所有 link/joint 名稱與偏移方向,出錯機率高很多。

常見錯誤與除錯技巧

錯誤一:xacro 展開時報錯找不到某個 property 或參數

text
xacro.xacro.XacroException: undefined property: upper_arm_length

原因:xacro 是由上往下、依照檔案裡出現的順序解析的(本質是前處理階段的文字替換),如果 property 定義在使用它的地方之後,或是拼錯了名稱,就會出現「找不到定義」的錯誤。

排除方式:確認所有 xacro:property 都定義在第一次被引用之前,這也是為什麼實務上習慣把所有 property 集中寫在檔案最上方。

錯誤二:兩個 macro 呼叫之後,RViz2 或 Gazebo 抱怨 link 名稱重複

text
Link left_upper_arm is already defined

原因:呼叫 macro 時忘記傳入不同的 prefix(例如兩次都寫成 prefix="left"),導致 macro 展開後產生了兩組名稱完全相同的 link/joint,URDF 規範不允許同名的 link 同時存在。

排除方式:每次呼叫 macro 時,確認負責產生唯一名稱的參數(這裡是 prefix)確實給了不同的值,展開後可以用 xacro ... | grep 'link name' 快速檢查是否有重複:

bash
xacro ~/ros2_ws/src/my_package/urdf/dual_arm.xacro | grep -o 'link name="[^"]*"'
text
link name="base_link"
link name="left_upper_arm"
link name="left_forearm"
link name="right_upper_arm"
link name="right_forearm"

小結

xacro 是展開成標準 URDF 之前的一層前處理,property 解決數字重複散落各處的問題,macro 解決結構重複(多條腿、多個手指)的問題,include 讓大型機器人描述可以拆成多個檔案管理。理解「xacro 只在展開階段生效、不是執行期可調整的東西」這一點,能避免很多誤用。有了完整的機器人模型之後,下一節要處理的是這些部件之間的座標系,怎麼在系統執行期間被即時追蹤與轉換——這就是 TF2 的工作。

延伸閱讀

常見問題

faq_01.log
xacro 檔案可以直接拿給 robot_state_publisher 用嗎?
不行,robot_state_publisher 只認得展開後的純 URDF(XML)。必須先用 xacro 指令把 .xacro 檔案展開成純 URDF 字串,再把這個字串傳給 robot_description 參數,這個轉換步驟通常會寫進 launch 檔案裡自動完成,實務上很少手動執行。
faq_02.log
xacro 的 property 跟 ROS2 的 parameter 是同一回事嗎?
完全不是。xacro property 是在『展開 URDF 檔案』這個階段生效的純文字替換機制,展開完成後就固定寫死在最終的 URDF 裡;ROS2 parameter 則是節點執行期間可以動態查詢、修改的值。想要執行期間可調整的數值(例如 PID 增益),要用參數系統,不是 xacro property。
faq_03.log
macro 可以互相呼叫、巢狀使用嗎?
可以,一個 xacro:macro 內部可以呼叫另一個已定義的 macro,這在描述階層式結構(例如一隻腳由多個關節組成,四隻腳組成一台四足機器人)時很常見,能大幅減少重複定義。