← 返回文章库

ROS 怎么接足式机器人?unitree_ros 的 Gazebo 仿真与 URDF 模型

最后更新 2026-08-23
⏱ 约 34 分钟 🟡 涉接线/强电
你将学到
  • 拿到任何一个陌生的 ROS 机器人仓库,能在五分钟内定位 description / gazebo / controller 三类包
  • 讲清 URDF 描述了什么、碰撞体与视觉体为什么要分开、以及为什么要用 xacro 而不是裸写 URDF
  • 理解「模型定义一次、到处使用」这条 ROS 生态主线,以及仿真到真机之间那座桥为什么必须单独建

你装完 ROS,跑通了 turtlesim,那只小乌龟在窗口里画了几个圈。教程到这儿就结束了。

然后你想接一台真机器人——不是仿真里的乌龟,是有腿、有关节、会摔倒的那种。你 clone 下来一个官方仓库,解压,ls。屏幕上刷出十几个文件夹,每个下面还有 meshesxacrolaunchconfig,加起来上千个文件。你盯着看了两分钟,不知道该从哪个文件打开。

这一步卡住的人非常多。卡住的原因不是 ROS 难,而是没人告诉过你一个 ROS 机器人项目应该长什么样。它其实有固定的骨架,只是这套骨架从来没被单独讲过一遍。

这篇我们就拿宇树的 unitree_ros 当样本,把这套骨架完整拆一遍。看完你再打开任何一个 ROS 机器人仓库——不管是谁家的、什么形态的——都能在五分钟内知道东西都藏在哪。

先说清边界:本文基于公开仓库的 README 与文件树结构写成,没有真机验证,也没有在本地跑过这套仿真。 所有关于「怎么跑」的说法都来自仓库文档本身,所有关于「怎么组织」的判断来自文件与目录的排列方式。涉及运行效果的地方,我会明确写成「README 里写的是」。

三件套:先把包分成三堆

unitree_ros 的 README 开门见山地把包分了组,这个分组本身就是答案:

Robot description(机器人描述)
  a1  a2  aliengo  aliengoZ1  b1  b2  b2w  dexterous_hand
  g1  go1  go2  go2w  h1  h1_2  h2  laikago  r1  r1_air  z1

Robot and joints controller(机器人与关节控制器)
  unitree_controller
  z1_controller

Simulation related(仿真相关)
  unitree_gazebo
  unitree_legged_control

这就是 ROS 机器人项目的标准三件套:描述包说明「这台机器是什么」,仿真包说明「让它在哪个世界里动」,控制包说明「怎么驱动它的关节」。

三者的依赖方向是单向的。描述包最底层,它不知道自己会被谁用;仿真包和控制包都往下依赖描述包。这个方向很重要——它意味着你换一台机器人只需要换描述包,仿真和控制那两层理论上不用动。README 里那条启动命令直接印证了这一点:

roslaunch unitree_gazebo normal.launch rname:=a1 wname:=stairs

rname 是机器人名,wname 是世界名。一个 launch 文件,靠两个参数切换机型和场景。能做到这件事的前提,就是所有描述包遵守同一套结构约定。

拿到一个陌生仓库时,你要做的第一个动作就是这个分堆。名字里带 description 的归一堆,带 gazebo / sim / simulation 的归一堆,带 control / controller 的归一堆。剩下没归进去的,往往是消息定义、工具脚本或者示例。

description 包里到底装了什么

a1_description 来看,因为它的结构最完整、最典型:

robots/a1_description/
├── CMakeLists.txt
├── package.xml
├── config/
│   └── robot_control.yaml
├── launch/
│   ├── a1_rviz.launch
│   └── check_joint.rviz
├── meshes/
│   ├── trunk.dae
│   ├── hip.dae
│   ├── thigh.dae
│   ├── thigh_mirror.dae
│   ├── calf.dae
│   └── trunk_A1.png
├── urdf/
│   └── a1.urdf
└── xacro/
    ├── const.xacro
    ├── materials.xacro
    ├── leg.xacro
    ├── transmission.xacro
    ├── gazebo.xacro
    ├── robot.xacro
    └── stairs.xacro

package.xmlCMakeLists.txt 是 ROS 包的身份证,任何 ROS 包都有,跳过不谈。剩下五个目录,每个都对应一件正经事。

URDF:机器人的身体定义

urdf/a1.urdf 是这个包的核心。URDF 全称 Unified Robot Description Format,统一机器人描述格式,本质是一个 XML 文件。

它描述的东西可以归成两类:连杆(link)和关节(joint)

