ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于ROS 2与URDF的四足机器人仿真:从模型构建到关节运动

基于ROS 2与URDF的四足机器人仿真:从模型构建到关节运动 最近机器人赛道的热度一直很高宇树科技作为国内四足机器人领域的代表性企业屡次进入大众视野。很多读者对四足机器人的第一印象是“酷炫”“能跑能跳”但真正接触后会发现这背后并不是单一算法能搞定的而是一整套机器人工程体系。本文就不聊资本和商业层面了我们把目光聚焦到技术本身从环境搭建开始手写一个简易四足机器人 URDF 模型在 ROS 2 仿真环境中让它的腿部关节动起来。整个过程会涉及工作空间、功能包、话题通信、机器人描述文件、RViz 可视化等核心知识。无论你是刚接触机器人开发的学生还是想从传统后端转向机器人方向的开发者这套实操流程都可以作为入门的第一站。1. 机器人开发背景与核心概念1.1 四足机器人背后的技术体系四足机器人之所以能实现行走、奔跑、越障等动作是因为它融合了机械结构、电机驱动、传感器融合、运动控制、状态估计和环境感知等多个技术模块。从硬件上讲一条腿往往包含髋关节和膝关节每个关节都有电机、减速器和编码器从软件上讲机器人需要实时读取关节角度、机身姿态计算每个电机的目标力矩同时还要通过激光雷达或视觉相机感知环境。工程上常用的技术栈包括底层控制一般使用 MCU 或实时内核通过 CAN、EtherCAT 等总线驱动关节电机。上层算法使用 C 或 Python完成运动学、动力学、步态规划、状态估计。通信与调度使用 ROS / ROS 2 作为中间件连接各个独立节点。仿真验证在 Gazebo、Isaac Sim、MuJoCo 等环境中先行验证算法再迁移到真机。本文不会深入到电机驱动那层而是把重点放在 ROS 2 的软件层。我们用简化的四足机器人模型在 RViz 中完成关节运动的可视化验证。这个过程虽然不能让你立刻造出一台真实机器狗但它能把机器人开发中“模型描述、话题通信、节点编写、可视化调试”这条主链路完整跑通。1.2 ROS 在机器人开发中扮演什么角色ROS 的英文全称是 Robot Operating System虽然名字里有“操作系统”但它本质上是一个分布式的机器人软件开发框架。它提供了进程间通信、硬件驱动、功能包管理、工具链等一系列能力。ROS 2 是第二代版本相比 ROS 1 改进很大。它基于 DDSData Distribution Service通信协议支持实时性更好的节点通信也支持多机分布式部署。在机器人产品中ROS 2 已经成为事实上的标准中间件。开发中我们经常接触几个核心概念节点Node一个可执行程序负责某一类具体功能。话题Topic节点间异步通信的通道适合传感器数据、控制指令等持续流式数据。服务Service同步请求-响应通信适合触发一次性指令。动作Action适合耗时较长的任务例如机械臂抓取。参数Parameter节点运行时可配置的参数。本文的实战会重点用到话题通信和节点。四足机器人的关节状态就是通过话题发布的RViz 和 robot_state_publisher 节点则负责把关节状态转换成可视化的机器人模型位姿。1.3 URDF 与机器人描述文件URDFUnified Robot Description Format是一种基于 XML 的机器人描述格式。它用 link 描述机器人的连杆用 joint 描述连杆之间的连接关系。每个 joint 必须明确 parent link 和 child link也要定义关节类型、旋转轴、坐标变换和运动范围。RViz 显示机器人模型时读取的就是 URDF 文件中的信息。而 robot_state_publisher 节点负责订阅真实的关节角度数据并结合 URDF 计算出每个 link 在世界坐标系下的位置和姿态最终发布出 TF 变换。所以在本文的实战里我们要做三件事编写一个简化四足机器人的 URDF 文件。编写一个发布关节角度的 Python 节点。通过 robot_state_publisher 和 RViz 观察机器人腿部运动。2. 环境准备与版本说明2.1 操作系统与硬件建议ROS 2 目前对 Ubuntu 系统的支持最好。不同 Ubuntu 版本对应不同的 ROS 2 发行版本文示例使用的是 Ubuntu 22.04 和 ROS 2 Humble。版本需要根据你的系统实际情况来调整如果你是 Ubuntu 24.04可能需要选择对应的 Jazzy 版本。硬件方面做仿真验证对机器要求不算高建议至少 8GB 内存CPU 四核以上。如果电脑性能一般也可以使用虚拟机但需要注意虚拟机里 3D 加速可能受限RViz 运行起来会有些卡。本文示例不需要接入真实机器人硬件所以普通开发机完全够用。在开始之前确保你已经能够正常访问 Ubuntu 的软件源并且拥有普通用户权限不需要额外使用 root 身份操作。2.2 ROS 2 Humble 安装这里给出完整安装命令。如果你的系统不是 Ubuntu 22.04请先对照 ROS 2 官方文档确认合适的发行版名称。sudo apt update sudo apt install -y ros-humble-desktopros-humble-desktop是一个完整的桌面版安装包包含 ROS 2 核心、rqt 工具、RViz、demo 示例等。安装完成后需要手动 source 一下环境变量source /opt/ros/humble/setup.bash为了以后不用每次打开终端都手动 source可以把环境变量写入用户配置文件中echo source /opt/ros/humble/setup.bash ~/.bashrc source ~/.bashrc接下来安装 colcon 构建工具它负责编译整个 ROS 2 工作空间sudo apt install -y python3-colcon-common-extensions还可以顺手安装 rosdep 工具后续如果功能包依赖外部库它会自动帮你处理sudo apt install -y python3-rosdep安装完成后可以输入以下命令检查 ROS 2 版本ros2 --help ros2 topic list如果正常输出帮助信息和空的话题列表说明环境已经准备好。2.3 示例项目总体结构我们将在用户主目录下创建一个 ROS 2 工作空间。工作空间的标准结构如下~/ros2_ws/ ├── src/ │ ├── mini_quadruped_sim/ │ │ ├── launch/ │ │ ├── urdf/ │ │ ├── mini_quadruped_sim/ │ │ ├── resource/ │ │ ├── setup.py │ │ └── package.xml ├── build/ ├── install/ └── log/其中src存放功能包源码build是构建中间目录install是构建结果安装目录log保存编译日志。我们在后续实战中会创建mini_quadruped_sim这个功能包。3. ROS 2 核心机制拆解3.1 工作空间与功能包ROS 2 中的功能包package是代码组织和分发的基本单元。一个功能包可以包含 Python/C 节点、配置文件、launch 文件、URDF 模型等。创建功能包有两种常用构建类型ament_python适合 Python 编写的节点本文使用这种方式。ament_cmake适合 C 节点也常用于包含大量配置文件的包。创建命令如下mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src ros2 pkg create --build-type ament_python mini_quadruped_sim创建完成之后功能包目录下会自动生成package.xml、setup.py、setup.cfg、resource等文件。构建整个工作空间cd ~/ros2_ws colcon build source install/setup.bash以后每次新建功能包或修改了setup.py中的入口点都需要重新执行colcon build然后再次source install/setup.bash。这里的小坑在于很多初学者忘记 source 新的环境导致运行自己的节点时报错找不到包。3.2 话题通信基础话题是 ROS 2 中最常用的通信方式。一个节点可以发布话题另一个节点可以订阅话题双方不需要知道对方是否存在。话题的数据结构由消息类型定义例如关节状态消息类型是sensor_msgs/msg/JointState。一个最小的话题发布节点如下import rclpy from rclpy.node import Node from std_msgs.msg import String class SimplePublisher(Node): def __init__(self): super().__init__(simple_publisher) self.publisher_ self.create_publisher(String, demo_topic, 10) self.timer self.create_timer(1.0, self.timer_callback) def timer_callback(self): msg String() msg.data Hello ROS 2 self.publisher_.publish(msg) self.get_logger().info(fPublish: {msg.data}) def main(argsNone): rclpy.init(argsargs) node SimplePublisher() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()对应的订阅节点import rclpy from rclpy.node import Node from std_msgs.msg import String class SimpleSubscriber(Node): def __init__(self): super().__init__(simple_subscriber) self.subscription self.create_subscription(String, demo_topic, self.callback, 10) def callback(self, msg): self.get_logger().info(fReceived: {msg.data}) def main(argsNone): rclpy.init(argsargs) node SimpleSubscriber() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()代码解释如下create_publisher第一个参数是消息类型第二个参数是话题名第三个参数是队列长度。create_timer让节点定时执行回调。rclpy.spin(node)会阻塞当前线程让节点持续处理回调事件。发布端和订阅端只要话题名一致消息类型一致就能自动完成通信。ROS 2 还提供了一组成命令行工具可以帮助调试。例如ros2 topic list ros2 topic echo /demo_topic ros2 node list3.3 服务与动作的区别话题适合持续流式数据但有些场景需要“请求一次得到一次结果”这时候应该使用服务。服务是同步的客户端发送请求后会等待服务器返回响应。例如控制机械臂移动到指定位置ros2 service list动作则适合更长时间的任务它不仅有请求和最终响应还会持续反馈进度。比如机械臂从 A 点移动到 B 点可能需要几秒钟期间我们希望能知道当前进度。动作在机器人导航和机械臂控制中很常见。理解这三个通信方式之后写机器人节点时就能做出合理选型。本文实战中关节状态数据是持续的流式变化所以使用话题。3.4 关节角度的表达方式在机器人控制中关节位置通常用弧度而不是角度表示。例如一个关节允许活动范围是-90°到0°在 URDF 的 limit 中写的是-1.57到0.0。JointState消息的格式如下std_msgs/Header header string[] name float64[] position float64[] velocity float64[] effort其中name是关节名字符串数组position是每个关节的角度位置velocity是角速度effort是力矩。四个数组必须一一对应长度一致。4. 完整实战在 ROS 2 中仿真简易四足机器人4.1 创建项目结构假设我们已经按照上面的步骤创建了mini_quadruped_sim功能包并且完成了环境配置。现在补充 URDF 和 launch 文件目录。cd ~/ros2_ws/src/mini_quadruped_sim mkdir -p urdf launch rviz接下来在urdf目录下创建mini_quadruped.urdf在launch目录下创建display.launch.py在功能包源码目录下创建joint_state_publisher_node.py。4.2 编写四足机器人 URDF 模型下面这个 URDF 是一个高度简化的四足机器人模型。它包含一个躯干base_link和四条腿每条腿由髋关节hip_joint和膝关节knee_joint组成为了看起来更直观我把每条腿的下半部分简化成一根连杆。这部分代码比较长但它是完整可运行的。关节命名采用LF、RF、LH、RH分别代表左前、右前、左后、右后。?xml version1.0? robot namemini_quadruped material namegray color rgba0.5 0.5 0.5 1.0/ /material material nameblue color rgba0.2 0.3 0.8 1.0/ /material material nameblack color rgba0.1 0.1 0.1 1.0/ /material link namebase_link visual geometry box size0.30 0.15 0.08/ /geometry material namegray/ /visual collision geometry box size0.30 0.15 0.08/ /geometry /collision /link !-- 左前腿 LF -- link nameLF_hip visual geometry sphere radius0.03/ /geometry material nameblue/ /visual /link joint nameLF_hip_joint typerevolute parent linkbase_link/ child linkLF_hip/ origin xyz0.15 0.10 0.0/ axis xyz0 1 0/ limit lower-1.5 upper1.5 effort10.0 velocity5.0/ /joint link nameLF_leg visual geometry cylinder radius0.02 length0.16/ /geometry origin xyz0 0 -0.08/ material nameblack/ /visual collision geometry cylinder radius0.02 length0.16/ /geometry origin xyz0 0 -0.08/ /collision /link joint nameLF_knee_joint typerevolute parent linkLF_hip/ child linkLF_leg/ origin xyz0 0 -0.03/ axis xyz0 1 0/ limit lower-2.0 upper0.0 effort10.0 velocity5.0/ /joint !-- 右前腿 RF -- link nameRF_hip visual geometry sphere radius0.03/ /geometry material nameblue/ /visual /link joint nameRF_hip_joint typerevolute parent linkbase_link/ child linkRF_hip/ origin xyz0.15 -0.10 0.0/ axis xyz0 1 0/ limit lower-1.5 upper1.5 effort10.0 velocity5.0/ /joint link nameRF_leg visual geometry cylinder radius0.02 length0.16/ /geometry origin xyz0 0 -0.08/ material nameblack/ /visual /link joint nameRF_knee_joint typerevolute parent linkRF_hip/ child linkRF_leg/ origin xyz0 0 -0.03/ axis xyz0 1 0/ limit lower-2.0 upper0.0 effort10.0 velocity5.0/ /joint !-- 左后腿 LH -- link nameLH_hip visual geometry sphere radius0.03/ /geometry material nameblue/ /visual /link joint nameLH_hip_joint typerevolute parent linkbase_link/ child linkLH_hip/ origin xyz-0.15 0.10 0.0/ axis xyz0 1 0/ limit lower-1.5 upper1.5 effort10.0 velocity5.0/ /joint link nameLH_leg visual geometry cylinder radius0.02 length0.16/ /geometry origin xyz0 0 -0.08/ material nameblack/ /visual /link joint nameLH_knee_joint typerevolute parent linkLH_hip/ child linkLH_leg/ origin xyz0 0 -0.03/ axis xyz0 1 0/ limit lower-2.0 upper0.0 effort10.0 velocity5.0/ /joint !-- 右后腿 RH -- link nameRH_hip visual geometry sphere radius0.03/ /geometry material nameblue/ /visual /link joint nameRH_hip_joint typerevolute parent linkbase_link/ child linkRH_hip/ origin xyz-0.15 -0.10 0.0/ axis xyz0 1 0/ limit lower-1.5 upper1.5 effort10.0 velocity5.0/ /joint link nameRH_leg visual geometry cylinder radius0.02 length0.16/ /geometry origin xyz0 0 -0.08/ material nameblack/ /visual /link joint nameRH_knee_joint typerevolute parent linkRH_hip/ child linkRH_leg/ origin xyz0 0 -0.03/ axis xyz0 1 0/ limit lower-2.0 upper0.0 effort10.0 velocity5.0/ /joint /robot这里要注意几个细节每条腿的hip_joint都是revolute类型轴选择 Y 轴这样髋关节可以在身体前后方向摆动。knee_joint的父连杆是髋关节连杆子连杆是腿部连杆。因为腿部连杆的视觉原点在圆柱体中间我把它的可视化原点向下偏移了 0.08 米这样看起来就像一条腿从髋部垂下去。limit中定义了关节旋转范围、最大力矩和最大速度。RViz 显示时如果关节角度超出范围会影响模型姿态计算的合理性。4.3 编写关节状态发布节点接下来在功能包的 Python 源码目录下创建节点文件。文件路径是~/ros2_ws/src/mini_quadruped_sim/mini_quadruped_sim/joint_state_publisher_node.py这个节点会按照固定频率发布 8 个关节的关节角度。为了让腿部看起来像在协调运动我给每个关节设定了不同的相位偏移。#!/usr/bin/env python3 import math import rclpy from rclpy.node import Node from sensor_msgs.msg import JointState from std_msgs.msg import Header JOINT_NAMES [ LF_hip_joint, LF_knee_joint, RF_hip_joint, RF_knee_joint, LH_hip_joint, LH_knee_joint, RH_hip_joint, RH_knee_joint, ] # 每个关节的相位偏移用来模拟四条腿交替摆动的效果 PHASE_OFFSETS [0.0, 0.5, 3.14, 3.64, 1.57, 2.07, 4.71, 5.21] class QuadrupedJointStatePublisher(Node): def __init__(self): super().__init__(quadruped_joint_state_publisher) self.publisher_ self.create_publisher(JointState, /joint_states, 10) self.timer self.create_timer(0.05, self.timer_callback) self.t 0.0 def timer_callback(self): msg JointState() msg.header Header() msg.header.stamp self.get_clock().now().to_msg() msg.name JOINT_NAMES positions [] for i, joint_name in enumerate(JOINT_NAMES): if joint_name.endswith(hip_joint): value 0.4 * math.sin(self.t * 2.0 PHASE_OFFSETS[i]) else: value 0.3 * math.cos(self.t * 2.0 PHASE_OFFSETS[i]) - 0.3 positions.append(value) msg.position positions msg.velocity [0.0] * len(JOINT_NAMES) msg.effort [0.0] * len(JOINT_NAMES) self.publisher_.publish(msg) self.t 0.05 def publish_periodic(self): pass def main(argsNone): rclpy.init(argsargs) node QuadrupedJointStatePublisher() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ __main__: main()代码逻辑并不复杂。核心点如下create_publisher(JointState, /joint_states, 10)创建话题发布器话题名必须和robot_state_publisher订阅的话题一致。create_timer(0.05, self.timer_callback)表示每 0.05 秒发布一次也就是 20Hz。关节角度用正弦和余弦函数生成目的是让髋关节和膝关节产生周期性变化视觉上能够明显看到腿部摆动。为了让ros2 run能够找到这个节点需要在setup.py中注册入口点。打开setup.py在entry_points部分添加节点名称entry_points{ console_scripts: [ quadruped_joint_state_publisher mini_quadruped_sim.joint_state_publisher_node:main, ], },还需要在setup.py的data_files中把 URDF 和 launch 文件一起安装到功能包目录下。修改后的setup.py可以参考下面这样import os from glob import glob from setuptools import setup package_name mini_quadruped_sim setup( namepackage_name, version0.0.1, packages[package_name], data_files[ (share/ament_index/resource_index/packages, [resource/ package_name]), (share/ package_name, [package.xml]), (os.path.join(share, package_name, urdf), glob(urdf/*.urdf)), (os.path.join(share, package_name, launch), glob(launch/*.launch.py)), ], install_requires[setuptools], zip_safeTrue, maintainerdeveloper, maintainer_emaildeveloperexample.com, descriptionMini quadruped robot simulation with ROS 2, licenseApache-2.0, tests_require[pytest], entry_points{ console_scripts: [ quadruped_joint_state_publisher mini_quadruped_sim.joint_state_publisher_node:main, ], }, )4.4 编写 launch 启动文件单独的节点和模型文件还不够我们需要一个 launch 文件把 robot_state_publisher、关节状态发布节点和 RViz 一起启动。在launch目录下创建display.launch.pyimport os from launch import LaunchDescription from launch_ros.actions import Node from ament_index_python.packages import get_package_share_directory def generate_launch_description(): package_share get_package_share_directory(mini_quadruped_sim) urdf_path os.path.join(package_share, urdf, mini_quadruped.urdf) with open(urdf_path, r) as f: robot_description f.read() robot_state_publisher Node( packagerobot_state_publisher, executablerobot_state_publisher, parameters[{robot_description: robot_description}], outputscreen ) quadruped_joint_state_publisher Node( packagemini_quadruped_sim, executablequadruped_joint_state_publisher, outputscreen ) rviz2 Node( packagerviz2, executablerviz2, namerviz2, outputscreen ) return LaunchDescription([ robot_state_publisher, quadruped_joint_state_publisher, rviz2, ])解释一下 launch 文件做了什么事情get_package_share_directory获取安装后功能包所在目录。读取 URDF 文件内容并通过robot_description参数加载到robot_state_publisher节点中。robot_state_publisher会订阅/joint_states话题根据 URDF 模型计算出每个连杆的 TF 变换。quadruped_joint_state_publisher是我们自己编写的关节状态发布节点。最后启动 RViz 用于可视化。4.5 构建并编译工作空间回到工作空间根目录执行构建命令cd ~/ros2_ws colcon build --packages-select mini_quadruped_sim --symlink-install这里使用--symlink-install的好处是安装目录中的 Python 文件会以软链接方式指向源码之后如果只是修改 Python 脚本而不涉及setup.py变更就不需要重新编译。构建完成后source 一下安装环境source install/setup.bash4.6 运行仿真并观察结果启动 launch 文件ros2 launch mini_quadruped_sim display.launch.py此时会弹出 RViz 窗口。第一次打开时界面是空白的需要做两步手动配置在左侧 Display 面板中点击Add选择By topic然后选择/robot_description下的 RobotModel 类型或者直接添加/TF和 RobotModel。在 Global Options 中把Fixed Frame设置为base_link。设置完成后RViz 中应该能看到一个灰色的躯干和四条腿并且四条腿会以一定节奏摆动。如果你看不到模型先检查 RViz 左下角是否有红色错误提示常见原因是Fixed Frame不是base_link或者robot_state_publisher没有正常启动。可以在另一个终端中查看关节话题数据ros2 topic echo /joint_states正常输出会类似这样header: stamp: sec: 1234 nanosec: 567890123 frame_id: name: - LF_hip_joint - LF_knee_joint - RF_hip_joint - RF_knee_joint - LH_hip_joint - LH_knee_joint - RH_hip_joint - RH_knee_joint position: - 0.0 - -0.3 - 0.4 - -0.3 ...这些数据就是机器人模型的关节角度来源。通过调整 Python 节点中的正弦函数参数可以让腿部运动幅度、频率和相位发生变化。5. 常见问题与排查思路新手第一次跑通 ROS 2 仿真时很容易遇到各种环境问题。下面是我觉得非常典型的几个问题整理成了表格方便快速对照。问题现象常见原因解决思路启动 launch 后提示找不到包忘记 source 安装环境执行source install/setup.bash再运行ros2 launchRViz 中不显示任何模型Fixed Frame 设置不对将 Global Options 中的 Fixed Frame 改为base_linkRViz 中模型显示为红色URDF 加载失败或关节角度非法检查 URDF 的 XML 格式确认 joint 的 limit 范围合理腿部完全不动自定义节点没有发布话题用ros2 topic echo /joint_states检查是否有数据输出colcon build报找不到 python 依赖缺少 setuptools 或相关库安装python3-pip和python3-colcon-common-extensions启动节点时提示module not foundPython 文件路径或入口点配置错误检查setup.py中console_scripts是否指向正确模块RViz 打开后非常卡顿虚拟机图形加速不足或电脑
返回列表