1. 四足仿真这件事,为什么值得认真搭一次
做足式机器人的人应该都有过这种体验:算法理论上跑得好好的,一上真机就各种翻车,电机过流、机身侧翻、关节限位报错,一次调试下来,不是修代码而是修机器人。Unitree A1这类四足狗虽然已经是市面上性价比很高的开源平台,但真机价格仍然摆在那里,摔倒、撞击、暴力降落的代价都不小。如果每改一个参数都要上真机验证,开发节奏会慢得让人怀疑人生。
所以我一直坚持一个流程:先在Gazebo里把仿真环境搭到“足够接近真机”,再带着可信的仿真结果上真机。这里说的“足够接近”,不是指动力学参数完全复刻,而是指自由度数、关节配置、质量分布、控制接口这些结构性的东西要和真机一致。A1有12个自由度,每条腿3个,如果你的仿真模型只有8个自由度或者关节轴方向定义错了,那后面跑强化学习、模型预测控制、状态估计,全部都是在错误的地基上盖楼。
这篇文章做的事情,就是把从零搭建Unitree A1的Gazebo仿真环境的完整过程拆开讲清楚。核心内容包括:用URDF描述A1的机械结构、用Xacro宏来优雅地管理四足这种高重复度的模型文件、以及打通Gazebo仿真中“加载模型—启动控制器—关节可控”的完整链路。适合刚入门ROS但已经知道基本概念的同学,也适合那些被URDF模板吓到、不知道从哪下手的人。
先说清楚这篇文章的边界。我会用一份简化但结构完整的A1模型,不走官方unitree_ros仓库的“拿来主义”,而是从零定义坐标系和关节,这样你才能真正理解每个参数的含义。等你自己能写出这个模型,再去看官方仓库或者任何一款四足机器人的URDF,都会觉得非常轻松。
2. 环境选型和安装:版本搭配决定你能少踩多少坑
2.1 ROS版本与Gazebo版本怎么选
接触ROS的人一定听过“版本地狱”这种说法。Ubuntu、ROS、Gazebo三个版本的兼容关系基本决定了你后续要花多少时间在装环境上。
绝大多数四足机器人相关的开源代码、运动控制库、LQR/MPC示例,都是基于ROS Noetic或更早的ROS Kinetic写的。Unitree官方仓库的ROS 1分支也维护得比较全。这方面我的建议很直接:如果你不是有明确理由必须用ROS 2,首选Ubuntu 20.04 + ROS Noetic + Gazebo 11,这条组合的教程数量最多,遇到问题最容易搜到答案。
当然,ROS 2 Humble/Iron也在快速普及,如果你的项目已经基于ROS 2,那Ubuntu 22.04 + Humble + Gazebo 11也是可以跑的,只是需要自己适配一些原来基于ROS 1的接口。另外我看到最近已经有人在Ubuntu 24.04上折腾ROS 2 Jazzy + Gazebo Harmonic(也就是新版的gz-sim),这个方向是未来主流,但目前资料偏少,新手不建议一上来就挑战这个组合。
2.2 鱼香ROS一键安装到底解决了什么问题
这里必须提到“鱼香ROS一键安装”这个工具。很多教程一上来就是让你添加ROS apt源、配置公钥、再apt update,一套操作下来如果网络状况不好,可能卡在下载阶段。鱼香ROS脚本做的就是把这些步骤自动化了,它会检测你的Ubuntu版本,选择合适的ROS发行版,顺便把Gazebo也装上。
安装方式很简单,不需要逐条执行apt命令:
wget http://fishros.com/install -O fishros && bash fishros运行之后会有一个交互式选项菜单,选“一键安装ROS”,它会自动判断系统版本。实测下来,在干净系统上装Noetic + Gazebo 11大概需要十几分钟,取决于网络。安装完ROS后再跑一次脚本,它会提示你把哪些环境变量写进~/.bashrc,基本做到开箱即用。
不过我还是要补充一句:一键安装脚本帮你完成了95%的工作,剩下5%的坑它没法帮你避开。比如安装完成后,你至少要做一次下面的验证,确认环境真的可用:
source /opt/ros/noetic/setup.bash roscore看到“started core service”之类信息就说明ROS基础环境正常。再验证Gazebo:
gazebo如果Gazebo能弹出一个空世界窗口,且没有报libGL相关的错误,那环境基本就绪。
2.3 Gazebo界面闪烁问题的真相
搜索资料的时候能看到很多人问“为什么Gazebo界面一直在闪”。我在虚拟机和物理机上各踩过一次。如果你用的是VMware或VirtualBox,且没有开启3D加速,Gazebo的渲染窗口大概率会闪烁、撕裂甚至黑屏。这不是Gazebo本身的问题,而是OpenGL渲染没有可用的硬件加速。
虚拟机用户先检查虚拟机设置里有没有打开“加速3D图形”。如果不想依赖硬件加速,可以试试强制使用软件渲染:
export LIBGL_ALWAYS_SOFTWARE=1 gazebo物理机如果也闪,优先怀疑显卡驱动。NVIDIA用户装好官方驱动后基本能解决;集成显卡用户遇到问题的概率小很多。对纯仿真用途来说,不考虑视觉传感器、不需要高帧率画面的话,软件渲染其实也能凑合跑,只是CPU占用会高一些。
3. URDF建模拆解:A1机器狗的关节拓扑到底长什么样
3.1 从机械结构到URDF的抽象
先把A1的腿结构讲明白。每条腿有3个关节:髋关节横滚(hip roll)、髋关节俯仰(hip pitch)和膝关节俯仰(knee pitch)。四条腿乘以3,一共12个自由度。很多人容易忽略的是,髋关节这里其实叠了两个电机,一个管侧向摆动,一个管前后摆动,而不是像传统的单自由度关节那样只绕一个轴转。
在URDF里,一个link对应一个刚体,joint连接两个link并定义它们之间的相对运动。A1的模型可以拆成这样:
base_link ├── FL_hip_link ── FL_thigh_link ── FL_calf_link ├── FR_hip_link ── FR_thigh_link ── FR_calf_link ├── RL_hip_link ── RL_thigh_link ── RL_calf_link └── RR_hip_link ── RR_thigh_link ── RR_calf_link每个关节的类型都是revolute,也就是有限角度旋转,需要给<limit>限制关节范围,否则Gazebo里关节会转飞。
3.2 坐标系的约定
URDF建模最关键的一步是坐标系的定义。我习惯把base_link的坐标系定为:x轴朝前,y轴朝左,z轴朝上。这个约定符合ROS的标准,也更方便后续做运动学。
在这个坐标系下,右前腿的hip关节原点大概在base_link前方0.18到0.19米、左侧负方向0.045米左右的位置。注意,A1的hip roll关节绕x轴旋转,hip pitch和knee pitch绕y轴旋转。很多人在这一步搞混,导致模型加载后腿部扭曲,原因就是轴定义和机械结构对不上。
我的建议是:先画一张纸上的坐标系图,标出每个关节轴的朝向,再开始写URDF。这一步花十分钟,能省下后面调试的一整天。
3.3 一个完整的机身与单腿URDF片段
下面是一个可以运行的简化版base_link和单腿描述。为了控制篇幅,我先展示核心结构:
<link name="base_link"> <visual> <geometry> <box size="0.36 0.14 0.12"/> </geometry> <origin rpy="0 0 0" xyz="0 0 0"/> </visual> <collision> <geometry> <box size="0.36 0.14 0.12"/> </geometry> </collision> <inertial> <mass value="6.0"/> <inertia ixx="0.02" ixy="0" ixz="0" iyy="0.03" iyz="0" izz="0.03"/> </inertial> </link> <joint name="FL_hip_joint" type="revolute"> <origin xyz="0.18 0.045 0" rpy="0 0 0"/> <parent link="base_link"/> <child link="FL_hip_link"/> <axis xyz="1 0 0"/> <limit effort="33" lower="-0.5" upper="0.5" velocity="10"/> <dynamics damping="0.2" friction="0.0"/> </joint> <link name="FL_hip_link"> <visual> <geometry> <box size="0.05 0.05 0.04"/> </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>这里有个细节:<inertial>是Gazebo仿真必不可少的。如果你的URDF只写了<visual>和<collision>,Rviz里能正常显示模型,但一进Gazebo就会出问题,要么模型直接散架,要么因为惯性张量为零导致Gazebo崩掉或物理行为诡异。
接着是大腿和小腿的部分。大腿关节绕y轴旋转,位置就在hip_link的原点,然后大腿本体向下延伸0.21米,膝关节在大腿末端:
<joint name="FL_thigh_joint" type="revolute"> <origin xyz="0 0 0" rpy="0 0 0"/> <parent link="FL_hip_link"/> <child link="FL_thigh_link"/> <axis xyz="0 1 0"/> <limit effort="33" lower="-2.0" upper="2.0" velocity="10"/> <dynamics damping="0.2" friction="0.0"/> </joint> <link name="FL_thigh_link"> <visual> <geometry> <box size="0.04 0.04 0.21"/> </geometry> <origin rpy="0 0 0" xyz="0 0 -0.105"/> </visual> <inertial> <mass value="1.2"/> <inertia ixx="0.004" ixy="0" ixz="0" iyy="0.004" iyz="0" izz="0.0005"/> </inertial> </link> <joint name="FL_calf_joint" type="revolute"> <origin xyz="0 0 -0.21" rpy="0 0 0"/> <parent link="FL_thigh_link"/> <child link="FL_calf_link"/> <axis xyz="0 1 0"/> <limit effort="33" lower="-2.5" upper="0" velocity="10"/> <dynamics damping="0.2" friction="0.0"/> </joint>小腿部分的结构跟大腿类似,只是长度改成0.2米。写完这一条腿,你会发现剩下三条腿的逻辑完全一样,只是坐标位置和正负号不同。如果直接复制粘贴四遍,以后改一个长度参数就要改四个地方,而且极容易漏改。这就是引入Xacro的核心动机。
4. Xacro宏复用:四足模型最怕的就是重复,而宏就是解药
4.1 为什么要用Xacro而不是纯URDF
URDF本质是XML格式,它不支持变量、条件判断、循环。你写一条腿没问题,写四条腿就开始痛苦,写到按键布局相似的传感器支架、末端执行器,几乎是在做重复劳动。
Xacro(XML Macros)就是为了解决这个问题出现的。它的核心思路是:把URDF中重复的部分抽象成“宏”,用属性定义可复用的参数,用数学表达式计算坐标,一次定义、四处调用。
对四足机器人来说,Xacro几乎是必需品。A1这样的四足结构,四条腿的拓扑完全一致,只是安装位置和镜像方向不同。用宏定义好一条腿后,四条腿的代码可以缩减到四行调用。
4.2 用属性管理关键尺寸
先把所有需要之后统一调整的尺寸定义成属性:
<xacro:property name="body_length" value="0.18" /> <xacro:property name="body_width" value="0.045" /> <xacro:property name="thigh_length" value="0.21" /> <xacro:property name="calf_length" value="0.2" /> <xacro:property name="hip_mass" value="0.5" /> <xacro:property name="thigh_mass" value="1.2" /> <xacro:property name="calf_mass" value="0.4" />这里我把body_length和body_width定义成半长半宽,也就是从机体中心到髋关节的距离。这样做的好处是调用时只需要传正负号,比如body_length是0.18米,那么前腿的x坐标就是+0.18,后腿就是-0.18。
属性用${}包裹的表达式来引用,支持四则运算。比如大腿惯量张量里的iyy,我可以直接写${thigh_mass * thigh_length * thigh_length / 12},而不是手动算出一个硬编码数字。这样以后再改质量或长度,惯量会自动跟着变,不会出现“质量改了但惯量还是旧值”的低级错误。
4.3 把单腿封装成宏
宏的语法是<xacro:macro>,用params指定参数列表。我这里设计了四个参数:prefix是腿的前缀(FL/FR/RL/RR),x_sign和y_sign分别控制前后、左右方向的正负,mirror_pitch用来处理左右腿pitch方向的差异。
一个完整的腿宏内部结构如下:
<xacro:macro name="unitree_leg" params="prefix x_sign y_sign"> <!-- 髋关节横滚 --> <joint name="${prefix}_hip_joint" type="revolute"> <origin xyz="${x_sign * body_length} ${y_sign * body_width} 0" rpy="0 0 0"/> <parent link="base_link"/> <child link="${prefix}_hip_link"/> <axis xyz="1 0 0"/> <limit effort="33" lower="-0.5" upper="0.5" velocity="10"/> <dynamics damping="0.2" friction="0.0"/> </joint> <link name="${prefix}_hip_link"> <visual> <geometry> <box size="0.05 0.05 0.04"/> </geometry> </visual> <collision> <geometry> <box size="0.05 0.05 0.04"/> </geometry> </collision> <inertial> <mass value="${hip_mass}"/> <inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/> </inertial> </link> <!-- 髋关节俯仰 --> <joint name="${prefix}_thigh_joint" type="revolute"> <origin xyz="0 0 0" rpy="0 0 0"/> <parent link="${prefix}_hip_link"/> <child link="${prefix}_thigh_link"/> <axis xyz="0 1 0"/> <limit effort="33" lower="-2.0" upper="2.0" velocity="10"/> <dynamics damping="0.2" friction="0.0"/> </joint> <link name="${prefix}_thigh_link"> <visual> <geometry> <box size="0.04 0.04 ${thigh_length}"/> </geometry> <origin rpy="0 0 0" xyz="0 0 ${-thigh_length / 2.0}"/> </visual> <collision> <geometry> <box size="0.04 0.04 ${thigh_length}"/> </geometry> <origin rpy="0 0 0" xyz="0 0 ${-thigh_length / 2.0}"/> </collision> <inertial> <mass value="${thigh_mass}"/> <inertia ixx="${thigh_mass * thigh_length * thigh_length / 12}" ixy="0" ixz="0" iyy="${thigh_mass * thigh_length * thigh_length / 12}" iyz="0" izz="0.0005"/> </inertial> </link> <!-- 膝关节俯仰 --> <joint name="${prefix}_calf_joint" type="revolute"> <origin xyz="0 0 ${-thigh_length}" rpy="0 0 0"/> <parent link="${prefix}_thigh_link"/> <child link="${prefix}_calf_link"/> <axis xyz="0 1 0"/> <limit effort="33" lower="-2.5" upper="0.0" velocity="10"/> <dynamics damping="0.2" friction="0.0"/> </joint> <link name="${prefix}_calf_link"> <visual> <geometry> <box size="0.035 0.035 ${calf_length}"/> </geometry> <origin rpy="0 0 0" xyz="0 0 ${-calf_length / 2.0}"/> </visual> <collision> <geometry> <box size="0.035 0.035 ${calf_length}"/> </geometry> <origin rpy="0 0 0" xyz="0 0 ${-calf_length / 2.0}"/> </collision> <inertial> <mass value="${calf_mass}"/> <inertia ixx="${calf_mass * calf_length * calf_length / 12}" ixy="0" ixz="0" iyy="${calf_mass * calf_length * calf_length / 12}" iyz="0" izz="0.0002"/> </inertial> </link> </xacro:macro>写完宏之后,在<robot>标签内部调用四次:
<xacro:unitree_leg prefix="FL" x_sign="1.0" y_sign="1.0" /> <xacro:unitree_leg prefix="FR" x_sign="1.0" y_sign="-1.0" /> <xacro:unitree_leg prefix="RL" x_sign="-1.0" y_sign="1.0" /> <xacro:unitree_leg prefix="RR" x_sign="-1.0" y_sign="-1.0" />到这里,整个模型的JSON结构已经相当清晰。整个URDF文件只需要几十行调用代码,加上全局的link和joint定义,可读性和可维护性比复制粘贴好了不止一个量级。
4.4 左右腿的俯仰方向差异:一个容易忽略的细节
这里要特别注意一个问题。左右腿在机械结构上是镜像对称的,如果只改变hip roll关节的位置的正负号,而hip pitch和knee pitch的旋转轴方向不变,那么左腿和右腿的“向前摆腿”方向是相反的。
处理方式有两种:一是给宏增加一个mirror参数,当为1.0时,pitch关节的axis的y分量取反;二是在定义左右腿时,用不同的关节限位和初始位置来补偿。第一种方式更通用,也更好理解。在宏内部可以这样判断:给宏加一个pitch_sign参数,把<axis xyz="0 1 0"/>改成<axis xyz="0 ${pitch_sign} 0"/>,左腿传1.0,右腿传-1.0。
有读者可能会问,那关节限位不也反了吗?其实不会。因为你限位表达的还是“关节转角”,而不是“世界坐标系下的绝对角度”。限位是相对于关节自身的零位定义的,所以即使旋转轴方向取反,限位值本身不需要变化,只是正方向代表的空间运动方向取反。
不过下面的内容里,为了让初次搭建能够跑通,我会暂时用左右对称的简单方式:即左右腿pitch轴方向保持一致,只在初始装配时通过y方向正负区分左右。这样模型在Gazebo里可以正常站立和摆动,等后面做运动学时会再去精细化处理镜像逻辑。
5. 从URDF到Gazebo仿真:材质、传动、控制器一个都不能少
5.1 Gazebo中的材质与外观
URDF的<visual>标签里可以定义<material>,但在Rviz里显示的材质和Gazebo里显示的并不完全是一回事。很多时候你会遇到“Rviz里是灰色的,Gazebo里却是纯白色”或者“颜色泛蓝泛紫”的情况。
想让Gazebo里的模型颜色可控,最稳妥的方式是给每个link单独添加<gazebo>标签:
<gazebo reference="base_link"> <material>Gazebo/DarkGrey</material> </gazebo>同理,四条腿的link也可以分别指定颜色。注意Gazebo的材质名称用的是它内置材质库的名字,比如Gazebo/Red、Gazebo/Blue、Gazebo/White。如果你想要自定义RGB,可以用下面的方式:
<gazebo reference="FL_thigh_link"> <material> <ambient>0.1 0.1 0.1 1</ambient> <diffuse>0.2 0.2 0.8 1</diffuse> <specular>0.0 0.0 0.0 0</specular> </material> </gazebo>看起来繁琐,但确实是Gazebo里控制颜色和材质反射最直接的办法。
5.2 transmission和gazebo_ros_control插件
URDF描述的是机器人的运动学结构,但要让关节在Gazebo里真正被控制,必须做两件事:一是定义<transmission>,把joint和actuator对应起来;二是加载gazebo_ros_control插件,让Gazebo和ROS的控制器框架对接。
<transmission>的写法比较固定,每个关节一个:
<transmission name="FL_hip_trans"> <type>transmission_interface/SimpleTransmission</type> <joint name="FL_hip_joint"> <hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface> </joint> <actuator name="FL_hip_motor"> <hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface> <mechanicalReduction>1</mechanicalReduction> </actuator> </transmission>mechanicalReduction是减速比。A1的电机大多有减速器,但我们在仿真里如果不需要精确还原电机侧转速,保持1即可,这样控制量直接对应到关节输出。如果后面要做更精细的动力学仿真,再把这个参数改成真实值。
然后在<robot>标签内加载插件:
<gazebo> <plugin name="gazebo_ros_control" filename="libgazebo_ros_control.so"> <robotNamespace>/</robotNamespace> </plugin> </gazebo>插件加载后会读取URDF里所有的transmission定义,并在ROS端暴露一个control manager的接口。没有这个插件,你发多少ROS话题,关节都不会动弹。
5.3 控制器配置:位置控制还是力矩控制
A1真机在底层支持位置和力矩两种控制模式,仿真里也建议保留同样的接口。
我用的方案是在controllers.yaml里同时配置一个joint_state_controller和一组position_controllers/JointGroupPositionController。前者负责发布每个关节的角度、速度、力矩反馈,后者接收位置指令并完成闭环。
joint_state_controller: type: joint_state_controller/JointStateController publish_rate: 100 a1_position_controller: type: position_controllers/JointGroupPositionController joints: - FL_hip_joint - FL_thigh_joint - FL_calf_joint - FR_hip_joint - FR_thigh_joint - FR_calf_joint - RL_hip_joint - RL_thigh_joint - RL_calf_joint - RR_hip_joint - RR_thigh_joint - RR_calf_joint pid_gains: FL_hip_joint: {p: 100.0, i: 0.0, d: 0.2} FL_thigh_joint: {p: 150.0, i: 0.0, d: 0.5} FL_calf_joint: {p: 150.0, i: 0.0, d: 0.5}这里值得说一下PID参数的选择。底层的单关节PD控制器,p值决定“刚度”,d值决定“阻尼”。p太小,关节软绵绵,没法支撑机身重量;p太大,关节会剧烈振荡,模型在Gazebo里看起来像帕金森患者。初始建议大腿和膝盖p给到100到200,d给到0.2到0.5,然后在仿真里观察模型的稳定情况再微调。注意:不同版本的gazebo_ros_control对PID参数读取方式略有差别,如果你的控制量没有生效,先用rqt_gui查看控制器状态里有没有加载PID参数。
5.4 初始位姿与重力下的自平衡
把模型spawn进Gazebo时,初始高度很关键。如果从零高度开始,模型会和地面发生穿透,产生非常大的接触力,直接把模型弹飞。所以我会设置一个略高于理论站立高度的初始位置,让模型自然下落然后稳定下来。A1大腿加小腿总长约0.41米,站立时髋关节高度约为0.3米左右,所以初始z可以给0.35到0.4米。
在launch文件里,spawn_model节点用-z参数控制初始高度:
<node name="spawn_model" pkg="gazebo_ros" type="spawn_model" args="-urdf -param robot_description -model a1 -z 0.4" />如果没有设置合适的初始姿态,模型一进入Gazebo就可能侧翻、趴下甚至飞出去。这个时候不要慌,先看三条线索:是否所有关节的角度都从0开始、是否有惯性参数缺失的警告、初始高度是否足够。大多数情况下,把这三件事处理好,模型就能稳稳落地。
6. 把一切串起来:launch文件、验证流程与高频故障排查
6.1 完整launch文件怎么写
现在把前面所有零件拼装成一个可运行的launch文件。核心步骤有三个:一是用xacro把模型解析成robot_description参数,二是启动Gazebo空世界,三是把模型spawn进去。
<launch> <param name="robot_description" command="$(find xacro)/xacro '$(find unitree_a1_description)/urdf/a1.xacro'" /> <include file="$(find gazebo_ros)/launch/empty_world.launch"> <arg name="paused" value="false"/> <arg name="use_sim_time" value="true"/> <arg name="gui" value="true"/> </include> <node name="spawn_model" pkg="gazebo_ros" type="spawn_model" output="screen" args="-urdf -param robot_description -model a1 -z 0.4" /> <rosparam file="$(find unitree_a1_gazebo)/config/controllers.yaml" command="load" /> <node name="controller_spawner" pkg="controller_manager" type="spawner" output="screen" args="joint_state_controller a1_position_controller" /> </launch>这里有一个常见报错:很多人找不到empty_world.launch。这是因为没有安装gazebo_ros包,或者包路径没有配置。在Noetic下可以这样安装:
sudo apt install ros-noetic-gazebo-ros-pkgs ros-noetic-gazebo-ros-control装完source一下,再重新launch。
6.2 验证模型是否真的被“控制”了
模型加载完成不代表仿真环境就通了。我每次搭建完一个新的仿真模型,都会按下面的顺序做一遍验证:
第一,看模型在Gazebo世界里的形态。用鼠标旋转视角,检查每条腿是否完整,颜色是否正确,有没有link之间穿模。穿模通常意味着碰撞体尺寸或坐标写错了。
第二,在终端里查看关节状态话题是否在发布:
rostopic echo /joint_states如果这个话题有数据,说明joint_state_controller已经工作。用rqt_robot_monitor能更直观地看每个关节的角度、速度、力矩值。
第三,发布一个位置指令,让所有关节回到一个中间姿态。比如让大腿抬起来一点:
rostopic pub /a1_position_controller/command std_msgs/Float64MultiArray "data: [0, 0.5, -1.0, 0, 0.5, -1.0, 0, 0.5, -1.0, 0, 0.5, -1.0]"命令格式是12个浮点数,按你在controllers.yaml里定义的关节顺序排列。如果模型的大腿和小腿确实动了,并且最终停在指定角度,说明从URDF到Xacro到控制器的全链路已经打通。
6.3 高频故障与排查经验
故障一:模型加载后直接黑屏或消失。原因非常集中——要么是材质设置问题,要么是惯性参数缺了。URDF文件里如果漏了<inertial>,Gazebo会忽略该link的碰撞和视觉属性,导致模型看起来不完整。解决办法是逐个检查所有link的inertial,确保质量大于0且惯性张量所有项非负。
故障二:模型掉落地面后关节一直抖动。这是最折磨人的问题。首要原因是关节阻尼太小,模型落地后关节在重力作用下产生振荡。把<dynamics damping>从0改到0.2到0.5,情况会明显改善。其次检查PID中d值,如果d为0,关节相当于没有速度阻尼,也容易抖。实在搞不定的时候,有个偷懒的土办法:把关节阻尼调大,同时把PID的d也调大,模型会变得“黏稠”,虽然手感偏钝,但至少能稳定,后续再逐步降低数值。
故障三:关节反响或者转向不一致。定义左右腿时axis取反后,发现运动方向反了。这不一定是代码错误,而是关节坐标轴和限位的关系。检查<limit>的lower和upper是否匹配你的轴方向。如果轴方向取反,lower和upper也应该对应调整,否则可能出现“明明指令是正向旋转,关节却朝limit的另一头跑”。
故障四:仿真速度很慢或CPU占用极高。Gazebo默认物理步长是1毫秒,但如果你没有精简碰撞体,或者机器配置一般,就会卡得不行。我的做法是把带复杂mesh的link换成简单几何体(box、cylinder、sphere),只在视觉上保留mesh。另外,<gazebo>里可以设置<max_step_size>0.005</max_step_size>和<real_time_factor>0.5</real_time_factor>来降低仿真精度和加速比,对于纯运动学调试足够了。
7. 环境搭完以后,还能往哪个方向扩展
A1仿真环境搭好之后,你可以做的事情远超“让模型站着不动”。
最直接的方向是运动控制算法的仿真验证。Unitree官方开源了简易的步态控制器,你可以把它接到你刚搭好的模型上,跑一套trot步态。如果步态抖、迈步别扭,先不要怀疑算法,回头检查你的URDF参数——很多“算法问题”其实是模型参数和真机不一致导致的。比如大腿长度差了1厘米,在步态控制器里体现为落脚点位置偏了,最后你会发现是URDF写错了。
再进一步,可以接入强化学习训练框架。现在四足RL训练的主流路径是Isaac Gym或MuJoCo,但Gazebo的优势在于ROS生态、传感器仿真和控制器接口的完整性。把Gazebo里训练好的控制策略通过ROS话题转发到真机,中间只需要改一个话题名称和消息格式,这种迁移路径非常平滑。
如果你之前用过SolidWorks这类CAD软件,导出URDF时也要注意:SolidWorks导出的URDF往往会把所有link的坐标系设成CAD中的装配坐标,不会为每个关节单独建立子坐标系。这导致的结果是Gazebo里模型能显示,但关节轴方向乱七八糟。解决办法很简单:在URDF里手动添加<gazebo reference>标签,重新定义每个link的质量、惯性、材质,用xacro的数学表达式统一计算坐标偏移,而不是依赖CAD导出的原始值。
另外,现在CoppeliaSim(V-REP)以及Blender也能导入URDF模型。如果你的项目需要和机械臂、移动底盘共用一个仿真场景,URDF作为中间格式的优势就很明显。一个模型文件,Gazebo、Rviz、CoppeliaSim、Isaac Sim都能吃进去,只是各平台对传感器、控制器插件的要求略有不同。
最后再多说一句实操层面的经验。我见过太多人卡在“模型加载了但动不了”这一步,反复怀疑是控制器配置问题,结果发现是launch文件里spawn_model的-z参数没给够,模型在地面以下,被碰撞引擎疯狂挤压。遇到奇怪物理现象,第一反应永远应该是:检查初始位姿、检查碰撞体、检查惯性参数。这三样没问题,再往控制层面排查。
按这套思路走下来,你不仅能拥有一台能在仿真里站住的A1,更关键的是建立起一套“机械结构到URDF,URDF到仿真控制”的完整认知。这个认知一旦建立,以后看任何一款机器人的模型文件,你都会觉得像是在看一份带标注的机械图纸。