跳转至

第72章:浮动基座+臂统一动力学——质心动量矩阵

元信息
难度 ⭐⭐⭐(核心动力学推导 + 代码实现)
预计时间 2 周(30-35 小时)
前置依赖 足式/30_Pinocchio深度精读(Pinocchio CRTP), 足式/50_空间向量与浮动基座动力学(空间向量/浮基动力学), 足式/60_QP_NLP建模(WBC/TSID), 足式/110_OCS2完整栈与双线程MPC(RNEA/CRBA/ABA)
下游章节 复合/30_多模态MPC(多模态MPC), 复合/40_RL全身控制基础(RL全身控制), 复合/120_底盘臂联合规划(复合应用)

本章目标

学完本章后,你应能把四足、机械臂和浮动基座写进同一个动力学方程,解释质量矩阵分块、CMM、ZMP 偏移和零空间投影如何描述腿臂耦合,并能用 Pinocchio/OCS2 风格的代码提取这些量用于 WBC、MPC 和状态估计。

72.0 前置自测

在开始本章前,请独立回答以下五个问题。如果有两个以上答不出,建议先回顾对应章节。

# 问题 来源
1 Pinocchio 中 model.nqmodel.nv 的区别是什么?为什么浮动基座时 nq = nv + 1 足式/30_Pinocchio深度精读
2 写出质量矩阵 \(\mathbf{M}(\mathbf{q})\)\(2\times2\) 分块结构(浮基 vs 关节),并说明对角块和耦合块的物理含义。 足式/50_空间向量与浮动基座动力学
3 质心动量矩阵(Centroidal Momentum Matrix, CMM)\(\mathbf{A}_G\) 的定义是什么?它的维度和含义? 足式/50_空间向量与浮动基座动力学/足式/60_QP_NLP建模
4 WBC 中"任务优先级"的核心思想是什么?说出至少三个优先级层次。 足式/60_QP_NLP建模
5 复合/10_复合机器人全景 定义了几种复合机器人?Go2+Z1 的广义坐标维度 \(n_q / n_v\) 分别是多少? 复合/10_复合机器人全景

自测标准: - 5/5 正确:直接阅读本章 - 3-4/5 正确:边读边查阅前置章节 - 0-2/5 正确:建议先完成 足式/30_Pinocchio深度精读 + 足式/50_空间向量与浮动基座动力学 + 足式/60_QP_NLP建模


72.1 从足式到复合——URDF 维度爆炸 ⭐

动机:为什么维度爆炸是核心挑战?

在 足式/30_Pinocchio深度精读-足式/110_OCS2完整栈与双线程MPC 中,我们处理的是纯四足机器人:12 个腿关节加 6-DOF 浮动基座,总共 \(n_v = 18\)。质量矩阵 \(\mathbf{M} \in \mathbb{R}^{18 \times 18}\),CRBA 算法在微秒级完成。

但复合机器人的维度急剧上升。让我们看三个典型平台:

平台 浮基 腿 DOF 臂 DOF 腰 DOF \(n_q\) \(n_v\) \(\mathbf{M}\) 维度
Go2(纯四足) 7/6 12 0 0 19 18 \(18\times18\)
Go2+Z1 6-DOF 7/6 12 6 0 25 24 \(24\times24\)
Go2+Z1 7-DOF 7/6 12 7 0 26 25 \(25\times25\)
H1(人形) 7/6 12 14 3 36 35 \(35\times35\)
G1(人形) 7/6 12 14 3 36 35 \(35\times35\)
CENTAURO(双臂四足) 7/6 24 14 2 47 46 \(46\times46\)

注意\(n_q = n_v + 1\) 的 "+1" 来自浮动基座用四元数(4 维)表示方位而速度用角速度(3 维)。这是 Lie 群 \(SE(3)\) 的基本性质。

维度爆炸的计算后果

CRBA 算法的复杂度为 \(O(n)\)(链式结构)但质量矩阵求逆为 \(O(n^3)\)。MPC 中每一步需要求解一个 QP,其计算量与 \(n_v^3\) 成正比:

平台 \(n_v\) \(n_v^3\) 相对纯四足倍数 500Hz WBC 可行?
Go2 18 5,832 1.0x 轻松
Go2+Z1 24 13,824 2.4x 可行
H1 35 42,875 7.4x 紧张
CENTAURO 46 97,336 16.7x 需要简化模型

结论:从纯四足到人形,MPC 的单步计算量增长近一个数量级。这正是为什么 centroidal dynamics 简化模型如此重要——将 \(O(n_v^3)\) 降为 \(O(1)\)(质心模型只有 6 维)。

Pinocchio 统一加载

不论系统多复杂,Pinocchio 的加载接口始终一致:

import pinocchio as pin
import numpy as np

# ========== 方式1:从 URDF 加载 ==========
# Go2+Z1 的统一 URDF 中,臂通过 fixed joint 连接在 base_link 上
model = pin.buildModelFromUrdf(
    "go2_z1.urdf",
    pin.JointModelFreeFlyer()   # 浮动基座关节类型
)
data = model.createData()

print(f"nq = {model.nq}")  # 25:7(浮基) + 12(腿) + 6(臂)
print(f"nv = {model.nv}")  # 24:6(浮基) + 12(腿) + 6(臂)

# ========== 方式2:打印关节树(用于理解拓扑) ==========
# Pinocchio 内部的关节树结构:
# universe -> root_joint(FreeFlyer) -> FL_hip -> FL_thigh -> FL_calf
#                                   -> FR_hip -> ...
#                                   -> RL_hip -> ...
#                                   -> RR_hip -> ...
#                                   -> arm_joint1 -> arm_joint2 -> ... -> arm_joint6

for i in range(model.njoints):
    parent = model.parents[i]
    print(f"Joint {i}: {model.names[i]}, parent={parent}")

质量矩阵的增长可视化

import matplotlib.pyplot as plt

# 计算不同构型下的质量矩阵
q = pin.neutral(model)
pin.crba(model, data, q)
M = data.M.copy()
M = (M + M.T) / 2  # CRBA 只计算上三角,手动对称化

# 可视化 M 的稀疏结构
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 绝对值热力图
im0 = axes[0].imshow(np.log10(np.abs(M) + 1e-10), cmap='hot')
axes[0].set_title("log10(|M(q)|): 质量矩阵结构")
axes[0].set_xlabel("速度维度 (nv=24)")
axes[0].set_ylabel("速度维度 (nv=24)")

# 标注分块边界
for ax in axes:
    for pos in [6, 18]:  # 浮基|腿 边界, 腿|臂 边界
        ax.axhline(pos - 0.5, color='cyan', linewidth=2)
        ax.axvline(pos - 0.5, color='cyan', linewidth=2)
    ax.set_xticks([3, 12, 21])
    ax.set_xticklabels(['浮基', '腿', '臂'])
    ax.set_yticks([3, 12, 21])
    ax.set_yticklabels(['浮基', '腿', '臂'])

# 耦合项强度
M_coupling = M.copy()
M_coupling[:6, :6] = 0      # 清除基座自耦合
M_coupling[6:18, 6:18] = 0  # 清除腿自耦合
M_coupling[18:, 18:] = 0    # 清除臂自耦合
im1 = axes[1].imshow(np.abs(M_coupling), cmap='Reds')
axes[1].set_title("交叉耦合项 |M_coupling|")

plt.colorbar(im0, ax=axes[0])
plt.colorbar(im1, ax=axes[1])
plt.tight_layout()
plt.savefig("mass_matrix_structure.png", dpi=150)
plt.show()

易错点:CRBA 输出只有上三角

Pinocchio 的 crba() 函数为了性能,只填充 data.M 的上三角部分。如果直接使用 data.M 做矩阵运算,下三角全是零,会导致错误的求逆或分解结果。必须手动对称化:M = (data.M + data.M.T) / 2 或在 C++ 端 data.M.triangularView<Eigen::Lower>() = data.M.transpose()

练习 72.1

[A] ⭐ 加载 Go2 的 URDF(12-DOF)和 Go2+Z1 的 URDF(18-DOF 关节),分别计算质量矩阵并打印形状。验证 \(\mathbf{M}\) 的维度符合预期。

[B] ⭐⭐ 将 Go2+Z1 的质量矩阵可视化为热力图,标注 3x3 分块结构的边界。观察哪些耦合块最强、哪些几乎为零。解释原因。

[C] ⭐⭐ 测量 Pinocchio crba() 在不同 \(n_v\) 下的计算时间(Go2 / Go2+Z1 / H1),绘制 \(n_v\) vs 计算时间的散点图,验证是否接近 \(O(n)\)


72.2 统一动力学方程——三类力完整推导 ⭐⭐⭐

动机:为什么需要完整推导?

复合/10_复合机器人全景 给出了统一动力学方程的最终形式。但对于 MPC 和 WBC 的设计者来说,**理解每一项从何而来**是至关重要的——否则在实际调参时,面对振荡、漂移、跌倒等问题将无从下手。

本节从 Lagrange 力学出发,一步步推导出完整的统一动力学方程,并详细分析质量矩阵的 \(3\times3\) 分块结构。

从 Lagrangian 出发

第一步:定义广义坐标与广义速度

对于 Go2+Z1 系统,广义坐标和广义速度分别为:

\[ \mathbf{q} = \begin{pmatrix} \mathbf{q}_b \\ \mathbf{q}_\ell \\ \mathbf{q}_a \end{pmatrix} \in \mathbb{R}^{n_q}, \quad \mathbf{v} = \begin{pmatrix} \mathbf{v}_b \\ \dot{\mathbf{q}}_\ell \\ \dot{\mathbf{q}}_a \end{pmatrix} \in \mathbb{R}^{n_v} \]

其中 \(\mathbf{q}_b \in SE(3)\)(位置 + 四元数),\(\mathbf{v}_b \in \mathbb{R}^6\)(线速度 + 角速度),\(\mathbf{q}_\ell \in \mathbb{R}^{12}\)(四条腿各 3 关节),\(\mathbf{q}_a \in \mathbb{R}^{6}\)(臂 6 关节)。

符号约定:本章使用 \(\mathbf{v}\)(而非 \(\dot{\mathbf{q}}\))表示广义速度,因为浮动基座的速度 \(\mathbf{v}_b\) 不是 \(\dot{\mathbf{q}}_b\) 的简单时间导数——它们之间通过 \(\dot{\mathbf{q}}_b = \mathbf{E}(\mathbf{q}_b)\mathbf{v}_b\) 联系,其中 \(\mathbf{E}\) 是与四元数表示相关的映射矩阵。这是 Lie 群流形上微分方程的基本特征。

第二步:构造系统 Lagrangian

系统的总动能为所有刚体(基座 + 腿连杆 + 臂连杆)的动能之和:

\[ T(\mathbf{q}, \mathbf{v}) = \frac{1}{2}\mathbf{v}^T \mathbf{M}(\mathbf{q}) \mathbf{v} \]

其中 \(\mathbf{M}(\mathbf{q})\) 是**联合质量矩阵**(joint-space inertia matrix),由所有连杆的惯性参数通过 CRBA 算法递归计算得到。

系统的总势能为:

\[ V(\mathbf{q}) = -\sum_{i=1}^{N_{\text{body}}} m_i \mathbf{g}^T \mathbf{p}_{\text{com},i}(\mathbf{q}) \]

Lagrangian 为 \(\mathcal{L} = T - V\)

第三步:Euler-Lagrange 方程

对 Lagrangian 应用 Euler-Lagrange 方程。浮动基座严格地生活在 \(SE(3)\) 上,因此不能把姿态当成普通欧氏向量随意相减;工程实现通常在当前姿态的切空间中使用广义速度 \(\mathbf{v}\) 做局部坐标化。这样做的含义是:配置变量 \(\mathbf{q}\) 负责描述全局位姿,速度变量 \(\mathbf{v}\) 负责描述切空间中的 twist,变分推导在这个局部坐标中得到与固定基座操纵器同形的方程:

\[ \frac{d}{dt}\frac{\partial \mathcal{L}}{\partial \mathbf{v}} - \frac{\partial \mathcal{L}}{\partial \mathbf{q}} = \boldsymbol{\tau}_{\text{gen}} \]

展开后得到经典的操纵器方程形式:

\[ \mathbf{M}(\mathbf{q})\dot{\mathbf{v}} + \mathbf{C}(\mathbf{q}, \mathbf{v})\mathbf{v} + \mathbf{g}(\mathbf{q}) = \boldsymbol{\tau}_{\text{gen}} \]

其中: - \(\mathbf{M}(\mathbf{q})\dot{\mathbf{v}}\):惯性力项 - \(\mathbf{C}(\mathbf{q}, \mathbf{v})\mathbf{v}\):科里奥利力 + 离心力项 - \(\mathbf{g}(\mathbf{q})\):重力项

