- 写在前面
- 1. OCS2 是什么
- 2. MPC 在 OCS2 里解决什么问题
- 3. 先看整体结构图
- 4. OCS2 的 MPC 外壳:MPC_BASE
- 5. MPC 的关键参数:MPC_Settings
- 6. DDP 版本 MPC:GaussNewtonDDP_MPC
- 7. 四足机器人例子:LeggedRobotDdpMpcNode
- 8. SQP 版本 MPC:LeggedRobotSqpMpcNode
- 9. LeggedRobotInterface 到底做了什么
- 10. MPC 和 MRT 的关系
- 11. ROS 接口代码:MPC_ROS_Interface
- 12. SLQ / iLQR 大概在做什么
- 13. SolverBase:求解器统一接口
- 14. 学习时应该优先读哪些文件
- 15. 一个推荐的理解顺序
- 总结
写在前面
这篇是 OCS2 里 MPC 的学习笔记,主要参考 OCS2 官方文档和公开代码结构来梳理。
目标不是把所有源码细节一次性讲完,而是先回答几个核心问题:
- OCS2 里的 MPC 在代码里从哪里开始?
MPC_BASE做了什么?GaussNewtonDDP_MPC和SLQ/iLQR是什么关系?- 四足机器人例子里 MPC node 是怎么启动的?
- MPC 求出来的轨迹又是怎么给控制循环使用的?
1. OCS2 是什么
OCS2 的全称可以理解为:
Optimal Control for Switched Systems |
它是 ETH/Legged Robotics 开源的 C++ 最优控制工具箱,主要面向机器人这类复杂系统。
官方文档里提到,OCS2 支持多种算法:
SLQ:continuous-time domain constrained DDP。iLQR:discrete-time domain constrained DDP。SQP:基于 multiple shooting 和 HPIPM。IPM:非线性内点法。SLP:Sequential Linear Programming。
同时它还提供:
- 动力学建模接口。
- 代价函数接口。
- 约束接口。
- 自动微分。
- ROS 接口。
- Pinocchio / URDF 相关机器人建模工具。
所以 OCS2 不是“一个 MPC 算法”,而是一套用于构建和求解最优控制问题的框架。
2. MPC 在 OCS2 里解决什么问题
MPC,也就是 Model Predictive Control,核心思想是:
每隔一小段时间,根据当前状态,重新求解一个有限时域最优控制问题,只执行最前面一小段控制,然后继续滚动。
假设当前时间是 ,预测时域长度是 ,那么每次 MPC 求解的问题大概是:
约束包括系统动力学:
以及各种路径约束:
对于四足机器人来说,约束可能包括:
- 支撑脚接触约束。
- 摆动脚轨迹约束。
- 摩擦锥约束。
- 接触力约束。
- 零速度约束。
- 关节或输入限制。
MPC 的滚动过程可以写成:
读取当前状态 x(t) |
3. 先看整体结构图
最重要的主线是:
配置文件 / URDF / reference |
4. OCS2 的 MPC 外壳:MPC_BASE
核心代码位置:
ocs2_mpc/include/ocs2_mpc/MPC_BASE.h |
MPC_BASE 是 OCS2 MPC 的基类。它的作用不是求解具体最优控制问题,而是提供 MPC 的统一外壳。
核心接口大概是:
class MPC_BASE { |
最关键的是 run()。
源码逻辑可以简化为:
bool MPC_BASE::run(scalar_t currentTime, const vector_t& currentState) { |
也就是说:
MPC_BASE::run() |
所以 MPC_BASE 很薄,但它定义了 OCS2 里 MPC 的共同接口。
4.1 run() 逐行看
MPC_BASE::run() 是理解 OCS2 MPC 的第一段关键代码。可以把源码简化成下面这样:
bool MPC_BASE::run(scalar_t currentTime, const vector_t& currentState) { |
逐行理解:
if (!initRun_ && currentTime >= getSolverPtr()->getFinalTime()) |
这句是在检查:当前时间是不是已经超过上一次求解器给出的最终时间。
如果 MPC 上一次只优化到 5.0 秒,但当前机器人观测已经到了 5.1 秒,那么控制器就没有可用的未来轨迹了。这种情况下继续使用旧策略是不安全的,所以直接返回 false。
const scalar_t finalTime = currentTime + mpcSettings_.timeHorizon_; |
这句定义本轮 MPC 的优化终点:
例如:
currentTime = 12.3 s |
本轮优化就是在 [12.3, 13.3] 上重新规划。
calculateController(currentTime, currentState, finalTime); |
这句是最核心的调用。MPC_BASE 不知道你到底用 SLQ、iLQR、SQP 还是 IPM,所以它只调用一个抽象函数。真正怎么求解,由子类实现。
initRun_ = false; |
第一次 MPC 运行之后,initRun_ 被置为 false。后续 MPC 会开始检查当前时间是否超过 solver 的 final time。
所以 run() 的本质就是:
检查当前策略是否还覆盖当前时间 |
4.2 为什么 calculateController() 是虚函数
MPC_BASE 里有这一句:
virtual void calculateController( |
这是一个纯虚函数,说明 MPC_BASE 只定义接口,不定义具体算法。
原因是 OCS2 想支持多种 MPC 后端:
MPC_BASE |
这样设计的好处是:
- ROS 接口不需要关心底层求解器。
- 四足机器人建模可以复用。
- 同一个
OptimalControlProblem可以换不同 solver。 - MPC 的滚动调用逻辑统一。
换句话说:
MPC_BASE管“什么时候求解”,具体子类管“怎么求解”。
5. MPC 的关键参数:MPC_Settings
代码位置:
ocs2_mpc/include/ocs2_mpc/MPC_Settings.h |
核心结构体:
struct Settings { |
几个参数很重要:
| 参数 | 含义 |
|---|---|
timeHorizon_ |
MPC 每次向未来优化多久 |
solutionTimeWindow_ |
从优化结果里取多长时间窗口 |
debugPrint_ |
是否打印 MPC 调试信息 |
coldStart_ |
是否每次重置求解器,冷启动求解 |
mpcDesiredFrequency_ |
MPC 循环频率,主要用于测试/仿真 loop |
mrtDesiredFrequency_ |
MRT/跟踪控制循环频率 |
其中最关键的是:
timeHorizon_ |
它决定本轮优化区间:
比如当前时间是 3.2 s,timeHorizon_ = 1.0,那么本轮 MPC 求解区间就是:
6. DDP 版本 MPC:GaussNewtonDDP_MPC
代码位置:
ocs2_ddp/include/ocs2_ddp/GaussNewtonDDP_MPC.h |
这个类继承 MPC_BASE:
class GaussNewtonDDP_MPC final : public MPC_BASE |
构造函数里,根据配置选择 SLQ 或 ILQR:
switch (ddpSettings.algorithm_) { |
核心求解逻辑:
void calculateController( |
所以它的结构是:
GaussNewtonDDP_MPC |
运行过程是:
MPC_BASE::run() |
6.1 构造函数在选择求解器
GaussNewtonDDP_MPC 的构造函数里最关键的是这段:
switch (ddpSettings.algorithm_) { |
这里有几个对象特别重要:
| 对象 | 作用 |
|---|---|
ddpSettings |
DDP/SLQ/iLQR 的迭代次数、容差、算法类型等参数 |
rollout |
给定控制策略后,向前积分系统动力学,生成状态轨迹 |
optimalControlProblem |
动力学、代价、约束的总定义 |
initializer |
当还没有可用控制器时,用来初始化状态/输入轨迹 |
其中 rollout 很关键。DDP 类算法需要不断做:
给一条控制轨迹 |
所以如果没有 rollout,求解器就没法知道当前控制策略会把系统带到哪里。
6.2 calculateController() 真正触发求解
GaussNewtonDDP_MPC 里真正触发求解的是:
void calculateController( |
这里最值得注意的是 coldStart_。
如果:
coldStart_ = true; |
每次 MPC 都会先:
ddpPtr_->reset(); |
这表示不要用上一轮结果热启动,而是重新开始。
如果:
coldStart_ = false; |
通常可以复用上一轮的控制策略和轨迹作为初值,这就是 MPC 里常说的 warm start。
为什么 warm start 有用?因为相邻两次 MPC 的优化问题通常很像:
第 k 次:求 [t, t+T_h] |
两次时间窗口只平移了一点点,上一轮解通常是下一轮很好的初值,可以减少迭代次数,提高实时性。
一句话:
GaussNewtonDDP_MPC本身不直接写 SLQ 细节,它只是把 MPC 的时间窗口交给ddpPtr_->run()。
7. 四足机器人例子:LeggedRobotDdpMpcNode
代码位置:
ocs2_robotic_examples/ocs2_legged_robot_ros/src/LeggedRobotDdpMpcNode.cpp |
这个文件是四足机器人 DDP-MPC 的 ROS 入口。
关键逻辑可以简化为:
LeggedRobotInterface interface(taskFile, urdfFile, referenceFile); |
这段代码说明了几件事:
LeggedRobotInterface负责把机器人问题组装好。GaussNewtonDDP_MPC只是拿到这些组件后开始求解。RosReferenceManager负责接收参考轨迹。GaitReceiver负责接收/同步步态信息。MPC_ROS_Interface负责把 MPC 包装成 ROS node。
因此四足机器人的 MPC 不是直接散落在一个巨大 main 函数里,而是拆成了几个层次:
ROS node |
7.1 main() 入口逐段看
四足机器人 DDP-MPC 的入口代码可以拆成几段看。
第一段:初始化 ROS 和读取参数。
::ros::init(argc, argv, robotName + "_mpc"); |
这几个参数非常重要:
| 参数 | 作用 |
|---|---|
taskFile |
任务配置,通常包含 MPC、DDP、代价权重、约束等参数 |
urdfFile |
机器人模型文件 |
referenceFile |
参考轨迹或默认目标配置 |
multiplot |
是否开启一些调试观测量发布 |
第二段:创建机器人接口。
LeggedRobotInterface interface(taskFile, urdfFile, referenceFile); |
这行看起来简单,但背后做了很多事:
读取配置 |
可以把 interface 理解成“四足机器人最优控制问题生成器”。
第三段:创建 gait receiver。
auto gaitReceiverPtr = std::make_shared<GaitReceiver>( |
腿式机器人和普通机械臂不一样,它有接触模式切换:
哪只脚支撑? |
GaitReceiver 就是用来接收步态命令,并把 gait schedule 同步给求解器。
第四段:创建 ROS ReferenceManager。
auto rosReferenceManagerPtr = std::make_shared<RosReferenceManager>( |
ReferenceManager 管理:
目标轨迹 TargetTrajectories |
比如你给机器人发送目标速度或目标位姿,这些参考信息最终要进入最优控制问题的 cost / constraints。
第五段:创建 MPC。
GaussNewtonDDP_MPC mpc( |
这行代码把前面准备好的组件全部交给 MPC:
mpcSettings → MPC 时间窗口、频率、冷/热启动 |
第六段:把 reference manager 和 gait receiver 挂到 solver 上。
mpc.getSolverPtr()->setReferenceManager(rosReferenceManagerPtr); |
这里的 getSolverPtr() 返回底层 solver,比如 SLQ/iLQR solver。
这两句的意思是:
求解前后,solver 要同步最新参考轨迹和步态信息。 |
第七段:启动 ROS MPC node。
MPC_ROS_Interface mpcNode(mpc, robotName); |
MPC_ROS_Interface 负责:
订阅当前机器人状态 observation |
所以整个 main() 可以浓缩成:
读参数 |
8. SQP 版本 MPC:LeggedRobotSqpMpcNode
代码位置:
ocs2_robotic_examples/ocs2_legged_robot_ros/src/LeggedRobotSqpMpcNode.cpp |
它和 DDP 版本非常像,只是 MPC 类型换成了 SqpMpc:
SqpMpc mpc( |
然后也是:
mpc.getSolverPtr()->setReferenceManager(rosReferenceManagerPtr); |
这说明 OCS2 的设计是:
同一个机器人问题 OptimalControlProblem |
也就是说,MPC 外壳和问题建模相对独立,求解器可以替换。
9. LeggedRobotInterface 到底做了什么
LeggedRobotInterface 是学习四足机器人 OCS2 MPC 时最值得重点看的部分。
它大致负责:
读取 taskFile / urdfFile / referenceFile |
可以把它理解成:
机器人业务建模层。
相比 MPC_BASE,它更接近机器人控制真正需要关注的内容。
对四足机器人来说,它通常会涉及:
动力学 dynamics
例如质心动力学:
其中状态 可能包含:
- base pose。
- base velocity。
- joint position。
- joint velocity。
输入 可能包含:
- contact force。
- joint velocity / torque 相关量。
- foot placement 相关控制量。
具体取决于 OCS2 legged robot 例子的建模方式。
代价 cost
常见代价包括:
- base 位置跟踪。
- base 姿态跟踪。
- 速度跟踪。
- 输入惩罚。
- 足端位置惩罚。
- terminal cost。
形式上常见为二次型:
约束 constraints
四足机器人 MPC 最关键的是接触约束,例如:
- 支撑脚不能滑。
- 摆动脚不能穿地。
- 接触力满足摩擦锥。
- 支撑腿足端速度为 0。
- 接触切换按照 gait schedule 发生。
这些约束决定了 MPC 的可行性和运动质量。
ReferenceManager 和 GaitSchedule
腿式机器人是 switched system,因为不同时间段接触模式不同。
例如:
左前脚支撑 / 摆动 |
这些接触模式随时间切换,因此 OCS2 特别适合这类系统。
10. MPC 和 MRT 的关系
OCS2 里经常看到两个概念:
MPC: Model Predictive Control |
可以粗略理解为:
MPC 低频求解未来轨迹。 |
例如:
MPC: 20 Hz |
MPC 求出的是一段未来轨迹和反馈策略:
x*(t), u*(t), K(t) |
MRT 在控制循环中根据当前时间插值,得到当前要执行的控制:
这里:
- :前馈控制。
- :反馈增益。
- :优化出来的参考状态。
- :机器人当前真实状态。
这点很重要:
OCS2 的 MPC 不是简单开环播放轨迹,而是可以输出带反馈的控制策略。
11. ROS 接口代码:MPC_ROS_Interface
代码位置:
ocs2_ros_interfaces/include/ocs2_ros_interfaces/mpc/MPC_ROS_Interface.h |
MPC_ROS_Interface 是 MPC 和 ROS 系统之间的桥。它自己不负责求解最优控制问题,而是负责通信和触发。
头文件里有几个关键成员:
MPC_BASE& mpc_; |
这些成员对应:
| 成员 | 作用 |
|---|---|
mpc_ |
真正的 MPC 对象,比如 GaussNewtonDDP_MPC |
mpcObservationSubscriber_ |
订阅机器人当前观测 |
mpcTargetTrajectoriesSubscriber_ |
订阅目标轨迹 |
mpcPolicyPublisher_ |
发布优化得到的控制策略 |
mpcResetServiceServer_ |
提供重置 MPC 的 ROS service |
11.1 接收 observation 后怎么触发 MPC
最关键的回调函数是:
void MPC_ROS_Interface::mpcObservationCallback( |
这段代码非常关键,它说明 OCS2 MPC 的触发源是 observation:
收到当前状态 observation |
也就是说,真实运行时不是某个 while 循环直接不停调用 MPC,而是 ROS observation callback 在驱动 MPC 更新。
11.2 优化结果怎么发布出去
MPC 求解完成后,会调用:
copyToBuffer(currentObservation); |
其中核心逻辑是:
scalar_t finalTime = mpcInitObservation.time + mpc_.settings().solutionTimeWindow_; |
这里的 PrimalSolution 可以理解为 MPC 的主要优化结果,里面通常包含:
timeTrajectory |
然后发布线程会调用:
createMpcPolicyMsg( |
这个函数会把 C++ 内部数据打包成 ROS 消息:
ocs2_msgs::mpc_flattened_controller |
其中包括:
initObservation |
最后:
mpcPolicyPublisher_.publish(mpcPolicyMsg); |
所以 MPC 的输出链路是:
solver 内部 PrimalSolution |
这部分代码对于理解 OCS2 很重要,因为它回答了一个工程问题:
MPC 求解器算出来的 C++ 对象,怎么变成控制端能用的 ROS 消息?
12. SLQ / iLQR 大概在做什么
SLQ / iLQR / DDP 的核心思想是:
把非线性最优控制问题,在当前轨迹附近近似成一系列时变 LQR 问题,然后迭代改进轨迹。
大概流程:
给一条初始轨迹 |
动力学线性化:
代价二次近似:
然后求出局部控制律:
在 MPC 语境里,求解器每次都在当前状态附近重新做这个过程。
13. SolverBase:求解器统一接口
代码位置:
ocs2_oc/include/ocs2_oc/oc_solver/SolverBase.h |
SolverBase 是底层求解器的统一接口。MPC 不直接关心 SLQ、SQP、IPM 的所有细节,而是通过这个接口拿结果。
几个重要函数:
void run(scalar_t initTime, |
这是求解器主入口。MPC 最终会调用到这里。
virtual scalar_t getFinalTime() const = 0; |
返回当前 solver 优化到的最终时间。MPC_BASE::run() 会用它检查当前时间是否已经超过已有策略覆盖范围。
virtual void getPrimalSolution( |
获取优化结果。ROS 接口发布策略时会调用它。
void setReferenceManager( |
设置参考轨迹管理器。四足机器人例子里就是:
mpc.getSolverPtr()->setReferenceManager(rosReferenceManagerPtr); |
void addSynchronizedModule( |
添加需要和 solver 同步的模块,例如 gait receiver。
所以 SolverBase 在架构里的角色是:
MPC_BASE |
从工程角度看,这种接口设计让 ROS 层、MPC 层和具体 solver 层解耦。
14. 学习时应该优先读哪些文件
我建议按层次读。
第一层:MPC 外壳
ocs2_mpc/include/ocs2_mpc/MPC_BASE.h |
目标:理解 OCS2 的 MPC 如何滚动调用求解器。
第二层:DDP-MPC 包装
ocs2_ddp/include/ocs2_ddp/GaussNewtonDDP_MPC.h |
目标:理解 SLQ / iLQR 如何被封装成 MPC。
第三层:四足机器人 ROS 入口
ocs2_robotic_examples/ocs2_legged_robot_ros/src/LeggedRobotDdpMpcNode.cpp |
目标:理解实际例子如何创建并启动 MPC node。
第四层:机器人问题建模
ocs2_robotic_examples/ocs2_legged_robot |
重点找:
LeggedRobotInterface |
目标:理解机器人问题如何变成 OCS2 可以求解的 optimal control problem。
15. 一个推荐的理解顺序
如果你刚开始学,我建议不要一上来就钻 Riccati 方程。可以先从代码结构入手:
第一步:看 MPC_BASE::run()
记住一句话:
run() 根据当前时间和状态,计算 finalTime,然后调用 calculateController()。 |
第二步:看 GaussNewtonDDP_MPC
记住一句话:
GaussNewtonDDP_MPC 把 SLQ / iLQR 包成 MPC。 |
第三步:看 LeggedRobotDdpMpcNode.cpp
记住一句话:
这个文件展示了四足机器人例子如何创建 interface、mpc、reference manager、gait receiver 和 ROS node。 |
第四步:看 LeggedRobotInterface
记住一句话:
这里才是机器人建模的核心:动力学、代价、约束、参考、步态都在这里汇总。 |
第五步:再补 SLQ / iLQR 原理
这个时候再看:
- 线性化。
- 二次近似。
- Riccati backward pass。
- rollout forward pass。
- line search。
- constraint handling。
会更容易理解。
总结
OCS2 里的 MPC 可以概括为:
用
MPC_BASE做滚动时域外壳,用SLQ/iLQR/SQP/IPM做单次最优控制求解器,用OptimalControlProblem描述机器人动力学、代价和约束,再通过 ROS/MRT 把优化轨迹变成实际控制输入。
最重要的代码主线是:
LeggedRobotDdpMpcNode.cpp |
如果只是为了入门,先别被算法细节吓到。先把代码结构和数据流看懂:
当前状态从哪里来? |
这些问题想清楚后,再深入 DDP、SQP、约束处理和腿式机器人的接触切换,会顺很多。