连杆是刚体,是机器人身上那些不会自己变形的块——躯干、大腿、小腿、脚。一个 link 通常写三样东西:

  • visual:视觉体。长什么样,给人看的。通常指向一个网格文件,也可以是简单的立方体或圆柱。
  • collision:碰撞体。物理引擎拿它算碰撞。
  • inertial:惯性参数。质量、质心位置、惯量张量。这三样决定了它在物理世界里的行为——同样形状的两根杆,质量分布不同,甩起来完全是两回事。

关节是连接两个连杆的东西,它规定了父连杆和子连杆之间允许怎么相对运动。URDF 的关节有几种类型,最常用的是这几个:

  • revolute:转动关节,有角度上下限。膝盖就是这种,它转不过头。
  • continuous:也是转动,但没有限位,可以无限转。轮子是典型。
  • prismatic:滑动关节,沿一条轴平移。
  • fixed:固定,不动。用来把传感器、装饰件死死焊在某个连杆上。

revolute 关节还带一组 limit 参数:角度上下限、速度上限、力矩上限。这几个数字非常关键——它们不只是给仿真器看的,运动规划器也靠它们判断一个姿态可不可行。写错了,规划器会给你算出物理上根本达不到的轨迹。

仓库里有个例子把关节结构说得很清楚。h1_2_descriptionh2_description 的网格文件里,踝部出现了这样一组命名:

left_ankle_A_link.STL
left_ankle_A_rod_link.STL
left_ankle_B_link.STL
left_ankle_B_rod_link.STL
left_ankle_pitch_link.STL
left_ankle_roll_link.STL

A、B 两套 link 加 rod(连杆/推杆),这是连杆机构的典型命名。而 h2_description 目录下同时躺着 H2.urdfH2_loop.urdf 两个文件。loop 一般指闭链(闭合运动链)。这里有个 URDF 的固有限制值得知道:URDF 描述的是一棵树,每个连杆只能有一个父节点,它天然表达不了闭环结构。 我不去猜作者具体是怎么处理的,但从「同一台机器出两份 URDF、其中一份文件名里标着 loop」这个安排看,闭链确实是被单独对待的一件事。

视觉体和碰撞体为什么要分开

这是 URDF 里最容易被新手忽略、又最影响仿真质量的一个设计。

视觉体追求好看,碰撞体追求算得快。物理引擎每一帧都要做大量碰撞检测,如果你直接把几万个三角面的精细模型丢给它当碰撞体,仿真会慢到没法用,而且容易出数值问题——两个复杂网格在接触点上算出的接触力可能剧烈跳变。

所以标准做法是:视觉体用精细网格,碰撞体用简化几何(长方体、圆柱、胶囊,或者面数极少的凸包)。

这个仓库里有两处直接的证据。第一处在 g1_with_brainco_hand/meshes/ 下:

left_knee_link.STL
left_knee_link_simple_collision.STL
right_knee_link.STL
right_knee_link_simple_collision.STL

文件名把话说完了——膝关节有一份正常网格,还有一份专门给碰撞用的简化网格。

第二处更有意思,在 go2_description/urdf/ 目录里,除了 URDF 本身还放了两张图片:

go2_description/urdf/Normal_collision_model.png
go2_description/urdf/Amended_collision_model.png

「常规碰撞模型」和「修正后的碰撞模型」。有人专门截了两张图放进仓库做对比,说明碰撞模型是被单独调过的,而且调完之后觉得这个差异值得让使用者看见。

碰撞体调不好会怎样?腿在仿真里互相穿模、脚底和地面接触时抖个不停、机器人莫名其妙自己弹起来——这些坑基本都出在碰撞几何上。

meshes:三种格式各有各的活

meshes/ 里的文件格式在这个仓库里不统一,而这种不统一本身有信息量。

  • .dae(COLLADA):带材质和贴图信息。a1_description 用的就是 dae,而且 meshes/ 里躺着一张 trunk_A1.png——那是躯干的贴图。aliengo_description 同理,配的是 trunk_uv_base_final.png,文件名里的 uv 是 UV 展开的意思。这类模型在 RViz 和 Gazebo 里看起来有颜色、有 logo。
  • .STL:纯几何,没有材质。a2_descriptionas2_descriptiong1_description 这些较新的目录基本全是 STL。
  • .objb2_description_mujoco/meshes/ 下用的是 obj。

h1_description 把这件事演到了极致——同一个连杆两份文件并排放着:

left_knee_link.STL
left_knee_link.dae
torso_link.STL
torso_link.dae

