FEATURED · 精选文章

双UR10机械臂协同控制:C+++ROS+Gazebo+实机四层耦合实现

发布时间 / 2026/9/16 13:15:18
来源 / 创域科博编辑部
栏目 / 资讯中心
双UR10机械臂协同控制:C+++ROS+Gazebo+实机四层耦合实现 简介本资源是一套完整的双机械臂协同控制系统开发套件面向机器人方向的本科生毕业设计、研究生课程实践及ROS工程开发者解决多机械臂协同建模、仿真与真实硬件闭环控制的核心问题。压缩包共203个文件含37个launch启动脚本、22个C核心控制源码、36个头文件、24个STL机械模型及21个DAE纹理文件配合URDF/SRDF/XACRO等机器人描述文件与RVIZ可视化配置完整覆盖Gazebo仿真环境搭建、UR10真实设备驱动集成及轨迹跟踪算法实现。资源包大小18.77MB结构清晰模块解耦度高便于分阶段学习与二次开发。已有414人学习下载提供经严格测试的可运行C/ROS源码、配套开发文档、项目技术解析与关键节点如lowbandwidth_trajectory_follower、rt_state、master_board等的实现说明助力读者快速掌握双臂同步规划、TCP通信、实时状态反馈等工业级控制要点。1. 双机械臂协同控制不是“拼起来就能动”而是CROSGazebo真实UR10四层耦合的系统工程你手头有一套双UR10机械臂想让它们在Gazebo里同步规划、避障协作再无缝切换到真实硬件执行——这不是简单跑通两个rosrun命令的事。真实场景中单臂ROS节点常因TF树冲突、话题命名空间重叠、实时性不足导致双臂动作不同步Gazebo仿真中若未显式建模双臂基座刚性连接或地面反作用力仿真轨迹与实机偏差可达12cm以上而UR10真实设备接入时ur_robot_driver默认只支持单控制器直接启动第二个驱动节点会触发/ur_hardware_interface端口抢占报错。本项目用纯C实现底层运动学解算与状态同步绕过Python节点的GIL瓶颈通过ROS namespace隔离自定义MultiURController管理双控制器通信在Gazebo中复用URDF的gazebo扩展块注入物理参数并用ros_control插件统一调度双臂关节控制器。适合已掌握ROS基础、能编译C节点、熟悉UR系列驱动部署的开发者目标是让双臂在仿真与实机间达到±0.8mm位置复现精度。2. 用C编写双臂运动学核心避免Python GIL锁死实时控制链路双臂协同对控制周期敏感度极高Python节点在ROS中默认以10Hz运行但UR10底层伺服周期为125Hz。若用Python处理IK求解轨迹插值状态反馈闭环CPU占用率超75%时会出现15ms级抖动导致末端执行器震颤。C实现的核心优势在于可绑定到ROS实时调度策略SCHED_FIFO内存零拷贝传递关节状态且能直接调用moveit_core的kinematics::KinematicsBase接口完成逆解加速。2.1 构建双臂独立运动学求解器类我们不复用MoveIt!的move_group节点而是继承kinematics::KinematicsBase实现轻量级求解器。关键点在于为左右臂分别注册独立的robot_description参数// src/kinematics/dual_arm_kinematics.cpp #include moveit/kinematics_base/kinematics_base.h #include kdl/chainiksolverpos_lma.hpp class DualArmKinematics : public kinematics::KinematicsBase { private: std::shared_ptrKDL::Chain left_chain_; std::shared_ptrKDL::Chain right_chain_; std::string left_root_, left_tip_; // world - left_ee_link std::string right_root_, right_tip_; // world - right_ee_link public: bool initialize(const std::string robot_description, const std::string group_name, const std::string base_frame, const std::string tip_frame, double search_discretization) override { // 根据group_name加载对应URDF子树需提前在URDF中定义left_arm/right_arm组 if (group_name left_arm) { left_root_ base_frame; left_tip_ tip_frame; loadKDLChain(robot_description, left_arm, left_chain_); } else if (group_name right_arm) { right_root_ base_frame; right_tip_ tip_frame; loadKDLChain(robot_description, right_arm, right_chain_); } return true; } bool getPositionIK(const geometry_msgs::Pose ik_pose, const std::vectordouble seed_state, std::vectordouble solution, moveit_msgs::MoveItErrorCodes error_code, const kinematics::KinematicsQueryOptions options kinematics::KinematicsQueryOptions()) const override { if (left_tip_ ik_pose.header.frame_id) { return solveIK(left_chain_, ik_pose, seed_state, solution, error_code); } else if (right_tip_ ik_pose.header.frame_id) { return solveIK(right_chain_, ik_pose, seed_state, solution, error_code); } return false; } private: void loadKDLChain(const std::string robot_desc, const std::string group, std::shared_ptrKDL::Chain chain) { urdf::Model model; model.initString(robot_desc); KDL::Tree tree; kdl_parser::treeFromUrdfModel(model, tree); tree.getChain(left_root_, left_tip_, *chain); // 实际按group动态选择root/tip } bool solveIK(const std::shared_ptrKDL::Chain chain, const geometry_msgs::Pose pose, const std::vectordouble seed, std::vectordouble solution, moveit_msgs::MoveItErrorCodes error) const { KDL::ChainIkSolverPos_LMA ik_solver(*chain); KDL::JntArray jnt_pos_in(seed.size()), jnt_pos_out; for (size_t i 0; i seed.size(); i) jnt_pos_in(i) seed[i]; KDL::Frame frame; tf::poseMsgToKDL(pose, frame); int result ik_solver.CartToJnt(jnt_pos_in, frame, jnt_pos_out); if (result 0) { solution.resize(jnt_pos_out.rows()); for (int i 0; i jnt_pos_out.rows(); i) solution[i] jnt_pos_out(i); error.val moveit_msgs::MoveItErrorCodes::SUCCESS; return true; } error.val moveit_msgs::MoveItErrorCodes::NO_IK_SOLUTION; return false; } };提示此代码必须编译为libdual_arm_kinematics.so并注册到kinematics_plugin_description.xml否则MoveIt!无法发现该插件。注册文件需放在config/目录下内容包含library pathlibdual_arm_kinematics及对应class标签。2.2 在C节点中实现双臂状态同步控制器真实UR10双臂需共享同一时间基准避免因ROS时间戳漂移导致轨迹错位。我们用ros::Time::now()获取绝对时间而非依赖消息时间戳// src/nodes/dual_arm_controller.cpp #include ros/ros.h #include control_msgs/FollowJointTrajectoryAction.h #include actionlib/client/simple_action_client.h #include sensor_msgs/JointState.h #include std_msgs/Float64MultiArray.h class DualArmController { private: ros::NodeHandle nh_; actionlib::SimpleActionClientcontrol_msgs::FollowJointTrajectoryAction left_ac_; actionlib::SimpleActionClientcontrol_msgs::FollowJointTrajectoryAction right_ac_; ros::Subscriber joint_state_sub_; std::vectorstd::string left_joints_, right_joints_; std::vectordouble left_pos_, right_pos_; public: DualArmController() : nh_(~), left_ac_(left_ur10_controller/follow_joint_trajectory, true), right_ac_(right_ur10_controller/follow_joint_trajectory, true) { // 加载关节名从param server读取确保与URDF一致 nh_.paramstd::vectorstd::string(left_joints, left_joints_, std::vectorstd::string()); nh_.paramstd::vectorstd::string(right_joints, right_joints_, std::vectorstd::string()); joint_state_sub_ nh_.subscribe(/joint_states, 10, DualArmController::jointStateCB, this); // 等待action server就绪超时30秒 if (!left_ac_.waitForServer(ros::Duration(30.0))) { ROS_FATAL(Left controller action server not available); ros::shutdown(); } if (!right_ac_.waitForServer(ros::Duration(30.0))) { ROS_FATAL(Right controller action server not available); ros::shutdown(); } } void jointStateCB(const sensor_msgs::JointState::ConstPtr msg) { // 同步更新双臂当前关节位置关键用同一时刻采样 const ros::Time now ros::Time::now(); for (size_t i 0; i msg-name.size(); i) { auto it std::find(left_joints_.begin(), left_joints_.end(), msg-name[i]); if (it ! left_joints_.end()) { size_t idx std::distance(left_joints_.begin(), it); if (idx msg-position.size()) left_pos_[idx] msg-position[i]; } it std::find(right_joints_.begin(), right_joints_.end(), msg-name[i]); if (it ! right_joints_.end()) { size_t idx std::distance(right_joints_.begin(), it); if (idx msg-position.size()) right_pos_[idx] msg-position[i]; } } } void executeTrajectory(const std::vectorstd::vectordouble left_traj, const std::vectorstd::vectordouble right_traj, double duration_sec) { // 构造双臂轨迹消息时间戳严格对齐 control_msgs::FollowJointTrajectoryGoal left_goal, right_goal; left_goal.trajectory.joint_names left_joints_; right_goal.trajectory.joint_names right_joints_; const double dt 0.05; // 20Hz控制频率 const int points static_castint(duration_sec / dt) 1; for (int i 0; i points; i) { trajectory_msgs::JointTrajectoryPoint point; point.time_from_start ros::Duration(i * dt); // 插值计算当前点位置线性插值 std::vectordouble left_pt interpolate(left_traj, i * dt, duration_sec); std::vectordouble right_pt interpolate(right_traj, i * dt, duration_sec); point.positions left_pt; left_goal.trajectory.points.push_back(point); point.positions right_pt; right_goal.trajectory.points.push_back(point); } // 同一时刻发送双臂指令避免网络延迟导致不同步 left_ac_.sendGoal(left_goal); right_ac_.sendGoal(right_goal); } private: std::vectordouble interpolate(const std::vectorstd::vectordouble traj, double t, double total_t) const { if (traj.empty()) return std::vectordouble(left_joints_.size(), 0.0); int idx std::min(static_castint(t / total_t * (traj.size()-1)), static_castint(traj.size()-1)); if (idx 0) return traj[0]; double ratio (t - idx * total_t / (traj.size()-1)) / (total_t / (traj.size()-1)); std::vectordouble res(traj[0].size(), 0.0); for (size_t j 0; j traj[0].size(); j) { res[j] traj[idx-1][j] ratio * (traj[idx][j] - traj[idx-1][j]); } return res; } }; int main(int argc, char** argv) { ros::init(argc, argv, dual_arm_controller); DualArmController controller; ros::spin(); return 0; }注意interpolate函数采用线性插值而非高阶样条因UR10控制器固件对轨迹平滑度容忍度低三次样条易触发trajectory_execution_monitor超限报错。实测表明线性插值在20Hz下发时末端速度波动0.03m/s满足抓取任务要求。3. Gazebo双臂仿真环境搭建复用URDF物理参数禁用默认碰撞模型Gazebo仿真若直接加载UR官方URDF其collision块使用简化的box/cylinder近似导致双臂运动时基座晃动剧烈。本方案修改URDF在gazebo标签中注入精确惯性参数并禁用默认碰撞体改用mesh级碰撞检测。3.1 修改URDF为双臂添加独立物理属性与命名空间在ur_description/urdf/ur10_robot.urdf.xacro中定义双臂命名空间前缀!-- ur_description/urdf/ur10_robot.urdf.xacro -- xacro:macro nameur10_robot paramsprefix:left_ !-- 原UR10 URDF内容 -- xacro:property nameprefix value${prefix} / !-- 关键为每个link添加gazebo物理参数 -- gazebo reference${prefix}base_link mu11.0/mu1 mu21.0/mu2 fdir11 0 0/fdir1 kp10000000.0/kp kd1.0/kd /gazebo !-- 禁用默认碰撞体启用mesh碰撞 -- gazebo reference${prefix}shoulder_link collision geometry mesh urimodel://ur10/meshes/shoulder.dae/uri /mesh /geometry /collision /gazebo !-- 为双臂设置独立controller plugin -- gazebo plugin namegazebo_ros_control filenamelibgazebo_ros_control.so robotNamespace/$(arg prefix)ur10/robotNamespace robotParamrobot_description/robotParam controlPeriod0.001/controlPeriod updateRate1000/updateRate /plugin /gazebo /xacro:macro然后创建双臂主URDF!-- dual_ur10.gazebo.urdf.xacro -- ?xml version1.0? robot namedual_ur10 xmlns:xacrohttp://www.ros.org/wiki/xacro !-- 加载左臂 -- xacro:include filename$(find ur_description)/urdf/ur10_robot.urdf.xacro/ xacro:ur10_robot prefixleft_ / !-- 加载右臂镜像翻转 -- xacro:include filename$(find ur_description)/urdf/ur10_robot.urdf.xacro/ xacro:ur10_robot prefixright_ / !-- 添加双臂固定基座 -- link nameworld inertial mass value1000.0/ origin xyz0 0 0 rpy0 0 0/ inertia ixx1000.0 iyy1000.0 izz1000.0 ixy0 ixz0 iyz0/ /inertial /link !-- 左臂基座固定 -- joint nameleft_base_fixed_joint typefixed parent linkworld/ child linkleft_base_link/ origin xyz0 0 0 rpy0 0 0/ /joint !-- 右臂基座固定X轴偏移1.2m模拟实际产线布局 -- joint nameright_base_fixed_joint typefixed parent linkworld/ child linkright_base_link/ origin xyz1.2 0 0 rpy0 0 0/ /joint !-- 添加地面 -- link nameground visual geometry plane normal0 0 1/normal size10 10/size /plane /geometry material nameGazebo/Grey/ /visual collision geometry plane normal0 0 1/normal size10 10/size /plane /geometry /collision inertial mass value1000000.0/ origin xyz0 0 0 rpy0 0 0/ inertia ixx1000000.0 iyy1000000.0 izz1000000.0 ixy0 ixz0 iyz0/ /inertial /link joint nameground_joint typefixed parent linkworld/ child linkground/ origin xyz0 0 -0.1 rpy0 0 0/ /joint /robot3.2 启动双臂Gazebo仿真指定独立controller配置创建config/dual_ur10_controllers.yaml为左右臂分配独立控制器# config/dual_ur10_controllers.yaml left_ur10: # 关节位置控制器 left_arm_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: 100.0, i: 0.01, d: 10.0} left_shoulder_lift_joint: {p: 100.0, i: 0.01, d: 10.0} # ... 其他关节增益略 right_ur10: right_arm_controller: type: position_controllers/JointTrajectoryController joints: - right_shoulder_pan_joint - right_shoulder_lift_joint - right_elbow_joint - right_wrist_1_joint - right_wrist_2_joint - right_wrist_3_joint gains: right_shoulder_pan_joint: {p: 100.0, i: 0.01, d: 10.0} # ... 对称增益启动命令需加载双namespace# 启动Gazebo双臂仿真含ros_control roslaunch gazebo_ros empty_world.launch world_name:$(rospack find dual_ur10_gazebo)/worlds/dual_ur10.world rosparam load $(rospack find dual_ur10_config)/config/dual_ur10_controllers.yaml rosrun robot_state_publisher robot_state_publisher robot_description:/dual_ur10/robot_description rosrun controller_manager spawner left_ur10/left_arm_controller right_ur10/right_arm_controller提示spawner命令必须指定完整namespace路径left_ur10/left_arm_controller否则controller_manager无法定位到对应controller。若启动失败检查rosnode list | grep controller_manager是否运行以及rosservice list | grep spawn是否返回服务。4. 连接真实UR10双臂解决ur_robot_driver单控制器限制ur_robot_driver官方包默认仅支持单台UR机器人因其硬编码了/ur_hardware_interface话题名。要控制双UR10必须修改驱动源码使其支持多实例化。4.1 修改ur_robot_driver源码支持多控制器实例定位到ur_robot_driver/src/hardware_interface/ur_modern_driver.cpp修改构造函数签名// ur_robot_driver/src/hardware_interface/ur_modern_driver.cpp URModenDriver::URModenDriver(ros::NodeHandle nh, const std::string robot_ip, const std::string script_file, const std::string tool_comm_port, const int tool_comm_baudrate, const std::string prefix) : nh_(nh), robot_ip_(robot_ip), script_file_(script_file), tool_comm_port_(tool_comm_port), tool_comm_baudrate_(tool_comm_baudrate), prefix_(prefix) { // 新增prefix参数 // 所有topic/service名称添加prefix joint_state_pub_ nh_.advertisesensor_msgs::JointState(prefix_ /joint_states, 1); io_pub_ nh_.advertiseur_msgs::IOStates(prefix_ /io_states, 1); speed_slider_service_ nh_.advertiseService(prefix_ /speed_scaling_factor, URModenDriver::setSpeedSlider, this); // ... 其他topic同理 }在ur_robot_driver/src/ur_driver_node.cpp中支持传入prefixint main(int argc, char** argv) { ros::init(argc, argv, ur_driver); ros::NodeHandle nh(~); std::string robot_ip, script_file, tool_comm_port; int tool_comm_baudrate; std::string prefix; // 新增 nh.paramstd::string(robot_ip, robot_ip, 192.168.56.101); nh.paramstd::string(script_file, script_file, ); nh.paramstd::string(tool_comm_port, tool_comm_port, ); nh.paramint(tool_comm_baudrate, tool_comm_baudrate, 115200); nh.paramstd::string(prefix, prefix, left_); // 默认left_ URModenDriver driver(nh, robot_ip, script_file, tool_comm_port, tool_comm_baudrate, prefix); driver.run(); return 0; }4.2 部署双UR10真实硬件IP与启动脚本分离为两台UR10分配独立IP如192.168.56.101和192.168.56.102并创建启动文件!-- launch/real_dual_ur10.launch -- launch !-- 启动左臂驱动 -- node nameleft_ur_driver pkgur_robot_driver typeur_driver_node outputscreen param namerobot_ip value192.168.56.101/ param nameprefix valueleft_/ param nameuse_ros_control valuetrue/ /node !-- 启动右臂驱动 -- node nameright_ur_driver pkgur_robot_driver typeur_driver_node outputscreen param namerobot_ip value192.168.56.102/ param nameprefix valueright_/ param nameuse_ros_control valuetrue/ /node !-- 启动双臂controller -- node namecontroller_spawner pkgcontroller_manager typespawner args left_ur10/left_arm_controller right_ur10/right_arm_controller --namespace /left_ur10 --namespace /right_ur10 / !-- 启动robot_state_publisher -- node namerobot_state_publisher pkgrobot_state_publisher typerobot_state_publisher param namerobot_description value$(find dual_ur10_description)/urdf/dual_ur10.urdf/ /node /launch注意UR示教器中必须关闭“远程控制”安全限制并在“设置”→“系统”→“外部控制”中启用“允许外部控制”。若连接失败用telnet 192.168.56.101 30003测试端口连通性——UR10默认开放30003primary interface端口。5. 源码结构与开发文档要点快速定位关键模块与调试入口本项目源码按功能分层组织避免新手陷入ROS包依赖迷宫。核心目录结构如下目录用途关键文件dual_ur10_description/双臂URDF与mesh资源urdf/dual_ur10.urdf.xacro,meshes/dual_ur10_gazebo/Gazebo仿真配置launch/gazebo.launch,config/dual_ur10_controllers.yamldual_ur10_control/C控制器与运动学src/kinematics/,src/nodes/dual_arm_controller.cppdual_ur10_bringup/真实硬件启动脚本launch/real_dual_ur10.launch,config/ur10_real.yamldocs/开发文档setup.md,debugging.md,performance_benchmarks.md5.1 开发文档核心章节解析docs/setup.md明确列出三类环境要求Ubuntu 20.04 ROS Noetic因UR10官方驱动仅支持NoeticROS2 Humble需额外移植Gazebo 11.0低于11.0版本不支持gazeboplugin嵌套语法URCap 1.0.5安装于UR示教器提供外部控制协议支持docs/debugging.md提供故障速查表现象检查点命令Gazebo中双臂抖动物理参数是否过小gz sdf -p $(rospack find dual_ur10_gazebo)/urdf/dual_ur10.gazebo.urdf.xacro | grep -A5 inertial真实UR10无响应驱动节点是否崩溃rosnode info /left_ur_driver查看Publications是否为空双臂轨迹不同步时间戳是否统一rostopic echo /left_ur10/joint_states/header/stamp与/right_ur10/joint_states/header/stamp对比差值5.2 性能基准测试数据验证双臂协同精度在docs/performance_benchmarks.md中记录实测数据基于激光跟踪仪测量场景位置误差mm角度误差°控制周期msGazebo单臂轨迹跟踪±0.3±0.151.2Gazebo双臂协同搬运±0.7±0.221.8真实UR10单臂执行±0.6±0.202.1真实UR10双臂同步抓取±0.8±0.252.5关键结论双臂同步误差主要来自真实硬件的EtherNet/IP通信延迟平均1.8ms而非算法本身。若需更高精度建议启用UR10的Real-Time Ethernet模式需专用交换机。验证双臂同步性的最简方法发布一个正弦波轨迹观察两臂末端执行器在RViz中的距离变化# 启动RViz并加载双臂模型 roslaunch dual_ur10_bringup rviz.launch # 发布同步正弦轨迹持续30秒 rosrun dual_ur10_control sine_trajectory_generator _amplitude:0.1 _frequency:0.5 _duration:30.0在RViz中添加TF显示观察left_ee_link与right_ee_link坐标系距离——稳定状态下应保持在1.200±0.001m即基座间距。若波动超过±0.003m需检查/joint_states消息发布频率是否一致用rostopic hz /left_ur10/joint_states对比。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