通常将后两项合并为 \(\mathbf{h}(\mathbf{q}, \mathbf{v}) = \mathbf{C}(\mathbf{q}, \mathbf{v})\mathbf{v} + \mathbf{g}(\mathbf{q})\),即**非线性力项**。在 Pinocchio 中,\(\mathbf{h}\)rnea(model, data, q, v, 0) 一次计算得到(RNEA 输入零加速度)。

第四步:广义力的三类分解

\(\boldsymbol{\tau}_{\text{gen}}\) 是所有外力在广义坐标上的投影。对于复合机器人,外力来自三个源头:

源头1:关节力矩(Joint Torques)

关节电机只能驱动铰链关节,无法直接驱动浮动基座。用选择矩阵 \(\mathbf{S}\) 描述这一约束:

\[ \mathbf{S} = \begin{pmatrix} \mathbf{0}_{(n_\ell+n_a) \times 6} & \mathbf{I}_{(n_\ell+n_a)} \end{pmatrix} \]

对于 Go2+Z1:\(\mathbf{S} \in \mathbb{R}^{18 \times 24}\),前 6 列为零(浮基不可驱动),后 18 列为单位阵。

关节力矩对广义力的贡献为 \(\mathbf{S}^T \boldsymbol{\tau}\),其中 \(\boldsymbol{\tau} \in \mathbb{R}^{18}\) 是所有可驱动关节的力矩向量。

源头2:足端接触力(Foot Contact Forces)

四足机器人的每只脚可能与地面接触。设第 \(i\) 只脚的接触力为 \(\boldsymbol{\lambda}_i \in \mathbb{R}^3\)(三维空间力),接触雅可比为 \(\mathbf{J}_{c,i} \in \mathbb{R}^{3 \times n_v}\),则所有接触力对广义力的贡献为:

\[ \sum_{i \in \mathcal{C}_{\text{foot}}} \mathbf{J}_{c,i}^T \boldsymbol{\lambda}_i \]

其中 \(\mathcal{C}_{\text{foot}}\) 是当前接触足集合(随步态相位变化)。

源头3:臂末端力/力矩(End-Effector Wrench)

这是复合机器人相比纯四足的**核心新增项**。臂末端可能: - 主动施力(推门、搬运物体) - 承受被动外力(碰撞、被抓物体重力)

设末端 wrench(6D 力螺旋)为 \(\mathbf{f}_{\text{ee}} \in \mathbb{R}^6\),末端雅可比为 \(\mathbf{J}_{\text{ee}} \in \mathbb{R}^{6 \times n_v}\),则其对广义力的贡献为:

\[ \mathbf{J}_{\text{ee}}^T \mathbf{f}_{\text{ee}} \]

第五步:合并得到统一动力学方程

将三类力代入 Euler-Lagrange 方程:

\[ \boxed{ \mathbf{M}(\mathbf{q})\dot{\mathbf{v}} + \mathbf{h}(\mathbf{q}, \mathbf{v}) = \mathbf{S}^T \boldsymbol{\tau} + \sum_{i \in \mathcal{C}_{\text{foot}}} \mathbf{J}_{c,i}^T \boldsymbol{\lambda}_i + \mathbf{J}_{\text{ee}}^T \mathbf{f}_{\text{ee}} } \]

这是本章(及后续 复合/30_多模态MPC, 复合/40_RL全身控制基础)的**核心方程**。三类力的对比总结:

力类型 符号 维度(投影后) 物理来源 可控性
关节力矩 \(\mathbf{S}^T\boldsymbol{\tau}\) \(n_v\) 电机输出 完全可控(决策变量)
足端接触力 \(\mathbf{J}_{c,i}^T\boldsymbol{\lambda}_i\) \(n_v\) 地面反力 间接可控(通过步态规划)
末端 wrench \(\mathbf{J}_{\text{ee}}^T\mathbf{f}_{\text{ee}}\) \(n_v\) 操作力/外力 部分可控(力控)或不可控(扰动)

质量矩阵的 \(3\times3\) 分块结构

\(\mathbf{M}(\mathbf{q})\) 按浮基(b)、腿(\(\ell\))、臂(a) 三个子空间分块:

\[ \mathbf{M}(\mathbf{q}) = \begin{pmatrix} \mathbf{M}_{bb} & \mathbf{M}_{b\ell} & \mathbf{M}_{ba} \\ \mathbf{M}_{b\ell}^T & \mathbf{M}_{\ell\ell} & \mathbf{M}_{\ell a} \\ \mathbf{M}_{ba}^T & \mathbf{M}_{\ell a}^T & \mathbf{M}_{aa} \end{pmatrix} \]

各分块的物理含义:

分块 维度 物理含义 数值特征
\(\mathbf{M}_{bb}\) \(6 \times 6\) 基座**锁关节惯量**(locked inertia):所有关节锁定时基座的等效惯量 最大,主导
\(\mathbf{M}_{\ell\ell}\) \(12 \times 12\) 腿关节的等效惯量矩阵 近对角,4 条腿之间耦合弱
\(\mathbf{M}_{aa}\) \(6 \times 6\) 臂关节的等效惯量矩阵 构型依赖强
\(\mathbf{M}_{b\ell}\) \(6 \times 12\) 基座-腿耦合:腿运动对基座的惯性反力矩 中等
\(\mathbf{M}_{ba}\) \(6 \times 6\) 基座-臂耦合:臂运动对基座的惯性反力矩(本章重点) 构型依赖极强
\(\mathbf{M}_{\ell a}\) \(12 \times 6\) 腿-臂耦合:臂运动通过基座间接影响腿 通常较小

关键洞察\(\mathbf{M}_{ba}\) 是理解"臂扰动基座"的核心。当臂加速度 \(\ddot{\mathbf{q}}_a \neq 0\) 时,统一动力学方程的基座行为:

\[\mathbf{M}_{bb}\dot{\mathbf{v}}_b + \mathbf{M}_{b\ell}\ddot{\mathbf{q}}_\ell + \mathbf{M}_{ba}\ddot{\mathbf{q}}_a + \mathbf{h}_b = \sum_i \mathbf{J}_{c,i,b}^T \boldsymbol{\lambda}_i + \mathbf{J}_{\text{ee},b}^T \mathbf{f}_{\text{ee}}\]

其中 \(\mathbf{M}_{ba}\ddot{\mathbf{q}}_a\) 项正是臂加速度对基座产生的**惯性反力矩**。忽略此项就是 复合/10_复合机器人全景 所述的"独立控制"方案失败的根源。

Pinocchio 中提取分块

import pinocchio as pin
import numpy as np

model = pin.buildModelFromUrdf("go2_z1.urdf", pin.JointModelFreeFlyer())
data = model.createData()
q = pin.neutral(model)

# 计算质量矩阵
pin.crba(model, data, q)
M = data.M.copy()
M = (M + M.T) / 2  # 对称化(CRBA 只输出上三角)

# 分块索引
idx_b = slice(0, 6)      # 浮基:前 6 行/列
idx_l = slice(6, 18)     # 腿:第 6-17 行/列
idx_a = slice(18, 24)    # 臂:第 18-23 行/列

# 提取 3x3 分块
M_bb = M[idx_b, idx_b]   # (6, 6)  基座惯量
M_ll = M[idx_l, idx_l]   # (12,12) 腿惯量
M_aa = M[idx_a, idx_a]   # (6, 6)  臂惯量
M_bl = M[idx_b, idx_l]   # (6, 12) 基座-腿耦合
M_ba = M[idx_b, idx_a]   # (6, 6)  基座-臂耦合(关键!)
M_la = M[idx_l, idx_a]   # (12, 6) 腿-臂耦合

print("=== 质量矩阵分块结构 ===")
print(f"M_bb (基座惯量):     Frobenius范数 = {np.linalg.norm(M_bb):.3f}")
print(f"M_ll (腿惯量):       Frobenius范数 = {np.linalg.norm(M_ll):.3f}")
print(f"M_aa (臂惯量):       Frobenius范数 = {np.linalg.norm(M_aa):.3f}")
print(f"M_bl (基座-腿耦合):  Frobenius范数 = {np.linalg.norm(M_bl):.3f}")
print(f"M_ba (基座-臂耦合):  Frobenius范数 = {np.linalg.norm(M_ba):.3f}")
print(f"M_la (腿-臂耦合):    Frobenius范数 = {np.linalg.norm(M_la):.3f}")

# 计算耦合强度比:衡量耦合项相对主对角块的强度
print(f"\n=== 耦合强度比 ===")
print(f"臂-基座耦合/基座惯量 = {np.linalg.norm(M_ba)/np.linalg.norm(M_bb)*100:.1f}%")
print(f"腿-臂耦合/腿惯量   = {np.linalg.norm(M_la)/np.linalg.norm(M_ll)*100:.1f}%")

三类力的 Pinocchio 计算

import pinocchio as pin
import numpy as np

model = pin.buildModelFromUrdf("go2_z1.urdf", pin.JointModelFreeFlyer())
data = model.createData()

q = pin.neutral(model)
v = np.zeros(model.nv)
a = np.zeros(model.nv)

# === 非线性力 h(q,v) = C(q,v)v + g(q) ===
# RNEA 在零加速度下等于非线性力
h = pin.rnea(model, data, q, v, a)  # h ∈ R^24
print(f"h(q,v) shape: {h.shape}")    # 包含重力 + 科里奥利力

# === 选择矩阵 S ===
n_actuated = model.nv - 6  # 18 个可驱动关节
S = np.zeros((n_actuated, model.nv))  # (18, 24)
S[:, 6:] = np.eye(n_actuated)
print(f"S shape: {S.shape}")

# === 接触雅可比 ===
# 假设 FL_foot 是左前脚的 frame
FL_foot_id = model.getFrameId("FL_foot")
pin.computeJointJacobians(model, data, q)
pin.updateFramePlacements(model, data)
J_FL = pin.getFrameJacobian(
    model, data, FL_foot_id, pin.LOCAL_WORLD_ALIGNED
)
print(f"J_FL shape: {J_FL.shape}")   # (6, 24)

# 只取线性部分(3D 接触力,忽略力矩)
J_FL_linear = J_FL[:3, :]  # (3, 24)

# === 末端雅可比 ===
ee_frame_id = model.getFrameId("gripper_link")
J_ee = pin.getFrameJacobian(
    model, data, ee_frame_id, pin.LOCAL_WORLD_ALIGNED
)
print(f"J_ee shape: {J_ee.shape}")   # (6, 24),完整 6D wrench

易错点:雅可比的参考坐标系

Pinocchio 支持三种雅可比参考系:LOCAL(body-fixed),WORLD(空间固定),LOCAL_WORLD_ALIGNED(位于 body 原点但轴平行于世界系)。WBC/MPC 中通常使用 LOCAL_WORLD_ALIGNED,因为它保持了世界系的物理直觉,同时避免了 WORLD 参考系在远离原点时数值不稳定的问题。选错参考系是最常见的 WBC 调试陷阱之一——症状是末端跟踪看似正确但力方向完全错误。

练习 72.2

[A] ⭐⭐ 写出 Go2+Z1 系统中选择矩阵 \(\mathbf{S}\) 的显式形式。验证 \(\mathbf{S}\mathbf{S}^T = \mathbf{I}_{18}\)\(\mathbf{S}^T\mathbf{S}\) 是一个什么矩阵?它的物理含义是什么?

[B] ⭐⭐⭐ 手动推导:将统一方程按浮基/腿/臂分块展开为三个子方程。特别关注浮基方程(第一行)——为什么左侧没有 \(\mathbf{S}^T\boldsymbol{\tau}\) 的贡献?这一性质对 WBC 设计意味着什么?

[C] ⭐⭐⭐ 在 Pinocchio 中,给定 \(\mathbf{q}, \mathbf{v}, \boldsymbol{\tau}\),使用 ABA(pin.aba())直接计算 \(\dot{\mathbf{v}}\)。与通过 \(\dot{\mathbf{v}} = \mathbf{M}^{-1}(\boldsymbol{\tau}_{\text{gen}} - \mathbf{h})\) 的结果对比,验证数值一致性并比较计算时间。


72.3 臂反力矩对基座稳定性 ⭐⭐⭐

动机:1kg 就能让四足摔倒?

Unitree Go2 的总质量约 15 kg,Z1 臂的自重约 4.3 kg(含 gripper)。当臂完全外伸并抓取 1 kg 物体时,臂末端相对基座中心的力臂约 0.5 m。这意味着臂产生的附加力矩为:

\[ \tau_{\text{arm}} \approx (m_{\text{arm}} + m_{\text{payload}}) \times g \times r_{\text{arm}} = (4.3 + 1.0) \times 9.81 \times 0.5 \approx 26 \text{ N}\cdot\text{m} \]

而 Go2 的步态稳定裕度(support polygon 内的 ZMP 余量)通常只有几厘米。26 N*m 的力矩足以将 ZMP 推到支撑多边形边缘,导致倾覆

这就是为什么"臂反力矩"是复合机器人的第一个也是最重要的稳定性挑战。

ZMP 偏移推导

回顾 ZMP 定义

ZMP(Zero Moment Point)是地面上使得绕该点的合力矩的水平分量为零的点。对于受重力和接触力作用的刚体系统,ZMP 坐标的实用简化形式为:

\[ x_{\text{ZMP}} = x_{\text{CoM}} - \frac{z_{\text{CoM}}}{g + \ddot{z}_{\text{CoM}}} \ddot{x}_{\text{CoM}} \]

这是经典的 LIPM(Linear Inverted Pendulum Model) 近似。它假设所有质量集中在一个质点,且忽略角动量效应。

引入臂外力的完整推导

当臂末端施加或承受外力时,系统的角动量平衡方程需要修正。从牛顿-欧拉方程在质心处展开:

\[ \dot{\mathbf{L}}_G = \sum_{i \in \mathcal{C}} (\mathbf{p}_{c,i} - \mathbf{p}_G) \times \boldsymbol{\lambda}_i + (\mathbf{p}_{\text{ee}} - \mathbf{p}_G) \times \mathbf{f}_{\text{ee,lin}} \]

\(y\) 分量(pitch 方向),并利用 ZMP 定义(绕 ZMP 的水平力矩为零),化简得到:

\[ \boxed{ x_{\text{ZMP}} = x_{\text{CoM}} - \frac{z_{\text{CoM}}}{g} \ddot{x}_{\text{CoM}} + \frac{\tau_{\text{arm},y}}{m_{\text{total}} \cdot g} } \]

其中 \(\tau_{\text{arm},y}\) 是臂绕 \(y\) 轴(pitch 方向)对质心产生的力矩。第三项 \(\frac{\tau_{\text{arm},y}}{m_{\text{total}} \cdot g}\) 就是**臂引起的 ZMP 偏移量**。

推导注记:完整推导需要考虑 \(\dot{\mathbf{L}}_G\)(角动量变化率)的贡献。在准静态近似下(\(\dot{\mathbf{L}}_G \approx 0\)),上式是精确的。动态情况下需加入 \(\frac{\dot{L}_{G,y}}{m g}\) 修正项。

Go2+Z1 数值案例

参数 来源
\(m_{\text{total}}\) 15 + 4.3 + 1.0 = 20.3 kg Go2 + Z1 + payload
\(z_{\text{CoM}}\) 0.3 m 站立高度
\(g\) 9.81 m/s\(^2\)
\(\tau_{\text{arm},y}\) 26 N*m 上面计算
支撑多边形半长 ~0.15 m Go2 前后足距

ZMP 偏移量(准静态,\(\ddot{x}_{\text{CoM}} = 0\)):

\[ \Delta x_{\text{ZMP}} = \frac{\tau_{\text{arm},y}}{m_{\text{total}} \cdot g} = \frac{26}{20.3 \times 9.81} \approx 0.131 \text{ m} \]

结论:ZMP 偏移 13.1 cm,而支撑多边形的半长只有约 15 cm。ZMP 已接近边界,极易倾覆

不同臂姿态下的完整对照:

臂姿态 \(\tau_{\text{arm},y}\) (N*m) \(\Delta x_{\text{ZMP}}\) (m) 占支撑半长比 风险等级
收拢(Home) 2.1 0.011 7% 安全
半伸展(45度) 12.5 0.063 42% 注意
全伸展,空载 21.1 0.106 71% 危险
全伸展,1 kg 26.0 0.131 87% 临界
全伸展,2 kg 30.9 0.155 >100% 倾覆

摩擦锥收紧

除了 ZMP 偏移,臂操作还会导致摩擦锥约束收紧。当臂末端施加水平力 \(f_{\text{ee},x}\) 时,足端需要产生额外的水平反力来平衡。牛顿第二定律在水平方向:

\[ \sum_{i \in \mathcal{C}} \lambda_{i,x} = m \ddot{x}_{\text{CoM}} - f_{\text{ee},x} \]

足端的可用水平力受摩擦锥约束 \(|\lambda_{i,x}| \leq \mu \lambda_{i,z}\)。当 \(f_{\text{ee},x}\) 消耗了部分摩擦裕度后,等效摩擦系数降为:

\[ \mu_{\text{eff}} = \mu - \frac{|f_{\text{ee},x}|}{\sum_i \lambda_{i,z}} \]

| 场景 | \(\mu\) | \(|f_{\text{ee},x}|\) | \(\sum \lambda_{i,z}\) | \(\mu_{\text{eff}}\) | 打滑风险 | |------|-------|---------------------|---------------------|-------------------|---------| | 无臂力 | 0.7 | 0 | 200 N | 0.70 | 极低 | | 推门 10N | 0.7 | 10 | 200 N | 0.65 | 低 | | 推重物 50N | 0.7 | 50 | 200 N | 0.45 | 中 | | 拉拽 80N | 0.7 | 80 | 200 N | 0.30 | 高 |

三种补偿策略

策略 方法 响应速度 适用场景 实现复杂度
主动补偿(Proactive) MPC 提前规划质心/步态轨迹以抵消已知臂扰动 最快(前馈) 已知操作任务(搬运)
反应补偿(Reactive) WBC 实时检测 ZMP 偏移并调整足端力分配 中等(反馈) 未知外力扰动
被动补偿(Passive) 增大支撑多边形(张开双腿)、降低重心 最慢(姿态调整) 重载静态操作

主动补偿的 Pinocchio 实现

import pinocchio as pin
import numpy as np

model = pin.buildModelFromUrdf("go2_z1.urdf", pin.JointModelFreeFlyer())
data = model.createData()

def compute_zmp_shift(model, data, q, v, f_ee):
    """
    计算臂末端力导致的 ZMP 偏移。

    参数:
        model, data: Pinocchio 模型和数据
        q: 广义坐标 (nq,)
        v: 广义速度 (nv,)
        f_ee: 末端 wrench [fx, fy, fz, mx, my, mz],世界系
    返回:
        delta_zmp: (dx, dy) ZMP 偏移量,单位 m
    """
    # 1. 计算质心位置
    pin.centerOfMass(model, data, q, v)
    com = data.com[0]         # 质心世界坐标
    m_total = data.mass[0]    # 系统总质量

    # 2. 获取末端位置
    pin.forwardKinematics(model, data, q)
    pin.updateFramePlacements(model, data)
    ee_id = model.getFrameId("gripper_link")
    p_ee = data.oMf[ee_id].translation

    # 3. 计算臂力对质心的力矩
    r_ee = p_ee - com  # 末端相对质心的力臂
    tau_arm = np.cross(r_ee, f_ee[:3]) + f_ee[3:]  # 力矩 = r x F + M

    # 4. ZMP 偏移
    g = 9.81
    delta_x = tau_arm[1] / (m_total * g)   # pitch 力矩 -> x 方向偏移
    delta_y = -tau_arm[0] / (m_total * g)   # roll 力矩 -> y 方向偏移

    return np.array([delta_x, delta_y])

# === 示例:臂末端承受 1kg 物体重力 ===
q = pin.neutral(model)
v = np.zeros(model.nv)
f_ee = np.array([0, 0, -1.0 * 9.81, 0, 0, 0])  # 1kg 物体的重力

delta = compute_zmp_shift(model, data, q, v, f_ee)
print(f"ZMP 偏移: dx={delta[0]*100:.1f} cm, dy={delta[1]*100:.1f} cm")

# === 扫描不同臂构型 ===
shoulder_angles = np.linspace(-np.pi/2, np.pi/2, 50)
zmp_shifts = []
for theta in shoulder_angles:
    q_test = pin.neutral(model)
    q_test[18] = theta  # 修改 shoulder pitch
    delta = compute_zmp_shift(model, data, q_test, v, f_ee)
    zmp_shifts.append(delta[0])

import matplotlib.pyplot as plt
plt.figure(figsize=(8, 5))
plt.plot(np.degrees(shoulder_angles), np.array(zmp_shifts) * 100)
plt.axhline(15, color='r', linestyle='--', label='支撑边界 (15cm)')
plt.axhline(-15, color='r', linestyle='--')
plt.xlabel("Shoulder pitch (deg)")
plt.ylabel("ZMP x-shift (cm)")
plt.title("臂构型 vs ZMP 偏移")
plt.legend()
plt.grid(True)
plt.show()

"Carry while walking" 问题

当机器人需要在行走过程中搬运物体时,问题变得更加复杂:

  1. 步态相位耦合:摆动腿(swing leg)离地时支撑多边形缩小为三角形,ZMP 裕度进一步降低
  2. 动态负载波动:行走产生的基座加速度叠加臂负载,使 ZMP 在步态周期内剧烈波动
  3. 摩擦锥动态变化:摆动相中只有三只脚着地,总法向力降低,摩擦裕度减小

qm_control 的解决方案:

# qm_control 风格的前馈补偿伪代码
def compensated_mpc_step(mpc_solver, arm_state, payload_mass):
    """
    在 MPC 代价函数中加入臂负载补偿项。
    qm_control 的核心思想:将臂负载视为已知扰动,通过修正
    质心参考轨迹来前馈补偿。
    """
    # 1. 计算臂负载对质心的力矩
    tau_load = compute_arm_torque(arm_state, payload_mass)

    # 2. 修正 MPC 的 CoM 参考轨迹——向反方向偏移
    com_ref_shift = -tau_load / (total_mass * 9.81)
    mpc_solver.update_com_reference(com_ref_shift)

    # 3. 收紧 MPC 中的摩擦锥约束
    friction_margin = compute_friction_margin(arm_state)
    mpc_solver.update_friction_cone(friction_margin)

    # 4. 求解修正后的 MPC
    return mpc_solver.solve()

易错点:静态 vs 动态 ZMP

上面的 ZMP 偏移公式是**准静态**近似(忽略了 \(\ddot{x}_{\text{CoM}}\) 项)。在实际行走中,\(\ddot{x}_{\text{CoM}}\) 不为零,且与步态相位强相关。完整的动态 ZMP 需要同时考虑惯性力和角动量变化率 \(\dot{\mathbf{L}}_G\)。这正是下一节 CMM 的用武之地——通过 \(\mathbf{A}_G\) 直接追踪角动量,不需要逐关节计算。

练习 72.3

[A] ⭐⭐ 如果 Go2 改用更宽的站姿(支撑多边形半长从 0.15 m 增加到 0.20 m),重新计算各臂姿态下的"占支撑半长比"。这说明什么工程设计原则?

[B] ⭐⭐⭐ 推导包含角动量变化率 \(\dot{\mathbf{L}}_G\) 的完整动态 ZMP 公式。提示:从质心处的角动量平衡 \(\dot{\mathbf{L}}_G = \sum (\mathbf{p}_i - \mathbf{p}_G) \times \boldsymbol{\lambda}_i + m(\mathbf{p}_G - \mathbf{p}_{\text{ZMP}}) \times \mathbf{g}\) 出发。

[C] ⭐⭐⭐ 在 Pinocchio 中实现完整的 compute_zmp_shift 函数,扫描臂关节角并绘制 ZMP 偏移曲线。确定对 ZMP 影响最大的臂关节是哪一个?为什么?


72.4 质心动量矩阵 CMM 复合扩展 ⭐⭐⭐

动机:从 ZMP 到质心动量

上一节用 ZMP 分析了臂负载对稳定性的影响,但 ZMP 是**足端**视角——它告诉我们"地面反力够不够",却没有直接回答"系统的动量如何变化"这个更本质的问题。

质心动量矩阵(Centroidal Momentum Matrix, CMM)提供了一个更本质、更统一的视角:直接从**质心动量**出发描述整个系统的动力学行为。它将"维度爆炸"的关节空间动力学压缩为 6 维的质心空间,同时保留了每个关节对总动量的贡献信息。

CMM 定义回顾与扩展

系统在质心处的空间动量(centroidal momentum)定义为:

\[ \mathbf{h}_G = \begin{pmatrix} \mathbf{k}_G \\ \mathbf{l}_G \end{pmatrix} \in \mathbb{R}^6 \]

其中 \(\mathbf{k}_G \in \mathbb{R}^3\) 是绕质心的角动量,\(\mathbf{l}_G = m_{\text{total}} \dot{\mathbf{p}}_{\text{CoM}} \in \mathbb{R}^3\) 是线动量。

