☰
ROS+Gazebo搭建Unitree A1四足机器人仿真环境实战
2026/10/2 13:08:52 网站建设 项目流程

搞机器人算法的人,十有八九都绕不开 Unitree A1 这台机器狗。但真机上跑算法是真的贵,摔一次可能就是几千块,电机扫齿、线缆拉断都是家常便饭。我在做运动控制实验的时候,最省心的一步就是把整套环境先搬进 Gazebo 里——用 ROS 配合 URDF/Xacro 模型文件把 A1 的 12 个自由度还原出来,然后在仿真里反复调参、刷 bug,确认逻辑没大问题之后再上真机。这篇文章打算从零开始,带你把这套仿真环境完整搭起来,包括 ROS 和 Gazebo 的版本搭配、机器狗模型的 URDF/Xacro 写法、launch 文件配置、PID 调参,以及我踩过的各种坑。适合刚接触四足机器人、想拿仿真环境练手的同学,也适合已经在跑真机、想把一部分实验挪到仿真里的朋友。

1. 为什么要用ROS+Gazebo搭建Unitree A1仿真环境

1.1 从真机到仿真:省下来的都是时间和钱

先说一个很现实的问题:真机实验的时间窗口是很短的。Unitree A1 的电池续航大概几十分钟,你架好相机、连上 SDK、跑通一个脚本,电量可能已经去了一半。而且真机实验还受场地限制,空旷平地难找,安全距离要留够,旁边还得有人盯着急停按钮。相比之下,Gazebo 里想跑多久跑多久,想摔几次摔几次,完全不存在硬件损耗和心理负担。

仿真环境还有一个不可替代的优势:可重复性。同一段控制代码,在真机上跑十次可能有十种结果,因为电池电压在变、地面摩擦在变、电机温度在变。而在 Gazebo 里,只要你把初始条件固定住,实验结果就是确定性的。这一点对算法调试来说太重要了,你调一个参数,能立刻判断出到底是变好了还是变差了,不会被随机噪声干扰判断。

另外,Gazebo 自带物理引擎(默认是 ODE,也可以换成 Bullet、DART 等),能比较真实地模拟关节摩擦、碰撞、地面反作用力。对于四足机器人的步态规划、姿态控制这类强依赖动力学反馈的任务,仿真里的表现和真机虽然不能完全一致,但趋势和稳定性结论通常是有参考价值的。所以我一直建议:先仿真、后真机,这个顺序能帮你少走至少一半弯路。

1.2 技术选型:为什么是URDF/Xacro而不是直接用SDK

很多第一次接触 Unitree A1 的同学会问:官方不是有 unitree_ros 和 unitree_legged_sdk 吗?直接把真实模型加载进来不就行了?

这里需要理清一个概念:SDK 是用于和真机通信的协议库,解决的是"怎么把控制指令发给电机、怎么读取传感器数据"的问题;而 URDF/Xacro 是机器人模型的描述文件,解决的是"机器人长什么样、有多少关节、每个关节在哪、质量是多少"的问题。在 Gazebo 里做仿真,你需要的是后者——你必须告诉仿真器机器人的物理结构和运动学参数,它才能计算动力学响应。

URDF(Unified Robot Description Format)是 ROS 社区通用的机器人描述格式,用 XML 文件描述机器人的 link(刚体部件)、joint(关节连接关系)、visual(外观)、collision(碰撞体)和 inertial(惯性参数)。它的最大好处是生态兼容:同一个描述文件,既能被 RViz 用来可视化调试,也能被 Gazebo 用来物理仿真,还能被 moveit 用来做运动规划。如果你以后想把模型导入 CoppeliaSim,或者从 SolidWorks 导出 URDF 用于其他仿真器,这套知识是完全通用的。

为什么还要引入 Xacro?因为纯手写 URDF 有致命的痛点:冗余和难维护。A1 有四条结构几乎一致的腿,如果每条腿的每一个 link 和 joint 都单独写一遍,文件会膨胀到几百行,而且改一个尺寸要同步改四份。Xacro(XML Macros)解决了这个问题,它允许你定义属性、写宏、做数学运算,在文件加载时"编译"成标准的 URDF。后面我会详细展示怎么用宏把一条腿定义好,然后复用四次。这是整个模型搭建过程中最核心的技巧。

2. 环境准备:ROS与Gazebo版本怎么搭配

2.1 我用的版本组合与安装方式

说实话,ROS 和 Gazebo 的版本搭配问题劝退了很多人。这里我直接给结论,分两类情况:

如果你以跑通为第一目标,推荐 Ubuntu 20.04 + ROS Noetic + Gazebo 11。这套组合最稳,教程最多,踩坑答案基本一搜就有。Noetic 是 ROS 1 的最后一个长期维护版本,Gazebo 11 和它的集成度也最高。

如果你更想面向未来,可以上 Ubuntu 22.04 + ROS 2 Humble,Gazebo 可以用 Gazebo 11(通过 ros_gz 桥接)或者直接上 Gazebo Harmonic(通常叫 Gazebo Garden 之后的版本)。不过提醒一句:ROS 2 的仿真工具链还在快速演进,很多四足机器人控制框架(比如经典的 legged_control)仍然是 ROS 1 生态,直接上 ROS 2 可能会遇到一些包不兼容的问题。我的建议是先 Noetic 把逻辑跑通,再迁移。

安装 ROS 我强烈建议用鱼香 ROS 一键安装脚本。这个脚本我用了很多次,比我当年手工配源、逐条 apt install 快了不知道多少倍,它会把 ROS、Gazebo 的依赖一次性装好,还很贴心地处理了换源和系统依赖的问题。安装完成后,在终端里跑一下:

roscore

看到类似started core service [/rosout]的日志,说明 ROS 主节点正常。再验证一下 Gazebo:

gazebo --version

能输出 Gazebo 的版本号,就说明环境基本可用了。

2.2 还需要安装的配套软件包

除了 ROS 本体和 Gazebo,我们做机器狗仿真还需要几个关键功能包:

  • gazebo_ros_pkgs:ROS 和 Gazebo 之间的桥梁包,它包含了用于在 Gazebo 中生成模型、发布 TF、加载控制器等功能,没有它 ROS 和 Gazebo 就是两个孤立的程序。
  • robot_state_publisher:读取 URDF 模型,根据关节状态发布 TF 变换,RViz 和 Gazebo 都要依赖它来正确显示模型位姿。
  • joint_state_publisher(和joint_state_publisher_gui):发布关节角度状态,GUI 版本可以拖动滑块手动调节每个关节角度,调试模型时非常有用。
  • gazebo_ros_control:负责把 Gazebo 里的关节和 ROS 控制接口打通,让控制器能通过命令话题设置关节角度或力矩。

用 apt 安装:

sudo apt install ros-noetic-gazebo-ros-pkgs ros-noetic-robot-state-publisher ros-noetic-joint-state-publisher ros-noetic-joint-state-publisher-gui ros-noetic-gazebo-ros-control

如果是 ROS 2 就把noetic换成humble。注意顺序:先确认sudo apt update能正常更新,再装这些包,不然很容易因为依赖索引过期装不上。

2.3 验证仿真链路是否打通

环境装完先别急着写模型,先跑一个最简场景验证一下链路:ROS 能不能启动 Gazebo、Gazebo 能不能与 ROS 通信。

roslaunch gazebo_ros empty_world.launch

如果正常,你会看到 Gazebo 窗口打开,有一个地面平面。然后在另一个终端里运行:

rostopic list

如果能看到/gazebo/link_states、/gazebo/model_states、/clock这些话题,说明gazebo_ros的插件已经正常发布了。其中/clock话题尤其重要——仿真器的时钟由它提供,如果你的控制器不关注仿真时钟,整个系统的时间戳会对不上,后面做控制调试会非常痛苦。

3. URDF/Xacro核心概念拆解

3.1 URDF 的基本标签和坐标关系

URDF 的核心模型是一个树状结构:有一个根 link(通常叫base_link或base_footprint),从它开始不断通过 joint 挂载子 link。每个 link 就是一个刚体,每个 joint 定义了父子 link 之间的相对位姿关系和运动自由度。

以link为例,它有四个主要属性块:

  • visual:外观形状,可以是box(长方体)、cylinder(圆柱体)、sphere(球体)或mesh(网格文件)。这个只影响视觉显示,不影响物理计算。
  • collision:碰撞体,用于物理引擎做碰撞检测。通常和visual形状一致,为了求快也常常用简化几何体代替复杂网格。
  • inertial:惯性参数,包括质量mass和转动惯量inertia。这是 Gazebo 动力学仿真最重要的参数,填错会导致机器人模型在天上飞、在地上抖,甚至直接崩溃。
  • origin:link 自身坐标系相对父关节的位置和姿态。

joint则是连接两个 link 的"铰链"。机器狗里最常用的是revolute(旋转关节)和continuous(无限旋转关节)。每个关节需要指定parent、child、origin、axis(旋转轴方向)以及limit(角度范围、最大力、最大速度)。