为什么要存两份?因为下游工具吃的格式不一样。这是一个很现实的工程妥协:与其让每个用户自己转格式,不如仓库里直接备齐。

b2_description_mujoco/meshes/ 里还藏着另一类东西:

fake_head_Link.STL
fake_imu_link.STL
fake_tail_link.STL
unitree_ladar.obj

fake_ 前缀的连杆,通常是为了挂一个坐标系而存在的虚拟连杆。IMU 装在机身某个位置上,你需要知道它相对于机身原点的精确位姿,才能把它测出来的加速度转换到机身坐标系。做法就是在 URDF 里定义一个几乎没有质量的连杆,用 fixed 关节焊在躯干上,位置就是传感器的实际安装位置。它不参与物理,只提供坐标系。

这个技巧在你自己写 URDF 时会反复用到。

xacro:为什么不直接写 URDF

看回 a1_description/xacro/ 那七个文件,答案就在文件名里:

const.xacro          常量:尺寸、质量、限位数值
materials.xacro      材质:颜色定义
leg.xacro            腿:一整条腿的宏
transmission.xacro   传动:关节与执行器的映射
gazebo.xacro         仿真:Gazebo 专用标签与插件
robot.xacro          总装:把上面这些拼起来
stairs.xacro         楼梯:场景物件

四足机器人有四条腿,结构完全一样,区别只是安装位置和左右镜像。如果直接写 URDF,你要把同样的 link 和 joint 定义抄四遍,每条腿三到四个关节,每个关节十几行 XML。抄完之后改一个尺寸,你得在四个地方同步改,改漏一个就是一台歪的机器人。

xacro 是 XML Macro 的缩写,它给 XML 加了三样东西:宏、参数、简单计算。于是 leg.xacro 里定义一次腿的宏,robot.xacro 里调用四次、传不同的位置和镜像参数,就完事了。const.xacro 把所有数值常量集中在一处,改一个数字全身生效。

xacro 不是运行时的东西。它在 launch 阶段被展开成标准 URDF,再交给下游。所以你会发现很多描述包里 xacro/urdf/ 是并存的:xacro 是源码,urdf 是编译产物,给那些不认识 xacro 的工具用。

这个仓库里的组织方式并不统一,反而更真实。a1aliengob2b2wgo1go2 都是 xacro/urdf/ 双目录;b1_description 却把 URDF 直接扔进了 xacro 目录里(xacro/b1.urdf);a2_descriptionas2_description 干脆没有 xacro,只有 urdf/a2.urdf 加 meshes——这两个包看起来是纯模型包,不参与 Gazebo 仿真。

还有一个包值得单独说:aliengoZ1_description。它的内容是这样的:

robots/aliengoZ1_description/
├── CMakeLists.txt
├── package.xml
├── config/robot_control.yaml
├── launch/aliengoZ1_gazebo.launch
├── launch/aliengoZ1_rviz.launch
├── worlds/earth.world
└── xacro/
    ├── const.xacro
    ├── gazebo.xacro
    ├── robot.xacro
    └── stairs.xacro

注意它没有 meshes 目录,也没有 leg.xacro。这是一个四足底盘加机械臂的组合包,几何资源从被组合的那两个包里来。这就是 xacro 的另一个价值:描述包之间可以互相引用,组合出新构型时不用复制任何网格文件。

同样的组合思路在 g1_description 里也能看到。那里有一组 URDF 文件,用后缀区分不同的关节配置和不同的末端装配——g1_23dof.urdfg1_29dof.urdfg1_29dof_lock_waist.urdfg1_29dof_with_hand.urdfg1_dual_arm.urdf 等等,还有一串 mode_11mode_18 的变体。目录里另外放着一个 inspire_hand/ 子目录(里面是几份手部 URDF 加一个 config.yaml),以及一个 Jupyter notebook:

robots/g1_description/merge_g1_29dof_and_inspire_hand.ipynb

用脚本把机身 URDF 和手的 URDF 合并成新文件。手本身在 dexterous_hand_description/ 下有独立的一族描述(dex1_1dex2_5dex3_1dex5_1),甚至还有 Left_Hand_G1_5010_Wrist.urdf 这种已经带上特定手腕的组合件。

URDF 是可组合的——这句话听起来平平无奇,但它意味着模型资产可以像库一样复用,而不是每出一个新构型就手工画一份新模型。

launch 和 config

launch/a1_rviz.launchlaunch/check_joint.rviz,这两个是一对:前者是启动文件,后者是 RViz 的界面配置(存了摄像机视角、显示了哪些图层、TF 树怎么画)。

