V-REP这个系列的教程写到这里,已经过了基础的建模、运动学、路径规划这些环节,但说实话,很多做机器人仿真的朋友卡住的地方不是怎么搭场景,而是怎么让仿真里的关节“像真的电机一样干活”。尤其是关节力矩控制这块,位置控制和速度控制文档里到处都是,一说到力矩控制,能讲清楚的不多,能调出稳定表现的更少。再加上物理引擎属性这个老生常谈又很玄学的选项,选哪个引擎、参数怎么配,直接决定你仿出来的东西到底靠不靠谱。这篇文章就把这两块掰开揉碎聊清楚,重点是实操层面怎么设、怎么调、怎么判断结果,适合已经在V-REP里搭过简单模型的读者,也适合那些刚想从位置控制往力矩控制过渡、又不知道怎么下手的朋友。
聊力矩控制之前,得先把V-REP里关节控制的一种基本关系理清楚,不然很多人一上来就卡在“我设了力矩为啥不动”这种问题上。
1. 关节力矩控制的前置认知:三种控制模式怎么选
1.1 位置、速度、力矩三种模式的本质区别
V-REP里的关节动态控制,本质上就三条路可走:位置控制、速度控制、力矩控制。位置控制是默认模式,也是大家最熟悉的——你给一个目标角度,控制器通过PID把关节从当前角度拽到目标角度。速度控制类似,给的是目标角速度。这两种模式都是“闭环控制”,也就是说仿真器会不停读关节当前的位置/速度,再根据误差去调整驱动力。
力矩控制则完全是另一套逻辑。它不经过PID调节,而是直接告诉关节电机“你现在给我输出多少力或力矩”,相当于开环地给定一个驱动力。关节最后转不转、转多快,完全取决于这个力和当前负载、摩擦力、重力这些外部作用力之间的博弈。
我在V-REP里调试时最常碰到的情况是,用户把关节设为力矩模式后,给了一个1N·m的力矩,结果关节纹丝不动,就开始怀疑模型有问题。其实大概率是没搞明白:这1N·m到底是抵抗重力还不够,还是被Fmax限幅给吃了,还是干脆物理引擎没启用。
1.2 什么时候必须用力矩控制
现实里用到力矩控制的地方,恰恰是那些位置控制做不好的场景。典型的有这样几个:
第一个是力控抓取。夹爪夹住一个易碎物体,如果只靠位置控制夹紧,很容易把东西夹碎,因为位置控制只看“到了没到”,不看“出没出力”。力矩控制可以设定一个合适的夹持力,让夹爪刚好稳住物体又不破坏它。
第二个是关节拖动示教。协作机器人的那种“人拖着手臂走”的功能,本质上就是让每个关节处于力矩模式,并且给一个很小的目标力矩,让人手一推关节就跟着走,而不是像位置控制那样和你较劲。
第三个是柔顺控制。关节在接触环境时需要一个“弹性”的状态,比如打磨、装配,这种场景下力矩控制是基础。
第四个是平衡控制。倒立摆也好,双足机器人也好,要想在平衡点附近做微小调节,位置控制反应慢且生硬,力矩控制才能做到像真机一样的连续发力。
所以判断标准很直接:如果你关心的是“送到哪”,用位置控制;如果关心的是“出多大劲”,用力矩控制。很多时候两种模式还会混着用,比如运动时用位置模式,接触后用力矩模式,这在V-REP里通过切换控制模式就能实现,不需要重新搭建模型。
1.3 力矩控制为什么难上手
搞清楚了概念不代表能跑通。V-REP里力矩控制难上手,主要有三个原因。
第一个原因是参数耦合。力矩输出上限Fmax、目标力矩值、物理引擎的迭代次数、阻尼系数,这些参数相互影响。同一个模型,Bullet引擎下表现正常,换到ODE引擎可能直接飞掉。
第二个原因是反馈少。位置控制有明确的“到达目标位置”这个结果,力矩控制的结果体现在加速度和速度的变化上,在graph里看数值比较抽象,不利于判断对错。
第三个原因是反直觉。很多人按直觉以为给了扭矩关节就会匀速转动,但现实是有恒定力矩时关节是匀加速转动的,速度会一直涨,除非被阻尼或者Fmax限制住。这跟物理直觉一致,但跟日常经验不一致,所以初学时容易觉得“不对”。
我建议刚开始练习时,不要急着做复杂场景,先在单关节模型上把力矩控制跑明白,再看怎么扩展到多关节,这也是这篇文章后面实操案例的思路。
2. 在V-REP里把关节力矩控制跑起来
2.1 关节动态属性里的关键开关
打开任意一个关节的动态属性对话框,里面有这么几个关键选项。首先是Motor enabled,不勾这个,关节就是个被动铰链,整个力矩控制无从谈起。其次是Control loop enabled,这个是区分控制模式的总开关,关掉之后就是纯力矩/纯速度开环控制,打开之后才能做位置控制。
在V-REP旧版本和新版本CoppeliaSim里,控制模式的选择会直接在动态属性里体现。你要用力矩控制,就需要把控制模式切到Force/torque control,同时设置好目标力矩。有个很容易被忽略的细节是,切换模式以后,关节仍然显示的是位置控制相关的UI参数,但那些参数已经失效了,真正生效的是Target force/Torque和最大力矩。
我自己在操作中习惯用脚本统一处理,而不是每次去点对话框。因为对话框改参数只对当前场景有效,脚本才能把参数固化下来,这也是长期做仿真项目的一个好习惯。
2.2 target force、Fmax、实际输出力矩的区别
这三个值必须分清,不然调试时会被数值搞晕。
Target force(目标力矩)是你“希望”电机输出的力矩值,是设定值。Fmax(最大力/力矩)是电机的物理上限,可以理解为真实世界里的电机扭矩极限。实际输出力矩则是物理引擎每步解算后真正施加到关节上的值,它受限于Fmax,同时受接触、摩擦、载荷影响,不是你说多少就是多少。
我在实际项目里常用一个简单的规则:Fmax设为目标力矩的1.2到1.5倍以上。这样目标力矩不会轻易被Fmax截断,你观察到的输出曲线也才能反映真实控制效果。如果把Fmax设得比目标力矩还小,那么目标力矩永远达不到,输出直接被限幅削平,表现出来就是关节无力、动作缓慢。
这里补一个实用的代码片段。在V-REP的embedded script里,设置力矩控制只需要几行:
function sysCall_init() joint = sim.getObjectHandle('Joint') sim.setJointMaxForce(joint, 5.0) -- 电机最大输出5N·m sim.setJointTargetForce(joint, 1.0, true) -- 目标力矩1N·m,带符号 end第三个参数如果为true,表示力矩带符号,根据目标方向输出正负力矩;如果为false,则按绝对值输出,方向由当前关节运动趋势决定。这个细节特别容易踩坑,我调试夹爪时,曾因为设了false导致夹爪往反方向使劲,好一阵子没找到原因。
2.3 从真实案例切入:算一算单关节需要多大力矩才能保持水平
力矩控制不在真空里用,最终要落到具体负载上。这里用一个最简单也最有代表性的案例切入:一根关节悬臂,水平伸着,关节需要输出多大力矩才能让悬臂保持不往下掉。
设悬臂质量为m,质心到关节轴距离为d,重力加速度g=9.8m/s²,那么重力产生的力矩就是mgd。如果你要让悬臂保持水平,且不考虑关节摩擦,目标力矩就应当等于这个值,以反方向输出。
举个例子。悬臂质量2kg,质心距离0.3m,算下来重力力矩约为29.80.3=5.88N·m。那么目标力矩就该设成5.88N·m附近,Fmax至少设成8N·m。这时候关节会有一个稳定的输出,悬臂悬停,不会下落。你仔细观察graph里的实际输出力矩曲线,会发现它有微小的波动,因为仿真里每一步的数值都在微调,这是物理引擎解算的正常现象。
这个例子极其重要,它是理解力矩控制的基础:力矩控制是做“力的平衡”,而不是做“位置跟踪”。你把5.88设对了,悬臂悬停;给得少了,悬臂下沉;给得稍微大了,悬臂会缓慢上抬,直到被其他结构挡住或者达到一个新的平衡。这种微妙的物理反应,正是力矩控制跟位置控制最大的不同。
2.4 怎么把实际输出力矩读回来做分析
很多人设完了力矩,但是不知道怎么验证,这时候要用sim.getJointForce这个API。它返回的是当前关节实际经历的力或力矩,在动态模式下可以理解为电机的实际输出。配合graph模块,把关节目标力矩和实际输出力矩画在同一个坐标系里,就能很直观地看到限幅、滞后和震荡。
读回数据的脚本一般长这样:
function sysCall_sensing() local applied = sim.getJointForce(joint) local amplitude = sim.getJointTargetForce(joint) local vel = sim.getJointVelocity(joint) -- 然后按你需求把变量写到graph流里面 end注意一点,sim.getJointForce返回的是一个向量还是标量,在不同版本里有差异。旧版V-REP里返回的可能是关节轴方向的力矩值,你也可以通过sim.getJointForce(joint, {0,0,0})这样带参数的形式拿到更完整的数据。用的时候要以你手头版本的API文档为准,建议先在脚本里加一句打印,确认返回值的形态再往下写逻辑。
3. 物理引擎属性:选型与参数调整的完整指南
3.1 四种物理引擎的特点与差异
V-REP的优势之一就是内置了多套物理引擎,可以在不重写模型的情况下切换。但是引擎不是越多越好,选错引擎经常会让结果千差万别。V-REP/CoppeliaSim里主要能选的引擎有Bullet、ODE、Vortex和Newton四种。
Bullet是最常用的,默认也是它,特点是速度快、迭代参数直观,适合大多数刚体动力学仿真。ODE在关节约束处理上表现不差,处理速度也不错,但是版本更新慢,有些边缘碰撞问题处理得比较粗糙。Vortex是商业引擎,物理精度最高,摩擦、接触模型丰富,适合对精度要求极高的工业级仿真,但是需要授权。Newton在V-REP里的存在感不如前几个,特点是对碰撞和堆叠比较稳定,适合特定场景。
我给一般用户的建议是:没有特殊需求就老老实实先用Bullet,把关节和力矩控制跑通再说。等你要做轮式机器人、履带、履带与地面附着这些对接触模型敏感的场景时,再考虑Vortex。别一上来就迷信“越贵越好”,仿真里90%的问题都不是引擎不够强,而是参数没调对。
3.2 仿真设置里的引擎属性和公共属性
物理引擎属性分成两级,一级是仿真设置里“全局”的引擎属性,另一级是每个物体shape的“个体”引擎属性。很多人只调全局,忽略了每个shape自己的摩擦系数、弹性系数,这是导致仿真结果偏离预期的常见原因。
以Bullet为例,在Simulation settings对话框里,可以设置约束求解迭代次数(constraint solving iterations)、时间步长(time step)、线性阻尼、角阻尼等全局参数。迭代次数越高,仿真越不容易抖动,但计算也越慢。时间步长越小,结果越精确,代价同样是性能。这两个参数的合理组合,决定了整个仿真的稳定性和速度。
每个shape的引擎属性里,最需要注意的是摩擦系数。V-REP里物体接触时的摩擦力强度,很多情况下不是全局设置的,而是shape属性里的值在起主要作用。比如我做双足机器人时,脚底与地面的摩擦系数设太低,机器人一直打滑,全局参数怎么调都没用,最后修改脚底shape的摩擦系数才解决。
3.3 时间步长、迭代次数、阻尼的组合策略
这三组参数在综合起作用。物理引擎每仿真一步,都要解一遍约束方程,迭代次数就是约束求解器最多重复计算的次数。时间步长则是两步之间的物理时间间隔。如果你步长太大,物体单步位移过大,碰撞检测容易漏,也就是俗称的“穿透”。如果迭代次数太少,约束解算得不够收敛,物体就像在“发抖”。
我的经验是:做关节力矩控制,时间步长默认的50毫秒对应50Hz控制频率其实偏低,你可以第一步先缩到20毫秒,也就是50Hz,不行再缩到10毫秒甚至5毫秒。迭代次数Bullet默认的值通常够用,但如果关节连接处出现抖动,优先把迭代次数往上调而不是继续调阻尼。
阻尼这块也要小心。线性阻尼和角阻尼能抑制运动,但用多了会让运动看起来“发黏”。力矩控制的本质是力的交互,阻尼过高会让关节响应迟钝,你给的力矩像打在棉花上。所以调阻尼应该是辅助手段而不是主要手段,优先保证步长和迭代合理,阻尼只做细微修正。
3.4 引擎属性对关节力矩控制的隐藏影响
很多人不知道,同一个关节目标力矩,换一个物理引擎,结果完全可能不一样。原因在于各引擎对关节约束的处理方式不同,尤其是摩擦力、阻尼这些隐式参数,各引擎的默认值差异很大。
Bullet在默认参数下,关节的电机输出会比较“脆”,目标力矩变化能很快反映到关节运动上。ODE的关节约束相对硬一些,力矩加到关节上以后,表现出的响应没有Bullet那么灵敏。Vortex因为模型更精细,同一个目标力矩在低速和高速下的表现会有微妙差别,更适合以后做高保真控制算法验证。
这提醒我们一件事:在V-REP里做力矩控制,结论一定不能脱离引擎属性单独看。你这个力矩增益调得“刚好合适”,很可能只是刚好适配了某个引擎、某组参数。想要结论稳健,至少要在两种引擎下各跑一遍,看看差异在可接受范围里才算数。这也是我在给客户做仿真评估时比较坚持的一条原则。
4. 实操案例:从零搭一个关节力矩斜坡测试台
4.1 场景搭建思路
理论说再多,不如亲手搭一个例子。我这里给出一个非常简洁但信息量足够的测试台:一个基座,一个旋转关节,一根悬臂连杆。
基座固定在世界的原点,关节安装在基座顶端,连杆通过关节连接,初始状态让连杆水平伸出。场景的关键是,连杆和关节之间要建立正确的父子关系:关节是连杆的父对象。在V-REP里如果你把父子关系搞反了,整个链会乱套,力矩控制也没法正常工作。
搭建好以后,先把连杆的mass和inertia数据确认一遍。如果你是用V-REP自带的正方体/圆柱体形状,它会自动计算质量和惯性。如果是从外部CAD导入的模型,必须手动检查形状的密度、质量,不然算出来的重力矩会错。
4.2 参数配置清单
参数配置直接给一个可复现的清单,方便大家对照着设。
关节的电机最大力矩Fmax按前面算的留出余量,设成10N·m。目标力矩设成力矩斜坡,从0开始逐渐增加,方便观察关节从静止到运动的临界点。连杆质量设成2kg,质心到关节轴0.3m,这样理论上重力矩是5.88N·m,临界目标力矩应该在这个值附近。
物理引擎用Bullet,时间步长设成20ms,约束求解迭代次数按默认或者稍微调高一点,建议设成100。整个仿真时长设成5秒,方便采样足够多的数据点。
从这里能看出来一个规律:把“理论上应该发生什么”先算清楚,再去看仿真输出去印证。否则你连自己做出来的结果是对是错都判断不了。
4.3 用脚本实现力矩斜坡与数据记录
脚本逻辑很清晰:每仿真步,目标力矩从0开始,按每秒增加2N·m的速度往上加,然后记录当前时间、目标力矩、实际输出力矩、关节位置和关节速度。
function sysCall_init() joint = sim.getObjectHandle('Revolute_joint') sim.setJointMaxForce(joint, 10.0) sim.setJointTargetForce(joint, 0, true) dataFile = io.open('torque_ramp_log.txt', 'w') dataFile:write('time target actual position velocity\n') end function sysCall_sensing() local t = sim.getSimulationTime() local target = 2.0 * t -- 目标力矩不超过Fmax if target > 8.0 then target = 8.0 end sim.setJointTargetForce(joint, target, true) local actual = sim.getJointForce(joint) local pos = sim.getJointPosition(joint) local vel = sim.getJointVelocity(joint) dataFile:write(string.format('%.4f %.4f %.4f %.4f %.4f\n', t, target, actual, pos, vel)) end function sysCall_cleanup() dataFile:close() end注意sysCall_sensing里写文件可能会影响仿真实时性,但因为我们关心的是数据统计,不是实时性,所以完全没问题。如果你关心和控制频率相关的表现,就把数据记录放到后处理,不要在每个仿真步都做IO操作。
4.4 结果怎么看、怎么判断物理引擎行为正常
仿真跑完以后,把torque_ramp_log.txt拖到绘图工具里画曲线。你会看到典型的过程:目标力矩线性增加,实际输出力矩也跟随升高,但在目标力矩超过重力矩之前,连杆基本不动,速度和位置都趋近于0。等目标力矩逼近5.88N·m时,连杆开始缓慢转动,速度从0往上爬。
这个临界点出现的时间,就是你判断仿真是否“符合物理直觉”的关键。如果临界点在3N·m就出现了,说明你的连杆质量或者重力方向没设对。如果一直到8N·m连杆还静止不动,说明要么Fmax被限幅,要么阻尼系数过大了。
有个容易被忽视的点是,仿真里的关节即使不做力矩控制,也有默认的电机和摩擦特性。你在每一步设目标力矩之前,最好先确认关节没有开启位置控制模式,否则前面的力矩设置会被位置PID覆盖,表现出来的行为就会很奇怪。这类问题排查起来很浪费时间,我一般在搭建模型初期就把关节控制模式梳理清楚,再也不会在复现时产生这种混乱。
5. 常见问题与避坑经验
5.1 关节力矩控制排错速查表
我把这几年被人问得最多的问题整理成一张表,每个问题的排查思路都按优先级排好,照着顺序查基本都能找到原因。
| 问题现象 | 首要排查点 | 次要排查点 | 处理建议 |
|---|---|---|---|
| 设了目标力矩但关节不动 | Motor是否勾选、控制模式是否切到力矩模式 | Fmax是否小于目标力矩 | 用脚本强制设置模式并打印目标力矩确认 |
| 关节运动方向相反 | target force的signed参数是否设置正确 | 连杆的建模方向是否跟期望运动方向一致 | 把signed改为false或取反 |
| 关节出现高频抖动 | 时间步长是否过大 | 约束迭代次数不足 | 减小步长到10ms,迭代次数调到100以上 |
| 实际输出力矩始终达不到目标 | Fmax限幅了 | 阻尼参数被调得过高 | 提高Fmax到目标力矩的1.5倍并降低阻尼 |
| 同一套参数换引擎后表现差异大 | 引擎的隐式物理属性不同 | 各引擎默认参数不一致 | 按引擎分别调参,不要追求同一套参数通吃 |
| 目标力矩已经很大但速度还是上不去 | 角阻尼过大 | 关节处的摩擦被默认开启 | 调低角阻尼或关闭关节阻尼 |
这张表不是万能的,但覆盖了80%新手常踩的坑。遇到没有列出的问题,建议第一个动作永远是打印输出。你完全可以把目标力矩、实际输出、关节速度、位置这四组数据全部打印出来,观察是哪一步出了问题。仿真调试跟代码调试一样,不要靠猜,要靠数据。
5.2 我建议的引擎属性初始模板
最后分享一套我常用在关节机器人仿真上的初始参数模板。这套模板不一定最优,但能让绝大多数场景先稳定跑起来,再做微调。
Bullet引擎下,时间步长20ms,迭代次数100,线性阻尼0.1,角阻尼0.05。关节的Fmax设为关节理论最大负载的1.5倍。如果目标力矩需要考虑重力补偿,优先用脚本计算,不要在对话框里手填静态值,因为负载一变化静态值就废了。
ODE引擎下,建议把global erp设得比默认值略低,global cfm略高一点,这样关节约束会更软,不容易弹开。Vortex下尽量开启自动摩擦模型,但要把每个接触物体的摩擦系数过一遍,不然会出现一件“摩擦力过大,机器人立定不动”的怪事。
另外一个小建议:一个模型持续调了很久还是不稳定,别硬扛,直接把物理引擎换掉试试。换引擎这一步,往往比调几十个参数还快见效。V-REP对多引擎的支持本身就是很大的优势,好好利用它,不要让它变成劣势。
5.3 关于“仿真里的力矩控制”与“真实机器人力矩控制”的差别
写到这里,我想多说一句。很多人做仿真久了,会觉得仿真里的力矩控制参数能直接迁移到真机上,这是很危险的假设。仿真里的电机是理想模型,没有电流环延迟,没有摩擦非线性,没有温度漂移,这些在V-REP里要么被简化要么被忽略。
我做的真实力矩控制项目里,同样的目标力矩,真机上会发生明显的抖动和响应滞后,因为真实电机有惯性负载、减速器背隙、连接件弹性这些仿真里很难精确建出来的东西。仿真里建模的目标,是验证你的算法逻辑、计算力的大小、观察运动的趋势,而不是复刻真机的每一个细节。
所以我的习惯是,仿真结果要有“物理正确性余量”:仿真里留了10%余量的力矩值,到真机上要留30%以上。这样算法迁移时才不会因为一点细节差异就崩掉。这也算是我做多年机器人项目积累下来的一个实在经验。