ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于ROS的双UR10机械臂协同控制:从Gazebo仿真到真机部署

基于ROS的双UR10机械臂协同控制:从Gazebo仿真到真机部署 简介机器人协同控制是工业自动化中的重要方向双臂系统相比单臂在搬运、装配等场景下具有显著效率优势。在ROS框架中多机械臂协同面临模型配置、运动规划与碰撞避免等核心挑战。通过URDF/Xacro建立统一的双臂模型借助MoveIt进行多规划组管理并使用Gazebo完成仿真验证能够有效解决双臂避碰的任务需求。该类技术可广泛应用于分拣、打螺丝、长物体协同搬运等场景。本文基于UR10实例详细介绍了从仿真搭建、运动规划到真实机器人接入的完整实现过程为双臂协同控制的工程落地提供参考。 做双机械臂系统这件事最开始是因为一个分拣场景的需求单臂的效率不够双臂又能协同配合。真正动手才发现把两台UR10放到同一个ROS控制框架里难度并不是简单乘以2而是在仿真、规划、通信、安全上的全面升级。这个项目最终交付了一套完整方案在Gazebo里可以跑的双UR10仿真以及能连接两台真实UR10并完成双臂协同控制的代码和文档。如果你也准备做类似的双臂项目这篇文章可以省下你至少两周的踩坑时间。我这里的核心做法是用ROS Noetic作为主控系统URDF/Xacro建模双臂Gazebo做仿真验证MoveIt做运动规划真实UR10通过ur_robot_driver接入。整体思路是先仿真后真机同一套任务脚本可以在两种模式下切换。这篇文章会把架构、仿真搭建、双臂避碰、真机接入、代码组织、典型坑位全部拆开讲清楚。1. 双机械臂系统的整体架构与选型逻辑1.1 为什么是双机械臂而不是两台独立单臂很多人一开始会觉得双机械臂不就是两台单臂放一起吗各跑各的不就行了。真做起来就发现不是那么回事。两台独立单臂最大的问题是彼此不知道对方在干什么。规划时没有把对方当作障碍物分拣任务中很容易撞到一起。更重要的是如果任务本身需要双手配合比如搬运长物件、左右同时拧螺丝那单靠独立指令根本同步不起来。所以我从需求层面就先明确了这套系统需要统一的状态树、统一的TF坐标关系、统一的任务调度入口。在这个基础上双机械臂的控制可以解释为“两个规划组 一个共享规划场景 一个协调任务节点”。这样左臂和右臂都有独立的运动规划能力但它们的碰撞空间会实时同步给对方。这是双臂系统和“两台单臂拼在一起”的本质区别。1.2 系统组成和版本选型我的主力环境是Ubuntu 20.04 ROS Noetic。为什么没有一开始就上ROS2因为UR10的成熟驱动、MoveIt与Gazebo的联调资料在Noetic上最全。如果你准备用ROS2也可以迁移但UR10生态里Noetic依然是相对稳的选择。如果只是为了仿真学习Ubuntu 22.04 ROS2 Humble MoveIt2也能走通但下文以Noetic为例。系统组成包括机器人本体两台UR10工作半径约1.3米有效载荷10公斤。仿真环境Gazebo 11加载双UR10的URDF模型。运动规划MoveIt OMPL两个规划组分别管理左臂和右臂。底层控制仿真里用ros_control gazebo_ros_control插件真实机器上用ur_robot_driver提供的手腕驱动接口。视觉/外围工作台上的模拟目标物、可选的RGB-D相机、夹爪模型。这套组合在实际测试中非常顺因为每一步都有现成的ROS包可以拼。UR10相关的ur_description和ur_robot_driver是官方维护Gazebo的ros_control生态也稳定。我不建议从零手写底层关节控制浪费时间且容易出安全问题。1.3 信息流和节点划分从顶层看系统的数据流是这样的双臂协调任务节点负责拆解任务把左臂目标位姿和右臂目标位姿分别发给两个MoveIt动作客户端每个MoveIt规划组在收到目标后从planning scene里读取当前环境并把对侧臂的关节状态当成动态障碍物规划完成的轨迹通过FollowJointTrajectory动作接口发给Gazebo控制器或真实UR10控制器。关键的话题和服务大致如下名称类型作用/arm_left/joint_statessensor_msgs/JointState左臂关节状态反馈/arm_right/joint_statessensor_msgs/JointState右臂关节状态反馈/arm_left/move_groupmoveit_msgs/MoveGroupAction左臂规划请求入口/arm_right/move_groupmoveit_msgs/MoveGroupAction右臂规划请求入口/arm_left/follow_joint_trajectorycontrol_msgs/FollowJointTrajectoryAction左臂轨迹执行/arm_right/follow_joint_trajectorycontrol_msgs/FollowJointTrajectoryAction右臂轨迹执行/planning_scenemoveit_msgs/PlanningScene共享规划场景同步这样的设计让双臂之间解耦又通过planning_scene共享环境能有效避免碰撞。任务调度节点反而很简单只负责规划目标和等待执行结果。2. Gazebo仿真环境的搭建与验证2.1 双臂URDF模型的组织方式在Gazebo里让两台UR10同时出现的第一个坑就是URDF里的link和joint名字不能重复。如果你直接把官方ur10.urdf.xacro复制两份就会出现两个base_link导致TF树直接冲突。我采用的做法是把单臂模型写成一个xacro宏调用两次再用不同的名前缀包裹。例如左臂的所有link叫left_upper_arm_link右臂叫right_upper_arm_linkjoint同理。下面的xacro片段思路很典型xacro:macro namedual_ur10 xacro:include filename$(find ur_description)/urdf/ur10_macro.xacro / xacro:ur10_macro prefixleft_ joint_limitedtrue/ xacro:ur10_macro prefixright_ joint_limitedtrue/ /xacro:macro这里面最关键的是prefix参数。ur_description自带的宏本身支持加前缀相当于把整棵TF子树的命名空间都隔开了。不过需要注意某些link名称如果不通过参数传入可能会漏掉前缀。初始化完成之后我会用rosrun tf2_tools view_frames.py生成TF树检查一遍确保left_base_link和right_base_link都挂在同一个world下。我把左右臂安装在同一个水平底座上两臂底座间距大约1.5米。这样既留出了重合工作区又不会让机械臂在待机位就互相干涉。底部框架用一个固定的world_link表示底座高度0.8米方便放置台架。2.2 ros_control控制器配置要让Gazebo能真正执行MoveIt规划的轨迹URDF里必须有transmission标签并且加载gazebo_ros_control插件。仿真控制我采用的是JointTrajectoryController因为MoveIt的FollowJointTrajectory动作直接能对接。每个关节需要设置合适的PID一开始我直接用默认参数结果Gazebo里关节抖得厉害后面把P降到80D提高到1.5左右才稳定下来。一个典型的controllers.yaml长这样arm_left_joint_trajectory_controller: type: position_controllers/JointTrajectoryController joints: - left_shoulder_pan_joint - left_shoulder_lift_joint - left_elbow_joint - left_wrist_1_joint - left_wrist_2_joint - left_wrist_3_joint gains: left_shoulder_pan_joint: {p: 80.0, i: 0.5, d: 1.5} left_shoulder_lift_joint: {p: 80.0, i: 0.5, d: 1.5} left_elbow_joint: {p: 60.0, i: 0.5, d: 1.2} left_wrist_1_joint: {p: 30.0, i: 0.2, d: 0.8} left_wrist_2_joint: {p: 30.0, i: 0.2, d: 0.8} left_wrist_3_joint: {p: 20.0, i: 0.2, d: 0.5}右臂类似。注意控制器名称里的arm_left前缀要和MoveIt配置里对应的action命名空间一致否则MoveIt找不到执行器。2.3 场景建模和初始位姿仿真场景里我在两台机械臂之间放了一个金属台架台架高度大概0.9米台面上放几个模拟工件。这样稍后测试规划避碰时机械臂不会单纯在自由空间瞎动而是真的有环境约束。初始位姿设置也很讲究。左臂和右臂的home点我都设置在靠近底座两侧、略微抬高的位置六轴角度大约接近“待机折叠”状态。目的是在Gazebo刚启动时就让双臂远离相互碰撞区同时避免重力作用下关节突然下沉导致仿真崩坏。设置初始位姿可以直接在spawn模型时通过-J参数传也可以发布一份joint_states让robot_state_publisher刷新。2.4 跑通第一个双臂仿真测试仿真启动的顺序是先启动Gazebo世界再启动robot_state_publisher和ros_control控制器最后启动MoveIt。一个常用的launch组合类似这样roslaunch dual_ur10_gazebo gazebo_world.launch roslaunch dual_ur10_moveit_config moveit_planning_execution.launch sim:true等Gazebo稳定后先在RViz里给左臂定一个工作台上方的目标位姿规划并执行。如果左臂能平滑到达再给右臂定一个对称位姿双臂同时执行。我第一次跑通时左右臂竟然同时把末端伸到同一个点附近虽然没有物理碰撞但那一下冷汗就出来了——所以从仿真阶段开始就要把避碰机制加进去而不是等到真机再考虑。3. 双机械臂运动规划避碰与协同3.1 两个MoveIt规划组在同一套环境中共存我的做法是启动两个MoveIt节点一个管左臂一个管右臂。它们加载的URDF是同一份完整的双机械臂模型各自的planning_group分别设置为left_arm和right_arm。这样每个MoveIt都拥有全部机器人的状态但只规划自己负责的那组关节。这里有个容易踩坑的地方如果你在MoveIt Setup Assistant里用单臂模型生成配置拿到双臂环境中直接用MoveIt会因为找不到部分关节名字而报错。正确做法是开启双臂模型在生成的config/srdf中保留两个group并设置默认规划参数。MoveIt允许同一个机器人模型里有多个group这就是双系统并存的基础。两个move_group的命名空间可以通过ns参数隔离例如先启动roslaunch dual_ur10_moveit_config moveit_group.launch ns:arm_left group:left_arm roslaunch dual_ur10_moveit_config moveit_group.launch ns:arm_right group:right_arm这样在topic层面左、右臂的MoveIt接口就完全分开了任务节点可以分别调用。3.2 把对侧机械臂变成动态障碍物双臂规划最核心的问题左臂规划时必须知道右臂此刻在哪里。最简单的办法是在规划前把对侧臂的每个link当成一个CollisionObject加到当前MoveIt的planning scene里。我在协调任务节点里维护一个定时器每200毫秒读取对侧臂的/joint_states然后根据URDF里的link尺寸将这些link的位置转化为一组shape_msgs/SolidPrimitive和geometry_msgs/Pose通过PlanningSceneInterface发布到当前规划场景中。这样规划时MoveIt就会认为另一条臂是环境中不能碰的障碍物。这种方法实时性好代码也容易理解。代价是每次更新都要重新发布collision object如果频率太高会挤占规划时间。实测200毫秒刷新足够因为UR10的正常运动速度不会在两三百毫秒内产生致命位移变化。如果要求更高可以在一段轨迹执行前先推演对侧臂未来的轨迹片段再生成动态障碍物但工程量大很多我暂时没有追求。还有一种方法是使用MoveIt的AllowedCollisionMatrix直接把对侧臂的link加入“不可接触”列表。这个办法更快但对复杂环境模型不够细致我建议还是用CollisionObject更直观。3.3 双臂协调任务的具体实现我这里举一个非常常见的任务双臂同时抓取一根长杆。你可以把任务拆成三个动作左臂运动到长杆左边抓取点右臂运动到长杆右边抓取点两个机械臂同时抬起长杆到某个高度。每一步都依赖上一步完成所以要有一个顶层调度状态机。Python伪代码如下import rospy from moveit_commander import MoveGroupCommander, PlanningSceneInterface rospy.init_node(dual_arm_task_node) left_group MoveGroupCommander(left_arm) right_group MoveGroupCommander(right_arm) scene PlanningSceneInterface() left_group.set_planning_time(5.0) right_group.set_planning_time(5.0) def move_both(left_pose, right_pose): left_group.set_pose_target(left_pose) right_group.set_pose_target(right_pose) left_plan left_group.plan() right_plan right_group.plan() if left_plan and right_plan: left_group.execute(left_plan, waitFalse) right_group.execute(right_plan, waitTrue) else: rospy.logwarn(plan failed, try again)注意execute方法里的waitTrue/False的组合。如果两个都waitTrue第一个会阻塞导致第二只臂迟迟不启动。这里我先让左臂开始执行再让右臂执行并用waitTrue等待右臂完成。实际项目中还需要等待两个动作都结束后再进入下一阶段可以用一个Future或者线程去轮询MoveIt的状态。如果单纯是同步运动上面的方式足够了。但如果两个目标的完成时间差距太大可能出现一臂先到位等待另一臂的情况。这时我会把两段轨迹的时间手动对齐在后处理时把较长轨迹的时间作为总时间对短轨迹做时间缩放保证双臂同时启动、同时到位。代价是一条臂的速度被压低好处是整体姿态更安全。3.4 规划参数调整和实时性优化MoveIt默认的规划时间只有1秒规划尝试次数也很少。在双臂场景中由于对侧臂不断变成障碍物规划失败的可能性比单臂高很多。我把planning_time调到5秒num_planning_attempts调到10。这对仿真和真机都适用但如果用于实时产线5秒规划时间太长需要换更快的规划器或预生成轨迹库。另外可以把max_velocity_scaling_factor和max_acceleration_scaling_factor分开设置。仿真时可以设0.8速度拉满看效果真机第一次跑我建议设0.1尤其双臂同时动的时候低速度给紧急停止留足反应时间。还有一个小技巧给规划场景里的障碍物加上padding。MoveIt支持为collision object设置padding参数相当于给物体膨胀一圈。两只臂之间的距离本来就近时稍微膨胀一点可以把规划结果往安全方向推。我通常会设置0.02米到0.05米的padding。4. 从仿真切换到真实UR104.1 硬件连接和驱动安装真实UR10和仿真的差距主要体现在底层通信。UR10控制器支持通过以太网口与外部PC通信。我把两台UR10分别接到工控机的两个独立网口设置静态IP避免共用一个网口导致带宽争抢。驱动我用的是ur_robot_driver相比老旧的ur_modern_driver它对Polyscope新版系统支持更好还能通过dashboard接口控制机器人的上电和加载程序。为了配合驱动必须在UR10的示教器上安装externalcontrol.urcap并在程序里加入External Control节点这样机器人才能接受来自ROS侧的速度/位置指令。准备好之后启动roslaunch ur_robot_driver ur10_bringup.launch robot_ip:192.168.1.50这时可以在另一个终端里检测能否收到关节状态rostopic echo /joint_states如果没有任何数据先检查机器人的External Control节点是否正在运行以及网络是否能ping通。4.2 MoveIt从仿真切到真实的配置改动MoveIt本身不关心底层是仿真还是真实它只通过FollowJointTrajectory动作发送轨迹。区别在于执行这条动作的服务端是谁。仿真时是ros_control的joint_trajectory_controller真实时是ur_robot_driver提供的scaled_pos_joint_traj_controller。我在整个项目里用一个顶层launch参数做切换arg namesim defaulttrue/ arg namerobot_ip_left default192.168.1.50/ ... group unless$(arg sim) node namearm_left_driver pkgur_robot_driver typeur10_bringup.launch/ /group group if$(arg sim) node namearm_left_gazebo pkgdual_ur10_gazebo .../ /group在MoveIt配置里需要把controllers.yaml也根据sim参数加载不同的版本。仿真版本对应控制器名字arm_left_joint_trajectory_controller真实版本对应scaled_pos_joint_traj_controller。否则MoveIt执行轨迹时会一直报“controller not found”。还有一处必须要改use_sim_time。仿真中要设为true让所有ROS节点使用Gazebo的仿真时钟真机时要设为false使用系统时钟。这个参数如果忘记切换最典型的症状是MoveIt规划正常但执行时卡住不动或者机器人收到一串零速度指令。4.3 真机调试的安全流程真机和仿真最大的不同就是一旦出错代价很高。我的经验是分四步走首先在示教器上把工具速度限制设到最低例如10%。然后把ROS侧的max_velocity_scaling_factor设为0.05到0.1先做单臂小范围往复运动确认关节方向和驱动反馈一致。第二步用MoveIt在RViz里规划一条非常简单的轨迹比如从home点到旁边10厘米的位置。发送执行观察真实机械臂的移动方向是否与RViz中的预演完全一致。UR10的关节方向和URDF模型默认是匹配的但如果你改过零点或者安装方向这一步最容易发现偏差。第三步再测试双臂低速联动。先让左臂从home点运动到目标位右臂原地不动验证对侧臂作为障碍物时不会产生误报。然后让右臂运动左臂不动。最后才让双臂同时运动。第四步准备紧急停止。UR10示教器上的红色急停按钮是最后防线同时我还在工控机上监听机器人的safety_mode话题如果进入保护性停止立刻发送停止轨迹指令。4.4 网络延迟和实时性注意事项真实UR10的控制方式并不像仿真那样每个关节直接接受位置指令而是通过UR控制器内部进行平滑和速度规划。因此ROS侧发过来的轨迹到了机器人端肯定会有一小段缓冲。网络延迟主要体现在几个地方指令传输延迟、状态反馈延迟、dashboard命令延迟。如果发现真实机器人执行轨迹时“一顿一顿”优先排查网络。我用有线直连时ping延时通常小于1毫秒非常稳定。如果用无线网络经常出现几十毫秒甚至几百毫秒的抖动这时候机器人很容易进入保护停。如果是路由器转发也建议关闭其他带宽占用较大的服务。在代码层面我给FollowJointTrajectory客户端设置了执行超时检查。如果5秒内没有收到执行结果反馈就自动取消轨迹并报警。这个保护在实际产线上非常重要因为MoveIt的plan()只能保证规划成功不能保证底层真的执行完毕。5. 源码组织和文档应该怎么写5.1 包结构划分一个好的双臂项目源码不应该把所有launch和脚本塞到一个包里。我最终交付的源代码按功能拆成了五个包dual_ur10_description双UR10的URDF/xacro、左右臂模型、底座和夹爪模型。dual_ur10_gazeboGazebo世界文件、控制器配置、PID参数、仿真launch。dual_ur10_moveit_configMoveIt配置、SRDF、OMPL规划配置、controllers.yaml。dual_ur10_bringup顶层入口launch支持sim/real切换。dual_ur10_tasks双臂任务调度节点、脚本、状态机、服务定义。这样划分的好处是改仿真设置不会影响真机配置改运动规划参数不影响任务逻辑。项目维护起来非常清晰多人协作也不会互相覆盖文件。5.2 顶层启动流程和参数传递我写了一个总入口launch用sim参数区分模式roslaunch dual_ur10_bringup main.launch sim:true roslaunch dual_ur10_bringup main.launch sim:false内部启动顺序大致是先启动机械臂驱动Gazebo spawn或ur_robot_driver再启动robot_state_publisher然后启动MoveIt最后启动双臂任务节点。顺序不能乱尤其是MoveIt启动时如果读不到robot_description会直接退出。我给每个节点加respawntrue这样某个节点崩溃后能快速重启。在文档里一定要写清楚如何检查每一层是否启动成功。例如看/joint_states是否有数据。看/tf里是否存在左右臂各自的TF树。看MoveIt的RViz插件是否正常加载机器人模型。用rosnode list确认move_group节点都在运行。5.3 文档说明的要点文档并不是简单贴几个命令而是要让人拿到源码后能顺利跑起来。我的项目文档包含以下几块环境依赖清单Ubuntu 20.04、ROS Noetic、Gazebo 11、MoveIt、ur_robot_driver、moveit_commander、catkin。编译步骤catkin_make或catkin build以及哪些包需要额外安装。快速启动先仿真后真机的两条完整命令。双机械臂任务说明每个任务节点的功能、依赖的topic和service、可调参数。常见问题表例如Gazebo启动后机械臂下坠、MoveIt执行时找不到控制器、真机连接失败等都列出排查思路。文档中的常见问题表我建议采用如下结构症状可能原因解决方法Gazebo中机械臂剧烈抖动PID参数不合适降低P提高DMoveIt执行轨迹无反应控制器名字不匹配检查controllers.yaml真机连接失败机器人程序未启动External Control检查URCap和程序节点双臂规划时碰撞检测失效对侧臂未加入planning scene检查协调节点的joint_states订阅6. 实测中避不开的坑6.1 TF树和命名空间的坑这是第一次在RViz里看到两台机械臂时最容易出问题的地方。症状是两台臂重叠在一起或者关节位置乱跳。原因基本是URDF中左右臂没有统一加前缀。如果左臂的某个link仍叫base_link而右臂也叫base_linkTF树里会出现两个名字相同的linkRViz只能随机显示一个看起来就像模型“穿越”了。解决方法是加前缀后用check_urdf或直接打开生成的URDF文件搜索每一个link name和joint name确保左臂相关名字都带left_右臂带right_。一旦有漏网之鱼立刻修。不要指望只靠robot_state_publisher来区分。6.2 仿真的抖动和过冲Gazebo里的机械臂抖动大多数不是模型问题而是控制器参数问题。UR10关节的力矩相对较大如果PID的P值过高系统容易震荡如果D值太小又会出现到位后反复摇摆。我从单臂调起先把所有关节P设为20D设为0.2确定系统稳定后再逐渐增大P直到轨迹跟踪精度满足需求。最后测试下来腕部关节因为惯性小P和D可以比大臂关节更低。另一个细节是Gazebo的仿真步长。默认步长1000Hz对多关节双臂来说计算压力很大如果电脑性能不够可以把max_step_size设为0.002秒但步长太大反而更易造成抖动。我的建议是先保持0.001秒调好控制器后再考虑提高性能。6.3 真机与仿真不一致的问题有一类问题在仿真里几乎发现不了UR10的关节零点偏移和方向定义。虽然官方URDF和真实机器人一致但在长期使用后机器人零点可能需要重新标定。实际跑真机之前我建议在示教器上先手动把每个关节转到0度附近再对比ROS里读到的/joint_states确认关节序号和方向对应关系。另外真实环境中不能完全依赖MoveIt的规划场景。仿真里桌面的尺寸是已知的但真实环境可能有线缆、物料堆、围栏等物体。我后来在真实工作台上加了一个静态点云发布节点把深度相机数据转成OccupancyMap再叠加进PlanningScene这样MoveIt才能有效地避开地面杂散物体。6.4 效率优化和后续扩展仿真和真机都跑通之后项目才算真正可用。再往后扩展的话有几个方向比较有价值第一加入视觉引导。用ArUco码或者3D点云获取工件目标位姿让双臂根据视觉结果动态调整抓取点。这比固定位姿更贴近产线。第二加入力控或导纳控制。UR10的末端如果要做装配、打磨这类接触任务纯位置控制不够。可以在末端装六维力传感器通过force_torque_sensor_controller把力反馈接入ROS。第三迁移到ROS2。如果你要长期维护这套系统ROS2的节点生命周期和通信机制更适合分布式部署。不过需要把MoveIt2、gazebo_ros2、ur_robot_driver的ROS2版本全部对齐工作量不小。我个人在实际使用中的体会是双机械臂项目的成败一半在规划算法另一半在工程细节。如果你只是想在仿真里玩做到这里已经算完整但如果要落地到真实设备建议先把仿真里的成功率跑到95%以上并把安全链路全部测通再碰真机。最后再分享一个小技巧不管代码写得有多顺手每次改完模型或控制器参数都先让机械臂在home点附近做一次低速循环测试确认关节状态、TF树和控制指令都没有异常再进行正式任务。这个习惯能帮你挡掉很多莫名其妙的“炸机”问题。本文还有配套的精品资源点击获取
返回列表