check_joint 这个名字值得停一秒。它是一个专门用来逐个检查关节的视图配置。写完 URDF 之后你必须验证一遍:每个关节的旋转轴对不对、正方向是不是符合预期、限位卡在哪儿。这活儿在 RViz 里配合关节滑块做最方便。仓库里把这个视图配置存下来共享出去,说明这是一个高频动作。

README 也给了对应命令:

roslaunch laikago_description laikago_rviz.launch

先在 RViz 里把模型看明白,再谈仿真。 这个顺序不能反——模型本身就错的话,你在 Gazebo 里看到的所有诡异现象都会把你引向错误的方向。

顺带一提,launch 文件的命名在这个仓库里也有代际差异:老机型用 <name>_rviz.launchb2b2wh1 这些较新的用 display.launchgazebo.launch。同一个仓库里两套命名习惯并存,这是长期演进的项目的正常样貌。

config/robot_control.yaml 留到下一节讲,它是喂给 ros_control 的。

Gazebo 要凑齐哪几样才能跑

仿真器不是把模型丢进去就能动的。它需要四样东西同时到位:几何、物理、驱动、世界

几何和物理来自 URDF——就是上面说的 visual、collision、inertial。这部分描述包已经给全了。

驱动是关键的一环,也是最容易被忽略的。Gazebo 默认不知道你的关节该由谁来控制。URDF 里的 transmission 标签负责说明「这个关节由哪个执行器驱动、用什么接口」,然后 gazebo_ros_control 这个插件把 Gazebo 的关节和 ros_control 的控制器接起来。这就是为什么每个参与仿真的描述包里都有 transmission.xacrogazebo.xacro——前者定义传动,后者塞 Gazebo 专用标签和插件声明。

世界来自 .world 文件。README 里说 wname 可以是 earthspacestairs,默认是 earthspace 这个名字挺有意思,一个改重力常数的世界文件就能让你看到低重力下的步态是什么样——这是仿真独有的乐趣,真机上做不到。

关于世界文件,README 里还写了一个非常真实的坑。unitree_gazebo/worlds/stairs.world 文件末尾有这么一段:

<include>
    <uri>model:///home/unitree/catkin_ws/src/unitree_ros/unitree_gazebo/worlds/building_editor_models/stairs</uri>
</include>

一个写死的绝对路径,而且是仓库作者机器上的路径。README 明确要求你改成自己机器上的实际路径。这类硬编码路径是 Gazebo 项目里的经典绊脚石,很多人第一次跑楼梯场景时看到的是一片空地,原因就在这儿。

传感器也是插件挂上去的

go1_description 里有一组文件把这件事说得很清楚:

xacro/depthCamera.xacro       xacro/ultraSound.xacro
meshes/depthCamera.dae        meshes/ultraSound.dae

深度相机和超声,各自一个 xacro 宏加一个网格。宏里做两件事:定义传感器的物理外形和安装位置(一个连杆加一个 fixed 关节),以及声明对应的 Gazebo 传感器插件——插件负责在仿真里生成假的点云或测距数据,并发布到 ROS 话题上。

b2_description_mujoco/meshes/unitree_ladar.obj 也是同类东西的痕迹。

传感器作为可插拔的 xacro 宏存在,这个设计的好处是:同一台机器人,你可以出一个带相机的版本和一个不带的版本,只需要在 robot.xacro 里多调用或少调用一次宏。仿真里跑视觉算法、跑激光 SLAM,靠的就是这套机制。想深入这条线的话,可以顺着传感器库那边的内容对照着看。

一个诚实的细节

README 里有句话我觉得特别值得引出来:启动 Gazebo 之后,机器人是趴在地上的,关节没有激活

这不是 bug。仿真器把模型加载进去了,物理引擎开始算了,但没有任何控制器在往关节里送指令,所以它就是一堆被重力拉着的刚体,只能瘫着。你得再开一个终端,把控制器跑起来:

rosrun unitree_controller unitree_servo

它才会站起来。

加载模型和驱动模型是两件独立的事——这个认知很重要。很多人第一次跑仿真看到机器人瘫在地上就以为自己装错了。

README 还提到另一件事,措辞很明确:Gazebo 仿真里能做的是低层控制(力矩、位置、角速度),做不了高层控制,也就是走路。走路那套步态和平衡控制不在这个仿真包的能力范围内。这个边界要记清楚,仓库文档写了什么就是什么,没写的不要脑补。

