English
🤖 LateAI
首页做自己的机器人 › MIT Drake 真实模拟 SmallRobotArm(详细分册)
🦕

MIT Drake 真实模拟 SmallRobotArm 六轴机械臂(详细分册)

Real-Dynamics Simulation of a 6DoF Arm with MIT Drake (pydrake)
📶 进阶 ⏱ 约 1~2 晚 💰 软件免费 🦾 DIY · 站内原创

📌 本分册目标

这是「做自己的机器人」第 1 个项目中 ② MIT Drake 真实动力学模拟 的详细版教程:讲清「URDF 建模 → 加载与执行器 → 控制器设计 → 验证与调参」四件事,附可直接运行的完整 Python 程序与 URDF

你会亲手得到:一个六轴臂的真实重力/惯性响应、一份末端轨迹 CSV、以及一组「哪些参数会让电机打满扭矩(真机丢步)」的实测结论。这些结论直接决定真机该选多大减速比、多快速度、多重末端。

💡 先仿真、后花钱:真机打印 + 6 路步进 + 驱动板通常一两千元,而 Drake 完全免费(BSD-3-Clause)。本页所有数值均为本站实测,照着做可复现。

🧰 环境准备

⚠️ 版本搭配是本教程唯一的「硬门槛」:Drake 的 wheel 与 Python 版本、CPU 架构强绑定。装不上时先确认 python3 --versionuname -m(x86_64 / arm64),再挑对应版本;Apple Silicon(arm64)与 Linux x86_64 一般直接 pip install drake 即可。

🔧 第 1 步:两个文件,一个目录

1

下载本次两个文件

⬇ smallrobot_arm.urdf(机械臂模型:连杆、关节、质量、惯量、限位、扭矩上限)

⬇ arm_wave_drake.py(仿真主程序:建世界 + 控制器 + 采样记录)

两个文件放在同一个目录下(脚本按自身位置找 URDF,不需要改路径)。

2

先跑一次确认装好了

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)
J1base → carrier0 0 1(绕 Z)0 0 0.10±3.1480.6 / 0.25
J2carrier → upper_arm0 1 0(绕 Y)0 0 0.16±2.44120.5
J3upper_arm → forearm0 1 0(绕 Y)0.22 0 0±2.4480.4
J4forearm → wrist10 1 0(绕 Y)0.20 0 0±2.9750.4
J5wrist1 → wrist20 1 0(绕 Y)0.12 0 0±2.4430.1
J6wrist2 → tool0 0 1(绕 Z)0.08 0 0±3.141.50.05
smallrobot_arm.urdf · 关键结构(节选,完整文件见上方下载)
<!-- ① 必须有一个固定关节把底座焊在世界原点,否则整条臂会自由落体 -->
<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 步:跑起来,读懂输出

终端命令(无头模式,30 秒长程运行)
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 直接画出来。

💡 CSV 第一行(t=0.001s)是 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 只需纠正剩下的误差。

arm_wave_drake.py · 完整程序(复制即运行)
#!/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()

三个必须记住的设计点

🔧 第 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 视图与轨迹可视化

🦾 第 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)。看到的就是真机外观,跑的仍是同一套真实动力学。

arm_meshcat_real.py · 真实 3D 模型 + meshcat 录制(核心片段)
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 倍)
自动整定 kp2000 / 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]
⚠️ 真实模型的一个实战坑:部分连杆有效惯量只有 1e-6 量级,用离散 plant(半隐式欧拉)时零输入都会一步发散到 1e17。解决办法是改用连续积分器--time-step 0,由 Drake 自适应步长),控制仍保持 1 kHz 刷新。判断方法:先零扭矩跑几步,若关节角瞬间爆炸,就是这个问题。

动画页打开后自动循环播放(左下角「⏸ 暂停 / ▶ 播放」按钮可控制,鼠标拖拽旋转、滚轮缩放)——那是 Drake 真实动力学算出来的运动(含重力、惯性耦合与电机扭矩上限),不是预先录好的演示视频。网格文件在 real_arm/meshes/(OBJ/MTL,3.8 MB)。

🎯 延展 · 真实 3D 轨迹实验室:下面把「画末端轨迹」做成任务空间 + 逆运动学——同一台真实模型,让末端在画板上实际画出 正弦曲线 / 圆形 / 三角形 三种轨迹(每帧数值雅可比 + 阻尼最小二乘实时解算 J1~J3,J4/J5 锁 0 保持姿态稳定)。浏览器里看到的就是按真实几何求出的关节角,右侧代码区会随模式生成可复制的离线 numpy 预解脚本(直接跑出 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 镜像。

💡 要点提示

🔗 相关资源

🦾 返回做自己的机器人 下一篇:SimulIDE 分册 → ← LateAI 首页