本节摘要:关节给了机器自由度,执行器决定谁来驱动这些自由度、以什么方式驱动;equality 约束负责把树状拓扑拼出闭环, tendon 与传感器补齐力传递与感知。本节给出三类执行器(力矩电机、位置伺服、肌肉模型)的选型依据、equality 四兄弟的用途边界,以及一组拿来就能套的配置清单——配置不当的执行器是「仿真里能跑、真机上失灵」的头号惯犯。
2.2 节把机器的骨架搭完了,但那只是一台「可以被别人推动」的机器;要它自己动,得把动力接上去。本节处理三件事:动力从哪来(执行器)、运动的自由怎么限定(约束与等式)、以及机器如何「感觉到」自己的受力(传感器)。三者都在模型文件里配置,都在加载期被编译成求解器能直接消费的形式。
因为执行器是建模与现实「对不上账」的重灾区。新手最常见的写法是给每个关节挂一个裸电机,训练时发现策略输出永远顶在控制上限——因为裸电机的控制量是力矩,而 RL 初始策略输出随机值,力矩拉满的机器在仿真里疯狂抽搐,策略自然学不到东西。另一个极端是把位置伺服的 kp 调到天文数字,执行器变成理论上无限刚的魔法关节,真机上根本不存在这东西,Sim-to-Real 时全部露馅。
这两类翻车的共同根因,是没有想清楚「我要模拟的到底是哪种真实驱动器」。真实世界的驱动只有几类:直流电机输出力矩、伺服系统闭环跟随位置指令、肌肉按激活度收缩。MJCF 的执行器类型恰好与之一一对应——选型问题的本质是「你的真机用什么驱动」。
motor:裸力矩。 最接近底层的执行器,控制量就是关节力矩,gear 指定力矩放大比。它假设你的控制器自己闭环——做力控、做 MPC、做以力矩为动作空间的 RL,都直接用它。优点是无隐藏动态,所见即所得;缺点是把闭环的担子全压给你的算法。
position 与 velocity:内置伺服。 引擎在执行器内部实现一个比例微分环:你给目标角度,它按 kp、kv 算出力矩去追。这对「真实机器是位置控制的」场景是正确抽象——市面上绝大多数协作臂的底层就是位置环。kp 与 kv 不是装饰参数,它们物理上对应伺服电机的刚度与阻尼,应当按真机手册填,而不是按「训练好不好收敛」调。一个实用技巧:位置伺服加上 forcerange 限制输出力矩,立刻得到一台「有力度上限」的机器人,抓鸡蛋任务全靠它。
muscle:肌肉模型。 按激活度收缩,含激活动态与力—长度—速度关系,是 biomechanics 与肌肉骨骼 RL 的标配。它的控制量是无量纲激活度,天然有界,训练比裸力矩稳定得多;代价是参数多,没做人体实验就抄文献默认值。
三类执行器的控制量含义完全不同,这一点在 RL 里是生死攸关的:动作空间要与执行器类型配套——motor 配连续力矩(记得在策略输出端做缩放或压缩,别让力矩常年在上限打转),position 配目标角度,muscle 配激活度。动作空间与执行器类型错配,是「换了个模型训练就崩」的常见暗因。
| 执行器 | 控制量含义 | 对应的真实驱动 | 典型场景 |
|---|---|---|---|
| motor | 关节力矩 | 直流电机加减速器、力控关节 | MPC、力矩 RL、力控 |
| position | 目标角度 | 位置闭环伺服 | 机械臂操作、遥操作 |
| velocity | 目标速度 | 调速驱动 | 轮式底盘、传送带 |
| muscle | 激活度零到一 | 肌肉 | 肌骨仿真、步态研究 |
配置上还有一组「安全带」参数值得全默认打开:ctrlrange 限定控制输入的范围,forcerange 限定输出力矩范围,两者配合让「策略输出离谱值」在物理层就被拦住。RL 训练里这两个参数不是可选项——它们决定了动作空间的边界语义,环境定义要与之一致。
2.2 说过树不能有环,但真实机构偏偏充满环:四连杆、平行四边形臂、齿轮啮合、闭环腱驱动。MJCF 的答案是 equality 约束——在树之上叠加「这两个东西必须满足某关系」的方程,由求解器在每步强制或近似强制满足。常用的四件套:
<equality> <!-- connect:两点间距离恒定,等价于无质量球铰,拼四连杆用它 --> <connect body1="link_a" body2="link_b" anchor="0.05 0 0"/> <!-- weld:两体完全固连,比写死坐标灵活,可模拟可抓取物体的吸附 --> <weld body1="gripper" body2="object" solref="0.005 1"/> <!-- joint:两个关节角度满足给定关系,齿轮传动的标准写法 --> <joint joint1="wheel_left" joint2="wheel_right" polycoef="0 1 0 0 0"/> <!-- tendon:两条腱长度差恒定,耦合腱驱动用它 --> <tendon tendon1="t_a" tendon2="t_b" polycoef="0 1 0 0 0"/> </equality>
用 equality 的正确心态是「付租金」。每条约束都进入求解器的约束集,消耗迭代预算;solref 与 solimp 控制约束的软硬,调得太硬数值抖、太软关系失真。所以工程取舍是:能用树表达的绝不写约束,四连杆能简化成二连杆加虚拟刚体就简化。约束是表达力的救济渠道,不是免费午餐。
顺带把 tendon 说完整:tendon 是「穿过多个滑轮把力送到远处」的抽象,固定腱负责力传递,滑动腱把一条驱动量分配给多个关节——人手每根手指的伸肌腱就是天然滑腱。腱还带 range 与 limited,可以模拟「腱绳绷紧后不可再拉长」的止点效应,肌腱受伤的康复研究就靠这些细节撑住真实性。
模型的「感知面」由 sensor 元素定义,与动力学解耦、只读不写。控制与 RL 最常用的三款:jointpos 与 jointvel 直接读关节状态;force 与 torque 贴在 site 上读内力——力控与碰撞检测的关键配置;touch 放在足底或指腹 site 上,读该区域法向合力,做足底接触感知与抓取力反馈都用它。传感器数量不影响求解开销(它们是动力学的旁路输出),放心按需布置。
用 Python 读传感器的惯例代码值得背下来:
import mujoco import numpy as np model = mujoco.MjModel.from_xml_path("gripper.xml") data = mujoco.MjData(model) # 按名字查传感器在读数向量里的切片 adj = model.sensor_adr[model.sensor(name="force_tip").id] dim = model.sensor_dim[model.sensor(name="force_tip").id] for i in range(500): data.ctrl[:] = 0.3 # 给所有执行器一个温和的指令 mujoco.mj_step(model, data) reading = data.sensordata[adj:adj+dim] # 切片读出该传感器的当前值 print("末端力读数:", np.round(reading, 3))
这段代码里真正可迁移的是「按名字查地址再切片」的模式:sensordata 是一个大向量,所有传感器的读数按定义顺序平铺,硬背偏移是自虐,名字查址才是可持续的写法。
把本节的配置要点压成 checklist,建模时逐条对照:
这份清单的每一条都在为第六章排错埋伏笔——配置阶段多花的十分钟,是排错阶段省下的一整天。
四节知识已经齐了:结构、骨架、动力。下一节把它们串成一条真实产线——一台六轴机械臂从空白文件到稳定运行的完整复盘。