unitree_controller 里的三个可执行

README 列了三个节点,各有各的意思:

rosrun unitree_controller unitree_servo           # 站立控制
rosrun unitree_controller unitree_external_force  # 施加外部扰动
rosrun unitree_controller unitree_move_kinetic    # 位置与姿态发布

unitree_external_force 这个值得多说一句。README 描述它是「add external disturbances, like a push or a kick」——加一个推力或者踢一脚。

这正是仿真相对真机最大的优势之一:扰动可以精确定量,而且可以完全复现。 你在真机上踹机器狗一脚,力多大、方向多准、作用了多久,全凭手感,而且第二脚不可能和第一脚一样。仿真里这些都是参数,改一个数字重跑,实验条件严格一致。调平衡控制器的时候,这种可复现性是刚需。

unitree_move_kinetic 对应的源文件是 unitree_controller/src/move_publisher.cpp。README 说它演示的是「不用控制器直接控制机器人的位置和姿态」,让机器人绕原点转圈,并且源码里有个 def_frame 变量,改成 coord::ROBOT 再重新 catkin_make,运动就变成在机器人自身坐标系下进行。

这个节点的用途 README 也直说了:对 SLAM 或视觉开发有用。想想就明白——你要调一个建图算法,根本不关心腿怎么迈的,你只想让机身按一条已知轨迹在场景里移动,然后看传感器数据和建图结果。这时候把腿的动力学整个绕开,直接瞬移机身,是最高效的做法。

仿真不一定要仿全套。按需求砍掉不关心的那部分,是很正当的工程决策。

ros_control:controller 和硬件之间的插座

unitree_legged_control 这个包,README 的描述是:包含用于 Gazebo 仿真的关节控制器,允许用户以位置、速度、力矩三种方式控制关节,示例代码在 unitree_controller/src/servo.cpp

它是一个 ros_control 的控制器插件。要理解它在什么位置,得先搞清 ros_control 这套东西的角色划分。

README 的依赖安装命令里,把角色名字全列出来了:

sudo apt-get install \
  ros-melodic-controller-interface \
  ros-melodic-gazebo-ros-control \
  ros-melodic-joint-state-controller \
  ros-melodic-effort-controllers \
  ros-melodic-joint-trajectory-controller

Kinetic 那条命令更长,还多出 controller-managerros-controlros-controllersvelocity-controllersposition-controllersrobot-state-publisher。(仓库文档写的运行环境是 ROS Melodic 或 Kinetic 加 Gazebo8,这是 ROS1 时代的组合,具体版本要求以官方仓库当前文档为准。)

把这些名字翻译成角色,大致是这么几层:

hardware_interface —— 插座。 这是整套设计的枢纽。它定义了一组标准接口:读关节位置、读速度、读力矩,写位置指令、写速度指令、写力矩指令。控制器只认这个接口,不认接口后面是什么。

仿真时,gazebo_ros_control 实现这个接口,读写的是 Gazebo 里的虚拟关节。真机时,厂商的驱动实现这个接口,读写的是真实电机。

同一个控制器,插在不同的插座上,就分别在仿真和真机上跑起来了。 这是 ros_control 存在的全部理由,也是整套 ROS 控制生态最值钱的一个抽象。

controller —— 插头。 上面那串包名里的几种:

  • joint_state_controller:最特殊的一个,它不控制任何东西,只读。把所有关节的当前位置、速度、力矩打包成 sensor_msgs/JointState 消息发到 /joint_states 话题上。它是纯粹的状态出口。
  • effort_controllers:力/力矩控制。足式机器人上用得多,因为腿的柔顺性、落地缓冲这些事必须在力这一层做。
  • position_controllers / velocity_controllers:位置控制和速度控制。
  • joint_trajectory_controller:轨迹控制。你给它一串带时间戳的关节路点,它负责插值执行。机械臂上最常用。

unitree_legged_control 就是宇树自己实现的一个控制器插件,README 说它同时支持位置、速度、力矩三种模式。这类底层关节控制的实现思路,跟机器人 PID 控制那篇讲的是同一件事,只是搬到了 ROS 的插件框架里。

controller_manager —— 配电盘。 负责加载、启动、停止、切换控制器。它读的配置就是每个描述包里那个 config/robot_control.yaml——哪些控制器要加载、每个挂在哪个关节上、增益参数是多少,全在这个 YAML 里。