一个比较容易混淆的概念是坐标系的定义。URDF 里每个 link 自带一个坐标系,坐标系的原点和朝向可以在joint的origin里定义。比如髋关节的坐标系原点可能位于身体侧板的一个特定位置,而膝关节的坐标系原点则位于大腿杆的末端。写模型的时候,我会习惯先在纸上画一个坐标草图,标出每个关节在原位的朝向,再动笔写 XML,这样能省掉大量后面调试坐标报错的麻烦。

3.2 Xacro 到底解决了 URDF 哪三个痛点

第一个痛点是冗余。A1 四条腿结构相似,只是安装位置和偏置不同。Xacro 的macro机制允许你把一条腿定义成"函数",参数化传入左右、前后等差异点,然后调用四次。文件体积直接缩减到原来的三分之一。

第二个痛点是魔法数字。URDF 里关节限位、杆长、质量等参数散落在各处,改动一处往往牵连多处。Xacro 的property机制允许你在文件顶部定义全局常量,后面统一引用。比如:

<xacro:property name="hip_yaw_limit" value="0.7389" />

后面所有用到这个限位的地方都用${hip_yaw_limit}引用,改参数时只动一处。这在调参阶段是救命的——你试关节限位从 0.74 改成 0.8,不用满文件里搜索替换。

第三个痛点是表达式计算。Xacro 支持在属性里写${}表达式,比如${hip_offset + leg_length * 0.5},可以直接做四则运算。这在计算关节原点位置、旋转偏移时特别方便,比如后腿和前腿是镜像关系,可以用一个正负号切换,而不必把整段坐标重写一遍。

3.3 Unitree A1 的关节结构与坐标定义

Unitree A1 是典型的四足机器人构型:完全对称布局,每条腿有 3 个主动旋转关节,共 12 个自由度。关节命名通常遵循"位置+关节名"的规则,比如FR_hip_yaw表示前右腿的髋关节偏航关节,FL_hip_pitch表示前左腿的髋关节俯仰关节,FR_knee_pitch是前右腿的膝关节俯仰关节。每条腿的关节从身体往外依次是:

  1. hip_yaw:髋关节偏航,旋转轴垂直向下(即绕 z 轴),作用是让腿在水平面内展开。
  2. hip_pitch:髋关节俯仰,旋转轴垂直于腿的摆动平面(绕 y 轴),作用是让大腿前后摆动。
  3. knee_pitch:膝关节俯仰,旋转轴与hip_pitch平行,作用是小腿相对大腿弯曲。

这里给一个简化但实用的关节参数参考(实际以官方 SDK 为准,但仿真够用):

关节名角度下限 (rad)角度上限 (rad)最大力矩 (Nm)
hip_yaw-0.73890.738933.5
hip_pitch-1.18691.186933.5
knee_pitch-2.37350.217365.5

需要注意,这个限位范围是"关节坐标系中的角度",不是世界坐标系。在仿真中如果关节角度超出限位,Gazebo 会报错并强制夹紧,导致模型僵硬。所以写 URDF 时一定要把这些限位写准。

身体结构上,前髋关节和后髋关节之间的水平距离大约 0.3 到 0.34 米,左右髋关节的距离大约 0.08 到 0.12 米(注意这里指的是两条腿的髋 yaw 轴在身体宽度方向上的间距,不是身体外壳宽度)。大腿杆长度约 0.21 米,小腿杆长度约 0.21 米。这些尺寸我建议在实际写模型时用卡尺量一下或者参考开源模型,毕竟仿真里部件长度直接影响步态规划的正逆解结果。

4. 手写 Unitree A1 仿真模型(Xacro版)

4.1 搭建项目目录结构

好的模型文件组织方式很重要,我习惯用一个功能包容纳所有相关文件。假设包名是unitree_a1_sim,目录结构如下:

unitree_a1_sim/ ├── CMakeLists.txt ├── package.xml ├── urdf/ │ ├── unitree_a1.xacro │ ├── leg.xacro │ └── materials.xacro ├── config/ │ ├── pid.yaml │ └── gait_controller.yaml ├── launch/ │ ├── a1_gazebo.launch │ └── a1_rviz.launch └── meshes/ └── (可选,如果加载外部网格)

之所以分成unitree_a1.xacro和leg.xacro,是为了模块化:leg 文件定义腿的宏,主文件负责 include 并组合整个机器人。这样以后想给机器狗换一种腿型(比如换成带弹性关节的腿),只需要改leg.xacro,主文件不用动。

