
做机械臂仿真的朋友应该迟早会遇到这么一道坎纯位置控制下轨迹跟踪做得漂漂亮亮但只要机械臂一接触环境——推个门、打磨一个曲面、做一次插孔装配——系统要么瞬间把目标顶飞要么关节力矩直接爆炸。原因在于位置控制本质上把机械臂当成了一根刚性杆跟环境硬碰硬。现实中的很多任务恰恰需要“柔”而阻抗控制Impedance Control就是给机械臂的系统层面装配出一层“弹簧-阻尼”柔性。配合上MuJoCo这个在机器人仿真和强化学习社区里应用最广的物理引擎用它在关节空间实现阻抗控制是性价比最高的入门路径。这篇文章从我实际跑通的一个示例出发把阻抗控制的控制律拆解、MuJoCo的力控API、MJCF模型配置、参数整定和常见坑位全部过一遍完整代码附在正文里。适合已经在用MuJoCo做仿真、但还没碰过力控的朋友也适合想快速在仿真里验证柔顺控制思路的机械臂爱好者和机器人方向研究生。1. 阻抗控制到底在解决什么问题开始写代码之前先把动机聊透。阻抗控制不是这几年才出现的新东西Hogan在1985年提出的那一套阻抗/导纳框架今天依然是机器人交互控制的理论基石。理解它为什么存在比背诵公式更重要。1.1 位置控制的核心矛盾机械臂为何“不会退让”想象一个最朴素的场景机械臂末端装了一个刚性指头要去按一个按钮。位置控制下机械臂会以极高的刚度把末端推到规划好的目标点。如果按钮的行程比规划深度短机械臂不会“知道”该停它只会继续往前推结果就是接触力飙升轻则按钮损坏重则电机过流。我给一个生活化类比用指节去顶桌面感觉非常硬桌面收到的冲击力完全没缓冲但用手指肚去顶指肚皮肤和肌肉会变形接触力被分散到更大的时间尺度上这就是“柔性”带来的效果。位置控制的机械臂相当于指节阻抗控制则相当于给机械臂装了一层可以调节厚度的“指肚”。在装配、打磨、拖动示教这类场景里机械臂需要的是可控的柔顺性而不仅仅是“足够硬的位置刚度”。1.2 阻抗的本质不是控制力而是控制“力的响应”阻抗控制的核心思路是不去直接控制接触力而是控制机械臂在外力作用下的运动响应关系。这个关系可以理解成一个广义的弹簧-阻尼-质量系统公式写出来是[ M(q)\ddot{q} D\dot{q} K(q - q_{des}) \tau_{ext} ]其中 K 是关节刚度D 是阻尼M 是惯性项τ_ext 是外界施加到关节上的力矩。这个式子的物理含义非常直观外力越大关节偏离目标位置的幅度就越大刚度 K 越大同样的外力下偏离越小机械臂表现越“硬”阻尼 D 越大系统收敛到平衡点越快且越不容易振荡。在实际工程实现中我们通常在关节层面用如下控制律来逼近这个目标动力学[ \tau K(q_{des} - q) - D\dot{q} \tau_{gravity} ]也就是说把机械臂自身重力项补偿掉再用比例项当弹簧、阻尼项当缓冲最终输出的是关节力矩指令。这里的 K 和 D 就是我们可以调的阻抗参数用户看到的效果就是推它它会退让松开它它会回到目标附近。1.3 为什么选 MuJoCo 来做这个验证选择MuJoCo有几个非常现实的理由。第一MuJoCo在力矩层面的控制接口非常直接一个motor类型的执行器就能让你往关节里写扭矩指令不需要像某些引擎那样绕来绕去。第二MuJoCo的求解器是软约束模型处理接触时有天然的柔顺性配合力矩控制不容易出现数值爆炸这类问题。第三社区生态极其成熟Panda、Franka、UR系列甚至很多自研机械臂都有现成的MJCF模型拿来就能做力控实验。还有一个被很多人忽略的点MuJoCo的每个物理量都可以直接读取关节角、角速度、重力补偿力矩、外部接触力这些数据在调试阻抗参数时是刚需。真机上你很难单独测出“重力补偿力矩”这个量但在MuJoCo里 data.qfrc_bias 这一项就直接给出来了。这也是为什么仿真阶段能把阻抗控制原理吃透的原因——你能看到每一个中间量。2. MuJoCo 里做力控的基础设施进入代码之前先把MuJoCo里跟力控相关的几个基础设施说清楚。很多人卡住的第一个点不是控制律而是“通过哪个接口写力矩”“读哪个变量才是关节角”这些看起来很基础实际上坑非常多。2.1 执行器类型决定了 ctrl 指令的含义MJCF模型中的 actuator 决定了你写进 data.ctrl 的数值到底被解释成什么。常用的有三种类型motor力矩、position位置、velocity速度。阻抗控制一定要用 motor 类型因为我们要直接输出关节力矩。下面是一个六自由度机械臂的执行器配置片段actuator motor jointjoint1 gear1.0 ctrlrange-20 20/ motor jointjoint2 gear1.0 ctrlrange-30 30/ motor jointjoint3 gear1.0 ctrlrange-30 30/ motor jointjoint4 gear1.0 ctrlrange-10 10/ motor jointjoint5 gear1.0 ctrlrange-10 10/ motor jointjoint6 gear1.0 ctrlrange-10 10/ /actuator这里的 ctrlrange 是控制器指令的上下限。很多朋友在仿真里遇到“写了力矩但机械臂纹丝不动”的情况第一反应是控制律写错了但检查下来往往只是某个关节的 ctrlrange 设置成了 [0, 0]力矩输出被人为截断。遇到不动的情况先确认这个。2.2 三个核心数据接口MuJoCo的Python接口中阻抗控制最常用到三个量。第一个是 data.qpos它存放所有关节的位置。注意如果模型里有 free joint比如移动底盘或漂浮基座qpos 的前几个分量会被占用读机械臂关节角时要先搞清楚你自己的模型结构。上面这个六自由度机械臂假设底座是固定的所以 qpos 的前六个分量直接对应六个旋转关节单位是弧度。第二个是 data.qvel存放关节速度。qvel 的排列顺序通常是 nv 个自由度固定底座机械臂的话也是前六个对应六个关节。qvel 的单位是 rad/s。第三个是 data.qfrc_bias。这是MuJoCo里估算出的“让机械臂保持当前位形所需的额外力矩”它包含了重力项、科氏力和离心力。这个量作为重力补偿直接加到控制力矩里就行省去了手动推导机械臂质量矩阵和重力项的麻烦。注意它的下标排序和 qvel 一致取前六个就是关节力矩单位是 N·m。2.3 用现有模型还是自己建模型做关节空间阻抗验证我个人建议用一个结构简单的六自由度模型就够了甚至先用单个旋转关节也完全能把原理跑通。我自己初期用的是一个简化六轴模型每个关节一个 motor 执行器关节限位设宽一点方便观察。如果你的手上已经有 Panda 或者 UR 的 MJCF 模型理论上只需要确认 actuator 部分配的是 motor 就可以直接拿来做实验不需要重写XML。有一个容易忽略的细节有些公开模型默认用的是 position 控制的 actuator即使你往 data.ctrl 里写力矩引擎也只会把它解释成位置目标指令效果完全不对。所以拿到别人的模型第一件事就是打开 XML 看 actuator 部分的配置。3. 关节空间阻抗控制的完整实现理论讲清楚了基础设施也确认了接下来就是写代码实现关节空间阻抗控制。这一节我会分三段先拆解控制律再给出带注释的核心控制循环最后加一个末端外力扰动实验来验证阻抗效果。3.1 控制律拆解从目标位置到力矩指令回到前面的控制律[ \tau K(q_{des} - q) - D\dot{q} \tau_{gravity} ]在关节空间里q_des 是目标关节角q 是当前关节角K 是关节刚度对角矩阵D 是关节阻尼对角矩阵τ_gravity 这一项直接用MuJoCo的 qfrc_bias 替代。这个控制律本质上是 PD 控制器加上重力补偿但它的解释角度完全不同PD 控制是为了“跟踪轨迹”而阻抗控制是为了“塑造机械臂的接触行为”。参数 K 和 D 决定了机械臂在受外力时的柔性程度而不是单纯决定跟踪误差的大小。为什么工程实现里经常省略质量矩阵前馈因为对大部分中低速交互场景PD重力补偿已经足够接近期望阻抗特性而且参数少、好调、不容易出数值问题。只有当机械臂做高速动态运动或者要求很精准的阻抗响应时才需要把惯性矩阵前馈 M(q) 的项加回来。这一点后面单独说。3.2 核心控制循环代码下面是一段可以直接运行的关节空间阻抗控制核心代码假设你已经有一个人工设计的六自由度机械臂模型路径写在你自己的XML文件里。import numpy as np import mujoco model mujoco.MjModel.from_xml_path(six_dof_arm.xml) data mujoco.MjData(model) # 目标关节角注意单位是弧度 q_des np.array([0.5, -0.8, 1.2, 0.4, 0.2, -0.3]) # 关节刚度单位 N*m/rad kp np.array([200.0, 180.0, 150.0, 80.0, 60.0, 50.0]) # 关节阻尼取临界阻尼近似值2 * sqrt(kp) kd 2.0 * np.sqrt(kp) sim_time 5.0 steps int(sim_time / model.opt.timestep) qpos_log [] qvel_log [] for i in range(steps): # 读取当前关节位置和速度 q data.qpos[:model.nu].copy() qd data.qvel[:model.nu].copy() # 重力/科氏力/离心力补偿项 bias data.qfrc_bias[:model.nu].copy() # 关节空间阻抗控制律 tau kp * (q_des - q) - kd * qd bias # 写入力矩指令并步进仿真 data.ctrl[:model.nu] tau mujoco.mj_step(model, data) if i % 10 0: qpos_log.append(q.copy()) qvel_log.append(qd.copy())代码里用 model.nu 而不是写死的关节数这样做的好处是如果XML里执行器数量变化代码不用改。qpos 切片取前 model.nu 个分量前提是模型里没有 free joint如果你用的是移动机械臂切片位置要对齐到关节段。注意 kd 的计算。我给的公式 2sqrt(kp) 是一种工程简化它的来源是二阶系统的临界阻尼条件。严格来说临界阻尼阻尼系数是 2sqrt(kpI)I 是关节等效惯量。真实关节惯量和负载变化都会影响等效 I所以我一般先取 2sqrt(kp) 作为初始值如果系统响应过冲再往上调如果响应太肉就往下降。3.3 末端外力扰动测试验证“弹簧-阻尼”行为光写上段代码只能说明“机械臂能跟踪目标位置”还不足以证明它是阻抗控制。阻抗控制的标志性特征是“受外力时会产生偏移外力消失后回到目标位置”。为了验证这一点我们可以在仿真时间 t2.0s 到 2.3s 之间给末端连杆施加一个恒定外力。这里用到 data.xfrc_applied 接口。它接收一个 (nbody, 6) 数组前三列是力后三列是力矩单位分别是牛顿和牛米。注意这个力的坐标是 body 的局部坐标系不是世界系施加前心里要有数。# 获取末端连杆的body id end_body_id model.body(link6).id # 在控制循环里模拟时间0.2秒到2.3秒施加外力 start_step int(2.0 / model.opt.timestep) end_step int(2.3 / model.opt.timestep) for i in range(steps): # 其他控制代码不变 # 施力窗口 if start_step i end_step: data.xfrc_applied[end_body_id, :3] [10.0, 0.0, 0.0] else: data.xfrc_applied[end_body_id, :3] [0.0, 0.0, 0.0] data.ctrl[:model.nu] tau mujoco.mj_step(model, data)跑完仿真你会看到外力施加期间机械臂末端被推开一段距离但不会飞出去外力撤销后机械臂慢慢回到目标位置。推力越大偏移越大Kp 越大偏移越小Kd 越大恢复过程越“肉”、越不容易振荡。这就是阻抗控制最直观的行为。3.4 进阶把惯性矩阵前馈加上如果机械臂运动速度较快或者你要做一个更严格的阻抗响应只靠 PD重力补偿就不太够用了。因为期望阻抗动力学里有一项 M(q) ddq_ref它需要我们主动补偿关节惯量带来的动态耦合。好在MuJoCo可以直接读出质量矩阵 data.qM只是它是稀疏存储需要转成完整矩阵再用。# 在控制循环内 M_full np.zeros((model.nv, model.nv)) mujoco.mj_fullM(model, M_full, data.qM) # 期望关节加速度 ddq_ref kp * (q_des - q) - kd * qd # 带惯性前馈的关节空间阻抗控制律 tau M_full[:model.nu, :model.nu] ddq_ref bias加入这项之后机械臂对轨迹的跟踪会更“果断”同时接触力响应也更接近你设定的目标惯性特性。代价是计算量变大、参数调起来更容易混乱因为质量矩阵会把多关节之间的耦合带进来。我的建议是先跑通纯PD重力补偿版本再决定要不要加这一项。对验证阻抗概念来说纯PD已经完全足够了。4. 参数整定与调试实录阻抗控制里最磨人的不是写代码而是调参数。Kp 和 Kd 选得不好轻则响应慢重则直接发散。这一节写我实际调试过程中的经验数据和操作习惯。4.1 刚度与阻尼参数的经验范围关节空间阻抗控制的 Kp 跟关节的等效惯量直接相关。不同关节惯量差了一个量级所以用一个全局 Kp 是不太合理的。以我常用的六自由度模型为例前三个大臂关节惯量大概在 0.1~0.5 kg·m²后三个小臂和手腕关节惯量集中在 0.01~0.05 kg·m²。调参经验先设定期望的自然频率。如果想让系统在 0.5 秒左右收敛自然频率 ωn 大致取 6~10 rad/s。那么 Kp ≈ I * ωn²算出来大臂关节在几十到几百之间小臂关节在个位数到几十之间。我常用的一组值是大臂 Kp 150~250小臂 Kp 50~120。Kd 先取 2*sqrt(Kp)再根据响应微调。参数数值范围我的模型效果偏小效果偏大大臂关节 Kp150~250 N·m/rad跟随慢外力下偏移大接近刚性失去柔顺小臂关节 Kp50~120 N·m/rad末端下垂感明显接触力过大Kd2*sqrt(Kp) ± 30%收敛时振荡响应迟钝、类似黏滞仿真步长0.002s默认——Kp 600 可能出现抖动这个表格不是万能公式但它是一个很好的起点。换模型时先按当前模型的质量分布重新估算惯量再按这个逻辑推出 Kp 的初始量级。4.2 重力补偿缺失会怎样如果你把控制律里的 bias 这一项去掉你会很快看到一个现象机械臂停在某个静止位置但目标位置明明在很多弧度之外。这是因为所有关节都在“抵抗重力”的环境下工作P 项必须随时输出保持力矩来托住当前位形于是产生了稳态误差。这个稳态误差的大小取决于 Kp 和重力力矩的比值。重力力矩越大、Kp 越小误差越大。排除这个偏差的常规做法就是加 qfrc_bias 补偿。试过去掉 bias 之后你会发现机械臂直接软塌塌地垂下来只有施加了适当的保持力矩才能支撑住。4.3 如何用数据验证阻抗特性我习惯在参数调完之后记录关节位置误差随时间的变化曲线观察几个关键指标上升时间、超调量、稳态误差和扰动后的恢复时间。为了获得可信的数据我会在控制循环里把设定的 Kp、Kd 和当前位形一起存下来再单独写一个小脚本画图。还有一个实用小技巧把一次外力扰动实验跑两遍一遍 Kp100一遍 Kp300然后把末端位移曲线叠加对比。你会在数据里清楚看到大刚度对应的扰动位移更小、恢复更“硬”小刚度对应更大的柔顺空间。这个对比比任何文字描述都有说服力也是写论文或做报告时很直观的实验结果。4.4 稳定性边界和发散排查如果仿真中出现高频抖动或者数值发散我优先检查的是下面这四件事按概率排序第一Kp 是不是给得太大了。Kp 超过模型稳定边界之后再增大只会让系统振荡甚至发散。第二Kd 是不是太小。阻尼不足时系统表现为减幅很慢的振荡。第三仿真步长 model.opt.timestep 是不是太大。默认 0.002 秒在多数情况下没问题但如果你加了质量矩阵前馈、又开着重接触场景可以试着改成 0.001 秒看看稳定性有没有变好。第四控制律的正负号。别笑这是我自己踩过的坑目标位置减当前位形的方向写反系统会变成“正反馈”瞬间炸飞。符号问题检查起来也简单把初试目标改成相邻的微小偏移看机械臂往哪个方向动。5. 常见问题与避坑指南阻抗控制本身不复杂但MuJoCo在实际使用中的细节非常多。这一节我把遇到频次最高的几个问题汇总成速查表并把一些写不进代码注释的经验放在最后。5.1 常见问题速查表给一张可以直接对照的表格方便卡住时快速定位。现象可能原因排查方法机械臂完全不动actuator类型不是motor或ctrlrange为[0,0]检查MJCF文件里actuator配置机械臂缓慢飘移/飞走重力补偿缺失或控制符号写反先加bias再看方向确保 q_des - q 符号正确高频抖动或振荡发散Kp过大、Kd过小、dt过大降低Kp增大Kd必要时把timestep降到0.001到达目标附近但存在固定偏差重力补偿缺失或Kp过小加qfrc_bias增大Kp施加外力后偏移非常大Kp太小增大Kp或检查外力施加坐标系是否正确添加惯性前馈后反而发散了质量矩阵索引对错或Kp过大确认M_full的维度切分正确降低Kp5.2 我的几个实操心得第一目标关节角的变化不要整成阶跃。如果直接把 q_des 从当前值一步跳到目标值阻抗控制相当于被瞬态冲击初始力矩很大容易把系统“激”出振荡。我习惯在外部先用梯形速度规划生成一条平滑目标轨迹再把它当成随时间变化的 q_des 喂给控制器。这个做法对最后的效果提升非常明显。第二单位问题一定要较真。MuJoCo中关节角度默认是弧度力矩是N·m但很多公开模型的XML里关节初始值、关节限位可能是角度写进去的。如果你发现机械臂跑得极其诡异先检查一下是不是把度当成弧度用了。第三控制循环里的内存分配要注意。mujoco-py 里每次调 data mujoco.MjData(model) 都很慢控制循环里不要反复创建数组。把 numpy 数组的 copy 和中间变量提前分配好然后循环里只做赋值和运算这样仿真速度会快很多也更方便后续接强化学习。第四观察数据比盯着动画靠谱。动画里看起来“好像到位了”但误差曲线可能实际上还有 0.02 rad 的稳态偏差这在控制领域是完全不可接受的。我每次实验都会把关节误差曲线打出来靠数据说话。5.3 从关节空间到更复杂的扩展方向关节空间阻抗控制是整个柔顺控制体系里的地基。往上扩展的方向很多我简单列几个笛卡尔空间阻抗控制需要把关节角误差映射到操作空间核心工具是雅可比矩阵MuJoCo里可以用 mujoco.mj_jac 直接算可变阻抗控制则是把 Kp、Kd 设计成任务相关的函数比如装配过程中先高刚度接近、再低刚度入孔再往后就是和强化学习结合把阻抗参数作为策略输出的一部分让智能体自己学习在不同接触场景下该“硬一点”还是“软一点”。实际跑过这个实验之后我的直观感受是阻抗参数表面上是两个数字背后其实是对“机械臂想表现出什么行为模式”的刻画。Kp 和 Kd 调小一点它就温顺得像一只训练有素的导盲犬遇到障碍知道退让调大一点它就恢复成生产线上的硬核搬运工。理解了这个连续变化的过程你也就真正理解了为什么柔顺控制是现代机器人交互任务里绕不开的基石。