这就解释了一个之前留下的疑问:为什么控制器的参数文件会放在 description 包里,而不是控制包里?因为参数是跟具体机型走的。A1 的膝关节增益和 B2 的不可能一样,但用的是同一个控制器实现。控制器实现归控制包,控制器参数归描述包,这个划分是对的。

robot_state_publisher —— 翻译官。 它订阅 /joint_states,同时读 URDF,把两者一合,算出每个连杆此刻在空间里的位姿,发布成 TF 树。

这个节点是下一节的主角。

一套 URDF,喂给所有人

到这里,这篇文章真正想说的那件事可以摆出来了。

回头看:URDF 描述了连杆、关节、几何、惯量。这份描述被谁用了?

Gazebo 用它。 加载模型、算物理、算碰撞。

RViz 用它。 但 RViz 不直接读 URDF——它读的是 robot_state_publisher 算出来的 TF 树加上 URDF 里的视觉体。所以 a1_rviz.launch 背后的链条是:URDF 进 robot_state_publisher,关节角度从 /joint_states 来,TF 树出去,RViz 按 TF 摆放各个连杆的网格。你在屏幕上看到的那台机器人,是 URDF 加实时关节角度渲染出来的。

运动学求解器用它。 URDF 里的连杆长度和关节轴向,本身就是一棵完整的运动学树。KDL 这类库可以直接从 URDF 构建运动学模型,正解逆解都不用你手写 DH 参数。如果你手推过机械臂的正逆运动学,就知道这件事省掉了多少力气——机械臂运动学那篇讲的那套推导,在 ROS 里是可以从模型文件自动生成的。

碰撞检测和运动规划用它。 collision 几何直接就是规划器做自碰撞检查的输入。

其他仿真器也用它。 这是最有力的一条证据。

看这个仓库里 MuJoCo 相关的文件:

robots/b2_description_mujoco/xml/b2.xml
robots/b2_description_mujoco/xml/b2_description.urdf
robots/b2_description_mujoco/xml/scene.xml
robots/h1_description/mjcf/h1.xml
robots/h1_description/mjcf/h1_with_hand.xml
robots/h1_description/mjcf/scene.xml
robots/g1_description/g1_29dof.xml
robots/h2_description/H2_loop.xml

MJCF 是 MuJoCo 的模型格式。注意 b2_description_mujoco/xml/b2.xmlb2_description.urdf 是并排放着的——URDF 在旁边,MJCF 从它来。scene.xml 则是 MuJoCo 的场景文件,相当于 Gazebo 那边的 .world:机器人模型加地面、光照、摄像机,组装成一个可运行的场景。

再看另一个仓库 unitree_model。它整个仓库只装一样东西——USD 格式的模型文件:

G1/29dof/usd/g1_29dof_rev_1_0/
├── g1_29dof_rev_1_0.usd
└── configuration/
    ├── g1_29dof_rev_1_0_base.usd
    ├── g1_29dof_rev_1_0_physics.usd
    └── g1_29dof_rev_1_0_sensor.usd

USD 是 Isaac Sim 那条线用的格式。而这个仓库的 README 里有一节标题就叫 「Generate from urdf」(从 URDF 生成),下面给的是 URDF 导入的操作要点:连杆选 Movable Base、关节配置选 Stiffness、驱动类型选 Force、允许自碰撞。

白纸黑字:这些 USD 是从 URDF 转出来的。

而且 USD 那边的目录拆分方式也很值得看一眼——_base / _physics / _sensor 三个文件分层。几何是几何,物理是物理,传感器是传感器。跟 URDF 里 visual / collision+inertial / sensor 的分工是完全同一个思路,只是换了个生态、换了种文件组织方式。

(顺带记一笔:unitree_model 的 README 顶部标注该仓库已废弃,后续更新迁到了 Hugging Face 的数据集。这是仓库自己写的,用之前先看一眼当前状态。)

所以整张图是这样的:

                    URDF / xacro
                    (唯一的事实来源)
                          │
      ┌───────────┬───────┴───────┬────────────┬──────────┐
      ▼           ▼               ▼            ▼          ▼
   Gazebo      RViz          运动学求解      MuJoCo    Isaac Sim
   物理仿真    可视化         KDL / 规划      MJCF        USD

模型定义一次,到处使用。 这是 ROS 生态最大的价值之一,也是为什么哪怕你最后不打算用 ROS 跑生产环境,也值得把 URDF 写规范——它是你的模型资产,能带着你换仿真器、换算法框架。

反过来说,这也解释了为什么 description 包值得花时间做好。质量差的 URDF——惯量瞎填、碰撞体没简化、关节限位不准——污染的不是一个工具,是下游所有工具。