CMM \(\mathbf{A}_G(\mathbf{q})\) 是连接广义速度与质心动量的线性映射:

\[ \boxed{ \mathbf{h}_G = \mathbf{A}_G(\mathbf{q}) \mathbf{v} } \]

其中 \(\mathbf{A}_G \in \mathbb{R}^{6 \times n_v}\)。对于 Go2+Z1,\(\mathbf{A}_G \in \mathbb{R}^{6 \times 24}\)

CMM 的分块结构

\(\mathbf{A}_G\) 按浮基/腿/臂分块:

\[ \mathbf{A}_G = \begin{pmatrix} \mathbf{A}_{G,b} & \mathbf{A}_{G,\ell} & \mathbf{A}_{G,a} \end{pmatrix} \]
分块 维度 物理含义 数值特征
\(\mathbf{A}_{G,b}\) \(6 \times 6\) 基座速度对质心动量的贡献 总质量主导,近似常数
\(\mathbf{A}_{G,\ell}\) \(6 \times 12\) 腿关节速度对质心动量的贡献 构型依赖,对称步态时部分抵消
\(\mathbf{A}_{G,a}\) \(6 \times 6\) 臂关节速度对质心动量的贡献 构型依赖极强(本节重点)

质心动量分解为三部分贡献:

\[ \mathbf{h}_G = \underbrace{\mathbf{A}_{G,b}\mathbf{v}_b}_{\text{基座贡献}} + \underbrace{\mathbf{A}_{G,\ell}\dot{\mathbf{q}}_\ell}_{\text{腿贡献}} + \underbrace{\mathbf{A}_{G,a}\dot{\mathbf{q}}_a}_{\text{臂贡献}} \]

关键洞察\(\mathbf{A}_{G,a}\dot{\mathbf{q}}_a\) 表示臂运动对系统**总角动量和线动量**的贡献。特别是角动量部分 \(\mathbf{A}_{G,a}^{\text{ang}}\dot{\mathbf{q}}_a\) 意味着臂可以像"反作用轮"一样用于调节系统角动量——这对 push recovery(抗推恢复)和空中姿态调整至关重要。

CCRBA 算法推导

CCRBA(Composite Rigid Body Algorithm for Centroidal Momentum)是计算 CMM 的核心算法。它由 Orin & Goswami (2008) 提出,是 CRBA 的质心空间对偶。

第一步:单个连杆的动量贡献

\(i\) 个连杆在质心处的动量贡献为:

\[ \mathbf{h}_{G,i} = {}^G\mathbf{X}_i^* \, \mathbf{I}_i \, {}^i\mathbf{v}_i \]

其中: - \({}^G\mathbf{X}_i^*\) 是从连杆 \(i\) 坐标系到质心坐标系的**力变换**(force transform,\(6\times6\)) - \(\mathbf{I}_i\) 是连杆 \(i\) 的空间惯量矩阵(\(6 \times 6\),包含质量、质心偏移、转动惯量) - \({}^i\mathbf{v}_i\) 是连杆 \(i\) 在其 body frame 中的空间速度

回顾 足式/50_空间向量与浮动基座动力学:力变换 \(\mathbf{X}^*\) 是运动变换 \(\mathbf{X}\) 的逆转置:\(\mathbf{X}^* = \mathbf{X}^{-T}\)。它将 body \(i\) frame 中的力/动量变换到 frame \(G\)(质心)中。

第二步:连杆速度与广义速度的关系

每个连杆的空间速度可以通过体雅可比(body Jacobian)用广义速度表示:

\[ {}^i\mathbf{v}_i = {}^i\mathbf{J}_i(\mathbf{q}) \, \mathbf{v} \]

其中 \({}^i\mathbf{J}_i \in \mathbb{R}^{6 \times n_v}\) 是连杆 \(i\) 在 body frame 中的雅可比矩阵。

第三步:对所有连杆求和

\[ \mathbf{h}_G = \sum_{i=1}^{N_{\text{body}}} {}^G\mathbf{X}_i^* \, \mathbf{I}_i \, {}^i\mathbf{J}_i \, \mathbf{v} = \underbrace{\left(\sum_{i=1}^{N_{\text{body}}} {}^G\mathbf{X}_i^* \, \mathbf{I}_i \, {}^i\mathbf{J}_i\right)}_{\mathbf{A}_G(\mathbf{q})} \mathbf{v} \]

因此 CMM 为:

\[ \boxed{ \mathbf{A}_G(\mathbf{q}) = \sum_{i=1}^{N_{\text{body}}} {}^G\mathbf{X}_i^* \, \mathbf{I}_i \, {}^i\mathbf{J}_i } \]

计算复杂度:直接按上式计算需要 \(O(N_{\text{body}} \times n_v)\) = \(O(n^2)\)。Pinocchio 的 CCRBA 实现使用递归方法(与 CRBA 共享复合惯量的递推过程),将复杂度降为 \(O(n)\)

与质量矩阵的关系

CCRBA 和 CRBA 本质上计算的是同一组复合惯量,区别仅在于最终的汇聚参考点: - CRBA 汇聚到**关节空间**得到 \(\mathbf{M}(\mathbf{q}) \in \mathbb{R}^{n_v \times n_v}\) - CCRBA 汇聚到**质心**得到 \(\mathbf{A}_G(\mathbf{q}) \in \mathbb{R}^{6 \times n_v}\)

两者的精确关系为:

\[ \mathbf{A}_G = {}^G\mathbf{X}_0^* \, \mathbf{M}_{:6,:} \]

\(\mathbf{A}_G\) 可以从质量矩阵 \(\mathbf{M}\) 的**前 6 行**(基座行)通过一个从基座原点到质心的坐标变换得到。这个关系在数值验证时非常有用。

角动量守恒与操作

对于系统在质心处的动量,牛顿-欧拉方程给出:

\[ \dot{\mathbf{h}}_G = \begin{pmatrix} \dot{\mathbf{k}}_G \\ \dot{\mathbf{l}}_G \end{pmatrix} = \begin{pmatrix} \sum_i (\mathbf{p}_{c,i} - \mathbf{p}_G) \times \boldsymbol{\lambda}_i \\ m\mathbf{g} + \sum_i \boldsymbol{\lambda}_i + \mathbf{f}_{\text{ee,lin}} \end{pmatrix} \]

当唯一外力是重力时,\(\dot{\mathbf{k}}_G = 0\)(重力通过质心,不产生力矩),因此**角动量守恒**:

\[ \mathbf{k}_G = \mathbf{A}_{G}^{\text{ang}}(\mathbf{q})\mathbf{v} = \text{const} \]

这对复合机器人的三种运动相位有不同意义:

运动相位 角动量守恒? 臂的角色 应用
多支撑相(四脚站立) 否(地面反力提供力矩) 操作工具 正常操作
单支撑相(一只脚站立) 近似守恒 "反作用轮" + 操作 push recovery
空中相(跳跃/飞行) 严格守恒 纯"反作用轮" 空中翻转、猫式着陆

"免费的反作用轮":与卫星上用电机驱动的反作用轮不同,机械臂的"反作用轮效应"是**免费**的——臂在执行操作任务的同时,自然地改变了系统角动量。如果控制器能意识到这一点,就可以同时优化操作精度和姿态稳定性。

Pinocchio CMM 计算

import pinocchio as pin
import numpy as np

model = pin.buildModelFromUrdf("go2_z1.urdf", pin.JointModelFreeFlyer())
data = model.createData()

q = pin.neutral(model)
v = np.random.randn(model.nv) * 0.1  # 微小随机速度

# ========== 方法1:ccrba 计算 CMM ==========
pin.ccrba(model, data, q, v)
A_G = data.Ag.copy()  # CMM, (6, nv)
print(f"A_G shape: {A_G.shape}")  # (6, 24)

# ========== 方法2:验证与 M 的关系 ==========
pin.crba(model, data, q)
M = data.M.copy()
M = (M + M.T) / 2

pin.centerOfMass(model, data, q)
com = data.com[0]

# 构造从基座原点到质心的力变换
# X_G0 将基座 frame 的力变换到质心 frame
p_base = np.zeros(3)  # 基座原点在世界系(neutral config 时为原点)
r_G0 = com - p_base   # 从基座到质心的位移
X_G0_star = np.eye(6)
X_G0_star[:3, 3:] = pin.skew(r_G0)  # 力变换的叉积项

A_G_from_M = X_G0_star @ M[:6, :]
print(f"A_G (ccrba):  \n{A_G[:3, :6]}")
print(f"A_G (from M): \n{A_G_from_M[:3, :6]}")
print(f"差异范数: {np.linalg.norm(A_G - A_G_from_M):.6e}")

# ========== 分块提取与分析 ==========
A_G_base = A_G[:, :6]     # 基座列块 (6, 6)
A_G_legs = A_G[:, 6:18]   # 腿列块 (6, 12)
A_G_arm  = A_G[:, 18:]    # 臂列块 (6, 6)

# 各部分对动量的贡献
h_G = A_G @ v
h_base = A_G_base @ v[:6]
h_legs = A_G_legs @ v[6:18]
h_arm  = A_G_arm  @ v[18:]

print(f"\n=== 质心动量分解 ===")
print(f"总质心动量:     {h_G}")
print(f"基座贡献:       {h_base}")
print(f"腿贡献:         {h_legs}")
print(f"臂贡献:         {h_arm}")
print(f"验证(差值范数): {np.linalg.norm(h_G - h_base - h_legs - h_arm):.2e}")

# 分析臂列块中 线动量 vs 角动量 的贡献
# 注意排列顺序:Pinocchio 的 data.Ag / data.hg 采用 (线动量, 角动量) 布局,
# 即 [:3] 是线动量、[3:] 是角动量——这与本章数学定义 h_G = (k_G, l_G)(角动量在前,
# 沿用 Featherstone/Orin 约定)相反。用 data.Ag 切片时必须按 Pinocchio 的物理顺序取,不能照搬数学下标。
A_G_arm_linear  = A_G_arm[:3, :]  # 臂对线动量的贡献 (3, 6)
A_G_arm_angular = A_G_arm[3:, :]  # 臂对角动量的贡献 (3, 6)

print(f"\n=== 臂各关节对角动量的贡献能力 ===")
for j in range(A_G_arm.shape[1]):
    ang_norm = np.linalg.norm(A_G_arm_angular[:, j])
    lin_norm = np.linalg.norm(A_G_arm_linear[:, j])
    print(f"  臂关节{j}: 角动量贡献 = {ang_norm:.4f}, 线动量贡献 = {lin_norm:.4f}")

OCS2 中的 CentroidalModelInfo

OCS2 框架将质心动力学封装在 CentroidalModelInfo 结构体中。理解其数据布局是使用 OCS2 做复合机器人 MPC 的前提:

// OCS2 CentroidalModelInfo 关键字段(简化)
struct CentroidalModelInfo {
    size_t generalizedCoordinatesNum;  // nq = 25 (Go2+Z1)
    size_t actuatedDofNum;             // na = 18 (可驱动关节数)
    size_t numThreeDofContacts;        // 4 (四个足端,各 3D 接触力)
    size_t numSixDofContacts;          // 0 或 1 (末端 6D wrench)

    // 状态向量布局: x = [h_G(6), q(nq)]
    // 输入向量布局: u = [f_contact(3*4), f_ee(6), tau(na)]
    size_t stateDim;   // 6 + 25 = 31
    size_t inputDim;   // 12 + 6 + 18 = 36
};
OCS2 变量 对应物理量 Go2+Z1 值 Go2 值 说明
stateDim \(6 + n_q\) 31 25 质心动量(6) + 广义坐标
inputDim \(3n_c + 6n_{\text{ee}} + n_a\) 36 30 +6 来自末端 wrench
numThreeDofContacts 足端数 4 4 每个足端 3D 力
numSixDofContacts 末端数 1 0 复合机器人新增

易错点:OCS2 的状态定义

OCS2 的 centroidal model 使用 \(\mathbf{x} = (\mathbf{h}_G, \mathbf{q})\) 作为状态,而不是经典的 \((\mathbf{q}, \mathbf{v})\)。好处是质心动量 \(\mathbf{h}_G\) 的时间导数直接由外力给出(牛顿-欧拉方程),无需求解 \(n_v\) 维的关节加速度。动力学约束从 \(n_v\) 维降为 6 维,这正是 centroidal dynamics 的计算优势所在。代价是需要在每步通过 \(\mathbf{v} = \mathbf{A}_G^{\dagger} \mathbf{h}_G\) 恢复广义速度(其中 \(\mathbf{A}_G^{\dagger}\) 是 CMM 的伪逆)。

CMM 的时间导数与动力学约束

