UR机械臂Gazebo仿真集成FT300力传感器,强化学习力控观测搭建 上一篇文章把UR5在Gazebo里的仿真环境跑起来以后我以为后面就顺风顺水了。结果做插孔任务时位置控制能让机械臂稳稳怼到孔附近但一接触就失控policy大量输出了“死推”的行为——因为仿真环境里根本没有力反馈这个观测量。后来我研究了一下robotiq ft300力传感器的Gazebo集成把六维力和力矩信息接入了强化学习观测空间整个训练效果才算正常。今天这篇文章就是记录这个过程包括URDF模型改造、Gazebo插件配置、wrench消息处理和训练对接几个环节覆盖我从零到一踩过的所有坑。内容以ROS Gazebo UR5/UR10这套经典组合为例对想要在UR机械臂仿真里做力控、插孔、打磨、装配这类接触任务的强化学习玩家应该能省下不少排查时间。1. 为什么UR仿真必须折腾一个力传感器1.1 没有力反馈的接触任务训练出来的是“死力”许多人刚开始会想Gazebo里的contact sensor不也能检测碰撞吗接触传感器和六维力/力矩传感器的区别在于前者只告诉你“碰没碰到”最多给一个标量接触状态而ft300给出的是完整的三轴力分量和三轴力矩分量。对强化学习agent来说这六个数值里包含了接触发生的方向、扭转趋势甚至是末端偏差的线索。我举一个实际例子。插孔peg-in-hole任务里如果观测空间只有关节位置和关节速度策略经常会把销钉推歪然后在孔口附近反复打转。因为agent根本不知道“现在侧向受力有多大是不是应该往左偏一点”。接上ft300之后agent在碰到孔壁的瞬间就能从Fx、Fy方向判断出应该往哪个方向修正训练收敛的速度明显变快。这是我做了几组对比实验后的感受不是玄学。1.2 Gazebo不会白白送给你一个FT300Gazebo自带力/力矩传感器sensor typeforce_torque但默认的UR模型里并没有这一节。你通常得自己把FT300的link塞进URDF并且配置对应的Gazebo插件它才会发布话题。很多人这一步用错插件或者把sensor标签放在错误的link下面结果话题是空的绘图工具里一条直线。所以“在UR机械臂仿真环境里集成力传感器”并不是开箱即用的事。真机上你拧上FT300就有数据仿真里你得先建模。这也正是这篇文章存在的核心意义——把真实传感器的装配位置、数据方向、噪声特性尽量还原到仿真里。你后面训练出的策略能不能迁移到真机很大程度上取决于这一层的还原精度而不仅仅是算法选得好不好。1.3 本文使用的整体技术路线基础环境Ubuntu 20.04 ROS Noetic Gazebo 11如果你还在用Melodic配置基本一致个别插件名有区别我会在文中标出来。机械臂模型ur_description里的UR5或UR10对UR3、UR16e同样适用。传感器模型robotiq ft300通过URDF固定关节挂在tool0与末端工具之间。插件gazebo_ros包里的FTSensor插件发布geometry_msgs/WrenchStamped。对接gym环境里订阅wrench话题做重力补偿与滤波后拼进observation。这套方案不需要改动MoveIt和controller的现有结构训练链路保持独立后续替换真实传感器发布的话题时也能做到最小改动。2. 挂在哪个环节决定了你测到的是什么力2.1 tool0与末端工具之间才是FT300的“家”FT300在真机上通常装在机器人法兰盘与末端工具之间。仿真里你也应该按同样的顺序串联UR的tool0 → ft300_link → 工具链接。如果挂在别的位置读出的力就不代表“末端接触力”可能混入了整条手臂自身的重力和惯量。具体修改URDF时我习惯用固定关节把ft300_link接在tool0下面joint nametool0_ft300_joint typefixed parent linktool0/ child linkft300_link/ origin xyz0 0 0 rpy0 0 0/ /joint link nameft300_link inertial mass value0.45/ origin xyz0 0 0.01 rpy0 0 0/ inertia ixx0.002 ixy0 ixz0 iyy0.002 iyz0 izz0.001/ /inertial /link这里原点直接设成0是因为ft300的几何厚度和重心位置在仿真精度需求不高时可以简化但如果你之后要接视觉或碰撞模型还是建议按官方CAD数值填。你可能会问质量为什么取0.45kg因为FT300大概就是这个量级。质量字段不能省否则传感器在动力学层面体会不到负载力变化后面读出来的数据和真实情况会差得离谱。接着把gripper或末端工具的父link改成ft300_link整体链路就变成UR的ee_link → tool0 → ft300_link → tool_link → ...注意如果你用的UR包是旧版本tool0其实就是ee_link的child你需要先确认URDF里tool0是否真的存在。通常universal_robot和ur_description这两种常用package里都有。2.2 坐标系方向必须较真六维力传感器最坑的不是数值是方向。FT300的默认坐标系里z轴垂直于法兰面朝外x/y轴分别在法兰平面上。UR的tool0坐标轴定义也遵循这个约定多数情况下两者可以直接用单位矩阵对齐但有些网上下载的URDF为了省事给ft300_link的origin加了奇怪的欧拉角。一旦方向和真机不一致你训练的力控策略迁移到真机必出问题。我在集成后会在RViz里做一次TF核对打开TF面板查看tool0和ft300_link之间是否有旋转。只要有非零rpy就必须想清楚这是故意的设计还是原本就错了。我自己常用的验证方法是让机械臂末端朝下用一个向上的虚拟力去推工具然后看wrench的z分量符号是否符合预期。这个测试能同时检验插件的measure_direction是否设置正确。2.3 在URDF里插入Gazebo传感器标签link准备好了还要让Gazebo知道这个link上挂着一个force_torque传感器。在URDF的 标签里加入gazebo referenceft300_link sensor nameft300_sensor typeforce_torque always_ontrue/always_on update_rate500/update_rate force_torque framechild/frame measure_directionchild_to_parent/measure_direction /force_torque plugin nameft300_plugin filenamelibgazebo_ros_ft_sensor.so ros remapping~/out:/ft_sensor/raw/remapping /ros frame_nameft300_link/frame_name gaussian_noise0.05/gaussian_noise /plugin /sensor /gazebo这里有两个地方我反复栽过跟头。一是frame。取值child表示输出的力是在传感器的child坐标系就是一般声明的ft300_link下测量的取parent就是父坐标系。如果你填错了数据方向会翻转力和力矩还会产生交叉影响。看起来数据有变化实际完全是错的。二是measure_directionchild_to_parent会输出传感器本体施加到父link的力也就是外界对传感器的作用力方向。这个设置如果反过来力分量和力矩分量的正负号都会反向policy照样学不出正常行为。2.4 用检查工具和RViz确认模型与TF链路改完URDF先跑一遍模型检查再启动仿真check_urdf ur5_with_ft300.urdf urdf_to_graphiz ur5_with_ft300.urdf第一条会输出模型树结构第二条会生成一个pdf形状的链路图。随后启动Gazebo和control在RViz里添加RobotModel正常应该看到一个完整的机械臂传感器工具模型TF树从world一路到tool_link必须是连续的。有个经验Gazebo启动前在launch文件里加上物理引擎相关插件参数比如-s libgazebo_ros_factory.so和-s libgazebo_ros_force_system.so否则插件加载可能静默失败。加载成功后终端里输入rostopic list | grep ft_sensor只要出现自己命名的/ft_sensor/raw话题就说明插件已经跑起来了。如果这里没有输出就不要往下做先回头查plugin名字和URDF路径这是第一道闸门。3. 让仿真里的FT300开口说话频率与噪声的调校3.1 为什么我坚持把update_rate拉到500HzFT300真机的采样率可以达到500Hz以上而很多Gazebo控制器默认只有几十赫兹。在强化学习训练中观测频率如果低于控制的决策频率就会出现“传感器读数是上一时刻的动作却是这一时刻的”这种错位训练会直观地表现为loss震荡。我在实验里对比过50Hz、200Hz、500Hz三档结论是如果你的训练循环只有20~50Hz传感器给到200Hz基本够用但如果你做的是阻抗控制或力位混合控制建议直接拉到500Hz因为力控环路对相位延迟极其敏感。注意update_rate提高后话题发布频率也会提高下游如果没有做时间同步实际等效噪声会变大所以不能只无脑调高频率还得配合滤波。3.2 gaussian_noise一条容易偷懒但会坑死人的配置仿真里的传感器默认是“完美传感器”读数没有噪声。真实FT300在静止时读数也有零漂和高斯噪声量级大约在0.1~0.5N。直接把完美读数喂给强化学习等于给策略一个上帝视角迁移到真机后传感器噪声会让它瞬间不知所措。我建议在插件的gaussian_noise里至少写一个0.05~0.1N的基础噪声并且在实际训练时还要在代码里叠加一个范围更大的随机偏置。仿真里噪声设得比真机略大对训练来说是安全的因为真机环境对agent来说反而会更可预期。后面我还会单独讲一下仿真噪声与迁移之间的关系。3.3 完整启动后的验证清单配置完成后按下面顺序逐项确认每一步都要亲眼看数据不要只看有没有报错。运行rostopic echo /ft_sensor/raw确认有数据刷新实际频率与update_rate一致。让机械臂末端做小幅运动观察wrench是否随姿态变化——如果始终为全零直接跳到第5节排查。在Gazebo里用鼠标给末端工具一个扰动wrench数值应立即变化方向符合物理直觉。用rqt_plot同时绘制Fz和关节位置确认没有异常的尖峰。我把关键配置项和推荐值列成一个表方便对照检查配置项推荐值说明update_rate200~500 Hz低于100Hz时力控训练容易震荡framechild输出在传感器本地坐标系measure_directionchild_to_parent表示外界对传感器的力方向gaussian_noise0.05~0.1 N模拟真实传感器噪声防止过拟合话题名/ft_sensor/raw自定义但训练代码里要保持一致这套验证做完基本上就可以认为仿真里的FT300“能用”了。接下来要做的才是真正影响训练效果的部分把原始读数变成强化学习可以消化的观测。4. 从wrench原始读数到强化学习观测中间的三道关4.1 拿到数据后先做重力补偿否则agent学到的全是姿态FT300在真机上的原始读数里包含末端工具的重力分量。机械臂姿态变化时即使没有接触任何物体wrench的三个力分量也会跟着变。直接把这个原始读数作为observationagent会把重力分量当成接触力策略学习会受到极大干扰。真机上的做法是装好传感器后执行一次tare去皮。仿真里更简单在gym环境初始化时让机械臂运动到初始姿态读取此时的wrench作为bias之后的所有读数都减去这个bias。但注意这个bias只在固定姿态下有效。如果训练过程中机械臂大范围运动bias会随姿态变化最好在每次episode开始时更新一次或者用末端工具的质量和当前姿态做一个前馈补偿。我写了一个轻量版本训练时把当前末端旋转矩阵和工具质量带入g np.array([0, 0, -9.81]) tool_mass 0.45 R_world_to_sensor tf_lookup(world, ft300_link) gravity_bias tool_mass * R_world_to_sensor g force_contact raw_wrench.force - gravity_bias这个补偿对水平旋转姿态处理得很快但在复杂姿态下还是建议用episode起始时刻的测量值做bias更稳。实际做的时候也可以把重力补偿后的残差信号再做一次低通滤波防止因坐标变换插值产生的微小抖动。4.2 滤波与坐标变换不要让毫秒级毛刺毁掉策略接触力的特点是高频冲击明显但训练观测并不需要太高频的抖动。我在代码里用一个简单滑动窗口对wrench做平滑窗口大小取5对应500Hz发布频率下约10ms延迟这个延迟对强化学习的影响非常小但能有效去除接触瞬间的尖峰。坐标系变换同样容易被忽略。仿真里插件输出的frame_name被设置成ft300_link如果你的策略动作空间在基坐标系下定义或者奖励函数需要的是tool0坐标系下的力就需要做旋转变换。我的习惯是在gym环境里统一约定所有观测都转换到tool0坐标系这样策略不容易对环境模型过拟合后续换到其他机械臂型号也更方便。4.3 归一化是力传感器数据进神经网络的最后一道闸六维力数据的数值跨度极大力可能是几十牛顿量级力矩可能只有几个牛顿米。直接把Fx和Tz塞进同一个MLP训练很容易被数值大的维度主导。我的做法是对每个维度做线性归一化力分量映射到[-1, 1]力矩分量也映射到[-1, 1]并保存归一化系数方便真机部署时复用。还可以把力信号做一些变换比如计算合力大小sqrt(Fx^2Fy^2Fz^2)作为额外一个观测维度。接触是否发生这件事用一个标量表达有时比三轴分量更直接。我试过这种方式接触状态明确的短任务里收敛反而快一点。obs_force np.clip(force_contact / 50.0, -1.0, 1.0) obs_torque np.clip(torque_contact / 5.0, -1.0, 1.0)50N和5Nm是比较适合UR5末端操作任务的量级。真实训练时可以先记录一批无接触数据用统计值替代固定阈值这样归一化更贴合实际分布。5. 我在集成ft300时踩过的坑以及完整排查链路5.1 话题存在但一直输出全零这是我遇到的第一个问题排查链路值得记录。先是rostopic echo看到话题存在、频率正常但数值全是0。接着检查URDF里sensor的reference是否正确、插件有没有加载全都没发现问题。最后发现是Gazebo的物理引擎在加载完模型后力的计算没有正确传递到传感器。解决方案是给ft300_link加上一个极小的碰撞体和一个极小的惯性让物理引擎真正把外力算进传感器。具体做法是在link描述里加一个radius0.001的cylinder碰撞体。有时候更诡异加完碰撞体仍然全零重启整个roscore之后就好了。这个“重启治百病”在Gazebo里非常灵验。我建议收到全零数据时不要一上来就改代码先把Gazebo完全退出、再launch一次省得在错误配置上浪费半小时。5.2 仿真数据太干净训练后迁移真机怀疑人生我之前试过把gaussian_noise设成0训练出来的策略在仿真里很好看收敛快、成功率接近100%。但那个策略对数据分布的假设非常脆弱一旦传感器有点毛刺动作就剧烈抖动。后来我在仿真训练时固定加0.1N噪声策略反而稳健不少。别忘了真实传感器还有零点漂移它不是固定bias而是随温度缓慢移动。所以我在训练代码里还故意给bias加了一个缓慢变化的随机游走模拟温度漂移。这个小技巧让我的策略迁移真机后适应快了很多。这里不展开真机部署细节但仿真阶段把传感器的“脏”模拟出来是迁移学习的关键一环。5.3 传感器发布频率和控制循环频率打架因为我一开始把update_rate设成了200Hz训练循环也是20Hzraw话题每次拿到的都是最新值看着没问题。但接入自定义力控奖励后策略的loss开始震荡。后来发现是在reward计算里直接用了当帧的wrench而wrench和控制指令没有同步导致奖励在不同训练轮次里出现相位变化。解决办法是在环境里设一个保护锁确保每个step只在拿到新wrench后进行计算或者带上时间戳做插值对齐。如果懒得做同步还有一个投机取巧的办法把update_rate降低到与控制频率相同的20Hz等系统稳定后再调高。这个方法丢了高频信息但胜在简单适合先验证算法逻辑。5.4 FT300装到gripper前还是后读到的力完全不是一回事如果你的末端工具是Robotiq 2F-85夹爪FT300可以装在夹爪法兰之前也可以装在夹爪执行器之间。在真机上装的位置决定你测的是“夹爪整体受的力”还是“夹爪指尖受的力”。在仿真里也一样URDF里传感器的reference不同外载荷路径不同wrench含义就完全不同。如果后面的策略依赖指尖受力去做精密装配传感器挂在夹爪后侧会丢失很多有效信息如果挂在夹爪前侧夹爪开合动作又会对传感器读数造成扰动。这个取舍没有标准答案只能根据任务需求选。实际项目里我是把FT300放在tool0和gripper之间这样读取的是整个末端组件的受力对要区分“夹持目标”和“环境碰撞”的任务更合适。如果做的是抓取过程中的滑移检测那就必须把传感器尽量靠近指尖。最后说一个我自己的习惯每次改完URDF或插件配置先在仿真里做一个0.05Hz的慢速正弦运动同时把六个自由度的wrench记录下来随姿态变化曲线应当平滑且符合理论重力补偿曲线。这条曲线既能验证传感器配置是否正确也能在后续调参时作为对照基线。等到你在训练日志里看到策略开始利用力信号调整动作而不是硬推这套环境就算真正活起来了。下一步我准备做的一个方向是把FT300的读数接入阻抗控制的导纳外环做成一个完整的柔顺装配demo。到时候会继续把过程写出来。