如果你还没建立起对机器人本体结构的直觉,建议先补一下机器人由哪些部分组成,再回来看 URDF 里那些 link 和 joint,会具体很多。

那座桥:unitree_ros_to_real

仿真跑通了,接下来要上真机。这时候你会发现需要一个新的仓库:unitree_ros_to_real

为什么不能直接用? 因为仿真那边关节是被 Gazebo 插件驱动的,而真机上的关节握在机器人自己的实时控制器手里,ROS 根本碰不到。中间必须有一层翻译。

这个包的结构非常紧凑,一共两个 ROS 包:

unitree_legged_msgs/          消息定义
└── msg/
    ├── HighCmd.msg      HighState.msg
    ├── LowCmd.msg       LowState.msg
    ├── MotorCmd.msg     MotorState.msg
    ├── IMU.msg          Cartesian.msg
    ├── BmsCmd.msg       BmsState.msg
    └── LED.msg

unitree_legged_real/          接口实现
├── include/convert.h
├── launch/real.launch
├── launch/keyboard_control.launch
└── src/exe/
    ├── ros_udp.cpp
    ├── example_walk.cpp
    ├── example_position.cpp
    ├── state_sub.cpp
    ├── twist_sub.cpp
    └── control_via_keyboard.cpp

消息定义为什么单独成包

先注意一个容易滑过去的事实:unitree_ros 的依赖列表里明确写着,它需要 unitree_legged_msgs,而这个包在 unitree_ros_to_real 仓库里。

仿真仓库依赖真机仓库的消息定义。

这个方向乍看有点别扭,想通了很合理:消息定义是公共词汇表。仿真侧和真机侧必须说同一种话,否则你在仿真里调好的控制器代码,搬到真机上要重写一遍数据结构。把词汇表抽出来单独成包、两边共用,是 sim2real 能成立的第一步——也是最基础、最容易被跳过的一步。

msg 清单本身也说明了不少事。HighCmd/HighStateLowCmd/LowState 成对出现,这跟 SDK 那边 high_level / low_level 的分野是一致的——unitree_sdk2 架构那篇里也见过同样的划分。MotorCmd/MotorState 是单个电机的颗粒度,是低层控制的基本单位。IMU 是姿态传感,Cartesian 是笛卡尔坐标(足端位置这类),BmsCmd/BmsState 对应电池管理,LED 对应灯效。

一个通信接口暴露了哪些消息类型,基本就框定了它能做什么。

convert.h:桥的核心就一个头文件

unitree_legged_real/include/convert.h。名字直白得可爱——转换。

ROS 消息和 SDK 的 C++ 结构体是两套内存布局,桥接层要做的核心工作就是在这两套之间来回映射。这类代码写起来枯燥,但它是整座桥的承重结构。

src/exe/ros_udp.cpp 则是桥的另一半:跑 UDP 收发的那个节点。README 里说明这套连接是网线直连——把网线插在 PC 和机器人之间,ifconfig 找到端口名,改 ipconfig.sh 里的端口名并执行,让这张网卡拿到静态 IP,进到机器人所在的网段。想让它开机自动生效,就去改 /etc/network/interfaces

这一步是真机开发里卡住新手最多的地方,而且它跟 ROS 一点关系都没有——纯粹是 Linux 网络配置。

一个 launch,两种控制层级

roslaunch unitree_legged_real real.launch ctrl_level:=highlevel
# 或
roslaunch unitree_legged_real real.launch ctrl_level:=lowlevel

ctrl_level 是个参数。你在启动时就要决定这次是走高层还是低层,两者不能混。

对应的示例节点也分两套:高层跑 ros_example_walk(控制行走方向和速度),低层跑 ros_example_postion(控制所有关节)。另外还有 state_sub 用来订阅机器人反馈的状态,以及一个 keyboard_control.launch——README 说它让你能像玩 turtlesim 一样用键盘控制机器人。

src/exe/twist_sub.cpp 这个文件特别值得注意。Twistgeometry_msgs/Twist,ROS 里最通用的速度指令消息——线速度加角速度。订阅 Twist 意味着任何会发 Twist 的上游都能直接驱动这台机器人:键盘遥控节点、手柄节点、move_base 之类的导航栈、甚至你自己写的任何一个规划器。

这是接口对齐的价值。桥接层只要把 Twist 翻译成厂商的高层指令,整个 ROS 生态里成千上万个发 Twist 的东西就全接上了。这也是为什么机器人开发要用 ROS 那篇里反复强调的一点:生态的价值在于接口的统一,不在于某个具体功能包写得多好。