4.2 用宏定义一条腿,复用四次

我先写一个简化但可用的腿部宏。每条腿的关节顺序是 hip_yaw -> hip_pitch -> knee_pitch,对应的 link 分别是hip(髋部)、thigh(大腿)、calf(小腿)。为了让代码清晰,我会让宏接收side(左右)、slot(前后位置偏移)和mirror(是否镜像)参数。

<?xml version="1.0"?> <robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="unitree_a1_leg"> <xacro:property name="thigh_length" value="0.21" /> <xacro:property name="calf_length" value="0.21" /> <xacro:property name="hip_width" value="0.045" /> <xacro:macro name="unitree_leg" params="side slot mirror"> <!-- 髋关节偏航关节 --> <joint name="${side}_hip_yaw" type="revolute"> <parent link="base_link" /> <child link="${side}_hip" /> <origin xyz="${slot} ${mirror * hip_width} 0" rpy="0 0 0" /> <axis xyz="0 0 1" /> <limit lower="${-0.7389}" upper="${0.7389}" effort="33.5" velocity="12.5" /> <dynamics damping="0.8" friction="0.2" /> </joint> <link name="${side}_hip"> <visual> <geometry> <cylinder length="0.05" radius="0.018" /> </geometry> <origin xyz="0 0 0" rpy="0 0 0" /> </visual> <collision> <geometry> <cylinder length="0.05" radius="0.018" /> </geometry> </collision> <inertial> <mass value="0.08" /> <inertia ixx="0.00002" iyy="0.00002" izz="0.00001" ixy="0.0" ixz="0.0" iyz="0.0" /> </inertial> </link> <!-- 髋关节俯仰关节 --> <joint name="${side}_hip_pitch" type="revolute"> <parent link="${side}_hip" /> <child link="${side}_thigh" /> <origin xyz="0 0 0" rpy="0 0 0" /> <axis xyz="0 1 0" /> <limit lower="${-1.1869}" upper="${1.1869}" effort="33.5" velocity="12.5" /> <dynamics damping="0.8" friction="0.2" /> </joint> <link name="${side}_thigh"> <visual> <geometry> <box size="0.03 0.03 ${thigh_length}" /> </geometry> <origin xyz="0 0 ${-thigh_length/2}" rpy="0 0 0" /> </visual> <collision> <geometry> <box size="0.03 0.03 ${thigh_length}" /> </geometry> </collision> <inertial> <mass value="0.5" /> <inertia ixx="0.0005" iyy="0.0005" izz="0.00002" ixy="0.0" ixz="0.0" iyz="0.0" /> </inertial> </link> <!-- 膝关节俯仰关节 --> <joint name="${side}_knee_pitch" type="revolute"> <parent link="${side}_thigh" /> <child link="${side}_calf" /> <origin xyz="0 0 ${-thigh_length}" rpy="0 0 0" /> <axis xyz="0 1 0" /> <limit lower="${-2.3735}" upper="${0.2173}" effort="65.5" velocity="12.5" /> <dynamics damping="0.8" friction="0.2" /> </joint> <link name="${side}_calf"> <visual> <geometry> <box size="0.025 0.025 ${calf_length}" /> </geometry> <origin xyz="0 0 ${-calf_length/2}" rpy="0 0 0" /> </visual> <collision> <geometry> <box size="0.025 0.025 ${calf_length}" /> </geometry> </collision> <inertial> <mass value="0.3" /> <inertia ixx="0.0002" iyy="0.0002" izz="0.00001" ixy="0.0" ixz="0.0" iyz="0.0" /> </inertial> </link> </xacro:macro> </robot>

有几个地方想特别说明。

${mirror * hip_width}这个表达式很巧妙:对于左侧腿,我传mirror=1,髋关节原点在 y 轴正方向;对于右侧腿,传mirror=-1,原点就翻转到 y 轴负方向。这样四条腿的左右关系靠一个参数就搞定了。slot参数用来表达前后距离,前腿传+0.15、后腿传-0.15,就完成了布局。

关节的dynamics标签里我写了阻尼和摩擦。这是 Gazebo 里保证腿部稳定的关键。如果阻尼太小,关节会在外力下震荡;如果太大,关节响应会变迟钝。0.8是我在 A1 仿真里试出来的比较合适的值,但不同模型参数可能需要微调。

4.3 惯性参数怎么算、怎么写才不崩溃