质心动量的时间导数等于所有外力的总和(质心处的牛顿-欧拉方程):

\[ \dot{\mathbf{h}}_G = \begin{pmatrix} \dot{\mathbf{k}}_G \\ \dot{\mathbf{l}}_G \end{pmatrix} = \begin{pmatrix} \sum_i (\mathbf{p}_{c,i} - \mathbf{p}_G) \times \boldsymbol{\lambda}_i + (\mathbf{p}_{\text{ee}} - \mathbf{p}_G) \times \mathbf{f}_{\text{ee,lin}} + \boldsymbol{\tau}_{\text{ee}} \\ m\mathbf{g} + \sum_i \boldsymbol{\lambda}_i + \mathbf{f}_{\text{ee,lin}} \end{pmatrix} \]

在 Pinocchio 中,\(\dot{\mathbf{A}}_G\) 通过 dccrba 计算:

# 计算 CMM 的时间导数
pin.dccrba(model, data, q, v)
dAg = data.dAg  # d(A_G)/dt, (6, nv)

# 质心动量变化率(两种等价计算方式):
# 方式1:从 A_G 和加速度
# dh_G = dAg @ v + A_G @ a
#(需要先求出关节加速度 a = aba(...))

# 方式2:直接从外力(更常用于 MPC)
# dh_G = [sum(p_i x lambda_i); m*g + sum(lambda_i) + f_ee]
# 不需要关节加速度,这正是 centroidal dynamics 的优势

练习 72.4

[A] ⭐⭐ 在 Pinocchio 中计算 Go2+Z1 站立构型的 CMM,提取臂列块 \(\mathbf{A}_{G,a}\)。改变臂第一关节(shoulder pitch)从 \(-\pi/2\)\(\pi/2\),绘制角动量子块的 Frobenius 范数 \(\|\mathbf{A}_{G,a}^{\text{ang}}\|_F\) 随关节角的变化曲线。解释:为什么臂伸展时角动量贡献能力更强?

[B] ⭐⭐⭐ 验证 CMM 与质量矩阵前 6 行的关系:计算 \({}^G\mathbf{X}_0^* \mathbf{M}_{:6,:}\) 并与 data.Ag 对比。提示:需要构造正确的力变换矩阵。

[C] ⭐⭐⭐ 模拟一个场景:Go2+Z1 站立,臂从收拢位快速摆动到全伸展位(1 秒内完成)。使用 Pinocchio 逐帧(dt=0.01s)计算角动量 \(\mathbf{k}_G\)。对比四脚站立和单脚站立两种情况:角动量的变化模式有何本质不同?用角动量守恒解释。


72.5 零空间投影与冗余度利用 ⭐⭐⭐

动机:18+ DOF 的冗余度从何而来?

Go2+Z1 系统有 24 个广义速度维度(\(n_v = 24\))。但控制任务通常只占用有限维度:

任务 维度 说明
末端 6D 位姿跟踪 6 \(\mathbf{J}_{\text{ee}} \in \mathbb{R}^{6 \times 24}\)
四足接触约束(4 足 x 3D) 12 保持脚不滑
浮基"虚约束" 6 浮基不可驱动,通过接触力间接控制

总约束维度 = 6 + 12 = 18(浮基通过接触力实现,不额外占维度)。剩余的 **\(24 - 18 = 6\) 维冗余度**可自由利用。对于人形(\(n_v = 35\)),冗余度更大。

冗余度是复合机器人相比纯四足的核心优势之一。问题是:如何系统性地利用这些冗余度?

零空间投影原理

数学定义

给定一个主任务雅可比 \(\mathbf{J}_1 \in \mathbb{R}^{m \times n}\)\(m < n\)),其零空间投影矩阵定义为:

\[ \mathbf{N}_1 = \mathbf{I}_n - \mathbf{J}_1^{\dagger}\mathbf{J}_1 \]

其中 \(\mathbf{J}_1^{\dagger}\)\(\mathbf{J}_1\) 的伪逆。\(\mathbf{N}_1\) 将任意速度投影到 \(\mathbf{J}_1\) 的零空间——即**不影响主任务**的速度子空间。

物理直觉

想象你在抓住一个门把手(6D 约束),同时还想调整肘部位置。由于系统有冗余度,你可以在保持手不动的前提下移动肘部。\(\mathbf{N}_1\) 精确描述了这种"手不动、肘自由"的运动空间。

层级任务优先级

对于复合机器人,典型的任务优先级栈为(从高到低):

优先级 任务 维度 原因
P0(硬约束) 接触脚不滑 12 违反则摔倒
P1 基座稳定性(ZMP/CoM) 3-6 倾覆则任务失败
P2 末端位姿跟踪 6 操作任务目标
P3 关节居中/限位避免 \(n_v\) 防止关节到极限
P4 姿态舒适度/可操作度 \(n_v\) 优化性能指标

任务间通过零空间级联保证优先级:

\[ \dot{\mathbf{q}} = \mathbf{J}_1^{\dagger}\dot{\mathbf{x}}_1 + \mathbf{N}_1 \left(\mathbf{J}_2^{\dagger}\dot{\mathbf{x}}_2 + \mathbf{N}_2 \left(\mathbf{J}_3^{\dagger}\dot{\mathbf{x}}_3 + \cdots\right)\right) \]

其中 \(\mathbf{N}_2 = \mathbf{I} - (\mathbf{J}_2\mathbf{N}_1)^{\dagger}(\mathbf{J}_2\mathbf{N}_1)\) 是在 \(\mathbf{J}_1\) 零空间内 \(\mathbf{J}_2\) 的再次投影。

阻尼伪逆与数值稳定性

当雅可比接近奇异(行列式趋近零)时,标准伪逆 \(\mathbf{J}^{\dagger} = \mathbf{J}^T(\mathbf{J}\mathbf{J}^T)^{-1}\) 会发散。阻尼最小二乘(Damped Least Squares, DLS)引入正则化:

\[ \mathbf{J}^{\dagger}_{\text{DLS}} = \mathbf{J}^T(\mathbf{J}\mathbf{J}^T + \lambda^2\mathbf{I})^{-1} \]

其中 \(\lambda\) 是阻尼因子。工程中通常使用**自适应阻尼**,根据雅可比的条件数动态调整:

\[ \lambda^2 = \begin{cases} 0 & \text{if } \sigma_{\min} > \epsilon \\ \left(1 - (\sigma_{\min}/\epsilon)^2\right) \lambda_{\max}^2 & \text{otherwise} \end{cases} \]

其中 \(\sigma_{\min}\) 是雅可比的最小奇异值,\(\epsilon\) 是奇异性阈值(典型值 0.01-0.05)。

HQP vs 加权 QP

两种主流实现方式的对比:

特性 HQP(层级 QP) 加权 QP
优先级保证 严格:高优先级任务**绝不**被低优先级影响 近似:通过权重差异体现优先级
数学形式 逐级求解 QP + 零空间投影 单一 QP,不同任务用不同权重
求解时间 较慢(多次 QP) 快(单次 QP)
调参难度 低(自然优先级顺序) 高(权重比需要仔细调)
代表框架 TSID, Hierarchical WBC OCS2 WBC, wbc_toolbox

实践建议:如果任务之间的优先级是**绝对的**(如"宁可末端跟踪偏差也不能摔倒"),用 HQP。如果任务之间需要**柔性折中**(如"末端精度和能耗的 trade-off"),用加权 QP。qm_control 使用的是加权 QP 方案。

Pinocchio 零空间投影实现

import pinocchio as pin
import numpy as np

model = pin.buildModelFromUrdf("go2_z1.urdf", pin.JointModelFreeFlyer())
data = model.createData()

q = pin.neutral(model)
pin.computeJointJacobians(model, data, q)
pin.updateFramePlacements(model, data)

# === 获取末端雅可比 ===
ee_id = model.getFrameId("gripper_link")
J_ee = pin.getFrameJacobian(
    model, data, ee_id, pin.LOCAL_WORLD_ALIGNED
)  # (6, 24)

# === 阻尼伪逆 ===
def damped_pinv(J, damping=1e-3):
    """
    阻尼最小二乘伪逆。
    J: (m, n) 雅可比
    返回: (n, m) 阻尼伪逆
    """
    m = J.shape[0]
    return J.T @ np.linalg.inv(J @ J.T + damping**2 * np.eye(m))

# === 自适应阻尼伪逆 ===
def adaptive_damped_pinv(J, eps=0.05, lambda_max=0.1):
    """
    自适应阻尼:根据最小奇异值动态调整阻尼系数。
    """
    U, S, Vt = np.linalg.svd(J, full_matrices=False)
    sigma_min = S[-1]

    if sigma_min > eps:
        lam2 = 0.0
    else:
        lam2 = (1 - (sigma_min / eps)**2) * lambda_max**2

    m = J.shape[0]
    return J.T @ np.linalg.inv(J @ J.T + lam2 * np.eye(m))

# === 零空间投影 ===
J_ee_pinv = adaptive_damped_pinv(J_ee)
N_ee = np.eye(model.nv) - J_ee_pinv @ J_ee  # (24, 24)

print(f"零空间维度: {np.linalg.matrix_rank(N_ee, tol=1e-6)}")
# 期望: 24 - 6 = 18(但实际会略小,取决于构型和数值精度)

# === 层级任务:P1=末端跟踪, P2=关节居中 ===
# 期望末端速度
dx_ee_desired = np.array([0.1, 0, 0, 0, 0, 0])  # x 方向 0.1 m/s

# 关节居中速度(向 neutral 位置回归)
q_neutral = pin.neutral(model)
dq_centering = -0.5 * (q[7:] - q_neutral[7:])  # 浮基外的关节
dq_centering_full = np.zeros(model.nv)
dq_centering_full[6:] = dq_centering

# 层级合成
dq = J_ee_pinv @ dx_ee_desired + N_ee @ dq_centering_full

# 验证:P1 任务完美满足
dx_ee_actual = J_ee @ dq
print(f"末端速度误差: {np.linalg.norm(dx_ee_actual - dx_ee_desired):.6e}")

# P2 任务在零空间内尽力满足
dq_centering_actual = N_ee @ dq_centering_full
print(f"关节居中速度范数: {np.linalg.norm(dq_centering_actual):.4f}")

TSID 连接

TSID(Task-Space Inverse Dynamics)是 Pinocchio 生态中标准的层级 WBC 框架。对于复合机器人,TSID 的任务配置为:

# TSID 风格的任务配置(概念代码)
import tsid

# 创建 TSID 求解器
solver = tsid.SolverHQP("solver")
robot = tsid.RobotWrapper(model, data)

# === P0: 接触约束(硬约束) ===
for foot_name in ["FL_foot", "FR_foot", "RL_foot", "RR_foot"]:
    contact = tsid.Contact6d(foot_name, robot, foot_name)
    contact.setFrictionCoefficient(0.7)
    contact.setMinNormalForce(10)    # 最小法向力 10N
    contact.setMaxNormalForce(500)   # 最大法向力 500N
    solver.addConstraint(contact)

# === P1: CoM 位置跟踪 ===
com_task = tsid.TaskComEquality("com", robot)
com_task.setKp(100 * np.ones(3))
com_task.setKd(20 * np.ones(3))
solver.addTask(com_task, weight=1000, priority=0)

# === P2: 末端位姿跟踪 ===
ee_task = tsid.TaskSE3Equality("ee", robot, "gripper_link")
ee_task.setKp(200 * np.ones(6))
ee_task.setKd(30 * np.ones(6))
solver.addTask(ee_task, weight=100, priority=1)

# === P3: 关节正则化 ===
posture_task = tsid.TaskJointPosture("posture", robot)
posture_task.setKp(10 * np.ones(model.nv - 6))
posture_task.setKd(5 * np.ones(model.nv - 6))
solver.addTask(posture_task, weight=1, priority=2)

易错点:接触约束的维度

对于 3D 点接触(足端),约束维度为 3(速度为零)。对于 6D 面接触,约束维度为 6(包括不转动)。混淆两者会导致 QP 的约束矩阵维度错误,求解器直接报错或给出荒谬结果。Go2 的足端通常建模为 3D 点接触。

练习 72.5

[A] ⭐⭐ 计算 Go2+Z1 在站立构型下末端雅可比的奇异值分布。找到最小奇异值并判断是否接近奇异。改变臂构型,找到使最小奇异值最大的构型(最佳可操作度构型)。

[B] ⭐⭐⭐ 实现完整的两级层级逆运动学:P1 = 末端跟踪圆形轨迹,P2 = 关节居中。绘制末端轨迹跟踪误差和关节偏离中位角的时间曲线。验证 P1 严格满足而 P2 尽力满足。

