MIT Drake 真实模拟 SmallRobotArm 六轴机械臂(详细分册)
📌 本分册目标
这是「做自己的机器人」第 1 个项目中 ② MIT Drake 真实动力学模拟 的详细版教程:讲清「URDF 建模 → 加载与执行器 → 控制器设计 → 验证与调参」四件事,附可直接运行的完整 Python 程序与 URDF。
你会亲手得到:一个六轴臂的真实重力/惯性响应、一份末端轨迹 CSV、以及一组「哪些参数会让电机打满扭矩(真机丢步)」的实测结论。这些结论直接决定真机该选多大减速比、多快速度、多重末端。
🧰 环境准备
- Python 3.9 ~ 3.11(Drake 的 wheel 只支持特定 Python 版本,越新的 Drake 要求越新的 Python)
- pydrake:
python3 -m venv ~/drake-venv && source ~/drake-venv/bin/activate && pip install drake - 系统:Ubuntu 20.04 / 22.04 与 macOS 支持最好;Windows 请用 WSL2(Drake 无官方 Windows pip 包)
- Intel 版 macOS:官方较高版本已不再提供 x86_64 wheel,需装最后支持的版本
pip install drake==1.34.0(本站实测环境即为此组合) - 可选:浏览器(用于第 6 步
--meshcat的 3D 视图)、任意文本编辑器
python3 --version 与 uname -m(x86_64 / arm64),再挑对应版本;Apple Silicon(arm64)与 Linux x86_64 一般直接 pip install drake 即可。🔧 第 1 步:两个文件,一个目录
下载本次两个文件
⬇ smallrobot_arm.urdf(机械臂模型:连杆、关节、质量、惯量、限位、扭矩上限)
⬇ arm_wave_drake.py(仿真主程序:建世界 + 控制器 + 采样记录)
两个文件放在同一个目录下(脚本按自身位置找 URDF,不需要改路径)。
先跑一次确认装好了
python3 arm_wave_drake.py --duration 10
看到 [arm_wave] Drake controller started · plant=6 dof · gravity=-9.81 就说明环境正常,可以直接跳到第 4 步看代码;想先理解模型,继续第 2 步。
🔧 第 2 步:读懂 URDF —— 机械臂的「身份证」
URDF 用一棵树描述机器人:节点是 link(连杆),边是 joint(关节)。本模型的构型与 SmallRobotArm 一致:底座绕 Z 回转 → 肩/肘/腕绕 Y 摆动 → 末端绕 Z 滚转。每个关节写明转轴轴向、相对位置、角度限位、扭矩上限 effort;每个连杆写明质量与转动惯量——Drake 就是靠这些数算真实的动力学。
| 关节 | 父 → 子连杆 | 轴向 | 相对位置(m) | 限位(rad) | effort(N·m) | 连杆质量(kg) |
|---|---|---|---|---|---|---|
| J1 | base → carrier | 0 0 1(绕 Z) | 0 0 0.10 | ±3.14 | 8 | 0.6 / 0.25 |
| J2 | carrier → upper_arm | 0 1 0(绕 Y) | 0 0 0.16 | ±2.44 | 12 | 0.5 |
| J3 | upper_arm → forearm | 0 1 0(绕 Y) | 0.22 0 0 | ±2.44 | 8 | 0.4 |
| J4 | forearm → wrist1 | 0 1 0(绕 Y) | 0.20 0 0 | ±2.97 | 5 | 0.4 |
| J5 | wrist1 → wrist2 | 0 1 0(绕 Y) | 0.12 0 0 | ±2.44 | 3 | 0.1 |
| J6 | wrist2 → tool | 0 0 1(绕 Z) | 0.08 0 0 | ±3.14 | 1.5 | 0.05 |
<!-- ① 必须有一个固定关节把底座焊在世界原点,否则整条臂会自由落体 -->
<link name="world"/>
<joint name="world_joint" type="fixed">
<parent link="world"/><child link="base_link"/>
</joint>
<!-- ② 连杆 = 外观 + 碰撞 + 惯性(Drake 靠 mass 与 inertia 算真实受力) -->
<link name="upper_arm_link">
<visual><origin xyz="0.11 0 0"/><geometry><box size="0.22 0.06 0.06"/></geometry></visual>
<inertial>
<origin xyz="0.10 0 0"/>
<mass value="0.5"/>
<inertia ixx="0.00030" ixy="0" ixz="0" iyy="0.00217" iyz="0" izz="0.00217"/>
</inertial>
</link>
<!-- ③ 关节 = 父子关系 + 位置 + 轴向 + 限位 + 扭矩上限 + 阻尼 -->
<joint name="J2" type="revolute">
<parent link="carrier_link"/><child link="upper_arm_link"/>
<origin xyz="0 0 0.16" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
<limit lower="-2.4435" upper="2.4435" effort="12" velocity="2.5"/>
<dynamics damping="0.05"/>
</joint>
I = m/12·(a² + b²)。本站模型用「PLA 打印件 + 碳纤维杆 + NEMA17 电机」的量级估算(总重约 2 kg);想更精确,从 Fusion360 或称重实测得到每节质量与质心位置后替换即可,控制器一行都不用改。world 固定关节 → 机械臂整体自由落体(末端坐标会掉到几十米外);② 惯量张量不满足物理约束(如 ixx + iyy ≥ izz)→ Drake 直接报错拒绝加载。🔧 第 3 步:跑起来,读懂输出
python3 arm_wave_drake.py --duration 30 --freq 0.5 --amp-deg 30 --csv arm_wave_trail.csv
每行输出有三段含义:实测关节角(相当于真机编码器回读,单位弧度)、末端 XYZ(工具坐标系原点的全局坐标,米)、最后的 summary(本次运行的最大跟踪误差、扭矩饱和占比、末端轨迹范围)。
同时会写出 CSV:t,q1..q6,x,y,z,每 0.5 s 一行——这就是你的末端轨迹数据,可以用 Excel / Python 直接画出来。
0.620 0.000 0.260:机械臂初始为水平伸展状态,末端在前方 0.62 m、高度 0.26 m 处。随后 30 s 内末端在 x 0.43~0.62 / y −0.28~0.29 / z −0.15~0.62 的空间里划出周期轨迹。🔧 第 4 步(核心):完整程序与控制器设计
下面就是本站实测用的完整程序。核心是第 2 段控制律:u = kp·(q_des − q) + kd·(q̇_des − q̇) − τ_gravity,再按电机扭矩上限夹取。其中「− τ_gravity」是重力补偿——它先替电机托住手臂自重,PD 只需纠正剩下的误差。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
arm_wave_drake.py · LateAI 站内原创教学代码
在 MIT Drake (pydrake) 中「真实模拟」SmallRobotArm 六轴机械臂:
1) 用 Parser 加载 smallrobot_arm.urdf(含质量/惯量/关节限位/阻尼)
2) 6 个关节各配一个 JointActuator,施加 PD 位置控制扭矩
tau_i = kp*(q_des_i - q_i) - kd*qdot_i (并受 effort 限位夹取)
3) 重力场中真实积分动力学方程(含重力/惯性/科氏耦合)
4) 每 0.5 s 打印位置传感器实测角度(即 plant 状态),并把末端
全局坐标写入 CSV —— 等价于真机编码器回读 + 轨迹记录
用法:
python3 arm_wave_drake.py # 无头数值验证(默认)
python3 arm_wave_drake.py --meshcat # 打开浏览器 3D 视图
python3 arm_wave_drake.py --duration 30 --freq 0.5 --amp-deg 30
"""
import argparse
import math
import os
import numpy as np
from pydrake.all import (
AddMultibodyPlantSceneGraph,
DiagramBuilder,
Meshcat,
MeshcatVisualizer,
MultibodyPlant,
Parser,
Simulator,
)
HERE = os.path.dirname(os.path.abspath(__file__))
URDF = os.path.join(HERE, "smallrobot_arm.urdf")
JOINT_NAMES = [f"J{i}" for i in range(1, 7)]
def cmd_angle(i, t, freq=0.5, amp_deg=30.0):
"""第 i 个关节 (0-based) 的目标角度(rad):与站内 Canvas 演示同一公式。
freq: 摆动圆频率(rad/s);amp_deg: 摆动幅值(°)。真实电机有扭矩上限,
角速度/角加速度太大就会“堵转丢步”,教程里可借此讲明限幅的意义。
"""
return math.radians(amp_deg) * math.sin(freq * t + i * math.pi / 3.0)
# 1kHz 数字控制(Δt=0.001s):真实微控制器/伺服驱动器常见速率。
# 增益匹配:(a) 线性域 δ=effort/kp 取 ~0.04-0.05 rad(误差在此范围内
# 电机在扭矩上限内线性输出,不落入 bang-bang 开关饱和);
# (b) 带宽稳定判据 ω·Δt ≈ sqrt(kp/I)·0.001 < 1.5(宽松满足)。
# 惯性耦合是真实存在的(快速运动时关节互相牵动),采样越快、
# 线性域越宽,耦合扰动越难累积成误差。
KP_DEFAULT = [200.0, 300.0, 200.0, 120.0, 80.0, 30.0]
KD_DEFAULT = [12.0, 10.0, 4.5, 1.3, 0.4, 0.1]
def main():
ap = argparse.ArgumentParser(description="SmallRobotArm · MIT Drake 真实动力学模拟")
ap.add_argument("--duration", type=float, default=14.0, help="仿真时长(秒)")
ap.add_argument("--dt", type=float, default=0.001, help="控制刷新周期(秒,1kHz)")
ap.add_argument("--freq", type=float, default=0.5, help="摆动圆频率(rad/s)")
ap.add_argument("--amp-deg", type=float, default=30.0, help="摆动幅值(°)")
ap.add_argument("--kp", type=float, default=None, help="统一 PD 位置增益(默认分关节取值)")
ap.add_argument("--kd", type=float, default=None, help="统一 PD 速度阻尼(默认分关节取值)")
ap.add_argument("--print-every", type=float, default=0.5, help="打印周期(秒)")
ap.add_argument("--csv", default=os.path.join(HERE, "trail.csv"), help="末端轨迹 CSV 输出")
ap.add_argument("--meshcat", action="store_true", help="启动浏览器 3D 可视化")
ap.add_argument("--debug", action="store_true", help="打印命令/误差/重力矩调试行")
ap.add_argument("--no-ff", action="store_true", help="关闭重力补偿前馈(诊断用)")
ap.add_argument("--no-gravity", action="store_true", help="关闭重力场(诊断用)")
ap.add_argument("--static", action="store_true", help="目标恒为 0(诊断用)")
ap.add_argument("--test-joint", type=int, default=0,
help="仅让第 N 个关节(1-6)做 0.3 rad 阶跃,其余恒 0(诊断用)")
args = ap.parse_args()
# ---------- 1) 建世界:加载 URDF + 手动挂 6 个执行器 ----------
# Drake 的 URDF 不自动创建执行器:revolute 关节默认是被动关节,
# 必须用 AddJointActuator 把它变成“可控关节”(并声明扭矩上限 effort)。
# 离散时间 plant:与主循环控制刷新率保持一致(1kHz/Δt=1ms),
# 模拟真机“微控制器每秒发 1000 次位置命令 + 电机驱动器 1000Hz 更新”的节奏,
# 比连续积分器更符合真实数字控制,也天然稳定。
if args.meshcat:
builder = DiagramBuilder()
plant, scene_graph = AddMultibodyPlantSceneGraph(builder, time_step=args.dt)
else:
plant = MultibodyPlant(time_step=args.dt)
Parser(plant).AddModels(URDF)
# 扭矩上限按 NEMA17 步进电机 + 减速箱的真实量级取值:
# 肩/肘带 ~15:1 减速箱(输出 ~8-12 N·m),腕部带 ~5:1 减速箱(1.5-5 N·m)
EFFORT = [8.0, 12.0, 8.0, 5.0, 3.0, 1.5] # N·m
for n, ef in zip(JOINT_NAMES, EFFORT):
plant.AddJointActuator(n, plant.GetJointByName(n), effort_limit=ef)
plant.Finalize()
if args.no_gravity:
plant.mutable_gravity_field().set_gravity_vector([0.0, 0.0, 0.0])
if args.meshcat:
meshcat = Meshcat()
vis = MeshcatVisualizer(meshcat=meshcat)
builder.AddSystem(vis)
builder.Connect(
scene_graph.get_query_output_port(),
vis.get_geometry_query_input_port(),
)
diagram = builder.Build()
sim = Simulator(diagram)
root_ctx = sim.get_mutable_context()
ctx = plant.GetMyContextFromRoot(root_ctx)
else:
sim = Simulator(plant)
ctx = sim.get_mutable_context() # plant 独立 => root 即 plant
root_ctx = ctx
joints = [plant.GetJointByName(n) for n in JOINT_NAMES]
actuators = [plant.GetJointActuatorByName(n) for n in JOINT_NAMES]
effort = [act.effort_limit() for act in actuators]
tool = plant.GetBodyByName("tool_link")
kp_arr = np.full(6, args.kp) if args.kp else np.array(KP_DEFAULT)
kd_arr = np.full(6, args.kd) if args.kd else np.array(KD_DEFAULT)
u_port = plant.get_actuation_input_port()
u = np.zeros(u_port.size())
u_port.FixValue(ctx, u)
# ---------- 2) 仿真主循环:PD 位置控制 ----------
# 关节角度/速度用关节 API 直接读(plant 的完整状态向量含 world 自由体,
# 直接切片不可靠);每步把 PD 扭矩写进执行器输入,再推进积分。
vel_idx = [joints[i].velocity_start() for i in range(6)]
sim.Initialize()
t = 0.0
next_print = 0.0
err_max = np.zeros(6)
sat_steps = np.zeros(6) # 各关节扭矩饱和(堵转/丢步风险)步数
rows = []
print("[arm_wave] Drake controller started · plant=%d dof · gravity=%.2f"
% (plant.num_positions(), plant.gravity_field().gravity_vector()[2]))
if args.debug:
print("t | q1..q6 | cmd1..6 | err1..6 | tau_g1..6 | u1..6")
while t < args.duration - 1e-12:
q = np.array([joints[i].get_angle(ctx) for i in range(6)]) # 实测角度
qd = plant.GetVelocities(ctx)[vel_idx] # 实测角速度
if args.static:
cmd = np.zeros(6)
elif args.test_joint > 0:
cmd = np.zeros(6)
cmd[args.test_joint - 1] = 0.3
else:
cmd = np.array([cmd_angle(i, t, args.freq, args.amp_deg)
for i in range(6)])
cmd_dot = np.zeros(6) if (args.static or args.test_joint) else np.array(
[math.radians(args.amp_deg) * args.freq
* math.cos(args.freq * t + i * math.pi / 3.0) for i in range(6)])
# 控制器 = PD(误差) − 重力广义力前馈。
# CalcGravityGeneralizedForces 返回“重力把关节拉向下垂方向”的广义力
# (沿 +q 为正,见 q=0 时 J2≈+7.5 N·m),因此前馈要取负号才能托住自重;
# 真实机械臂上位机的重力补偿同理:先抵消重力项,PD 只纠正剩余误差。
tau_g = plant.CalcGravityGeneralizedForces(ctx)[vel_idx]
# PD + 速度前馈(减小正弦跟踪相位滞后)+ 重力补偿
u[:] = kp_arr * (cmd - q) + kd_arr * (cmd_dot - qd)
if not args.no_ff:
u -= tau_g
u[:] = np.clip(u, -np.array(effort), np.array(effort))
sat_steps += (np.abs(u) >= np.array(effort) - 1e-9).astype(float)
u_port.FixValue(ctx, u)
sim.AdvanceTo(t + args.dt)
t += args.dt
if t >= next_print:
next_print += args.print_every
if t >= 2.0: # 跳过启动瞬态,统计稳态跟踪误差
err_max = np.maximum(err_max, np.abs(cmd - q))
xyz = plant.EvalBodyPoseInWorld(ctx, tool).translation()
rows.append([t] + list(q) + list(xyz))
if abs(t - round(t)) < 1e-9 or t - next_print + args.print_every < 1e-9:
line = ("t=%.1fs 实测(rad): %s 末端XYZ(m): %.3f %.3f %.3f"
% (t, " ".join("%8.3f" % v for v in q), *xyz))
print(line)
if args.debug:
print(" cmd : %s\n err : %s\n tau_g: %s\n u : %s"
% (" ".join("%7.3f" % v for v in cmd),
" ".join("%7.3f" % v for v in cmd - q),
" ".join("%7.3f" % v for v in tau_g),
" ".join("%7.3f" % v for v in u)))
# ---------- 3) 汇总 ----------
with open(args.csv, "w") as f:
f.write("t,q1,q2,q3,q4,q5,q6,x,y,z\n")
for r in rows:
f.write(",".join("%.6f" % v for v in r) + "\n")
n_steps = max(int(args.duration / args.dt), 1)
print("[summary] 最大跟踪误差(rad): %s" % " ".join("%.3f" % v for v in err_max))
print("[summary] 扭矩饱和占比(%%): %s"
% " ".join("%.0f" % (100 * s / n_steps) for s in sat_steps))
xyz_a = np.array([r[7:] for r in rows])
print("[summary] 末端轨迹范围(m): x[%.3f, %.3f] y[%.3f, %.3f] z[%.3f, %.3f]"
% (xyz_a[:, 0].min(), xyz_a[:, 0].max(),
xyz_a[:, 1].min(), xyz_a[:, 1].max(),
xyz_a[:, 2].min(), xyz_a[:, 2].max()))
print("[done] 轨迹已保存 -> %s" % args.csv)
if __name__ == "__main__":
main()
三个必须记住的设计点
- URDF 里的 effort 不是自动生效的:Drake 解析 URDF 时
revolute关节默认是被动关节,必须用plant.AddJointActuator(name, joint, effort_limit=…)把它变成可控关节,否则get_actuation_input_port()为空、电机根本不存在。 - 重力补偿要减,不是加:
CalcGravityGeneralizedForces()返回的是「重力把关节往下拉」的广义力(沿 +q 为正),所以抵消它应该u -= τ_g。加错符号,控制器反而帮着重力把手臂拽下去。 - 增益要和扭矩上限匹配:若
kp · 误差常年远超effort,输出就一直被夹在上限,控制器退化成「开关式」bang-bang,抖动且跟不准。工程做法:让δ = effort / kp(线性区)覆盖你期望的误差量级,本例约 0.04~0.05 rad。
🔧 第 5 步(进阶):三个实验,看懂真实动力学
程序自带几个诊断开关,用它们做对照实验,比看公式直观得多。以下均为本站实测结果。
| 实验命令 | 现象(实测) | 结论 |
|---|---|---|
--static | 目标恒为 0,水平臂纹丝不动,最大误差 0.000 rad,末端坐标全程不变 | 重力补偿前馈精确托住了自重——若出现缓慢下垂,说明前馈符号反了 |
--no-ff | 关掉重力补偿:手臂被自重拽着往下掉,各关节误差迅速涨到 0.5 rad 以上 | 没有重力补偿,位置环必须靠误差「硬扛」自重,静态都站不住 |
| 默认运行 | 跟踪误差 0.000 / 0.017 / 0.064 / 0.069 / 0.035 / 0.001 rad(约 1°~4°) | 真实动力学下必然有小而有界的滞后误差,这是惯性+耦合+扭矩上限共同造成的 |
| 看饱和占比 | J2~J5 有 50%~78% 的时间扭矩打满上限 | 真机对应「步进电机堵转/丢步」——轨迹太猛就要么加减速比,要么降速 |
--freq 0.3 --amp-deg 20 | 任务变柔,误差与饱和占比同步下降 | 真机调试的正确顺序:先定能接受的轨迹速度,再谈精度 |
--freq 调大(比如 1.2)再跑一次,你会看到误差明显变大甚至「跟不上」——这就是真实机械臂选型的边界:给定电机扭矩,能跑多快是算出来的,不是猜出来的。🔧 第 6 步(可选):3D 视图与轨迹可视化
- 浏览器 3D:
python3 arm_wave_drake.py --meshcat,终端会给出本地地址,浏览器打开即可看到机械臂在动(可鼠标旋转/缩放)。 - 画末端轨迹:用
arm_wave_trail.csv的x,y,z三列直接出图(pandas 读 CSV → plot),就是机械臂真实走过的空间路径。 - 延伸练习:把同一组关节角喂给运动学正解公式自己算 XYZ,与 Drake 给出的坐标对比——不一致的地方,就是 URDF 里连杆长度/零点定义需要修正的地方。
🦾 第 7 步:换上真实 3D 模型,用 meshcat 看真机外观
前面用的是长方体示意模型(便于讲清原理)。这一步换成 SmallRobotArm 的真实 3D 模型:由 SolidWorks 导出 URDF,带 6 个真实网格(base / link1…link5,OBJ+STL),总质量 2.27 kg,5 个执行器 motor1…motor5(扭矩上限 100 / 50 / 30 / 20 / 10 N·m)。看到的就是真机外观,跑的仍是同一套真实动力学。
Parser(plant).AddModels("smallrobotarm_with_actuator.urdf")
# 真实 URDF 的 base 是根连杆:必须焊到世界,否则整臂自由落体
plant.WeldFrames(plant.world_frame(), plant.GetBodyByName("base").body_frame())
plant.Finalize()
# 真实 URDF 自带执行器(motor1..motor5),无需手动 AddJointActuator
# 动能法测有效惯量,再按扭矩上限与离散带宽自动整定 PD
inertia = effective_inertia(plant, ctx, vel_idx)
kp = np.minimum(effort / 0.05, 0.5 * (1.5 / dt) ** 2 * inertia)
kd = 1.6 * np.sqrt(kp * inertia)
# 录制并导出可在浏览器回放的 3D 动画(自包含 HTML)
meshcat.StartRecording()
... # 1 kHz 控制循环:u = kp*(cmd-q) + kd*(cmd_dot-qdot) - tau_g,再 clip(±effort)
meshcat.StopRecording(); meshcat.PublishRecording()
open("arm_real_3d.html", "w").write(meshcat.StaticHtml())
本站实测(8 秒运行,连续积分器):
| 项目 | 实测值 |
|---|---|
| 有效惯量(kg·m²) | 4.96e-3 / 5.88e-3 / 1.09e-3 / 2.95e-5 / 5.56e-6(首尾相差近 900 倍) |
| 自动整定 kp | 2000 / 1000 / 600 / 33.1 / 6.3(腕部必须小,否则离散控制发散) |
| 最大跟踪误差(rad) | 0.001 0.003 0.032 0.025 0.003(约 0.06°~1.8°,比示意模型更精准) |
| 扭矩饱和占比 | 13% 11% 15% 0% 0%(真实电机余量充足) |
| 末端轨迹范围(m) | x[-0.198, -0.075] y[-0.030, 0.082] z[0.174, 0.336] |
--time-step 0,由 Drake 自适应步长),控制仍保持 1 kHz 刷新。判断方法:先零扭矩跑几步,若关节角瞬间爆炸,就是这个问题。动画页打开后自动循环播放(左下角「⏸ 暂停 / ▶ 播放」按钮可控制,鼠标拖拽旋转、滚轮缩放)——那是 Drake 真实动力学算出来的运动(含重力、惯性耦合与电机扭矩上限),不是预先录好的演示视频。网格文件在 real_arm/meshes/(OBJ/MTL,3.8 MB)。
q_traj.npy);再把它当 q_des 时间表喂给 arm_meshcat_real.py 的 PD + 重力补偿控制器,即可在 Drake 真实动力学里复现同一轨迹。全屏打开 ↗🧪 验证对照表
| 检查项 | 预期结果(本站实测) |
|---|---|
| 启动信息 | plant=6 dof · gravity=-9.81(dof 为 6 说明底座已固定,若为 13 则 world 固定关节缺失) |
| 整秒输出行 | t=…s 实测(rad): … 末端XYZ(m): …,角度在 ±0.52 rad 内周期变化 |
| 最大跟踪误差 | 0.000 0.017 0.064 0.069 0.035 0.001 rad(量级应 ≤ 0.1 rad) |
| 扭矩饱和占比 | J1/J6 接近 0,J2~J5 在 50%~80%(说明肩肘正在用力,属真实工况) |
| 末端轨迹范围 | x[0.427, 0.620] y[-0.279, 0.291] z[-0.148, 0.621] m,周期性闭合 |
| CSV 输出 | 60 行(30 s / 0.5 s),首行 0.620 0.000 0.260(水平初始位姿) |
❓ 常见问题 FAQ
机械臂「掉下去」了,末端坐标几十米?
URDF 里缺少 world 固定关节。Drake 会把没有父关节的根连杆当作自由浮动体,于是整条臂在重力下自由落体。加一个 <link name="world"/> + <joint type="fixed"> 把 base_link 焊在世界原点即可。
报错说惯量不满足物理约束 / plant 拒绝加载?
转动惯量必须满足三角形不等式(如 ixx + iyy ≥ izz)。用长方体公式 I = m/12·(a² + b²) 逐连杆重算,别手写拍脑袋的数值。
get_actuation_input_port 为空 / 电机不存在?
URDF 的 revolute 关节在 Drake 里默认是被动的,必须用 plant.AddJointActuator(name, joint, effort_limit=…) 显式创建执行器(且要在 Finalize() 之前)。
手臂一直往下掉,控制器拉不住?
重力补偿符号反了。CalcGravityGeneralizedForces() 返回的是重力产生的广义力,抵消它应该是 u -= τ_g。用 --static 验证:正确时水平臂纹丝不动(误差 0.000)。
为什么腕部关节的 kp 比肩部小这么多?
各关节有效惯量差了近 1000 倍(本站实测 J2≈0.19、J6≈1.75e-4 kg·m²)。同样增益下腕部的闭环带宽高得多,离散控制会因 ω·Δt 过大而数值不稳定。按惯量配增益,或提高控制频率。
为什么要 1 kHz?200 Hz 不行吗?
可以,但本站实测 200 Hz 时腕部小惯量关节在快速运动中出现明显相位滞后与抖振(耦合扰动在采样间隙累积)。1 kHz 是真实伺服/高端步进驱动的常见速率,也是本模型稳定跟随的关键。
扭矩饱和占比高,是仿真失败了吗?
不是失败,而是最有价值的信号:它说明该关节已顶到电机上限,真机对应的就是步进电机堵转/丢步。要么降低轨迹速度/幅值(--freq / --amp-deg),要么加大减速比提高 effort,要么减重。
Windows 上装不了 drake?
Drake 没有官方 Windows pip 包。请在 WSL2(Ubuntu)里按本页步骤操作,或用 Docker 跑 Linux 镜像。
💡 要点提示
- 先跑通再美化:几何先用 Box 长方体(本模型即是),逻辑与动力学跑通后,再换真实 STL 网格。
- 固定世界是第一件事:没有
world固定关节,一切都无从谈起。 - 控制器三件套:PD + 速度前馈 − 重力补偿,缺了重力补偿静态就站不住,缺了速度前馈正弦会明显滞后。
- 把「扭矩饱和」当成设计指标看,而不是报错:它直接告诉你真机会不会丢步。
- 想复刻 SmallRobotArm 的真实质量:按装配 PDF 与实测称重替换 URDF 的
mass/inertia,控制器与流程完全不用改。 - Drake 不只能「看动画」:轨迹优化、逆运动学(IK)、接触仿真都能做,本站这条 6 轴链就是继续学
MultibodyPlant的最佳起点。
🔗 相关资源
- ⬇ arm_wave_drake.py(完整程序)1 kHz PD + 速度前馈 + 重力补偿,支持无头/meshcat/诊断开关
- ⬇ smallrobot_arm.urdf(六轴模型)含质量/惯量/限位/扭矩上限;改质量即可做自己的臂
- 回到「做自己的机器人」栏目首页卡片弹层可下载 SmallRobotArm 全部资料、阅读主线教程
- SimulIDE 模拟控制电路(另一篇分册)搭 Arduino Mega 电路验证固件脉冲逻辑,与 Drake 真实动力学互补
- MIT Drake 官网(文档 / 安装 / 教程)MultibodyPlant、Parser、MeshcatVisualizer 等 API 参考
- SmallRobotArm 原仓库(GitHub)STL / Fusion360 / 机械与电子 PDF / Arduino 固件(GPL-3.0)