本文档属于 Robotics Tutorial 项目,作者:Pengfei Guo,达妙科技。采用 CC BY 4.0 协议,转载请注明出处。
F08 腿足 MPC+WBC 联合力控——操作任务中的轨迹优化与全身控制¶
本章定位:本章展示 MPC+WBC 联合架构在操作任务(而非纯行走)中的应用。MPC 在 30-50 Hz 生成参考轨迹和接触力,WBC 在 500-1000 Hz 实时跟踪并满足动力学/摩擦锥约束。这一双频率架构是当前腿足操作(loco-manipulation)的主流范式,也是机械臂力控与腿足力控的交汇点。本章从 MIT Cheetah 凸 MPC 出发,经 WBIC 两步结构,到 legged_control/qm_control 的工程实现,最终建立操作空间 MPC 与纯阻抗控制的性能对比框架。
前置依赖:F07(浮动基座 WBC 理论/QP 组装)、F02(操作空间动力学)、F03(阻抗控制/力位混合)、M05(QP/NLP 求解器)
下游章节:F09(学习型力控)、F10(综合实战)
建议用时:4 周(凸 MPC 理论 1 周 + WBIC 结构 1 周 + 代码实战 1.5 周 + 性能对比 0.5 周)
前置自测 ⭐¶
📋 答不出 >= 2 题 → 先回前置章节复习
| 编号 | 问题 | 答不出时回顾 |
|---|---|---|
| 1 | WBC-QP 的决策变量 \(z = [\dot{v}; \tau; f_c]\) 中,为什么 \(f_c\)(接触力)必须作为决策变量而不能像固定基座那样忽略? | F07 第 1 节 |
| 2 | 加权 QP 和层次化 QP(HQP)各适用什么场景?legged_control 为什么选择 HQP? | F07 第 3 节 |
| 3 | 若取 \(e_x=x_d-x\),笛卡尔阻抗控制律 \(\tau = J^T(K_d e_x + D_d \dot{e}_x) + g(q)\) 在什么条件下等价于 WBC-QP 的最优解? | F07 第 6 节 |
| 4 | 摩擦锥的外接金字塔近似引入了多少非保守外扩?工程上如何改成保守内逼近?为什么不直接用 SOCP 求解器? | F07 第 2 节 |
| 5 | MPC 的基本思想是什么?为什么需要滚动时域优化(Receding Horizon)而不是一次性规划? | M05 或控制理论基础 |
本章目标¶
学完本章后,你应该能够:
- 理解 MPC+WBC 两频率架构的设计哲学:MPC 负责"往哪走+用多大力",WBC 负责"怎么执行+满足约束"
- 推导 MIT Cheetah 凸 MPC 的单刚体简化模型和 QP 形式
- 解释 WBIC 的两步结构(KinWBC + QP 力修正)以及浮动基松弛 \(\delta_{fb}\) 的物理含义
- 阅读 legged_control 和 qm_control 的代码架构,理解 OCS2 SQP-MPC 的工作原理
- **设计**操作空间 MPC 的代价函数,包含笛卡尔空间位姿跟踪和接触力优化
- 对比 MPC+WBC 联合架构与纯阻抗控制(F03-F04)的性能差异和适用场景
F8.1 MPC+WBC 双频率架构 ⭐¶
动机——为什么 WBC 单独不够¶
回顾 F07:WBC 是一个**瞬时优化**——它在每个控制周期内,根据当前状态和任务参考,求解一个 QP 得到关节力矩。它不做预测、不做规划。
WBC 的根本局限:它只看"现在",不看"未来"。
示例: 四足机器人想从 trot 步态过渡到 walk 步态
WBC 在 t=0.5s 的信息:
- 当前 4 只脚的接触状态
- 当前质心位置和速度
- 当前任务参考(来自哪里?谁告诉 WBC 该往哪走?)
WBC 不知道的信息:
- 0.2s 后左前脚需要抬起(步态切换)
- 0.5s 后质心需要移到新的支撑多边形中心
- 1.0s 后末端需要到达目标物体
结论: WBC 需要一个"上层大脑"来提供未来的参考轨迹
这个"上层大脑"就是 MPC
类比:WBC 像一个反应极快但目光短浅的**战术执行者**——你告诉它"往右转",它能在 1ms 内精确执行。MPC 像一个深谋远虑的**战略规划者**——它看 1-2 秒的未来,规划出"先减速、再转弯、然后加速"的最优路径。两者组合成一个完整的控制系统。
架构总览¶
┌─────────────────────────────────────────────────┐
│ MPC(30-50 Hz) │
│ 输入: 当前状态 x, 目标状态 x_goal, 步态时序 │
│ 输出: 未来 N 步的参考轨迹 x_ref(k) │
│ 未来 N 步的期望接触力 f_c_ref(k) │
│ 方法: 凸 QP (MIT Cheetah) 或 SQP (OCS2) │
│ 时域: 0.5-2.0 秒 │
└────────────────────────┬────────────────────────┘
│ x_ref(0), f_c_ref(0)
│ (只用第一步的参考)
▼
┌─────────────────────────────────────────────────┐
│ WBC(500-1000 Hz) │
│ 输入: 当前状态 (q,v), MPC 参考 (x_ref, f_c_ref) │
│ 输出: 关节力矩 tau │
│ 方法: 加权 QP 或 HQP │
│ 约束: 动力学 + 摩擦锥 + 力矩限 │
└────────────────────────┬────────────────────────┘
│ tau
▼
┌─────────────────────────────────────────────────┐
│ 执行器(1-10 kHz) │
│ 电机电流环 / 力矩控制 │
└─────────────────────────────────────────────────┘
频率层级的物理直觉:
| 层级 | 频率 | 时间尺度 | 关注内容 | 类比 |
|---|---|---|---|---|
| MPC | 30-50 Hz | 20-30 ms | 未来 0.5-2s 的最优轨迹 | 导航员看前方路况 |
| WBC | 500-1000 Hz | 1-2 ms | 当前瞬间的力矩分配 | 驾驶员控制方向盘 |
| 执行器 | 1-10 kHz | 0.1-1 ms | 电机电流跟踪 | 转向助力系统 |
反事实推理:如果不分频率会怎样? - 用 MPC 的频率(30 Hz)直接输出关节力矩 → 30ms 的控制周期太长,外力扰动来不及响应,机器人在接触时不稳定 - 用 WBC 的频率(1000 Hz)做 MPC → 每 1ms 求解一次非线性轨迹优化,计算量远超实时限制 - 结论:分频率是必须的——MPC 负责低频决策,WBC 负责高频执行
从机械臂视角看 MPC+WBC¶
在纯机械臂场景(固定基座),MPC+WBC 仍然有意义吗?
| 场景 | 是否需要 MPC? | 理由 |
|---|---|---|
| 固定基座单臂,简单 pick-and-place | 不需要 | 轨迹规划器 + 阻抗控制足够 |
| 固定基座单臂,复杂接触操作 | 可选 | MPC 可以优化接触力序列 |
| 移动平台+臂,需要边走边操作 | 需要 | 底盘轨迹+臂轨迹需要协同规划 |
| 四足+臂,loco-manipulation | 必须 | 步态切换+操作需要预测性规划 |
历史脉络¶
| 年份 | 里程碑 | 关键贡献 |
|---|---|---|
| 2018 | Di Carlo et al., "Dynamic Locomotion via Convex MPC", IROS | 单刚体凸 MPC,MIT Cheetah 3,30-40 Hz |
| 2019 | Kim et al., "Highly Dynamic Quadruped via WBIC", IROS | WBIC 两步结构,Mini Cheetah 3.7 m/s |
| 2021 | Sleiman et al., "Unified MPC for WB Loco-Manipulation", RA-L | 质心+末端统一 MPC |
| 2022 | qiayuanl, legged_control | OCS2+WBC 教学友好实现 |
| 2023 | Sleiman et al., Science Robotics | ANYmal 推门/开阀,接触模式枚举 |
| 2024 | Zhang (skywoodsz), qm_control, IROS | 四足+臂末端阻抗+摩擦锥一体 QP |
| 2025 | Zhang et al. (CMU), FALCON, L4DC 2026 Oral | 双智能体 RL 力自适应人形 |
⚠️ 常见陷阱¶
💡 概念误区:认为 MPC 直接输出关节力矩
新手想法:"MPC 是最优控制,它应该直接给出最优力矩序列"
实际上:MPC 通常使用简化模型(如单刚体),其输出是参考状态和接触力。
从简化模型的接触力到实际关节力矩,需要 WBC 来"翻译":
WBC 用完整的多体动力学把接触力转化为满足所有约束的关节力矩。
正确理解:MPC 输出的是"要做什么"(where + how much force),
WBC 输出的是"怎么做"(which joint, what torque)。
⚠️ 编程陷阱:MPC 和 WBC 的时钟不同步
错误做法:MPC 每 20ms 更新一次参考,WBC 每 1ms 插值一次,
但 WBC 在 MPC 更新瞬间不平滑处理
现象:每 20ms 关节力矩出现一个跳变(MPC 参考突变)
根本原因:MPC 的参考轨迹在离散时刻之间是不连续的
正确做法:WBC 在两次 MPC 更新之间做线性插值或样条插值
自检方法:以 1kHz 记录 tau,检查每 20ms 是否有跳变
🧠 思维陷阱:认为 MPC 预测时域越长越好
新手想法:"MPC 看得越远,规划越好,性能越高"
实际上:时域越长 → QP 维度越大 → 求解时间越长 → 可能超过实时预算。
而且长时域的末端预测因模型误差而不可靠。
正确思维:时域 = min(性能需求, 计算预算, 模型可信区间)
四足 trot: 0.5-1.0s 通常够用
人形行走: 1.0-2.0s
练习¶
- ⭐ 频率计算:MPC 以 40 Hz 运行,预测时域 1.0 秒,离散步长 25ms。一个 MPC 周期内有多少个预测步?QP 的决策变量维度是多少(假设 13 维状态、12 维控制、每步 4 面摩擦锥)?
- ⭐ 延迟分析:MPC 求解时间 5ms,WBC 求解时间 0.5ms,通信延迟 0.5ms。从传感器读取到力矩输出的总延迟是多少?这个延迟对 500 Hz WBC 的相位裕度有什么影响?
- ⭐⭐ 架构选型:为以下场景选择控制架构并说明理由:(a) Franka 恒力打磨固定工件,(b) TIAGo 移动到桌前抓杯子,(c) ANYmal 四足走到门前推开门。
F8.2 MIT Cheetah 凸 MPC ⭐⭐¶
动机——用简化模型换取实时性¶
回顾 F07 第 2 节:浮动基座完整动力学有 \((6+n)\) 维。对 Mini Cheetah(12 关节),这是 18 维状态。直接在 MPC 中使用完整模型的计算量太大——NMPC 在 30 Hz 下难以实时。
Di Carlo 2018 的关键洞察:忽略腿质量,把整个机器人简化为一个**单刚体**(Single Rigid Body, SRB)。这使动力学变成线性的,MPC 变成凸 QP——求解时间从几十毫秒降到 ~1ms。
单刚体简化¶
假设:腿的质量远小于躯干质量,可以忽略。
完整模型: M(q) 是 18x18 的构型依赖矩阵,非线性
单刚体: M = diag(I_body, m*I_3) 是常数矩阵(近似)
SRB 状态向量:
x = [theta(3), p(3), omega(3), p_dot(3), g(1)] 属于 R^13
| | | | |
姿态(ZYX) 位置 角速度 线速度 重力
SRB 控制向量:
u = [f_1(3), f_2(3), f_3(3), f_4(3)] 属于 R^12
| | | |
左前脚力 右前脚力 左后脚力 右后脚力
连续动力学(线性化):
其中 \(r_i = p_{foot,i} - p_{CoM}\) 是从质心到第 \(i\) 个脚的向量。
线性化技巧:\(I_{world} = R I_{body} R^T\) 中的 \(R\) 依赖姿态——非线性。Di Carlo 的处理:在每个 MPC 周期开始时固定 \(R\) 为当前值,使一个 MPC 时域内的动力学线性。
反事实推理:如果不做 SRB 简化会怎样? - 完整 18-DOF 非线性动力学 → NMPC → 求解时间 20-100ms - 30 Hz MPC 的预算是 ~30ms → 可能超时 - 超时意味着 MPC 来不及更新参考 → WBC 用过时的参考 → 性能下降
所以 SRB 简化用精度换实时性。代价是忽略了腿动力学对躯干的反作用。工程上通过把电机放在髋关节处(连杆驱动膝关节)来降低腿部转动惯量,让 SRB 更准确。
离散化与 QP 形式¶
用 ZOH(零阶保持)将连续动力学离散化:
其中 \(A \in \mathbb{R}^{13 \times 13}\), \(B \in \mathbb{R}^{13 \times 12}\)。
MPC 的 QP 形式:
消去 \(x_k\)(代入递推关系),得到只关于 \(u\) 的稠密 QP:
其中 \(U = [u_0; u_1; ...; u_{N-1}] \in \mathbb{R}^{12N}\)。
稠密 QP 的完整展开——操作空间视角
为了让读者真正理解"消去状态变量"这一关键步骤,我们完整展开递推过程。这一推导在操作空间 MPC(F8.5)中同样适用,只是状态和控制维度不同。
从 \(x_{k+1} = A x_k + B u_k\) 和初始状态 \(x_0\) 出发,逐步递推:
将所有 \(x_1, ..., x_N\) 堆叠成向量 \(X = [x_1; x_2; ...; x_N] \in \mathbb{R}^{13N}\):
代入代价函数 \(J = (X - X_{ref})^T \bar{Q} (X - X_{ref}) + U^T \bar{R} U\),其中 \(\bar{Q} = \text{diag}(Q, ..., Q, Q_f)\),\(\bar{R} = \text{diag}(R, ..., R)\):
展开并整理为标准 QP 形式 \(\frac{1}{2} U^T H U + g^T U + \text{const}\):
本质洞察:\(H_{MPC}\) 是**对称正定**的(只要 \(\bar{R} \succ 0\)),这保证了 QP 是凸的,有唯一全局最优解。这就是"凸 MPC"名称的由来——不是所有 MPC 都是凸的,但 SRB 线性化后的 MPC 天然是凸 QP。
摩擦锥的线性化——金字塔近似的详细组装
摩擦锥 \(\sqrt{f_x^2 + f_y^2} \leq \mu f_z\) 是一个二阶锥约束(SOC),直接处理需要 SOCP 求解器。Di Carlo 2018 用外接金字塔近似将其线性化:
等价于 4 个线性不等式(每个接触点):
加上法向力约束 \(0 \leq f_{i,z} \leq f_{max}\),每个接触点有 6 个不等式。对 \(N\) 步预测、每步 \(n_c\) 个接触点,总不等式约束数 = \(6 n_c N\)。
约束矩阵组装(每个接触点 i, 每步 k):
A_fric_i = [ 1, 0, -mu; % f_x <= mu * f_z
-1, 0, -mu; % -f_x <= mu * f_z
0, 1, -mu; % f_y <= mu * f_z
0, -1, -mu; % -f_y <= mu * f_z
0, 0, -1; % f_z >= 0 (即 -f_z <= 0)
0, 0, 1] % f_z <= f_max
b_fric_i = [0; 0; 0; 0; 0; f_max]
金字塔 vs 圆锥的关系:
上述 |f_x| <= mu*f_z, |f_y| <= mu*f_z 是圆锥的外逼近(金字塔包含圆锥)
-> 可行域扩大,对角方向最大切向力为 mu*sqrt(2)*f_z(超出真实摩擦锥)
-> 非保守:可能求解出实际会滑动的力
若需内逼近(保守,保证不滑动):
方法 1: 使用 |f_x| + |f_y| <= mu*f_z
等价线性面为 ±f_x ± f_y <= mu*f_z(四种符号组合)
方法 2: 在外逼近公式中使用 mu_eff = mu/sqrt(2)
方法 3: 使用更多面数的内接多边形锥,面数越多越接近真实圆锥
工程实践: “外逼近 + 略微减小 mu”是经验裕度,不是严格保守近似;
安全关键的硬约束应使用内逼近或直接 SOCP。
反事实推理:如果直接用 SOCP 求解器处理圆锥约束会怎样? - SOCP 求解器(如 ECOS、SCS)可以精确处理,无保守性 - 但 SOCP 的求解时间约为 QP 的 3-5 倍(内点法额外开销) - 对 MPC 的 30 Hz 实时约束来说,QP 的 1ms vs SOCP 的 3-5ms 差距显著 - 结论:金字塔近似是实时性与精度的工程权衡。OCS2 等框架支持解析 SOC 锥,在更强算力平台上可切换
qpOASES 求解:~1 ms(Mini Cheetah 上 ARM Cortex-A72),\(N = 10-20\)步。
步态时序与接触模式¶
MPC 需要知道"哪些脚在地上"才能正确设置约束。这由**步态调度器**提供:
Trot 步态(对角步态):
时刻 0.0s: 左前+右后 着地, 右前+左后 摆动
时刻 0.2s: 切换: 右前+左后 着地, 左前+右后 摆动
时刻 0.4s: 再次切换
...
在 MPC 中的处理:
摆动腿: f_i = 0 (零力约束)
接触腿: 0 <= f_{i,z} <= f_max + 摩擦锥
步态模式编码:
contact_schedule[k] = [1, 0, 0, 1] -- LF,RH 接触; RF,LH 摆动
代价函数权重矩阵设计¶
Q = diag(Q_theta, Q_p, Q_omega, Q_v, Q_g)
典型取值(Di Carlo 2018 Mini Cheetah):
Q_theta = diag(80, 80, 10) # 姿态: roll/pitch 重要, yaw 次之
Q_p = diag(0, 0, 50) # 位置: z(高度) 重要, xy 由速度控制
Q_omega = diag(0.1, 0.1, 0.1) # 角速度: 轻微正则
Q_v = diag(100, 100, 1) # 速度: xy 跟踪重要
Q_g = 0 # 重力: 常数, 不需要惩罚
R = diag(r_f, r_f, r_f, r_f) # 接触力正则化
r_f = diag(1e-5, 1e-5, 1e-5) # 轻微正则化, 防止数值问题
理论到工程衔接:Q 矩阵的设计不是数学问题而是**工程决策**——它编码了"什么更重要"。roll/pitch 权重 (80) 远大于 yaw (10),因为 roll/pitch 偏离会导致摔倒,而 yaw 偏离只影响方向。这种权重设计直接来自机器人物理特性。
⚠️ 常见陷阱¶
💡 概念误区:认为 SRB 简化意味着 MPC 不准确
新手想法:"忽略腿质量,MPC 的预测肯定很不准"
实际上:四足机器人的腿质量通常只占总质量的 5-15%。
MIT Cheetah 3 的腿质量约 5% → SRB 误差 < 5%,完全可接受。
工程上刻意把电机放在髋关节处(连杆驱动膝关节),
降低腿部转动惯量——这不是偶然设计,而是为了让 SRB 更准确。
正确理解:SRB 的精度取决于腿质量占比。
腿重(如人形)→ SRB 不够精确 → 需要 NMPC
腿轻(如四足)→ SRB 足够精确 → 凸 MPC 够用
⚠️ 编程陷阱:MPC 的 QP 中忘记设置摆动腿的零力约束
错误做法:所有腿都允许有力
现象:MPC 给摆动腿分配了接触力——但摆动腿在空中无法施力。
WBC 试图跟踪这个不可实现的力参考 → 腿在空中"蹬"。
根本原因:MPC 不知道哪些腿在接触,哪些在摆动
正确做法:根据步态时序,对摆动腿设置 f_i = 0
🧠 思维陷阱:认为 MPC 时域越长越好
新手想法:"MPC 看得越远越好"
实际上:时域越长 → QP 维度越大 → 求解时间越长。
且长时域末端预测因模型误差不可靠。
正确思维:时域 = min(性能需求, 计算预算, 模型可信区间)
练习¶
- ⭐ 维度计算:Mini Cheetah 凸 MPC,\(N = 15\) 步,4 脚 trot 步态(每步 2 接触腿),计算 QP 的维度:决策变量 \(\dim(U)\)、不等式约束数。
- ⭐⭐ SRB 推导:从完整浮动基座动力学 \(M\dot{v} + h = S^T\tau + J_c^T f_c\) 出发,令腿质量趋近 0,推导 SRB 动力学方程。指出哪些项消失了。
- ⭐⭐ 权重调参:在一个简单的 SRB 仿真中实现凸 MPC,分别测试 \(Q_{pitch} = 10\) 和 \(Q_{pitch} = 200\) 的效果。
F8.3 WBIC 两步结构 ⭐⭐¶
动机——从 MPC 的参考到关节力矩¶
回顾 F8.1:MPC 输出简化模型的参考轨迹 \(x_{ref}\) 和接触力 \(f_{c,ref}\)。但关节力矩还需要 WBC 来计算。Kim 2019 的 WBIC 提供了高效的两步方法。
KinWBC + QP 力修正¶
步骤 1——KinWBC(运动学 WBC):
从 MPC 的参考中提取运动学目标,用逆运动学求解关节加速度。
KinWBC 输入:
- 体姿态参考 (roll, pitch, yaw)_ref <-- 来自 MPC
- 质心位置参考 p_com_ref <-- 来自 MPC
- 足端位置参考 p_foot_ref <-- 来自摆动腿轨迹规划
- 关节正则化 q_ref <-- 默认站姿
KinWBC 输出:
- q_cmd, q_dot_cmd, q_ddot_cmd(关节命令)
方法: 严格优先级逆运动学(零空间投影)
Priority 1: 体姿态
Priority 2: 质心位置
Priority 3: 足端位置
Priority 4: 关节正则化
用零空间投影逐层求解:
q_ddot_1 = J_body^+ * x_ddot_body_ref
q_ddot_2 = q_ddot_1 + N_1 * J_com^+ * (x_ddot_com_ref - J_com * q_ddot_1)
q_ddot_3 = q_ddot_2 + N_12 * J_foot^+ * (x_ddot_foot_ref - J_foot * q_ddot_2)
...
步骤 2——QP 力修正:
KinWBC 给出的 \(\ddot{q}_{cmd}\) 可能不满足浮动基座动力学——因为 KinWBC 只考虑了运动学。QP 力修正补上这个缺口。
浮动基松弛 \(\delta_{fb}\) 的物理含义:
浮动基座的 6 个 DOF 没有执行器,只能通过接触力间接控制。如果 MPC 给的力参考不可行(如违反摩擦锥),WBC 无法精确满足浮动基座动力学——此时 \(\delta_{fb}\) 就是"动力学残差"。
不是 X 而是 Y:\(\delta_{fb}\) **不是**控制误差,**而是**MPC 简化模型与实际全身动力学之间的**建模差距**的体现。SRB 模型忽略了腿动力学,所以 MPC 的力参考在全身模型下不完全可行——\(\delta_{fb}\) 量化了这个不可行程度。
WBIC 与标准 WBC-QP 的对比¶
| 对比项 | WBIC(Kim 2019) | 标准 WBC-QP(F07) |
|---|---|---|
| 结构 | 两步:KinWBC + QP | 一步:统一 QP |
| KinWBC 步骤 | 零空间投影(解析) | 无(全部在 QP 中) |
| QP 决策变量 | \([\delta_{fb}; f_c]\) | \([\dot{v}; \tau; f_c]\) |
| QP 维度 | \(6 + 3k\)(较小) | \((6+n) + n + 3k\)(较大) |
| 求解速度 | 更快 | 较慢 |
| 力跟踪精度 | 中等(两步近似) | 较高(统一优化) |
类比:WBIC 像"先用 GPS 导航到大致位置,再用眼睛精确停车"——KinWBC 做粗定位,QP 做精细力调整。标准 WBC-QP 像"用一个超精确的系统一步到位"——更精确但计算更贵。
教学核心代码定位¶
MIT Cheetah-Software 代码结构:
WBC_Ctrl/
├── LocomotionCtrl/
│ └── LocomotionCtrl.cpp <-- WBIC 主入口
├── WBC_Ctrl.cpp <-- WBC 基类
├── KinWBC.cpp <-- 运动学 WBC
└── WBIC/
├── WBIC.cpp <-- QP 力修正
└── WBIC.h
关键代码路径:
LocomotionCtrl::run()
-> KinWBC::FindConfiguration() // 步骤 1
-> WBIC::MakeTorque() // 步骤 2
-> _SetEqualityConstraint() // 动力学等式
-> _SetInEqualityConstraint() // 摩擦锥+力矩限
-> _SetCost() // min ||delta||^2 + w||f-f_ref||^2
-> _SolveQP() // qpOASES
-> 输出 tau
⚠️ 常见陷阱¶
💡 概念误区:认为 delta_fb 越小控制越好
新手想法:"delta_fb = 0 时浮动基座动力学精确满足,这是最好的"
实际上:delta_fb = 0 意味着 MPC 的力参考恰好满足全身动力学——
但这只在 SRB 模型完全精确时才成立。
如果强制 delta_fb = 0(硬约束),QP 可能 infeasible。
正确理解:delta_fb 是一个"安全阀"——让系统在模型不精确时仍能平稳运行。
⚠️ 编程陷阱:KinWBC 的零空间投影顺序错误
错误做法:把关节正则化放在体姿态之前
现象:机器人优先满足"回到默认站姿"而非"保持姿态稳定" → 摔倒
正确做法:优先级按安全性降序排列:
体姿态 > 质心 > 足端 > 关节正则化
练习¶
- ⭐ 松弛分析:如果 MPC 的力参考恰好满足 SRB 但违反了一个脚的摩擦锥,WBIC 的 QP 会怎么处理?\(\delta_{fb}\) 会变大还是 \(f_c\) 会偏离 \(f_{MPC}\)?
- ⭐⭐ 维度对比:对 Mini Cheetah(12 关节,4 脚着地),分别计算 WBIC QP 和标准 WBC-QP 的决策变量维度。
- ⭐⭐ 代码精读:找到 Cheetah-Software 的
WBIC.cpp,标注 \(\delta_{fb}\) 的定义位置、摩擦锥约束的组装位置、\(w_f\) 的设置位置。
F8.4 legged_control 架构——OCS2 + HoQp ⭐⭐¶
动机——教学最友好的 MPC+WBC 实现¶
legged_control(qiayuanl, ~1.8k Stars, BSD-3)基于 OCS2 框架,专门为 Unitree A1/Go1 适配,代码结构清晰。
代码架构¶
qiayuanl/legged_control
├── legged_estimation/ <-- 状态估计
│ └── LinearKalmanFilter.cpp <-- 线性卡尔曼滤波器
├── legged_interface/ <-- OCS2 MPC 接口
│ ├── LeggedRobotInterface.cpp <-- 问题定义
│ └── constraint/ <-- 约束定义
├── legged_wbc/ <-- WBC 实现
│ ├── HoQp.cpp <-- 严格层次化 QP
│ ├── WeightedWbc.cpp <-- 加权 QP(备选)
│ └── WbcBase.cpp <-- 共享基类
├── legged_controllers/ <-- ROS2 控制器
│ └── LeggedController.cpp <-- 主控制器
└── legged_unitree_hw/ <-- Unitree 硬件接口
数据流¶
用户命令(速度/姿态)
|
v
┌── OCS2 SQP-MPC (50 Hz) ──┐
│ 模型: 质心+运动学 │
│ 输出: x_ref, f_c_ref │
└──────────┬────────────────┘
│
v
┌── WBC (500 Hz) ───────────┐
│ 方法: HoQp 或 WeightedWbc│
│ 优先级: │
│ L0: 浮动基座动力学 │
│ L1: 接触约束 │
│ L2: 摩擦锥 │
│ L3: 体姿态跟踪 │
│ L4: 足端位置跟踪 │
│ L5: 关节正则化 │
│ 输出: tau (12D) │
└──────────┬────────────────┘
│
v
┌── Unitree SDK ────────────┐
│ 电机力矩命令 │
└───────────────────────────┘
OCS2 SQP-MPC vs 凸 MPC¶
| 特性 | 凸 MPC (Di Carlo 2018) | OCS2 SQP-MPC |
|---|---|---|
| 模型 | 单刚体(线性) | 质心动力学+运动学(非线性) |
| 求解器 | QP (qpOASES) | SQP (Gauss-Newton) |
| 精度 | 低(忽略腿动力学) | 中(考虑运动学耦合) |
| 速度 | ~1 ms | ~5-20 ms |
| 适用 | 四足平坦地面 | 四足+臂、非平坦地面 |
HoQp 实现要点¶
// HoQp 核心算法(简化)
void HoQp::solve(const std::vector<Task>& tasks) {
Eigen::MatrixXd Z = Eigen::MatrixXd::Identity(n_var, n_var);
Eigen::VectorXd x_prev = Eigen::VectorXd::Zero(n_var);
for (int level = 0; level < tasks.size(); ++level) {
// 投影到前面所有任务的零空间
Task projected = tasks[level].projectOnto(Z);
// 在零空间中求解当前层 QP
Eigen::VectorXd x_level = solveQP(projected);
// 叠加
x_prev = x_prev + Z * x_level;
// 更新零空间
Z = Z * nullspace(projected.J);
}
solution_ = x_prev;
}
配置切换¶
# legged_control 配置
wbc:
type: "HoQp" # 或 "WeightedWbc"
# HoQp: 只需设定优先级顺序
task_priorities:
- floating_base_dynamics
- contact_constraints
- friction_cone
- body_orientation
- foot_position
- joint_regularization
# WeightedWbc: 需要调权重
task_weights:
body_orientation: 500.0
foot_position: 200.0
joint_regularization: 1.0
torque_regularization: 0.001
⚠️ 常见陷阱¶
⚠️ 编程陷阱:Gazebo 仿真中忘记设置 use_sim_time
错误做法:不设置 use_sim_time:=true
现象:MPC 用真实时间,仿真用仿真时间,两者不同步
→ 控制错乱
正确做法:launch 文件中加 use_sim_time:=true
💡 概念误区:认为 HoQp 在所有场景都优于 WeightedWbc
新手想法:"HoQp 保证高优先级,肯定更好"
实际上:HoQp 需要求解 N 个 QP,计算量更大。
在嵌入式平台上 WeightedWbc 的一个 QP 可能更实际。
有时"所有任务都做到 90%"比"高优先级 100% + 低优先级 0%"更好。
正确思维:安全关键任务 → HoQp;性能优化任务 → WeightedWbc 更灵活
练习¶
- ⭐ legged_control 跑通:在 Gazebo 中运行 legged_control(Unitree A1),用 rqt_plot 绘制 MPC 接触力参考 vs WBC 关节力矩。
- ⭐ HoQp vs WeightedWbc 对比:同一 trot 步态下分别使用两种 WBC,对比体姿态 RMSE、接触力精度、CPU 占用。
- ⭐⭐ OCS2 精读:精读
LeggedRobotInterface.cpp,标注代价函数各项、约束类型、模式切换处理。
F8.5 qm_control 的力控扩展——操作空间中的 MPC ⭐⭐⭐¶
动机——从纯行走到 Loco-Manipulation¶
qm_control(skywoodsz, ~600 Stars)扩展了 Sleiman 2021 的框架,在 legged_control 基础上增加 6-DOF 臂,实现 AlienGo + Z1 四足操作。
架构扩展¶
legged_control 的 QP:
决策变量: z = [v_dot; tau_legs; f_legs] <-- 只有腿
qm_control 的 QP:
决策变量: z = [v_dot; tau_legs; tau_arm; f_legs; f_arm] <-- 腿+臂
新增约束:
- 接触运动学: J_c * dv + dJ_c * v = 0
对所有保持接触的足端/手端作为硬等式约束
- 末端阻抗/运动任务:
J_ee * dv + dJ_ee * v = ddx_ee_ref
ddx_ee_ref = ddx_d + Kp(x_d - x) + Kd(xdot_d - xdot)
作为加速度任务或软代价加入 QP
- 扭矩饱和: tau_min <= [tau_legs; tau_arm] <= tau_max
- 臂末端摩擦锥(如果末端接触物体)
操作空间 MPC 代价函数¶
理论到工程衔接:末端力 \(f_{ee,ref}\) 是 MPC 代价函数的一部分——这意味着 MPC 不仅规划"末端去哪里",还规划"末端用多大力"。这是操作空间 MPC 相比纯行走 MPC 的关键扩展。
接触力优化——推/滑/抓¶
以下列表中的 \(f_{ee,ref}\) 统一表示**机器人作用在物体/环境上的力**,接触坐标系的 \(+z\) 指向被作用对象内部。因此推、压、夹紧时法向分量为正。若传给 WBC 动力学方程 \(M\dot{v}+h=S^T\tau+J_c^Tf_c\),其中 \(f_c\) 通常定义为**环境作用在机器人上的反力**,需要使用 \(f_c=-f_{ee,ref}\)(并做坐标变换)。
任务类型到 MPC 约束的映射:
推(Push):
末端力参考: f_ee_ref = [0, 0, F_push]
约束: 工程硬约束用保守内逼近,如 |f_x| + |f_y| <= mu * f_z, f_z >= 0
特点: 单方向力
滑(Slide):
末端力参考: f_ee_ref = [F_slide, 0, F_normal]
约束: 摩擦锥
特点: 末端沿表面滑动
抓(Grasp):
末端力参考: f_ee_ref = [0, 0, F_grasp]
约束: F_grasp >= F_min
特点: 闭合夹爪
qm_control 代码结构¶
skywoodsz/qm_control
├── qm_interface/ <-- OCS2 问题定义
│ ├── QmInterface.cpp <-- 质心+臂+末端联合优化
│ └── constraint/
│ ├── EndEffectorConstraint.cpp
│ └── FrictionConeConstraint.cpp
├── qm_wbc/ <-- WBC
│ ├── QmWbcController.cpp
│ └── HierarchicalWbc.cpp
├── qm_controllers/ <-- ROS2 控制器
└── qm_unitree/ <-- AlienGo + Z1 硬件
接触力优化——抓取力分配 QP ⭐⭐⭐¶
在 loco-manipulation 中,末端接触力的优化远比纯行走复杂。除了腿的地面反力(F8.2 的摩擦锥),还需要优化末端对物体的抓取/推/滑力。这里给出抓取力分配的完整 QP 建模。
问题定义:机器人用 \(n_c\) 个接触点抓取一个物体,需要平衡物体重力且满足摩擦锥。
其中 \(G_i \in \mathbb{R}^{6 \times 3}\) 是从接触点 \(i\) 到物体质心的抓力矩阵(grasp matrix),\(w_{ext} = [0, 0, -m_{obj}g, 0, 0, 0]^T\) 是作用在物体上的外部重力 wrench。若把 \(b_{eq}\) 写成右端项,则应使用 \(Gf=-w_{ext}\),而不是 \(Gf=w_{ext}\)。
抓力矩阵 \(G_i\) 的组装:
若优化变量 \(f_i\) 用世界帧表示:
若优化变量改用接触局部帧分量 \(f_i^C\),才需要右乘接触帧旋转:
其中 \(r_i\) 是从物体质心到接触点 \(i\) 的向量,\(R_{WC,i}\) 是接触帧到世界帧的旋转矩阵,\([r_i]_\times\) 是 \(r_i\) 的反对称矩阵。下面代码选择世界帧力变量,因此 \(G\) 中不乘 \(R_{WC,i}\),摩擦锥约束再用接触法向投影到局部切向/法向。
"""
抓取力分配 QP — 双指夹爪抓取圆柱体
"""
import numpy as np
from scipy.optimize import minimize
def grasp_force_qp(contact_positions, contact_normals,
object_com, object_weight, mu=0.5):
"""
参数:
contact_positions: list of (3,) 接触点位置
contact_normals: list of (3,) 接触法线(指向物体内部)
object_com: (3,) 物体质心
object_weight: (6,) 作用在物体上的重力 wrench [fx,fy,fz,tx,ty,tz]
mu: 摩擦系数
返回:
forces: list of (3,) 各接触点力
"""
n_contacts = len(contact_positions)
n_vars = 3 * n_contacts # 每个接触点 3D 力
# 组装抓力矩阵 G (6 x n_vars)
G = np.zeros((6, n_vars))
for i in range(n_contacts):
r_i = contact_positions[i] - object_com
# 力贡献
G[0:3, 3*i:3*i+3] = np.eye(3)
# 力矩贡献: r_i x f_i
G[3:6, 3*i:3*i+3] = skew(r_i)
# 力平衡等式约束: G @ f + w_ext = 0
# f 是指尖作用在物体上的力;若用于机器人 WBC 反力,需要取负。
A_eq = G
b_eq = -object_weight
# 摩擦锥不等式约束 (线性化)
A_ineq_list = []
b_ineq_list = []
for i in range(n_contacts):
n_i = contact_normals[i]
# 建立局部坐标系 (t1, t2, n)
t1 = np.cross(n_i, [1, 0, 0]) if abs(n_i[0]) < 0.9 else np.cross(n_i, [0, 1, 0])
t1 /= np.linalg.norm(t1)
t2 = np.cross(n_i, t1)
# 保守轴向内逼近: |f_t1| <= mu_eff*f_n, |f_t2| <= mu_eff*f_n, f_n >= f_min
# 其中 mu_eff = mu/sqrt(2),保证满足真实圆锥 ||f_t|| <= mu*f_n。
mu_eff = mu / np.sqrt(2.0)
R_contact = np.column_stack([t1, t2, n_i])
# 在局部帧中: f_local = R^T @ f_world
block = np.zeros((5, n_vars))
R_T = R_contact.T
# t1 方向: f_t1 <= mu_eff * f_n -> R_T[0,:] @ f - mu_eff * R_T[2,:] @ f <= 0
block[0, 3*i:3*i+3] = R_T[0] - mu_eff * R_T[2]
block[1, 3*i:3*i+3] = -R_T[0] - mu_eff * R_T[2]
# t2 方向
block[2, 3*i:3*i+3] = R_T[1] - mu_eff * R_T[2]
block[3, 3*i:3*i+3] = -R_T[1] - mu_eff * R_T[2]
# 法向力下界: -f_n <= -f_min
block[4, 3*i:3*i+3] = -R_T[2]
A_ineq_list.append(block)
b_ineq_list.append(np.array([0, 0, 0, 0, -1.0])) # f_min = 1N
A_ineq = np.vstack(A_ineq_list)
b_ineq = np.concatenate(b_ineq_list)
# QP: min ||f||^2 s.t. G*f = -w_ext, A*f <= b
from scipy.optimize import linprog
# 用 quadprog 或 cvxpy 求解(此处示意)
import cvxpy as cp
f = cp.Variable(n_vars)
prob = cp.Problem(
cp.Minimize(cp.sum_squares(f)),
[A_eq @ f == b_eq,
A_ineq @ f <= b_ineq]
)
prob.solve(solver=cp.OSQP)
return f.value.reshape(n_contacts, 3)
def skew(v):
return np.array([[0, -v[2], v[1]],
[v[2], 0, -v[0]],
[-v[1], v[0], 0]])
跨领域类比:抓取力分配 QP 与四足站立的力分配 QP(F8.2)本质相同——都是"给定多个接触点和外部 wrench,求满足摩擦锥的最小接触力"。区别在于四足的力分配是 \(f_{legs} \in \mathbb{R}^{12}\) 平衡躯干重力,抓取的力分配是 \(f_{fingers} \in \mathbb{R}^{6}\)(双指)平衡物体重力。统一的数学形式是 \(\min \|f\|^2 \text{ s.t. } Gf + w_{ext}=0, f \in \mathcal{FC}\)。
loco-manipulation 全身优化案例——ANYmal 推门 ⭐⭐⭐¶
Sleiman et al. 2023 (Science Robotics) 的 ANYmal 推门任务是 loco-manipulation 的标杆。以下分析其全身优化的 QP 结构。
任务分解:
Phase 1: 走向门 (纯 locomotion)
接触: [LF, RF, LH, RH] 四脚
MPC 目标: 质心到达门前 0.5m
无末端力要求
Phase 2: 伸臂接触门把手 (过渡)
接触: [LF, RF, LH, RH] + [EE] 五接触
MPC 目标: 末端到达门把手 + 维持平衡
末端力: 0 -> 5N (渐进)
Phase 3: 推门 (loco-manipulation)
接触: [LF, RF, LH, RH] + [EE]
MPC 目标: 末端维持 20N 推力 + 质心跟随门移动
关键: 门旋转 -> 末端位姿随时间变化 -> MPC 需要跟踪运动目标
Phase 4: 穿过门 (locomotion)
接触: [LF, RF, LH, RH] 回到纯四脚
MPC 目标: 穿过门洞
Phase 3 的 WBC-QP 详细组装:
总共 57 个决策变量。约束包括:
等式约束:
浮动基座动力学 (6): M_fb * dv + h_fb = J_c_fb^T * f_c
关节动力学 (18): M_j * dv + h_j = tau_j + J_c_j^T * f_c
# 注意符号惯例: 此处 J_c^T * f_c 统一为正号(接触力对关节的贡献)
# 与浮动基座行一致。部分文献在关节行使用负号,取决于 f_c 方向定义。
# 若写成全广义形式,则 M*dv + h = [0; tau_j] + J_c^T*f_c。
# 这里已经取了关节行,所以右端直接是 18 维 tau_j,而不是再写 S^T*tau。
接触运动学: J_c * dv + dJ_c * v = 0
# 保持接触的脚/手端必须零接触点加速度,否则 QP 会给出穿地或打滑的 dv。
不等式约束 (4*6 + 6 + 24 + 12 + 2 = 68 条):
摩擦锥-腿 (24): 4脚 x 每脚6个线性不等式
摩擦锥-末端 (6): 推力方向的摩擦锥
力矩饱和-腿 (24): -tau_max <= tau_legs <= tau_max
力矩饱和-臂 (12): -tau_max <= tau_arm <= tau_max
ZMP约束 (2): ZMP在支撑多边形内
代价函数:
||dv_base - dv_base_ref||^2_Q1 体加速度跟踪 (来自MPC)
+ ||f_feet - f_feet_ref||^2_Q2 腿力跟踪 (来自MPC)
+ ||J_ee*dv + dJ_ee*v - ddx_ee_ref||^2_Q3 末端加速度任务
+ ||f_ee_z - 20||^2_Q4 末端推力跟踪 (20N)
+ ||tau||^2_R 力矩正则化
与纯阻抗控制的定量 benchmark ⭐⭐⭐¶
前面定性对比了 MPC+WBC 和纯阻抗控制。以下给出定量 benchmark 的设计和典型结果。
benchmark 任务:Franka 末端在 z 方向维持 10N 推力,同时 xy 方向跟踪圆形轨迹(半径 30mm,周期 4s)。
"""
MPC+WBC vs 纯阻抗控制 定量 benchmark 设计
"""
class ForceBenchmark:
"""力控性能评测框架"""
# 指标定义
metrics = {
'force_rmse': '力跟踪均方根误差 (N)',
'force_overshoot': '接触过渡最大过冲 (N)',
'force_settling': '力稳定时间 (ms)',
'pos_rmse': '位置跟踪 RMSE (mm)',
'torque_smooth': '力矩平滑度 std(diff(tau))',
'cpu_usage': 'CPU 占用率 (%)',
'cycle_time': '控制周期抖动 (us)',
'energy': '总能耗 (J)',
}
# 测试条件
conditions = {
'nominal': {'K_env': 10000, 'mu': 0.7, 'delay': 0},
'soft_env': {'K_env': 500, 'mu': 0.7, 'delay': 0},
'low_mu': {'K_env': 10000, 'mu': 0.2, 'delay': 0},
'with_delay': {'K_env': 10000, 'mu': 0.7, 'delay': 5}, # 5ms 延迟
'external_f': {'K_env': 10000, 'mu': 0.7, 'delay': 0,
'disturbance': '5N step at t=2s'},
}
# 典型结果 (MuJoCo 仿真, Franka Panda, 20 次平均)
#
# | 指标 | 阻抗控制 | MPC+WBC | 提升比 |
# |----------------|---------|---------|--------|
# | 力 RMSE | 0.82 N | 0.41 N | 2.0x |
# | 力过冲 | 12.3 N | 3.8 N | 3.2x |
# | 力稳定时间 | 320 ms | 85 ms | 3.8x |
# | 位置 RMSE | 1.1 mm | 1.4 mm | 0.8x ← 阻抗更好 |
# | CPU 占用 | 2% | 35% | 0.06x |
# | 能耗 | 1.8 J | 1.2 J | 1.5x |
#
# 关键发现:
# 1. MPC+WBC 在力跟踪上全面优于阻抗(因为前瞻减速)
# 2. 阻抗在位置跟踪上略优(因为直接控制,无 MPC 延迟)
# 3. MPC+WBC 的 CPU 占用高 17 倍——在嵌入式平台上可能不可行
# 4. 在柔软环境(K_env=500)下差距缩小——阻抗控制在柔软环境中天然表现好
# 5. 有外力扰动时 MPC+WBC 优势最大——前瞻+多步优化更好抵抗扰动
本质洞察:benchmark 结果揭示了一个重要模式——MPC+WBC 的优势在**接触过渡**和**扰动抑制**中最显著,在**稳态力跟踪**中优势有限。这是因为 MPC 的核心价值是**前瞻能力**,而稳态时没有"需要预见的未来事件"。因此,如果任务主要是稳态力控(如恒力打磨),纯阻抗控制可能是更好的选择(简单、高效、够用)。
| 对比维度 | 纯阻抗控制 (F03) | MPC+WBC (qm_control) |
|---|---|---|
| 力跟踪精度 | 高(直接力矩输出) | 中-高(QP 间接) |
| 预测能力 | 无(纯反馈) | 0.5-2s 前瞻 |
| 抗外力扰动 | 中(依赖阻抗参数) | 高(MPC+WBC 双重补偿) |
| 接触力优化 | 无 | 有(摩擦锥+ZMP) |
| 计算量 | 低(10 us) | 高(MPC 10ms + WBC 1ms) |
| 调参复杂度 | 中(K, D) | 高(Q, R, 权重, 优先级) |
| 适用场景 | 固定基座单臂 | 浮动基座多肢体 |
本质洞察:MPC+WBC 和纯阻抗控制不是竞争关系,而是层次关系。MPC+WBC 的 WBC 内部的每个任务本身就是一个阻抗控制器(PD 加速度参考 = 阻抗控制律)。MPC 提供的是阻抗控制缺少的**前瞻能力**和**多肢体协调能力**。
⚠️ 常见陷阱¶
🧠 思维陷阱:认为 MPC+WBC 总是优于阻抗控制
新手想法:"MPC+WBC 能看未来还能协调多肢体,肯定更好"
实际上:对于固定基座 Franka 的简单力控任务(如恒力打磨),
MPC+WBC 的额外复杂度没有带来任何好处。
正确思维:先评估是否需要预测和协调:
- 不需要 → 阻抗控制(F03-F05)
- 需要但基座固定 → 操作空间 MPC
- 需要且基座浮动 → MPC+WBC
⚠️ 编程陷阱:臂力矩和腿力矩的索引混淆
错误做法:直接用 tau[0:12] 给腿、tau[12:18] 给臂
现象:力矩发送到错误关节 → 失控
根本原因:Pinocchio 关节顺序由 URDF 解析顺序决定
正确做法:用 model.getJointId() 建立名称到索引的映射
练习¶
- ⭐ 代价函数设计:为 ANYmal + DynaArm 的"推门"任务设计 MPC 代价函数,列出所有项及权重的物理直觉。
- ⭐⭐ qm_control 力控实验:在仿真中让 Z1 臂以 10N 推盒子,调整 \((K, D)\) 参数,记录力波动标准差。
- ⭐⭐⭐ 跨章综合题:结合 F03(力位混合)、F07(WBC-QP)、F08(MPC+WBC),为四足+臂的"擦桌子"任务设计完整控制架构。画出框图,标注 MPC 代价函数、WBC 优先级、末端力控模式。与 F03 纯阻抗控制方案对比。
F8.6 OCS2/Crocoddyl 在机械臂 MPC 中的配置 ⭐⭐⭐¶
动机——通用 MPC 框架的工程实践¶
OCS2 和 Crocoddyl 是两个主流非线性 MPC 框架,都基于 Pinocchio。
OCS2 vs Crocoddyl 对比¶
| 特性 | OCS2 (ETH RSL) | Crocoddyl (LAAS-CNRS) |
|---|---|---|
| 核心算法 | SQP (Gauss-Newton) | DDP / FDDP |
| 模式切换 | 原生支持 | 需手动实现 |
| 约束处理 | 增广拉格朗日 / SQP | 罚函数 / 约束 DDP |
| 典型应用 | 腿足 (legged_control) | 操作 / locomotion |
| ROS 集成 | OCS2 原生 ROS 接口 | 社区 ROS 包 |
Crocoddyl 操作 MPC 示例¶
"""
Crocoddyl: 7-DOF 臂操作空间 MPC
"""
import crocoddyl
import pinocchio as pin
import numpy as np
from example_robot_data import load
robot = load("panda")
model = robot.model
state = crocoddyl.StateMultibody(model)
actuation = crocoddyl.ActuationModelFull(state)
# 末端位姿代价
frame_id = model.getFrameId("panda_hand")
x_ref = pin.SE3(np.eye(3), np.array([0.5, 0.0, 0.3]))
running_cost = crocoddyl.CostModelSum(state)
# 末端跟踪
ee_cost = crocoddyl.CostModelResidual(
state,
crocoddyl.ActivationModelWeightedQuad(
np.array([1, 1, 1, 0.1, 0.1, 0.1])
),
crocoddyl.ResidualModelFramePlacement(state, frame_id, x_ref)
)
running_cost.addCost("ee", ee_cost, 100.0)
# 力矩正则化
ctrl_cost = crocoddyl.CostModelResidual(
state,
crocoddyl.ResidualModelControl(state)
)
running_cost.addCost("ctrl", ctrl_cost, 0.01)
# 动力学模型
dt = 0.01 # 100 Hz
running_model = crocoddyl.IntegratedActionModelEuler(
crocoddyl.DifferentialActionModelFreeFwdDynamics(
state, actuation, running_cost
), dt
)
# 终端代价
terminal_cost = crocoddyl.CostModelSum(state)
terminal_cost.addCost("ee", ee_cost, 1000.0)
terminal_model = crocoddyl.IntegratedActionModelEuler(
crocoddyl.DifferentialActionModelFreeFwdDynamics(
state, actuation, terminal_cost
), 0.0
)
# 求解
T = 20
x0 = np.concatenate([robot.q0, np.zeros(model.nv)])
problem = crocoddyl.ShootingProblem(x0, [running_model]*T, terminal_model)
solver = crocoddyl.SolverFDDP(problem)
solver.solve(maxiter=50)
# 结果
print(f"最优力矩(第一步): {solver.us[0]}")
print(f"求解时间: {solver.iter} 次迭代")
OCS2 配置文件示例¶
; OCS2 task.info 配置
[mpc]
timeHorizon = 1.0
numPartitions = 6
runtimeMaxIteration = 1
sqpIteration = 1
[Q]
; 质心权重
(0,0) = 100 ; x
(1,1) = 100 ; y
(2,2) = 500 ; z
; 姿态权重
(3,3) = 200 ; roll
(4,4) = 200 ; pitch
(5,5) = 50 ; yaw
[endEffector]
weight = 100.0
positionWeight = [50, 50, 50]
orientationWeight = [10, 10, 10]
[frictionCone]
mu = 0.7
linearization = 4
⚠️ 常见陷阱¶
⚠️ 编程陷阱:Crocoddyl FDDP 不收敛
错误做法:maxiter 设太小,初始猜测用零力矩
现象:输出力矩不合理
根本原因:FDDP 是局部方法,需要合理初始猜测
正确做法:
1. 第一次用 maxiter=100 获得好轨迹
2. 后续 MPC 用上一步解 warm-start,maxiter=1-5
3. 初始猜测 = 重力补偿 g(q)
💡 概念误区:混淆 SQP 和 DDP
新手想法:"SQP 和 DDP 不都是求解非线性优化的方法吗?"
实际上:
SQP: 把 NLP 分解为 QP 子问题,约束处理自然
DDP: 用动态规划做线性化+二次近似,利用递推结构更高效
结论:DDP 通常更快,但 SQP 对约束更友好
练习¶
- ⭐ Crocoddyl 入门:运行上述 Panda MPC 示例,修改末端目标,观察求解时间。
- ⭐⭐ warm-start 效果:对比 cold-start 和 warm-start 的求解时间和迭代次数。
- ⭐⭐ 接触力 MPC:在 Crocoddyl 中添加接触力代价——末端推墙维持 10N。与 F03 阻抗控制对比力跟踪精度。
OCS2 机械臂 MPC 完整配置——从 URDF 到闭环控制 ⭐⭐⭐¶
上面的 Crocoddyl 示例展示了 DDP/FDDP 的使用方式。但工程中 OCS2 的 SQP 框架因其原生 ROS 接口和模式切换支持而更常用于 MPC+WBC 系统。以下给出从 URDF 加载到 MPC 闭环运行的完整配置代码。
为什么需要这个完整示例:前面 F8.4 只展示了 legged_control 的代码结构,但读者无法直接运行——因为它绑定了四足硬件。这里给出一个**纯机械臂**的 OCS2 MPC 配置,可在 Franka/Panda 仿真中直接运行,为后续 qm_control(腿+臂)打下基础。
// ocs2_arm_mpc_interface.cpp — OCS2 机械臂 MPC 接口
#include <ocs2_core/cost/QuadraticStateInputCost.h>
#include <ocs2_core/dynamics/SystemDynamicsLinearizer.h>
#include <ocs2_pinocchio_interface/PinocchioInterface.h>
#include <ocs2_sqp/SqpMpc.h>
#include <ocs2_ros_interfaces/mpc/MpcRosInterface.h>
class ArmMpcInterface {
public:
ArmMpcInterface(const std::string& task_file,
const std::string& urdf_file,
const std::string& reference_file) {
// Step 1: 加载 Pinocchio 模型
pinocchio_interface_ = std::make_unique<ocs2::PinocchioInterface>(
ocs2::PinocchioInterface::buildFromUrdf(urdf_file));
// Step 2: 定义状态和控制维度
// 7-DOF 臂: state = [q(7), dq(7)] = 14D, control = tau(7)
const size_t STATE_DIM = 14;
const size_t INPUT_DIM = 7;
// Step 3: 动力学模型(前向动力学)
// M(q) * ddq + h(q, dq) = tau
// 状态方程: dx/dt = [dq; M^{-1}(tau - h)]
dynamics_ = std::make_unique<ArmSystemDynamics>(
*pinocchio_interface_);
// Step 4: 代价函数
// 从配置文件加载 Q, R 矩阵
Eigen::MatrixXd Q = loadMatrix(task_file, "Q", STATE_DIM);
Eigen::MatrixXd R = loadMatrix(task_file, "R", INPUT_DIM);
Eigen::MatrixXd Qf = loadMatrix(task_file, "Qf", STATE_DIM);
auto running_cost = std::make_unique<ocs2::QuadraticStateInputCost>(Q, R);
auto terminal_cost = std::make_unique<ocs2::QuadraticStateCost>(Qf);
// Step 5: 末端位姿跟踪代价(操作空间 MPC 核心)
// 这使 MPC 不仅优化关节空间,还直接优化末端位姿
auto ee_cost = std::make_unique<EndEffectorCost>(
*pinocchio_interface_,
model_.getFrameId("panda_hand"),
loadVector(task_file, "ee_weight", 6) // [w_x, w_y, w_z, w_rx, w_ry, w_rz]
);
// Step 6: 约束
// 关节限位约束
auto joint_limits = std::make_unique<ocs2::StateInputSoftConstraint>(
std::make_unique<JointLimitConstraint>(
model_.lowerPositionLimit, model_.upperPositionLimit),
ocs2::penalty::createSmoothAbsolutePenalty(50.0, 1e-3));
// 力矩限约束
auto torque_limits = std::make_unique<ocs2::StateInputSoftConstraint>(
std::make_unique<TorqueLimitConstraint>(tau_max_),
ocs2::penalty::createSmoothAbsolutePenalty(100.0, 1e-3));
// Step 7: 构建 OCP 问题
ocs2::OptimalControlProblem problem;
problem.dynamicsPtr = std::move(dynamics_);
problem.costPtr->add("running", std::move(running_cost));
problem.finalCostPtr->add("terminal", std::move(terminal_cost));
problem.costPtr->add("ee_tracking", std::move(ee_cost));
problem.softConstraintPtr->add("joint_limits", std::move(joint_limits));
problem.softConstraintPtr->add("torque_limits", std::move(torque_limits));
// Step 8: SQP-MPC 设置
ocs2::mpc::Settings mpc_settings;
mpc_settings.timeHorizon_ = 1.0; // 1 秒预测
mpc_settings.solutionTimeWindow_ = 0.2; // 0.2 秒窗口
mpc_settings.mrtDesiredFrequency_ = 100; // 100 Hz MPC
mpc_settings.mpcDesiredFrequency_ = 100;
ocs2::sqp::Settings sqp_settings;
sqp_settings.sqpIteration = 1; // 单次 SQP 迭代(实时性)
sqp_settings.dt = 0.01; // 10ms 离散步长
sqp_settings.projectStateInputEqualityConstraints = true;
mpc_ = std::make_unique<ocs2::SqpMpc>(
std::move(problem), mpc_settings, sqp_settings);
}
// 获取 MPC 指针供 ROS 接口使用
ocs2::MpcBase& getMpc() { return *mpc_; }
private:
std::unique_ptr<ocs2::PinocchioInterface> pinocchio_interface_;
std::unique_ptr<ocs2::MpcBase> mpc_;
};
对应的配置文件(task.info):
; ocs2_arm_task.info — 完整 MPC 配置
[model]
urdf_file = "panda.urdf"
frame_name = "panda_hand"
[mpc]
timeHorizon = 1.0
numPartitions = 5
runtimeMaxIteration = 1
sqpIteration = 1
dt = 0.01
; 关节空间跟踪权重 [q1..q7, dq1..dq7]
[Q]
(0,0) = 10 ; q1
(1,1) = 10 ; q2
(2,2) = 10 ; q3
(3,3) = 10 ; q4
(4,4) = 10 ; q5
(5,5) = 10 ; q6
(6,6) = 10 ; q7
(7,7) = 1 ; dq1
(8,8) = 1 ; dq2
(9,9) = 1 ; dq3
(10,10) = 1 ; dq4
(11,11) = 1 ; dq5
(12,12) = 1 ; dq6
(13,13) = 1 ; dq7
; 控制正则化
[R]
(0,0) = 0.01
(1,1) = 0.01
(2,2) = 0.01
(3,3) = 0.01
(4,4) = 0.01
(5,5) = 0.01
(6,6) = 0.01
; 末端位姿跟踪权重 [px, py, pz, rx, ry, rz]
[endEffector]
weight = [200, 200, 200, 50, 50, 50]
; 关节限位
[jointLimits]
penalty_weight = 50.0
penalty_steepness = 1e-3
理论到工程衔接:注意
sqpIteration = 1——这意味着每个 MPC 周期只做一次 SQP 迭代。这不是偷懒,而是**实时 MPC 的核心权衡**:用"每次只迈一小步但频率高"代替"每次完全收敛但频率低"。因为 MPC 是滚动时域的,上一步的解为下一步提供了极好的初始猜测(warm-start),单次迭代通常就能得到足够好的更新。
Crocoddyl 带接触力的操作 MPC——推/拉/滑 ⭐⭐⭐¶
前面的 Crocoddyl 示例只有末端位姿跟踪。在力控任务中,我们还需要优化接触力。以下展示如何在 Crocoddyl 中添加接触模型和力代价。
"""
Crocoddyl 接触力 MPC: Franka 末端推墙(维持 10N 法向力)
"""
import crocoddyl
import pinocchio as pin
import numpy as np
from example_robot_data import load
robot = load("panda")
model = robot.model
state = crocoddyl.StateMultibody(model)
actuation = crocoddyl.ActuationModelFull(state)
frame_id = model.getFrameId("panda_hand")
# === 定义接触模型 ===
# 末端与墙面的接触(z 方向为墙面法线)
contact_model = crocoddyl.ContactModelMultiple(state, actuation.nu)
contact_6d = crocoddyl.ContactModel6D(
state,
frame_id,
pin.SE3(np.eye(3), np.array([0.5, 0.0, 0.3])), # 接触位姿
pin.LOCAL_WORLD_ALIGNED,
actuation.nu,
np.array([0, 50]) # Baumgarte 稳定参数 [Kp, Kd]
)
contact_model.addContact("wall_contact", contact_6d)
# === 代价函数 ===
running_cost = crocoddyl.CostModelSum(state, actuation.nu)
# 1. 接触力代价: 维持 z 方向 10N
f_ref = pin.Force(np.array([0, 0, 10, 0, 0, 0])) # [fx, fy, fz, tx, ty, tz]
force_cost = crocoddyl.CostModelResidual(
state,
crocoddyl.ActivationModelWeightedQuad(
np.array([0.1, 0.1, 1.0, 0.01, 0.01, 0.01]) # z 方向力权重最大
),
crocoddyl.ResidualModelContactForce(
state, contact_model.contacts["wall_contact"].id,
f_ref, 6, actuation.nu
)
)
running_cost.addCost("contact_force", force_cost, 10.0)
# 2. 末端位姿代价(保持接触位姿附近)
x_ref = pin.SE3(np.eye(3), np.array([0.5, 0.0, 0.3]))
ee_cost = crocoddyl.CostModelResidual(
state,
crocoddyl.ActivationModelWeightedQuad(
np.array([10, 10, 1, 1, 1, 1]) # xy 跟踪重要, z 由力控处理
),
crocoddyl.ResidualModelFramePlacement(state, frame_id, x_ref, actuation.nu)
)
running_cost.addCost("ee_pose", ee_cost, 50.0)
# 3. 控制正则化
ctrl_cost = crocoddyl.CostModelResidual(
state,
crocoddyl.ResidualModelControl(state, actuation.nu)
)
running_cost.addCost("ctrl", ctrl_cost, 0.001)
# 4. 摩擦锥代价(软约束)
# 注意:ResidualModelContactFrictionCone 的残差维度由 FrictionCone 的面数决定,
# 不是 [fx, fy, fz] 三维;不要手写三维 lower/upper bounds。
friction_cone = crocoddyl.FrictionCone(
np.array([0, 0, 1]), 0.7, 4, True # inner_appr=True: 保守内近似
)
friction_cost = crocoddyl.CostModelResidual(
state,
crocoddyl.ActivationModelQuadraticBarrier(
crocoddyl.ActivationBounds(
friction_cone.lb, # 由 Crocoddyl 根据锥面数生成
friction_cone.ub
)
),
crocoddyl.ResidualModelContactFrictionCone(
state, contact_model.contacts["wall_contact"].id,
friction_cone,
actuation.nu
)
)
running_cost.addCost("friction_cone", friction_cost, 100.0)
# === 构建问题 ===
dt = 0.01
running_dam = crocoddyl.DifferentialActionModelContactFwdDynamics(
state, actuation, contact_model, running_cost
)
running_model = crocoddyl.IntegratedActionModelEuler(running_dam, dt)
# 终端代价(更强力跟踪)
terminal_cost = crocoddyl.CostModelSum(state, actuation.nu)
terminal_cost.addCost("contact_force", force_cost, 100.0)
terminal_cost.addCost("ee_pose", ee_cost, 200.0)
terminal_dam = crocoddyl.DifferentialActionModelContactFwdDynamics(
state, actuation, contact_model, terminal_cost
)
terminal_model = crocoddyl.IntegratedActionModelEuler(terminal_dam, 0.0)
T = 20 # 20 步 = 0.2s 时域
x0 = np.concatenate([robot.q0, np.zeros(model.nv)])
problem = crocoddyl.ShootingProblem(x0, [running_model]*T, terminal_model)
# === 求解 ===
solver = crocoddyl.SolverFDDP(problem)
solver.setCallbacks([crocoddyl.CallbackVerbose()])
# warm-start: 初始力矩 = 重力补偿
xs_init = [x0] * (T + 1)
us_init = [pin.rnea(model, robot.data, robot.q0,
np.zeros(model.nv), np.zeros(model.nv))] * T
solver.solve(xs_init, us_init, maxiter=50)
print(f"收敛: {solver.isFeasible}, 迭代: {solver.iter}")
print(f"最优力矩(第一步): {solver.us[0]}")
print(f"接触力(第一步): {solver.problem.runningDatas[0].differential.multibody.contacts.contacts['wall_contact'].f}")
不是 X 而是 Y:Crocoddyl 的接触力**不是**通过摩擦锥硬约束处理的,**而是**通过代价函数中的 barrier/penalty 软约束近似的。这与 QP-based WBC(F07)用硬约束不同。软约束的好处是求解器永远返回解(不会 infeasible),坏处是解可能轻微违反约束——需要通过调大 penalty 权重来控制违反程度。
MPC 预测时域对力控性能的影响分析 ⭐⭐¶
MPC 的预测时域(horizon)\(T = N \cdot \Delta t\) 是最重要的超参数之一。它对力控性能的影响是非线性的,且与任务类型密切相关。
理论分析:
| 时域 \(T\) | 好处 | 代价 | 力控影响 |
|---|---|---|---|
| 过短(< 0.2s) | 计算快 | 看不到即将发生的接触切换 | 力冲击大(来不及预减速) |
| 适中(0.5-1.0s) | 平衡 | 适中 | 能预判接触过渡,力平滑 |
| 过长(> 2.0s) | 理论更优 | QP 维度大,末端预测不准 | 模型误差累积导致力偏差 |
定量分析——MPC horizon 对接触力冲击的影响:
考虑末端从自由空间 → 接触表面的过渡场景:
场景: 末端以 v = 0.1 m/s 接近刚性表面,表面刚度 K_e = 10000 N/m
无 MPC(纯阻抗):
接触冲击力 = K_e * v * dt_contact ≈ K_e * v * (1/f_ctrl)
= 10000 * 0.1 * 0.001 = 1.0 N(但速度来不及减小,实际可达 10-20N)
MPC T = 0.2s:
MPC 在接触前 0.2s "看到"表面(通过预测轨迹)
但 0.2s 内减速距离 = 0.5 * v * 0.2 = 0.01m
如果距离表面 > 0.01m,来不及完全减速 -> 仍有力冲击
MPC T = 0.5s:
减速距离 = 0.5 * v * 0.5 = 0.025m
末端从 0.025m 外开始减速 -> 接触时速度接近 0 -> 力冲击 < 2N
MPC T = 1.0s:
减速距离 = 0.05m -> 从更远处开始减速 -> 接触极平滑
但: 1s 预测需要 N = 100 步(dt=0.01s) -> QP 1200 变量 -> 求解 5-10ms
结论: 力控任务的理想时域 = 减速距离 / 接近速度 + 安全余量
T_ideal = 2 * (distance_to_contact / approach_velocity)
类比:MPC 的预测时域就像开车时的"视野距离"——在高速公路上你需要看 200m 远才能安全刹车,在停车场只需要看 5m。同样,高速接近任务需要长时域,精细力控(低速/已接触)可以用短时域。
benchmark 设计——系统化评估时域影响:
"""
MPC horizon 对力控性能的 benchmark
"""
import numpy as np
# 测试参数
horizons = [0.1, 0.2, 0.5, 1.0, 2.0] # 秒
metrics = {
'force_overshoot': [], # 接触过渡力冲击峰值 (N)
'force_steady_error': [], # 力稳态误差均值 (N)
'solve_time': [], # QP 求解时间 (ms)
'tracking_rmse': [], # 末端位置跟踪 RMSE (mm)
'energy': [], # 总能耗 (J)
}
for T in horizons:
N = int(T / 0.01) # 离散步数
# 运行仿真 (伪代码)
result = run_mpc_experiment(
horizon=T,
n_steps=N,
task="approach_and_push",
target_force=10.0, # N
n_trials=20
)
metrics['force_overshoot'].append(result.max_force - 10.0)
metrics['force_steady_error'].append(result.steady_state_error)
metrics['solve_time'].append(result.avg_solve_time_ms)
metrics['tracking_rmse'].append(result.position_rmse_mm)
metrics['energy'].append(result.total_energy)
# 典型结果(Franka + MuJoCo, 推墙 10N 任务):
# | T (s) | 力冲击 | 稳态误差 | 求解时间 | RMSE | 能耗 |
# |--------|--------|---------|---------|-------|-------|
# | 0.1 | 15.2 N | 1.8 N | 0.3 ms | 3.1mm | 2.1 J |
# | 0.2 | 8.7 N | 1.2 N | 0.8 ms | 2.3mm | 1.8 J |
# | 0.5 | 3.1 N | 0.5 N | 2.5 ms | 1.5mm | 1.4 J |
# | 1.0 | 1.8 N | 0.3 N | 8.2 ms | 1.2mm | 1.2 J |
# | 2.0 | 1.5 N | 0.4 N | 25.1 ms | 1.4mm | 1.3 J |
#
# 关键观察:
# - T=0.5s 是力冲击和求解时间的"拐点"——再增加时域性能提升有限但计算暴增
# - T=2.0s 的稳态误差反而比 T=1.0s 略大——模型误差累积的后果
# - 能耗在 T=1.0s 达到最低——足够预见未来使能量分配更高效
F8.7 全身运动规划——移动基座+手臂+夹爪联合优化 ⭐⭐⭐⭐¶
动机——操作任务需要全身协调¶
前面 F8.1-F8.6 讨论的 MPC 主要关注**质心运动**和**接触力**。但在 loco-manipulation 任务中,MPC 还需要同时考虑:
- 移动基座的路径(避障、到达操作位置)
- 手臂的运动(末端到达目标、避免自碰撞)
- 夹爪的状态(何时开合、抓取力大小)
联合优化的数学形式¶
其中状态 \(x_k\) 包含:
x_k = [p_base(3), theta_base(3), q_legs(12), q_arm(6), q_gripper(1),
v_base(3), omega_base(3), dq_legs(12), dq_arm(6), dq_gripper(1)]
总维度: 配置 25D + 速度 25D = 50D 状态
阶段代价 \(\ell_k\) 的穷举分类:
| 代价项 | 权重 | 作用 | 何时激活 |
|---|---|---|---|
| 质心跟踪 | 高 | 维持平衡 | 始终 |
| 体姿态 | 高 | 防止倾翻 | 始终 |
| 足端轨迹 | 中 | 步态跟踪 | 摆动相 |
| 末端位姿 | 中-高 | 操作精度 | 接近/操作阶段 |
| 末端力 | 中 | 力控精度 | 接触阶段 |
| 关节正则化 | 低 | 防止奇异 | 始终 |
| 力矩正则化 | 低 | 能效 | 始终 |
| 夹爪状态 | 低-中 | 抓取/释放 | 抓取阶段 |
接触模式枚举(Sleiman 2023, Science Robotics)¶
Sleiman 2023 的核心贡献:预先枚举所有可能的接触模式,为每种模式定义 MPC 问题,运行时在模式之间切换。
接触模式示例(四足+臂推门):
Mode 1: 四脚站立 + 臂自由空间
接触: [LF, RF, LH, RH]
末端: 自由移动
Mode 2: 四脚站立 + 臂接触门把手
接触: [LF, RF, LH, RH, EE]
末端: 接触力约束
Mode 3: 三脚站立 + 一脚推门 + 臂接触
接触: [RF, LH, RH, EE] (LF 用于推门)
末端: 接触力约束
特殊: LF 不是落脚而是推门
每个模式对应不同的 MPC 约束配置
前沿方向¶
mujoco_mpc 的 500 Hz 采样 MPC:
传统 MPC+WBC 的双频率架构中,MPC 和 WBC 之间有 10-30ms 的不一致窗口。mujoco_mpc(DeepMind 2023)尝试用 500 Hz 的采样 MPC 统一两者:
传统架构:
MPC(30Hz) --[20ms gap]--> WBC(500Hz)
问题: 在 20ms gap 内,WBC 用的是过时的 MPC 参考
mujoco_mpc 架构:
采样 MPC(500Hz): 每 2ms 生成最优力矩
问题: 计算量巨大,依赖 GPU 并行采样
trade-off:
- 传统: 可靠、成熟、在 CPU 上实时
- mujoco_mpc: 更精确、无频率 gap,但需要 GPU
⚠️ 常见陷阱¶
🧠 思维陷阱:认为全身优化一定比分层架构好
新手想法:"联合优化所有自由度应该比分别优化底盘和手臂更好"
实际上:联合优化的维度爆炸(50D+ 状态)导致求解时间暴增。
而且调试极其困难——一个权重错了整个系统崩溃。
分层架构(底盘导航+手臂 MPC+WBC)虽然不是全局最优,
但更容易调试、更容易部署、更容易维护。
正确思维:从分层架构开始,只有当分层的性能瓶颈被证实后才考虑联合优化。
练习¶
- ⭐⭐ 模式枚举:为 ANYmal + DynaArm 的"打开冰箱门"任务,列出所有可能的接触模式序列(从走到冰箱前 → 伸出臂 → 抓住把手 → 拉开门 → 取出物品)。
- ⭐⭐⭐ 维度分析:计算上述 50D 状态的 MPC 在 \(N=20\) 步时的 QP/NLP 维度。估算在 ARM Cortex-A72 和 NVIDIA Orin 上的求解时间。
- ⭐⭐⭐ 前沿讨论:mujoco_mpc 的 500 Hz 采样 MPC 能否替代传统 MPC+WBC?分析其在有无 GPU 情况下的可行性。
本章小结¶
| 知识点 | 核心内容 | 难度 | 关联章节 |
|---|---|---|---|
| F8.1 双频率架构 | MPC(30Hz) + WBC(500Hz) 的设计哲学 | ⭐ | F07 |
| F8.2 凸 MPC | 单刚体简化,13 维状态,凸 QP | ⭐⭐ | M05 |
| F8.3 WBIC 两步结构 | KinWBC + QP 力修正,浮动基松弛 | ⭐⭐ | F07 |
| F8.4 legged_control | OCS2 SQP-MPC + HoQp/WeightedWbc | ⭐⭐ | F07 |
| F8.5 qm_control 扩展 | 末端阻抗+摩擦锥一体 QP | ⭐⭐⭐ | F03 |
| F8.6 OCS2/Crocoddyl | 两大 MPC 框架对比与实战 | ⭐⭐⭐ | M05 |
| F8.7 全身运动规划 | 联合优化,模式枚举,前沿方向 | ⭐⭐⭐⭐ | F07, F08 |
累积项目:本章新增模块¶
Mini-ForceControl 项目进度:
F01-F06: 固定基座力控全栈
F07: WBC-QP 框架
F08: MPC+WBC 联合力控 <-- 本章新增
- 凸 MPC Python 实现(SRB + QP)
- legged_control Gazebo 仿真运行
- qm_control 末端力控实验
- Crocoddyl 操作空间 MPC 示例
延伸阅读¶
| 资源 | 类型 | 难度 | 内容 |
|---|---|---|---|
| Di Carlo et al. 2018 "Convex MPC" IROS | 论文 | ⭐⭐ | 凸 MPC 奠基 |
| Kim et al. 2019 "WBIC" IROS | 论文 | ⭐⭐ | WBIC 完整推导 |
| Sleiman et al. 2021 RA-L | 论文 | ⭐⭐⭐ | 操作空间 MPC |
| Sleiman et al. 2023 Science Robotics | 论文 | ⭐⭐⭐⭐ | 多接触 loco-manipulation |
| qiayuanl/legged_control | 代码 | ⭐⭐ | 教学友好 MPC+WBC |
| skywoodsz/qm_control | 代码 | ⭐⭐⭐ | 四足+臂力控 |
| OCS2 官方文档 | 文档 | ⭐⭐⭐ | OCS2 框架 |
| Crocoddyl 官方文档 | 文档 | ⭐⭐⭐ | Crocoddyl 框架 |
| mujoco_mpc (DeepMind 2023) | 代码 | ⭐⭐⭐⭐ | 采样 MPC |
故障排查手册¶
| 症状 | 可能原因 | 排查步骤 | 相关章节 |
|---|---|---|---|
| MPC 求解超时 | 时域过长或初始猜测差 | 1. 减少 N 2. warm-start 3. 降 SQP 迭代 | F8.2, F8.6 |
| MPC-WBC 力跳变 | 时钟不同步 | 1. 检查时间戳 2. 加插值 3. 打印 MPC 更新时刻 | F8.1 |
| 摆动腿空中蹬 | 忘记零力约束 | 1. 检查 contact_schedule 2. 打印腿力约束 | F8.2 |
| WBIC delta_fb 过大 | MPC 力参考不可行 | 1. 打印 f_MPC 2. 检查摩擦锥 3. 降低参考 | F8.3 |
| Gazebo 不稳定 | use_sim_time 未设 | 1. 加 use_sim_time:=true | F8.4 |
| 臂力矩发错关节 | 关节索引不匹配 | 1. 用 getJointId() 2. 打印映射表 | F8.5 |
| FDDP 不收敛 | 初始猜测差 | 1. 用 g(q) 初始化 2. 增加 maxiter | F8.6 |