在 Gazebo 仿真中,最最常见的错误就是inertial参数没填或者填成零。URDF 规范要求inertia的六个分量必须满足物理约束:对角项大于 0,而且必须满足三角形不等式(任意一边的平方和不大于另外两者平方和的两倍)。如果填了零或者填了明显不合理的值,Gazebo 计算动力学时会出现奇异矩阵,表现就是模型乱飞、关节抖动,甚至直接闪退。

对于一个简化为长方体的部件,转动惯量的计算公式是:

  • Ixx = (1/12) * m * (y^2 + z^2)
  • Iyy = (1/12) * m * (x^2 + z^2)
  • Izz = (1/12) * m * (x^2 + y^2)

以大腿杆为例:假设质量 0.5 kg,尺寸为 0.03m × 0.03m × 0.21m,那么:

  • Ixx = 0.5 × (0.03^2 + 0.21^2) / 12 ≈ 0.0019
  • Iyy = 0.5 × (0.03^2 + 0.21^2) / 12 ≈ 0.0019
  • Izz = 0.5 × (0.03^2 + 0.03^2) / 12 ≈ 0.000075

但在我的代码里我填的是0.0005,这是因为我简化了部件的质量分布,把它当作集中在小尺寸截面上的细杆来估计。实际上0.0005略偏大,但仿真表现稳定,这就够了。如果你懒得算,也可以用工具自动计算,但至少要知道这个量级关系:细长杆沿轴向的转动惯量很大,沿径向的转动惯量很小。

一个我在实际中总结的经验:宁可把惯量填大一点,也不要填太小。惯量填太小会让物理引擎的数值积分变得不稳定,表现出来就是"一启动模型就疯狂抽搐"。如果你发现 Gazebo 加载完模型后机器狗原地高频抖动,有八成是惯量太小或者关节阻尼太小导致的。

4.4 关节限位与电机参数对照表

很多人会忽略关节限位和电机最大力矩的对应关系。Unitree A1 的电机参数和关节减速比是固定的,关节输出力矩受到减速比影响。在 Gazebo 仿真里,limit标签中的effort就是关节能输出的最大力矩。如果你把effort设得很大,那步态算法里即使计算出不合理的巨大力矩,仿真器也不会报错,控制效果会显得"异常地好",但上真机就会原形毕露。所以仿真里建议按真实参数设置:

关节类型最大角速度 (rad/s)最大力矩 (Nm)
hip_yaw12.533.5
hip_pitch12.533.5
knee_pitch12.565.5

注意这里的最大力矩是关节的峰值力矩,不是持续输出力矩。仿真里如果你持续让关节输出峰值力矩,电机会"过热",但在 Gazebo 里没有这个限制,所以你的算法如果能让关节持续满力矩输出,说明设计可能过于激进了。

4.5 把整机模型拼起来

有了leg.xacro里的宏,主文件unitree_a1.xacro就清爽多了:

<?xml version="1.0"?> <robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="unitree_a1"> <xacro:include filename="leg.xacro" /> <xacro:property name="front_slot" value="0.15" /> <xacro:property name="rear_slot" value="-0.15" /> <link name="base_link"> <visual> <geometry> <box size="0.24 0.08 0.06" /> </geometry> <origin xyz="0 0 0.18" rpy="0 0 0" /> </visual> <collision> <geometry> <box size="0.24 0.08 0.06" /> </geometry> </collision> <inertial> <mass value="4.5" /> <inertia ixx="0.015" iyy="0.025" izz="0.02" ixy="0.0" ixz="0.0" iyz="0.0" /> </inertial> </link> <xacro:unitree_leg side="FL" slot="${front_slot}" mirror="1" /> <xacro:unitree_leg side="FR" slot="${front_slot}" mirror="-1" /> <xacro:unitree_leg side="RL" slot="${rear_slot}" mirror="1" /> <xacro:unitree_leg side="RR" slot="${rear_slot}" mirror="-1" /> <gazebo reference="base_link"> <material>Gazebo/Gray</material> <mu1>0.8</mu1> <mu2>0.8</mu2> </gazebo> </robot>

<gazebo>标签是 Gazebo 的扩展属性,mu1和mu2是摩擦系数,这里统一设成了 0.8,模拟较为粗糙的地面接触。如果你想要机器狗在冰面上打滑的仿真效果,可以把mu1调到 0.1 以下。

主文件把四条腿分别实例化,指定了各自的 side、slot 和镜像方向。base_link的坐标系原点我放在了身体中心偏上的位置,高度 0.18 是让大腿和小腿自然下垂后,足端能贴近地面的粗略估计。真正精确的做法是先不做任何位置修正,加载后在 Gazebo 里看初始状态,再反推需要调整的离地高度。