[C] ⭐⭐⭐ 对比阻尼伪逆的三种策略(固定 \(\lambda\), 自适应 \(\lambda\), SVD 截断)在奇异构型附近的行为。哪种在实际 WBC 中最可靠?


72.6 浮基+臂联合状态估计 ⭐⭐

动机:统一模型需要统一估计

前面几节假设 \(\mathbf{q}\)\(\mathbf{v}\) 是精确已知的。但实际机器人上,浮动基座的状态(位姿和速度)必须通过传感器融合估计。复合机器人的估计问题比纯四足更复杂,因为**臂的运动会影响基座状态**。

传感器配置

传感器 测量量 频率 精度 安装位置
IMU 加速度 + 角速度 1 kHz 角度 drift: ~1 deg/min 基座(躯干)
腿关节编码器 关节角 \(\mathbf{q}_\ell\) 1 kHz ~0.01 deg 各腿关节
臂关节编码器 关节角 \(\mathbf{q}_a\) 1 kHz ~0.01 deg 各臂关节
足端力传感器 接触力 \(\boldsymbol{\lambda}_i\) 500 Hz ~1 N 足端(如有)
臂力/力矩传感器 \(\mathbf{f}_{\text{ee}}\) 500 Hz ~0.5 N 末端法兰(如有)

EKF 状态估计框架

状态向量

对于复合机器人的 EKF,状态向量扩展为:

\[ \mathbf{x}_{\text{est}} = \begin{pmatrix} \mathbf{p}_b \\ \mathbf{R}_b \\ \mathbf{v}_b \\ \mathbf{b}_a \\ \mathbf{b}_\omega \\ \mathbf{p}_{c,1} \\ \vdots \\ \mathbf{p}_{c,4} \end{pmatrix} \in \mathbb{R}^{3+4+3+3+3+12} = \mathbb{R}^{28} \quad \text{(四元数状态; 误差状态为 } \mathbb{R}^{27}\text{)} \]

其中 \(\mathbf{b}_a, \mathbf{b}_\omega\) 是 IMU 偏置,\(\mathbf{p}_{c,i}\) 是足端在世界系中的位置(用于腿式里程计)。

注意:臂关节角 \(\mathbf{q}_a\) 直接由编码器测量,不需要估计。但臂的存在影响估计的**观测模型**——通过臂的前向运动学可以提供额外的约束(如果末端有已知接触)。

预测模型(Process Model)

IMU 驱动的预测方程:

\[ \hat{\mathbf{p}}_b^{-} = \hat{\mathbf{p}}_b + \hat{\mathbf{v}}_b \Delta t + \frac{1}{2}(\hat{\mathbf{R}}_b(\mathbf{a}_m - \hat{\mathbf{b}}_a) + \mathbf{g})\Delta t^2 \]
\[ \hat{\mathbf{v}}_b^{-} = \hat{\mathbf{v}}_b + (\hat{\mathbf{R}}_b(\mathbf{a}_m - \hat{\mathbf{b}}_a) + \mathbf{g})\Delta t \]
\[ \hat{\mathbf{R}}_b^{-} = \hat{\mathbf{R}}_b \cdot \text{Exp}((\boldsymbol{\omega}_m - \hat{\mathbf{b}}_\omega)\Delta t) \]

其中 \(\mathbf{a}_m, \boldsymbol{\omega}_m\) 是 IMU 原始测量,\(\text{Exp}\)\(SO(3)\) 上的指数映射。

观测模型(Observation Model)

观测1:腿式里程计(Leg Kinematics)

当第 \(i\) 只脚与地面接触时,足端位置可由前向运动学计算:

\[ \mathbf{z}_{\text{leg},i} = \mathbf{p}_b + \mathbf{R}_b \cdot \text{FK}_i(\mathbf{q}_\ell) \approx \hat{\mathbf{p}}_{c,i} \]

这提供了 3D 位置约束。同时,足端零速度假设给出速度观测:

\[ \mathbf{0} = \mathbf{v}_b + \boldsymbol{\omega}_b \times \mathbf{r}_i + \mathbf{J}_i(\mathbf{q}_\ell)\dot{\mathbf{q}}_\ell \]

观测2:臂末端约束(新增)

当臂末端与已知物体接触时,可以提供**额外的位置约束**:

\[ \mathbf{z}_{\text{arm}} = \mathbf{p}_b + \mathbf{R}_b \cdot \text{FK}_{\text{arm}}(\mathbf{q}_a) \approx \mathbf{p}_{\text{object}}^{\text{known}} \]

这在抓取固定物体(如门把手、桌面)时特别有用,相当于给估计器增加了一个"锚点"。

观测类型 维度 可用条件 对估计的贡献
足端运动学 3/足 x 4 脚着地 位置 + 速度主要来源
足端零速度 3/足 x \(n_c\) 脚着地且不滑 速度约束
臂末端锚点 3 末端已知接触 位置额外约束(可选)
高度(地形先验) 1 平坦地面假设 z 方向校正

臂运动对估计的影响

纯四足的状态估计相对成熟,但加臂后出现新问题:

  1. IMU 耦合:臂快速运动产生的惯性力通过基座传导到 IMU,增加加速度噪声
  2. 质心偏移:臂构型变化导致系统质心位置变化,如果估计器假设固定质心,会产生偏差
  3. 接触模式复杂化:臂末端的接触/非接触状态切换需要额外的接触检测逻辑
# 臂运动对 IMU 测量的影响估算
import pinocchio as pin
import numpy as np

model = pin.buildModelFromUrdf("go2_z1.urdf", pin.JointModelFreeFlyer())
data = model.createData()

q = pin.neutral(model)
v = np.zeros(model.nv)
a = np.zeros(model.nv)

# 场景1:臂静止
a_arm_static = np.zeros(6)
a[18:] = a_arm_static
tau_static = pin.rnea(model, data, q, v, a)
imu_force_static = tau_static[:6]  # 基座行 = IMU 处的等效力

# 场景2:臂快速加速(关节1加速 10 rad/s^2)
a_arm_fast = np.zeros(6)
a_arm_fast[0] = 10.0  # shoulder pitch 加速
a[18:] = a_arm_fast
tau_fast = pin.rnea(model, data, q, v, a)
imu_force_fast = tau_fast[:6]

# IMU 感受到的额外力
delta_imu = imu_force_fast - imu_force_static
print(f"臂加速导致的 IMU 额外力/力矩:")
print(f"  力: {delta_imu[:3]} N")
print(f"  力矩: {delta_imu[3:]} N*m")
print(f"  等效额外加速度: {delta_imu[:3] / data.mass[0]} m/s^2")

InEKF 扩展

Invariant Extended Kalman Filter(InEKF)是一种利用 Lie 群结构的 EKF 变体,具有更好的收敛性和一致性。对于浮基机器人:

  • 状态 Lie 群\(SE_2(3)\)(扩展特殊欧几里得群),包含旋转、位置、速度
  • 优势:线性化误差与状态无关(右不变误差),保证估计的全局一致性
  • 实现:INEKF-based estimator 是 MIT Cheetah 和 Digit 人形机器人的标准方案

对于复合机器人,InEKF 的状态群扩展为:

\[ \mathbf{X} = \begin{pmatrix} \mathbf{R} & \mathbf{v} & \mathbf{p} & \mathbf{p}_{c,1} & \cdots & \mathbf{p}_{c,4} \\ \mathbf{0} & 1 & 0 & 0 & \cdots & 0 \\ \vdots & & \ddots & & & \vdots \\ \mathbf{0} & 0 & 0 & 0 & \cdots & 1 \end{pmatrix} \in SE_{2+N_c}(3) \]

易错点:接触检测

腿式里程计的前提是正确判断哪只脚着地。错误的接触检测会注入虚假观测,导致估计发散。对于复合机器人,臂末端的接触检测同样重要——特别是在力控操作时,末端的接触/脱离瞬间必须被准确检测。常用方法包括:力阈值检测、GRF 估计、基于概率的接触状态机。

练习 72.6

[A] ⭐⭐ 列出 Go2+Z1 系统中所有可用的传感器及其测量方程。比较腿式里程计和臂末端锚点在 EKF 中的数学形式。

[B] ⭐⭐ 定量分析臂运动对 IMU 测量的影响:在不同臂加速度(0, 5, 10, 20 rad/s^2)下,计算 IMU 感受到的等效额外加速度。这个量级与 IMU 的加速度计噪声(典型值 0.01 m/s^2)相比如何?

[C] ⭐⭐⭐ 设计一个简化的 EKF:状态 = [p, v, R],预测 = IMU 积分,观测 = 腿运动学。在 Python 中实现并用模拟数据测试。然后增加臂末端锚点观测,验证估计精度的提升。


72.7 项目精读:qm_control 与 OCS2 mobile_manipulator ⭐⭐

动机:从理论到工程

前面六节建立了复合机器人统一动力学的完整理论框架。本节通过精读两个开源项目,理解这些理论如何落地为可运行的代码。

qm_control 框架概览

qm_control 是基于 OCS2 框架的四足+臂(quadruped-manipulator)控制器,支持 Go2+Z1 等平台。

代码结构

qm_control/
├── qm_description/           # URDF + 机器人描述
│   ├── urdf/
│   │   ├── go2_description/  # Go2 基座 URDF
│   │   ├── z1_description/   # Z1 臂 URDF
│   │   └── go2_z1.urdf       # 拼接后的统一 URDF
│   └── config/
│       └── task.info          # OCS2 任务配置文件
├── qm_controllers/            # 控制器实现
│   ├── src/
│   │   ├── MPC/               # MPC 求解器
│   │   ├── WBC/               # WBC(加权 QP)
│   │   └── StateEstimate/     # 状态估计
│   └── include/
├── qm_gazebo/                 # Gazebo 仿真
└── qm_interface/              # ROS 接口

URDF 拼接关键细节

<!-- go2_z1.urdf 中臂的连接方式 -->
<!-- 臂通过 fixed joint 连接到基座的 trunk_link -->
<joint name="arm_base_joint" type="fixed">
    <parent link="trunk_link"/>
    <child link="arm_link0"/>
    <origin xyz="0.285 0.0 0.05" rpy="0 0 0"/>
    <!-- xyz: 臂安装在基座前方 28.5cm, 上方 5cm -->
</joint>

<!-- 关键:这个 fixed joint 不增加 DOF -->
<!-- 但它的位姿偏移对 M_ba 耦合项有重大影响 -->
<!-- 安装位置越偏离质心,臂运动对基座的力矩越大 -->
qm_control 配置项 对应理论
model.nq 25 7(浮基) + 12(腿) + 6(臂)
model.nv 24 6 + 12 + 6
MPC 频率 100 Hz OCS2 SLQ solver
WBC 频率 500 Hz 加权 QP
MPC 预测时域 1.0 s 步态周期覆盖
摩擦系数 0.7 摩擦锥约束

MPC 与 WBC 的分工

                    ┌──────────────────────┐
  参考轨迹 ───────> │   MPC (100 Hz)       │
  (CoM, 末端位姿)   │   OCS2 SLQ solver    │
                    │   centroidal model   │
                    └──────────┬───────────┘
                               │ 最优接触力 λ*
                               │ 最优状态轨迹 x*
                    ┌──────────▼───────────┐
  传感器反馈 ──────> │   WBC (500 Hz)       │
  (q, v, 接触状态)  │   加权 QP             │
                    │   full dynamics       │
                    └──────────┬───────────┘
                               │ 关节力矩 τ
                         关节电机驱动

MPC 在简化的 centroidal model 上规划最优力/状态轨迹(慢循环),WBC 在全维度动力学模型上跟踪 MPC 轨迹并输出关节力矩(快循环)。

OCS2 mobile_manipulator 模板

OCS2 框架提供了 ocs2_mobile_manipulator 作为移动操作的通用模板。它与 qm_control 的关键区别:

特性 qm_control OCS2 mobile_manipulator
底盘类型 四足(浮基) 轮式(SE(2))或浮基
动力学模型 centroidal + full 运动学或 centroidal
接触处理 四足步态切换 简化(底盘始终接地)
末端任务 SE(3) 位姿跟踪 SE(3) 位姿跟踪
典型 DOF 24 9-12

OCS2 模板扩展步骤

ocs2_mobile_manipulator 扩展到四足+臂:

// 步骤1:定义 CentroidalModelInfo
CentroidalModelInfo info;
info.generalizedCoordinatesNum = 25;     // nq
info.actuatedDofNum = 18;                // 12 腿 + 6 臂
info.numThreeDofContacts = 4;            // 四个足端
info.numSixDofContacts = 0;              // 或 1(如果启用末端力控)