README 里的安全提示,别跳过

这段我原样转述,因为它是安全红线:

在做低层控制之前,README 要求先按 L2+A 让机器人坐下,再按 L1+L2+start 进入可做关节级控制的模式,并且——务必先把机器人吊起来

为什么?因为低层控制是你直接往每个关节写指令。你的代码里一个符号写反、一个增益给大了,机器人就会以最大力矩把自己甩出去。吊起来意味着即使控制器完全失控,它也只是在空中乱蹬,而不是砸向地面或者砸向你。

在仿真里翻车只是重启一下,在真机上翻车是几万块钱和一次事故。 从仿真跨到真机的那一刻,成本函数完全变了。

版本耦合是桥接层的固有代价

README 里有一件事说得很明确:这个包的版本和 unitree_legged_sdk 的某个 release 绑定,而不同的 SDK release 支持的机型不一样,配置时要下载对应版本、放到工作空间的 source 目录下,而且文件夹必须叫 unitree_legged_sdk,不能带版本后缀(因为 CMake 里是按这个名字找的)。文件树里那个空的 unitree_legged_sdk 条目和根目录的 .gitmodules,说明 SDK 是以 git 子模块的形式挂进来的。

具体哪个版本对哪个机型,以官方仓库当前文档为准——这类对应关系会变,记死了反而害人。

要理解的是背后的原理:桥接层必然强耦合它桥接的两端。 一端是 ROS 的消息与 launch 体系,一端是厂商 SDK 的私有协议。任何一端改了,桥就得跟着改。这是这类包的固有代价,不是设计缺陷。

把这个仓库当教材读

如果你想系统学 ROS 机器人开发,这个仓库是个不错的样本。给一条读法:

第一步,只读一个 description 包。a1_description,把 xacro/ 下七个文件全打开看一遍。const.xacro 看常量怎么组织,leg.xacro 看宏怎么写和怎么处理镜像,robot.xacro 看总装怎么调用宏,transmission.xacro 看关节和执行器怎么绑定。这一步看懂了,你就能自己写 URDF 了。

第二步,在 RViz 里把模型看明白。check_joint.rviz 那个视图,拖每个关节的滑块,确认旋转轴和限位符合你的预期。这一步是所有后续工作的地基。

第三步,再进 Gazebo。 记住 README 说的:起来时机器人是趴着的,得单独跑控制器才会站。跑 unitree_external_force 推它一把,看看控制器的抗扰能力——这是仿真最好玩也最有用的部分。

第四步,读 ros_control 的配置。 打开 config/robot_control.yaml,对照上面讲的角色划分,看清楚哪个控制器挂在哪个关节上、参数怎么给的。

第五步,最后才是真机。unitree_ros_to_real,从 convert.hros_udp.cpp 入手。上电之前把 README 的安全提示读三遍。

有几件事需要提前知道:这个仓库是 ROS1 血统——catkin_make、Melodic/Kinetic、Gazebo8,都是 ROS1 时代的东西。如果你的目标是 ROS 2,接法不一样,那条线我们在这一卷的其他文章里单独讲。另外 README 有一句实在话也一并转述:如果 catkin_make 遇到依赖问题,再跑一次通常就好了——包之间的编译顺序在首次构建时可能没排对。

最后,回到开头那个问题:拿到任何一个陌生的 ROS 机器人仓库,怎么快速定位?

  1. 先按名字分三堆*_description 是模型,带 gazebo/sim 的是仿真,带 control/controller 的是驱动。
  2. 进 description 包,先找 URDF 或 xacro,那是这台机器的身体定义,也是所有下游工具的共同输入。
  3. meshes/ 里有没有简化碰撞网格——有,说明作者认真对待过物理仿真;没有,你可能要自己补。
  4. config/ 下的 YAML,控制器参数在那儿,机型差异也在那儿。
  5. 看有没有单独的 msgs 包,有的话它就是这套系统的公共词汇表,值得第一个读。
  6. 找桥接包,名字里带 realhardwaredriverbridge 的,那是从仿真通往真机的路。

想动手做点什么的话,不必上来就啃足式机器人。机器人与具身智能卷里有几个用 ESP32 就能做的项目,差速驱动小车是个很好的起点——把小车的 URDF 自己写一遍、在 RViz 里看它转起来,你对本文讲的这套东西的理解会牢固得多。等这些基础打通了,再回头读工业级仓库,很多当时看不懂的目录安排会突然变得理所当然。

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明