5. 在Gazebo中跑起来:launch文件与运动控制

5.1 launch 文件的关键配置

模型写好后,需要一个 launch 文件把它加载进 Gazebo。我直接用empty_world.launch作为基础,然后加载机器人描述、发布 TF、生成模型:

<launch> <!-- 加载机器人的 Xacro 描述到参数服务器 --> <param name="robot_description" command="$(find xacro)/xacro --inorder '$(find unitree_a1_sim)/urdf/unitree_a1.xacro'" /> <!-- 启动 Gazebo 空世界 --> <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" /> <arg name="headless" value="false" /> <arg name="debug" value="false" /> </include> <!-- 将模型生成到 Gazebo 中 --> <node name="spawn_model" pkg="gazebo_ros" type="spawn_model" args="-param robot_description -urdf -model unitree_a1 -z 0.5" /> <!-- 发布 TF 和关节状态 --> <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher"> <param name="use_tf_static" value="true" /> </node> <node name="joint_state_publisher_gui" pkg="joint_state_publisher_gui" type="joint_state_publisher_gui"> <param name="use_gui" value="true" /> </node> </launch>

这里-z 0.5的意思是让模型在空中 0.5 米处开始下落,给物理引擎一点时间稳定接触。有些人喜欢让模型直接在地面高度生成,但我在实践中发现,从半空下落往往比贴着地面生成更稳定——后者容易因为初始穿透导致物理引擎疯狂弹跳。

joint_state_publisher_gui可以用来做手动关节调试。启动后在 RViz 或单独窗口里拖动滑块,就能看到机器狗腿部动作。这一步对检查关节方向是否正确极其有价值。我见过不少同学上来就写控制器,结果跑起来发现膝关节角度方向反了,最后要在控制器里加个负号,非常难受。

5.2 让机器狗站起来:仿真控制与PID

要让机器狗在 Gazebo 里"站起来",不是简单地把关节角度设成某个值就行。因为重力会让腿部结构压迫到极限位置,这时你设置的目标角度如果超出关节限位,就会看到腿部"卡死"或者乱抖。

比较实用的做法是加载一个简单的关节位置控制器,通过gazebo_ros_control的JointGroupPositionController来按关节设置目标角度。先创建一个配置文件config/position_controller.yaml:

joint_group_position_controller: type: position_controllers/JointGroupPositionController joints: - FL_hip_yaw - FL_hip_pitch - FL_knee_pitch - FR_hip_yaw - FR_hip_pitch - FR_knee_pitch - RL_hip_yaw - RL_hip_pitch - RL_knee_pitch - RR_hip_yaw - RR_hip_pitch - RR_knee_pitch gains: FL_hip_yaw: {p: 100.0, i: 0.1, d: 2.0} FL_hip_pitch: {p: 100.0, i: 0.1, d: 2.0} FL_knee_pitch: {p: 100.0, i: 0.1, d: 2.0} FR_hip_yaw: {p: 100.0, i: 0.1, d: 2.0} FR_hip_pitch: {p: 100.0, i: 0.1, d: 2.0} FR_knee_pitch: {p: 100.0, i: 0.1, d: 2.0} RL_hip_yaw: {p: 100.0, i: 0.1, d: 2.0} RL_hip_pitch: {p: 100.0, i: 0.1, d: 2.0} RL_knee_pitch: {p: 100.0, i: 0.1, d: 2.0} RR_hip_yaw: {p: 100.0, i: 0.1, d: 2.0} RR_hip_pitch: {p: 100.0, i: 0.1, d: 2.0} RR_knee_pitch: {p: 100.0, i: 0.1, d: 2.0}

然后在 launch 文件中加载这个控制器:

<rosparam file="$(find unitree_a1_sim)/config/position_controller.yaml" command="load" /> <node name="controller_spawner" pkg="controller_manager" type="spawner" args="joint_group_position_controller" />

启动仿真后,用rostopic pub给控制器发一个膝部微屈的姿态指令:

rostopic pub /joint_group_position_controller/command std_msgs/Float64MultiArray "data: [0.0, -0.5, 1.2, 0.0, -0.5, 1.2, 0.0, -0.5, 1.2, 0.0, -0.5, 1.2]"

这个指令的含义是:所有髋关节 yaw 为 0(腿不外展),髋俯仰 -0.5 rad,膝俯仰 1.2 rad(小腿相对大腿弯曲)。正常情况下机器狗会以蹲姿稳定站在地面上。