// 步骤2:构建 Pinocchio 接口
PinocchioInterface pinocchioInterface(
    buildPinocchioModel("go2_z1.urdf", info)
);

// 步骤3:定义代价函数
// 质心跟踪代价 + 末端跟踪代价 + 力正则化
auto costPtr = std::make_unique<CentroidalModelCost>(
    pinocchioInterface, info, 
    comTrackingWeight,    // 质心跟踪权重
    eeTrackingWeight,     // 末端跟踪权重
    forceRegWeight        // 力正则化权重
);

// 步骤4:定义约束
// 摩擦锥 + 关节限位 + 接触序列
auto constraintPtr = std::make_unique<FrictionConeConstraint>(
    info, frictionCoefficient
);

qm_control 的关键创新

  1. 臂负载前馈:将臂末端力估计作为已知扰动注入 MPC,而非作为未知外力由 WBC 被动补偿
  2. 步态-操作协调:MPC 的步态周期可根据操作任务动态调整(如搬重物时降低步频、增大支撑)
  3. 紧凑状态定义:使用 \((\mathbf{h}_G, \mathbf{q})\) 状态而非 \((\mathbf{q}, \mathbf{v})\),将 MPC 动力学约束从 24 维降为 6 维

练习 72.7

[A] ⭐⭐ 克隆 qm_control 仓库,阅读 qm_description/urdf/go2_z1.urdf 的关节拓扑。画出关节树图,标注每个关节的类型和自由度。

[B] ⭐⭐ 阅读 qm_control 的 task.info 配置文件,理解 MPC 代价函数中各项权重的物理含义。特别关注质心跟踪权重和末端跟踪权重的数量级差异——这体现了什么优先级?

[C] ⭐⭐⭐ 比较 qm_control 的 WBC(加权 QP)和 足式/60_QP_NLP建模 TSID 的 HQP 在任务优先级保证上的差异。在 Go2+Z1 上,什么情况下加权 QP 会导致低优先级任务干扰高优先级任务?给出具体数值场景。


72.8 从 复合/20_浮动基座臂统一动力学 到下游章节桥接 ⭐

本章在课程体系中的位置

复合/20_浮动基座臂统一动力学 建立的统一动力学框架是后续所有复合机器人章节的基石。下面梳理每个下游章节如何使用本章内容:

复合/20_浮动基座臂统一动力学 统一动力学
├── 复合/30_多模态MPC 多模态 MPC
│   ├── 使用:centroidal dynamics + CMM
│   ├── 扩展:步态切换的混合动力学
│   └── 关键接口:CentroidalModelInfo → MPC solver
├── 复合/40_RL全身控制基础 RL 全身控制
│   ├── 使用:统一方程作为仿真器核心
│   ├── 扩展:观测空间包含臂状态 + 基座状态
│   └── 关键接口:M(q), h(q,v) → reward shaping
├── 复合/120_底盘臂联合规划 移动操作
│   ├── 使用:底盘+臂统一运动学(简化版 72.2)
│   ├── 扩展:导航-操作协调
│   └── 关键接口:J_ee, 零空间投影
├── 复合/160_四足臂动力学概览 四足臂
│   ├── 使用:完整的 72.2-72.5(核心用户)
│   ├── 扩展:qm_control 实战
│   └── 关键接口:全部
└── 复合/220_经典人形全身控制 人形
    ├── 使用:CMM 扩展到 30+ DOF
    ├── 扩展:双臂协调、上半身平衡
    └── 关键接口:A_G 分块、零空间层级

关键概念桥接表

复合/20_浮动基座臂统一动力学 概念 复合/30_多模态MPC 使用方式 复合/40_RL全身控制基础 使用方式 复合/160_四足臂动力学概览 使用方式
\(\mathbf{M}(\mathbf{q})\) 3x3 分块 centroidal 近似 仿真动力学 完整模型 WBC
三类力方程 MPC 约束 reward shaping QP 约束
ZMP 偏移 CoM 参考修正 稳定性奖励 前馈补偿
CMM \(\mathbf{A}_G\) MPC 状态方程 角动量观测 动量跟踪
零空间 \(\mathbf{N}\) 冗余度优化
状态估计 EKF 提供 MPC 初始状态 仿真中精确 EKF 实车

学习路径建议

根据你的研究方向,选择不同的深度:

路径 重点章节 复合/20_浮动基座臂统一动力学 必读部分 可略读部分
A: 四足+臂控制 复合/30_多模态MPC, 复合/160_四足臂动力学概览 全部
B: 人形全身控制 复合/30_多模态MPC, 复合/220_经典人形全身控制 72.2, 72.4, 72.5 72.3(数值部分), 72.7
C: 移动操作 复合/120_底盘臂联合规划 72.1, 72.2, 72.5 72.3, 72.4, 72.6
D: RL 端到端 复合/40_RL全身控制基础 72.1, 72.2 72.5, 72.6, 72.7

72.9 前沿:GR00T-WBC 与人形全身控制 ⭐⭐⭐

NVIDIA GR00T-WBC (2025-2026)

NVIDIA 在 GR00T 项目中提出了面向人形机器人的统一全身控制框架 GR00T-WBC,将本章的理论推向 30+ DOF 的极限。

核心挑战:维度的质变

从 Go2+Z1 的 \(n_v = 24\) 到人形的 \(n_v = 30-55\),不仅是量变,更是质变:

挑战 Go2+Z1 (24 DOF) 人形 (35+ DOF) 质变原因
QP 求解时间 ~0.5 ms ~5 ms \(O(n^3)\) 增长
任务层级数 3-4 5-7 双臂+头+腰
接触模式 \(2^4 = 16\) \(2^2 \times 3^2 = 36+\) 手掌多点接触
自碰撞约束 ~10 对 ~50 对 双臂交叉操作
CMM 列数 24 35+ \(\mathbf{A}_{G}\) 更稀疏

GR00T-WBC 的技术方案

GR00T-WBC 的核心思路是**学习+优化混合**:

  1. 离线学习阶段:用 RL 在 IsaacLab 中训练一个"动作先验"(action prior),输出粗糙的全身动作
  2. 在线优化阶段:WBC 将 RL 输出作为参考,在全维度动力学约束下精修为物理可行的关节力矩
  3. 关键创新:RL 负责"策略"(做什么),WBC 负责"物理"(怎么做到且不摔倒)
             RL Policy (400 Hz)
             ┌──────────────────┐
观测 ──────> │  神经网络推理     │──> 关节位置参考 q_ref
(proprioception)│  (action prior) │
             └──────────────────┘
             WBC Layer (1000 Hz)          ▼
             ┌──────────────────────────────────┐
q_ref ──────>│  统一动力学方程 (本章 72.2)       │
接触状态 ───>│  CMM 质心动量约束 (本章 72.4)     │──> τ (关节力矩)
力限制 ─────>│  零空间优先级 (本章 72.5)         │
             │  摩擦锥 + ZMP (本章 72.3)         │
             └──────────────────────────────────┘

与本章的技术连接

GR00T-WBC 组件 对应 复合/20_浮动基座臂统一动力学 知识 扩展点
统一动力学模型 72.2 三类力方程 扩展到 30+ DOF,双臂双末端
质心动量约束 72.4 CMM \(\mathbf{A}_G \in \mathbb{R}^{6 \times 35+}\),更多臂列块
稳定性约束 72.3 ZMP/摩擦锥 双足支撑,支撑多边形更小
冗余度利用 72.5 零空间 更深的层级栈,更多冗余
状态估计 72.6 EKF 双足的估计更难(接触面积小)

30+ DOF 系统的计算优化

面对人形的高维度,几个关键优化技术:

技术 原理 加速比 适用场景
稀疏 QP 求解器 利用 \(\mathbf{M}\)\(\mathbf{J}\) 的稀疏结构 2-5x HQP
GPU 并行 批量计算多个构型的 CRBA/RNEA 10-100x MPC rollout
近似惯量 冻结远端连杆惯量为常数 1.5-2x 实时 WBC
模型降阶 centroidal dynamics 代替 full dynamics \(O(n) \to O(1)\) MPC
# GPU 加速示例:使用 Pinocchio 3.x 的 CasADi 后端
import pinocchio.casadi as pin_casadi
import casadi

# CasADi 符号变量
q_sym = casadi.SX.sym("q", model.nq)
v_sym = casadi.SX.sym("v", model.nv)

# 符号化 CRBA(可自动微分 + GPU 编译)
import pinocchio.casadi as cpin
cmodel = cpin.Model(model)
cdata = cmodel.createData()
cpin.crba(cmodel, cdata, q_sym)
M_sym = cdata.M  # CasADi 符号表达式

# 编译为 GPU 可执行函数
M_func = casadi.Function("M", [q_sym], [M_sym])
# 后续可用于 MPC 的 CasADi NLP 求解器

人形全身控制的开放问题

问题 当前状态 关键难点 相关章节
实时性 500 Hz 勉强可行 QP 维度过大 复合/30_多模态MPC
双臂协调 学术阶段 14+ DOF 臂的零空间 复合/250_力敏感人形LocoMani
灵巧手操作 早期阶段 20+ DOF 手指 复合/260_VLA_Foundation_Model
全身力控 部分实现 力传感器+柔顺控制 复合/250_力敏感人形LocoMani
仿真到实物 主要瓶颈 接触模型差距 复合/240_ASAP_SimToReal, 复合/270_SimToReal统一方法论

练习 72.9

[A] ⭐⭐ 计算 H1 人形(\(n_v = 35\))的 \(\mathbf{A}_G\) 维度,并按双臂分块。与 Go2+Z1 的 \(\mathbf{A}_G\) 分块结构对比,双臂如何增加角动量控制的自由度?

[B] ⭐⭐⭐ 分析 GR00T-WBC 的 RL+WBC 混合架构:为什么不能纯用 RL?为什么不能纯用 WBC?从实时性、安全性、泛化性三个维度论证混合架构的必要性。

[C] ⭐⭐⭐ 阅读 GR00T 相关论文或技术报告,总结其 WBC 层如何处理双足的 ZMP 约束。与本章 72.3 的四足 ZMP 分析对比,双足的关键差异在哪里?


本章小结

核心公式速查

编号 公式 名称 首次出现
E1 \(\mathbf{M}\dot{\mathbf{v}} + \mathbf{h} = \mathbf{S}^T\boldsymbol{\tau} + \sum \mathbf{J}_{c,i}^T\boldsymbol{\lambda}_i + \mathbf{J}_{\text{ee}}^T\mathbf{f}_{\text{ee}}\) 统一动力学方程 72.2
E2 \(\mathbf{M} = \begin{pmatrix} M_{bb} & M_{b\ell} & M_{ba} \\ \cdot & M_{\ell\ell} & M_{\ell a} \\ \cdot & \cdot & M_{aa} \end{pmatrix}\) 质量矩阵 3x3 分块 72.2
E3 \(\Delta x_{\text{ZMP}} = \frac{\tau_{\text{arm},y}}{m g}\) 臂引起的 ZMP 偏移 72.3
E4 \(\mathbf{h}_G = \mathbf{A}_G(\mathbf{q})\mathbf{v}\) CMM 定义 72.4
E5 \(\mathbf{A}_G = \sum_i {}^G\mathbf{X}_i^* \mathbf{I}_i {}^i\mathbf{J}_i\) CCRBA 公式 72.4
E6 \(\mathbf{N} = \mathbf{I} - \mathbf{J}^{\dagger}\mathbf{J}\) 零空间投影 72.5

知识点掌握矩阵

知识点 理解级别 代码级别 数学级别 检验方法
URDF 加载与维度 能说出 nq/nv Pinocchio 加载 练习 72.1A
统一方程三类力 物理直觉 RNEA + Jacobian Lagrange 推导 练习 72.2B
M(q) 分块结构 各块物理含义 提取并可视化 分块展开 练习 72.2A
ZMP 偏移 工程影响 compute_zmp_shift 完整推导 练习 72.3B
CMM 定义与计算 h_G = A_G v ccrba + 分块 CCRBA 推导 练习 72.4B
零空间投影 优先级直觉 层级 IK 数值稳定性 练习 72.5B
状态估计 传感器融合概念 EKF 框架 观测方程 练习 72.6C
开源项目 架构理解 配置文件阅读 练习 72.7A

Pinocchio API 速查

