
簡介機器人協同控制是工業自動化中的重要方向雙臂系統相比單臂在搬運、裝配等場景下具有顯著效率優勢。在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樹和控制指令都沒有異常再進行正式任務。這個習慣能幫你擋掉很多莫名其妙的“炸機”問題。本文還有配套的精品資源點擊獲取