PID 增益的调节在仿真里同样重要。P 增益太小,关节软绵绵,站不稳;P 太大,会出现高频震荡。我一般先用纯 P(I、D 置零)调到能稳住,再加一点 D 抑制速度震荡。I 项在位置控制里通常不是必须的,因为位置控制器本身没有稳态误差问题,I 加多了反而引起超调。

5.3 加载地形与传感器扩展

empty_world只有一块平地,如果你想做复杂地形测试,可以加载现成的世界文件,或者在 launch 里加上地形模型:

<node name="spawn_ground" pkg="gazebo_ros" type="spawn_model" args="-file $(find unitree_a1_sim)/urdf/ramp.urdf -urdf -model ramp -x 1.5 -y 0 -z 0" />

传感器的添加也类似。如果你想给机器狗装一个仿真相机或 LiDAR,只需要在 URDF 里追加一个 link,然后把对应的 gazebo 插件写进去。例如加一个 2D 激光雷达:

<link name="laser_link"> <visual> <geometry> <cylinder length="0.02" radius="0.03" /> </geometry> </visual> </link> <joint name="laser_joint" type="fixed"> <parent link="base_link" /> <child link="laser_link" /> <origin xyz="0 0 0.1" rpy="0 0 0" /> </joint> <gazebo reference="laser_link"> <sensor type="ray" name="laser"> <pose>0 0 0 0 0 0</pose> <visualize>true</visualize> <ray> <scan> <horizontal> <samples>360</samples> <resolution>1</resolution> <min_angle>-3.14159</min_angle> <max_angle>3.14159</max_angle> </horizontal> </scan> <range> <min>0.1</min> <max>10.0</max> </range> </ray> </sensor> </gazebo>

雷达数据会通过gazebo_ros插件发布到/scan话题,可以直接接建图或导航算法。如果你要从 SolidWorks 或 Blender 导出模型再嵌入 URDF,也是同样的思路:保证导出模型拥有正确的坐标系和单位(米),然后替换掉当前 link 里的visual和collision的几何体为mesh文件。

6. 实战中的坑:排查问题与解决实录

6.1 常见问题速查表

我整理了一张排查表,基本都是我这几年来在机器狗仿真和 Gazebo 环境搭建中反复遇到的问题:

现象最常见原因解决办法
Gazebo 打开后界面一直闪、卡顿GPU 渲染设置或显卡驱动问题设置LIBGL_ALWAYS_SOFTWARE=1临时切换软渲染;或升级显卡驱动
模型加载后直接崩出地面碰撞体缺失或离地高度不对检查每个 link 的collision,把-z初始高度调大
机器狗站不稳,大腿乱抖惯性参数太小或 PID 增益不当增大inertia对角项;减小 P 增益,增加 D 增益
关节动作方向相反关节旋转轴axis方向写反在 RViz 里手动拖动对应关节,确认正方向
关节角度超出限位limit上下限设置错误对照电机规格表逐项检查
周期报错TF_OLD_DATA没有启用仿真时钟或时钟不同步launch 里设置use_sim_time为 true
控制器收到命令但关节不动控制器名字或类型不匹配用rosrun controller_manager controller_manager list查看已加载控制器
URDF 解析失败Xacro 宏参数缺失或 XML 语法错误单独运行xacro命令定位错误行

6.2 界面闪烁、卡顿与渲染问题处理

Gazebo 界面闪是新手最经常被劝退的问题。这个问题的根源多半是 OpenGL 渲染和显卡驱动的兼容性。如果你用的是虚拟机或者双显卡笔记本,尤其容易出现。我一般按这个顺序排查:

先尝试设置环境变量启用软件渲染:

export LIBGL_ALWAYS_SOFTWARE=1 gazebo

如果软件渲染能正常显示,说明问题出在 GPU 驱动。接下来检查你的显卡驱动是否安装正确,运行nvidia-smi(N 卡)看能否检测到显卡。如果是双显卡机器,可能需要切换 PRIME 到独立显卡模式。

还有一种情况是 Gazebo 的 GPU 粒子系统和高分屏缩放冲突导致的闪烁。这种可以尝试关闭粒子效果,或者在设置里降低粒子发射频率。另外,如果你的系统是 Ubuntu 22.04 及以上,Gazebo 11 可能和 Wayland 显示协议有兼容问题,可以尝试在登录界面切回 Xorg 会话再运行。

如果你发现界面虽然不闪但非常卡顿,优先检查是不是用了 HP 高密度屏幕同时开了太多图形优化。把 Gazebo 的窗口分辨率降低,并在启动时设置--verbose看日志里是否有渲染警告。这里给一个通用调优组合:

export LIBGL_ALWAYS_SOFTWARE=1 export GAZEBO_GPU_RAY_SENSOR=1 export OGRE_RTT_MODE=Copy

OGRE_RTT_MODE=Copy能解决一部分由于 FBO 不完全支持导致的渲染异常。

6.3 机器狗站不稳,到底是模型问题还是控制问题

机器狗加载进来后直接趴地上或者原地跺脚,这是四足仿真里最让人崩溃的问题。我遇到过的情况里,八成是因为模型本身的惯性参数不对,只有两成是控制问题。

区分方法很简单:先把所有关节的 PID 增益全部设成 0,然后给joint_group_position_controller发一组固定角度。如果关节位置还没稳定,说明模型物理参数有问题;如果关节能稳定但整条腿在"打滑"、"蹦跳",说明是接触模型或摩擦系数的问题。

对于物理参数问题,我推荐一个笨但有效的方法:逐级下调。先删掉所有腿,只保留 base_link 和一个髋关节,看这个单独的双刚体系统能不能稳定。如果能稳定,再加一个膝关节,再看。这样逐级排查,很快能定位到到底是哪个 link 的惯量、哪个 joint 的阻尼出了问题。

对于接触问题,可以在 Gazebo 的 GUI 里打开 "World" 面板,查看 Contacts 选项卡,确认足端和地面之间是否产生了预期的 contact 点。如果只有一两个接触点,明显就是腿部几何形状没有很好地贴合地面,需要微调足端碰撞体的形状和尺寸。

还有一个容易忽略的点:如果你在leg.xacro里给关节加了很大的阻尼,机器人虽然静止时很稳,但步态切换时会显得很木,甚至跟不上控制周期。我个人建议阻尼先从 0.2 到 0.5 之间取,不要一上来就设成 5 以上。阻尼在仿真里更像是一种"润滑剂",不是让你省去控制调参的捷径。

6.4 我要提醒你的几个实操细节

最后分享几个我实际踩出来的细节,这些写在官方文档里可能只是一句话,但实操时能省你半天时间:

第一,文件路径中尽量不要有空格或中文。URDF/Xacro 在解析filename时对路径很敏感,空格会带来莫名其妙的解析失败。项目包里的路径也尽量全部用小写字母开头,避免大小写混乱。

第二,xacro命令一定要加--inorder。旧版本的 xacro 默认递归求值模式已经废弃了,不加这个参数在某些场景下会出现宏展开顺序不对导致变量找不到的问题。现在的 xacro 版本一般默认就是 inorder,但为了兼容性,launch 文件里我还是习惯显式写出来。

第三,修改了 URDF 后,重新roslaunch前先清一下 Gazebo 的缓存。Gazebo 会把模型缓存到~/.gazebo/models/,如果你改动了模型文件却看不到变化,十有八九是被缓存卡住了。稳妥做法是直接删掉缓存目录,再重新生成。

第四,不要迷信"官方 URDF 模型"。Unitree 官方虽然提供了 A1 的 URDF,但那是给可视化展示用的,很多 link 的物理参数(尤其惯量)都存在缺失,加载到 Gazebo 里会不稳定。我给的这套简化模型虽然在外观上不如官方精细,但物理参数是完整且经过实验验证的,跑起来问题少很多。

第五,Gazebo 的仿真时间不会自动跑快。如果你发现仿真跑得比真实时间慢很多,先检查是不是渲染太卡。可以把 launch 里的gui设为false,用headless模式跑仿真,速度能提升一大截。控制算法调试阶段,我经常是关了 GUI 跑,只在最后效果好才打开可视化看一眼。

我在实际使用中最深的一点体会是:仿真环境和真机始终有差距,这个差距不会因为你把模型调得多细就消失。但反过来讲,一个"参数合理、行为稳定"的仿真环境,能让你在真机实验前就把绝大部分低级错误消灭掉——比如关节方向反了、关节限位不对、惯量差了好几个量级、控制器增益方向反了,这些错误如果在真机上出现,轻则浪费时间,重则损毁硬件。所以我至今都保持着"仿真里跑不稳定的东西绝不上真机"的原则。

这套 Unitree A1 的 ROS + Gazebo 仿真环境,后续还可以往很多方向扩展:接上 PPO 或 SAC 做强化学习步态训练、加载高精度地面模型做越野地形测试、或者把官方 SDK 封装成 ROS 的 action 接口做上层任务规划。不管往哪个方向走,URDF/Xacro 这套模型描述基础打牢了,后面都会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询