功能 API 输入 输出 复杂度
加载 URDF buildModelFromUrdf(path, rootJoint) URDF 路径 Model
质量矩阵 crba(model, data, q) q data.M(上三角) \(O(n)\)
非线性力 rnea(model, data, q, v, 0) q, v \(\mathbf{h}\) \(O(n)\)
正向动力学 aba(model, data, q, v, tau) q, v, tau \(\dot{\mathbf{v}}\) \(O(n)\)
CMM ccrba(model, data, q, v) q, v data.Ag \(O(n)\)
CMM 导数 dccrba(model, data, q, v) q, v data.dAg \(O(n)\)
质心 centerOfMass(model, data, q) q data.com[0] \(O(n)\)
雅可比 computeJointJacobians(model, data, q) q 内部缓存 \(O(n)\)
Frame 雅可比 getFrameJacobian(model, data, fid, ref) frame id, 参考系 \(\mathbf{J}\) \(O(n)\)

预计学习时间

内容 时间 方法
72.0-72.1 维度与加载 3h 阅读 + Pinocchio 实操
72.2 统一方程推导 8h 手推 + 代码验证
72.3 ZMP 偏移 5h 数值计算 + 可视化
72.4 CMM 推导与计算 8h 手推 CCRBA + 代码
72.5 零空间 5h 代码实现 + TSID
72.6 状态估计 4h 阅读 + 概念理解
72.7 项目精读 4h 源码阅读
72.8-72.9 桥接与前沿 3h 阅读
总计 ~40h 约 2 周

延伸阅读与参考文献

编号 论文 核心贡献 与本章关系
[1] Orin & Goswami, 2008, "Centroidal momentum matrix" CCRBA 算法原始论文 72.4
[2] Khatib, 1987, "Operational space formulation" 操作空间动力学 72.5
[3] Sentis & Khatib, 2005, "Whole-body control" 层级零空间投影 72.5
[4] Mistry et al., 2010, "Inverse dynamics with contacts" 浮基 WBC 里程碑 72.2
[5] Righetti et al., 2011, "Unified view" 投影消去接触力 72.2
[6] Featherstone, 2008, "Rigid Body Dynamics Algorithms" 空间向量、CRBA、ABA 全章
[7] Carpentier et al., 2019, "Pinocchio" 高效动力学库 全章
[8] qm_control, 2023, GitHub 四足+臂 MPC+WBC 72.7
[9] OCS2, ETH Zurich, GitHub 最优控制框架 72.4, 72.7
[10] Bloesch et al., 2013, "State estimation for legged robots" 腿式里程计 + EKF 72.6

下一章预告:复合/30_多模态MPC 多模态 MPC——将本章的统一动力学嵌入 OCS2 的 SLQ/SQP 求解器,实现行走、操作、过渡等多模态的实时最优控制。核心问题:如何在一个 MPC 中同时处理离散的接触模式切换和连续的动力学优化?## 章末回看:从浮动基座到全身控制

本章的核心不是“会调用 Pinocchio 计算一个质量矩阵”,而是理解为什么浮动基座系统必须用统一动力学来描述。固定基座机械臂的问题可以从关节空间开始;四足臂、轮足臂和人形上肢的问题必须从整机开始,因为基座本身既没有直接电机,又会被接触力、末端力和内部动量共同推动。

本质洞察

浮动基座臂的难点来自一个事实:机械臂看似只在操作物体,实际上也在操作自己的基座。臂的每一次加速、制动和接触都会通过质量矩阵的耦合项进入基座方程;腿或轮的每一次接触力调整,也会改变末端任务可用的力和运动范围。

因此,全身控制不是把“腿控制器”和“臂控制器”拼起来,而是在同一个动力学方程里决定三类量:驱动关节力矩、环境接触力、任务末端力。只要这三类量分开设计,系统就容易出现局部合理、全局矛盾的行为。

72.10 重新理解统一动力学方程

浮动基座系统常写成:

\[ \mathbf{M}(\mathbf{q})\dot{\mathbf{v}} + \mathbf{h}(\mathbf{q},\mathbf{v}) = \mathbf{S}^T\boldsymbol{\tau} + \mathbf{J}_c^T\boldsymbol{\lambda} + \mathbf{J}_{ee}^T\mathbf{f}_{ee} \]

这条公式的教学价值在于它把“谁能直接施力”讲得非常清楚。

第一项 \(\mathbf{S}^T\boldsymbol{\tau}\) 只作用在驱动关节上。浮动基座的前六维没有电机,所以选择矩阵 \(\mathbf{S}\) 的作用不是形式主义,而是在数学上阻止控制器直接给基座施加虚构力矩。

第二项 \(\mathbf{J}_c^T\boldsymbol{\lambda}\) 来自地面接触。腿足机器人能够移动基座,并不是因为基座被电机驱动,而是因为关节力矩通过腿和地面产生接触力,接触力再对整机施加空间力。

第三项 \(\mathbf{J}_{ee}^T\mathbf{f}_{ee}\) 来自末端操作。抓门把手、推箱子、拉抽屉时,末端力不只是任务输出,也会成为整机动力学的一部分。若控制器只在机械臂子系统里考虑末端力,就会低估它对基座平衡的影响。

72.11 分块质量矩阵的物理含义

把速度按基座、腿、臂分块后,质量矩阵可以写成:

\[ \mathbf{M}=\begin{bmatrix} \mathbf{M}_{bb} & \mathbf{M}_{b\ell} & \mathbf{M}_{ba}\\ \mathbf{M}_{\ell b} & \mathbf{M}_{\ell\ell} & \mathbf{M}_{\ell a}\\ \mathbf{M}_{ab} & \mathbf{M}_{a\ell} & \mathbf{M}_{aa} \end{bmatrix} \]

对角块描述各子系统自身的惯性;非对角块描述“一个子系统加速时,另一个子系统感受到的惯性反作用”。其中 \(\mathbf{M}_{ba}\) 尤其重要,因为它直接告诉我们机械臂运动如何牵动基座。

如果机械臂贴近身体,\(\mathbf{M}_{ba}\) 往往较小;如果机械臂伸得很远,末端负载又较重,\(\mathbf{M}_{ba}\) 会显著变大。这个变化不是控制器调参能消除的,它来自质量分布和杠杆臂长度。真正的工程处理方式是把臂姿态、支撑裕度和接触力一起纳入优化,而不是期待腿控制器事后补救。

72.12 三个容易混淆的量

符号 表示什么 常见误解 正确理解
\(\mathbf{q}\) 配置变量 与速度维度相同 四元数会让 \(n_q \neq n_v\)
\(\mathbf{v}\) 广义速度 普通关节速度拼接 浮动基座速度通常是李代数中的 6D twist
\(\boldsymbol{\tau}\) 驱动关节力矩 可以直接控制基座 只能通过 \(\mathbf{S}^T\) 进入驱动子空间
\(\boldsymbol{\lambda}\) 接触力 只是外部扰动 是腿足系统移动基座的主要通道
\(\mathbf{f}_{ee}\) 末端任务力 只影响物体 也会通过雅可比转置反作用到整机

⚠️ 易错点:把基座六维当成可控关节

很多初学者在写 WBC 或逆动力学 QP 时,会把所有广义加速度都当成可以自由指定的变量,然后用伪逆求出一个看似漂亮的解。这个解在仿真里可能短时间不爆炸,但它隐含了一个错误假设:基座六维可以像电机关节一样直接接受力矩命令。

正确做法是保留欠驱动约束:动力学方程的基座行必须由接触力和末端力平衡,而不是由 \(\boldsymbol{\tau}\) 直接平衡。若基座行残差很大,说明接触力、任务力或加速度目标之间存在冲突,不能简单靠增大关节增益解决。

💡 调试顺序:先看物理量,再看优化器

当全身控制出现异常时,不要第一时间怀疑求解器。更稳妥的顺序是:

  1. 检查 \(n_q\)\(n_v\)、关节索引和 frame id 是否一致。
  2. 在静止姿态下验证 \(\mathbf{h}\) 是否约等于重力项。
  3. 用单位接触力测试 \(\mathbf{J}_c^T\boldsymbol{\lambda}\) 的方向是否符合直觉。
  4. 用单位末端力测试 \(\mathbf{J}_{ee}^T\mathbf{f}_{ee}\) 是否让对应关节产生合理力矩。
  5. 再检查 QP 权重、约束松弛和摩擦锥。

这种顺序能避免一个常见误区:把坐标系、符号或索引错误包装成“优化器不稳定”。

故障排查

现象 可能原因 检查方法 修复方向
臂一伸出机身就明显倾斜 \(\mathbf{M}_{ba}\) 耦合被忽略 扫描臂姿态并绘制 \(\|M_{ba}\|_F\) 在 MPC/WBC 中加入质心和角动量约束
WBC 输出力矩很大但基座仍失稳 基座行动力学残差大 单独打印基座六维残差 放松任务目标或增加接触裕度
接触力方向反常 接触雅可比坐标系错 用单位法向力做静态测试 统一 WORLD/LOCAL 坐标约定
末端推力导致脚滑 摩擦锥没有覆盖操作反力 计算每只脚的切向/法向比值 限制末端力或重新分配支撑力
仿真稳定、真机振荡 延迟和执行器带宽未建模 记录命令与实际力矩相位差 降低带宽并加入延迟补偿

练习

A 型:给定一个四足臂模型,列出 \(\mathbf{M}_{bb}\)\(\mathbf{M}_{ba}\)\(\mathbf{M}_{aa}\) 的维度,并说明每个块在物理上代表什么。

B 型:令机器人静止站立,末端在世界系 \(x\) 方向施加 20 N 推力。推导这个力如何通过 \(\mathbf{J}_{ee}^T\mathbf{f}_{ee}\) 影响驱动关节力矩,并分析为什么支撑脚摩擦锥也会被改变。

C 型:设计一个调试实验:机械臂从身体内侧伸到身体外侧,记录质心、ZMP、\(\|M_{ba}\|_F\) 和基座姿态。要求解释这四个曲线之间的因果关系。

章末连接

下一章讨论多模态 MPC。它不是从零开始的新主题,而是把本章的统一动力学放进一个带模式序列的优化问题:不同接触模式决定 \(\mathbf{J}_c\) 和可用摩擦锥,不同操作阶段决定 \(\mathbf{J}_{ee}\) 和任务代价,优化器需要在这些结构变化之间保持动力学一致。

72.13 末端外力为什么不能只看机械臂

末端外力在固定基座机械臂里常被看成任务层的输出:给末端一个力,机械臂通过雅可比转置把它映射成关节力矩。这个视角在复合机器人里不够,因为末端力的反作用会进入整机动量平衡。

如果四足臂用末端推墙,墙给末端的反作用力会沿着机械臂传回躯干,再由支撑脚通过地面反作用力平衡。此时问题不是“机械臂关节能否输出这个力”这么简单,还要看支撑脚的摩擦锥、基座姿态、质心投影和关节力矩余量。

一个常用的判断方法是把末端力映射到广义力后按基座、腿、臂分块:

\[ \mathbf{J}_{ee}^T\mathbf{f}_{ee}=\begin{bmatrix} \mathbf{w}_b\\ \boldsymbol{\tau}_{\ell}\\ \boldsymbol{\tau}_a \end{bmatrix} \]

其中 \(\boldsymbol{\tau}_a\) 告诉我们机械臂本身需要多大力矩,\(\boldsymbol{\tau}_{\ell}\) 告诉我们腿部关节受到多大耦合影响,\(\mathbf{w}_b\) 则说明整机基座需要由接触力平衡多少空间力。若只看 \(\boldsymbol{\tau}_a\),就会漏掉最危险的基座和支撑约束。

⚠️ 易错点:把操作力当成已知扰动而不是约束来源

在推门、拉抽屉、拖动物体这类任务中,末端力不是一个可以事后补偿的小扰动,而是任务成功的必要条件。若控制器把它放在扰动通道里,常见结果是末端任务看起来完成了,支撑脚却接近滑动边界,或者基座姿态被慢慢推离稳定区。

更合理的建模方式是把末端操作力同时放进任务代价和全身约束:任务层要求它达到目标范围,动力学层要求支撑接触能够平衡它,安全层要求力矩和摩擦裕度仍然足够。这样做会让优化问题更复杂,但能避免把风险藏在控制器外面。

72.14 读懂一次全身控制日志

一次有价值的日志不应该只记录末端误差。对于浮动基座臂,至少要同时记录以下量:

日志量 解释 异常时的含义
基座六维动力学残差 欠驱动方程是否被满足 接触力或任务力不可行
每只脚摩擦裕度 支撑是否接近滑动 操作力过大或支撑分配不合理
末端广义力分块 操作力如何影响整机 只看臂力矩会漏掉基座风险
质心和 ZMP 稳定裕度 臂姿态或负载改变了平衡边界
力矩占限幅比例 执行器余量 仿真可行但真机可能饱和

这些量放在同一条时间轴上,才能看出因果关系。例如末端力上升后,基座残差先变大,随后某只脚摩擦裕度下降,再出现姿态偏差;这说明问题根源在操作力与支撑分配,而不是姿态控制器本身。