做四足机器人控制这两年,我身边不少人都是被同一个问题拦住的:论文里MPC(模型预测控制)的数学推导看得懂,GitHub上OCS2仓库也找到了,可真到在自己机器上跑起来,第一步编译装环境就能卡一个星期。更不用说OCS2封装了大量最优控制的底层逻辑,把urdf模型导进去、用Pinocchio做动力学解算、再和Gazebo或真实机器人对接,每一步都有看不见的坑。
这篇文章想把“用OCS2工具箱搭建四足机器人MPC控制器”这条路完整走一遍,重点放在标题里已经点名的Pinocchio配置上,以及从零到能跑通MPC的完整流程。内容覆盖:OCS2在整个四足控制系统中负责什么、环境怎么搭、Pinocchio怎么装怎么链、MPC控制器怎么初始化、参数怎么调。对已经看过一些MPC理论但被工程实现劝退的人,应该能省下不少走弯路的时间。
1. OCS2和MPC在四足控制里的角色定位
1.1 为什么四足机器人需要MPC,而不是一套固定步态
先聊一个最基本的问题:四足机器人为什么非要上MPC?
早年的四足控制多半是“规划一条固定轨迹,底层再用PID或者计算力矩去跟踪”。这套思路在平整地面上没问题,走到粗糙地形、遇到外力推搡、走过草丛这类场景就开始露馅:因为轨迹是事先算好的,机器人脚下突然多了块石头或者身体被撞偏了,控制器不会主动重新规划未来的运动,只能被动纠偏,结果要么越纠越晃,要么直接摔。
MPC的做法完全不一样。它每一控制周期都在做三件事:用当前状态做初值,基于动力学模型预测未来一段时间(几秒甚至零点几秒)的运动,在这段预测里求解一个最优控制问题,得到当前这一步该给的力矩。等下一时刻状态更新了,再重新预测、重新求解,这就是“滚动优化”。说白了,MPC不是瞄准一个固定终点硬跟,而是走一步看一步,每次都在根据实际情况重算接下来的路。
打个比方,传统控制像开车时只盯着眼前五米,压到坑了才猛打方向盘;MPC像开车时看着前方五十米的道路,提前规划好方向盘的转动,遇到突发情况再随时更新路线。这个“提前量”和“在线重算”的能力,正好命中四足机器人在非结构化地形上需要的动态稳定性。
OCS2正是为这类问题设计的开源工具箱。它的全称是Optimal Control for Switched Systems,由苏黎世联邦理工(ETH Zurich)机器人系统实验室开源。这名字里的“Switched Systems”不是白叫的——四足机器人跑步时脚与地面周期性接触,本质上就是一个混成系统,支撑相和摆动相不断切换,OCS2的处理方式正好契合这种切换结构。官方仓库里已经带了四足机器人相关的模块,不需要从零去写SQP求解器和约束处理,这也是我最终选定OCS2而不是自己撸轮子的原因。
1.2 OCS2的功能组成:最优控制工具箱与四足专用模块
OCS2不是一个单一大包,而是一组分工明确的模块。核心求解部分包括ocs2_core、ocs2_mpc、ocs2_sqp,这些负责最优控制问题的描述、求解和滚动实现;外围则有ocs2_pinocchio_interface、ocs2_robotic_tools这类与机器人模型和仿真打交道的部分;再往上还有针对具体机器人形态的模块,比如四足相关的ocs2_quadruped_*系列。
实际搭建控制器时,你打交道最多的是这几块:
ocs2_quadruped_interface:负责从urdf模型和配置文件里读出机器人信息,建立统一的模型接口,把Pinocchio的动力学计算结果暴露给MPC求解器。ocs2_quadruped_mpc:直接实现四足MPC的循环,包括状态更新、目标轨迹更新、MPC求解、控制指令输出。ocs2_pinocchio_interface:这是OCS2和Pinocchio之间的桥梁。它把Pinocchio的刚体动力学计算封装成OCS2想要的系统动态函数,让MPC在预测阶段能算“给这个力矩,下一步机器人会怎么动”。ocs2_mpc和ocs2_sqp:底层优化求解器。四足MPC默认走SQP路线(序列二次规划),把非线性最优控制问题拆成一系列二次规划子问题迭代求解。
所以“用OCS2搭建四足MPC控制器”这件事,本质上是:用Pinocchio提供动力学数学工具,OCS2提供最优控制的求解框架,你再把自家机器人的urdf、关节配置、MPC参数填进去,得到一个可以迭代优化的控制律。
1.3 OCS2与MPPI、传统工业MPC的差异
热词里出现了“mppi mpc”,也出现了“霍尼韦尔mpc控制”。这两类和OCS2里的MPC都不是一个东西。做了几年控制的人可能对霍尼韦尔这类工业DCS上的MPC更熟悉,那是用在化工、炼油这类慢流程上的模型预测控制器,采样周期以秒、分钟计;而机器人上的MPC采样周期通常是毫秒级,二者虽然同源于预测控制思想,但工程形态差别很大,千万不要拿工业MPC配置软件的心态来对待OCS2。
MPPI(Model Predictive Path Integral)则是另一种求解MPC的思路,不用解析梯度,而是通过大量采样轨迹加权平均得到控制量,在非线性强、成本函数复杂的时候有一定优势。OCS2里的SQP需要提供动力学雅可比,对模型质量要求高,但在计算效率和收敛性方面更稳定。不是说MPPI不好,而是OCS2的定位就是“基于模型的高效非线性最优控制”,搞懂这一层,后面调参时才不会拿MPPI的经验硬套。
2. 搭建前的环境准备:版本选型与依赖清单
2.1 Ubuntu和ROS版本怎么选
OCS2对系统的要求不算苛刻,但版本选错了会引发一连串编译问题。我实测下来最省心的组合是Ubuntu 20.04 + ROS Noetic,或者Ubuntu 22.04 + ROS 2 Humble。如果你是第一次接触这个工具箱,我的建议是直接上Ubuntu 20.04 + Noetic:四足机器人领域大量现成代码、教程、镜像都是围绕这个组合写的,遇到问题搜起来命中率高很多。
ROS 2版本的OCS2也支持,但现在的功能更新更多还是集中在ROS 1生态里。特别是你接下来要连Gazebo、用ocs2_ros话题收发机器人状态,Noetic下几乎每一步都有先例可查,踩坑成本低。系统装好后,建议先确认gcc、g++、cmake、git都就位,这几个是编译基础,版本太老会有莫名其妙的问题。
有一个容易忽略的点:OCS2官方仓库目前对C++标准要求是C++17,如果你系统里默认编译器比较老(比如Ubuntu 18.04自带的gcc 7.x),编译时经常冒出模板解析错误。别浪费时间排查,直接把编译器升级到gcc-9以上,或者换到20.04系统。
2.2 核心依赖包清单
在clone OCS2仓库之前,先把依赖装齐。依赖分两大块:ROS侧的和纯C++侧。
ROS侧建议用apt直接安装:
sudo apt install ros-noetic-robot-state-publisher ros-noetic-urdf \ ros-noetic-eigen-conversions ros-noetic-cmake-modules ros-noetic-controller-interface \ ros-noetic-gazebo-ros-pkgs ros-noetic-ros-control ros-noetic-ros-controllers纯C++侧的核心依赖包括Eigen3、Boost、CppADCodeGen、urdfdom、pybind11(部分接口需要)以及最重要的Pinocchio。CppADCodeGen是OCS2做自动微分和代码生成用的,如果你后面的MPC想追求实时性,CppADCodeGen生成代码的环节会很关键,建议用源码编译最新版。
sudo apt install libeigen3-dev libboost-all-dev liburdfdom-dev liburdfdom-headers-dev \ libcppad-dev libcppadcodegen-dev libyaml-cpp-dev这段装完后,再用一个命令确认Eigen版本:
pkg-config --modversion eigen3OCS2和Pinocchio对Eigen版本比较敏感。老版本的Eigen(3.3系列)在部分Pinocchio 2.x上能编译过但运行会崩,建议直接上Eigen 3.4以上。如果apt源里的Eigen版本不够新,可以源码装一份最新的Eigen,注意Eigen是头文件库,装的时候把/usr/local/include/eigen3软链到/usr/include/eigen3,否则有些CMake找不到。
2.3 用一套可复现的环境,避免污染系统
我自己的经验是:OCS2这套东西的依赖树不算浅,而且一旦Pinocchio、Eigen、CppADCodeGen版本之间出现错位,排查起来非常痛苦。强烈建议用Docker做一个固定镜像,或者至少用一个干净的虚拟机。这样即使环境折腾坏了,随时可以回滚,不用重装系统。
如果你选择Docker路线,推荐基于ros:noetic-ros-base镜像,在这个基础上把上述依赖装进去,再编译OCS2。https://github.com/leggedrobotics/ocs2仓库根目录下本身也有Dockerfile可以参考,虽然它可能针对的是他们自家开发环境,直接拿来改一改也比自己从零写省事。
3. Pinocchio配置指南:这篇博文的重头戏
3.1 Pinocchio在OCS2中承担什么任务
Pinocchio是法国LAAS-CNRS和INRIA联合开发的刚体动力学库。它在机器人学圈子里的地位,基本相当于“C++版的RBDL增强版”:能解析urdf模型,能做正逆运动学、正逆动力学,能算质量矩阵、科氏力、重力项,还提供CRBA、RNEA、ABA这些高效算法。
在OCS2里,Pinocchio承担的角色很具体:MPC求解器在做预测时,必须回答“给定当前状态、关节位置和力矩,下一步系统状态会是什么”。这个系统动态函数就是由Pinocchio的动力学计算提供的。可以说,Pinocchio是MPC对模型求导和积分的数学引擎。
OCS2之所以单独封装一个ocs2_pinocchio_interface,而不是让大家直接调用Pinocchio原生API,是因为OCS2的求解器需要“带自动微分信息的动力学”。Pinocchio内部虽然也支持计算动力学导数,但OCS2希望把这些导数和CppADCodeGen自动微分机制统一起来,生成高效的导数代码。这层封装也意味着:你在写自己的控制器时,通常不需要直接和Pinocchio的API打交道,但编译时必须让CMake能找到Pinocchio。
3.2 安装路线对比:apt和源码编译
Pinocchio的安装有两条路,各有利弊。
apt安装的好处是快:
sudo apt install libpinocchio-dev但坑也很明显:Ubuntu官方源里的Pinocchio版本往往偏旧,某些版本和最新的OCS2有接口兼容问题。如果你只是试试OCS2自带示例,apt版勉强能跑;一旦你想用Pinocchio的新功能,或者遇到“函数找不到定义”这类编译错误,大概率就是版本不匹配的锅。
源码编译更可控,推荐路线如下:
# 先装编译依赖 sudo apt install libeigen3-dev libboost-all-dev liburdfdom-dev liburdfdom-headers-dev \ libcppad-dev libassimp-dev liboctomap-dev git clone https://github.com/stack-of-tasks/pinocchio.git cd pinocchio mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_PYTHON_INTERFACE=OFF -DBUILD_TESTING=OFF .. make -j$(nproc) sudo make install编译参数里-DBUILD_PYTHON_INTERFACE=OFF值得多说一句:很多人在这一步被Python绑定编译卡住,因为编译Python接口需要额外的Boost.Python和pybind11依赖。OCS2用的是Pinocchio的C++接口,完全用不到Python绑定,直接关掉能省出一堆事。
-DBUILD_TESTING=OFF也一样,测试代码编译起来又慢又容易出警告,关掉就好。装完之后,用下面这段代码验证一下基本动力学接口能不能用:
#include "pinocchio/multibody/model.hpp" #include "pinocchio/multibody/data.hpp" #include "pinocchio/parser/urdf.hpp" int main() { std::string urdf_path = "your_robot.urdf"; pinocchio::Model model; pinocchio::urdf::buildModel(urdf_path, model); pinocchio::Data data(model); return 0; }能编译通过,说明Pinocchio基本安装没问题。
3.3 与OCS2链接时的坑和排查
Pinocchio装好了,和OCS2对接的时候才是真正容易出问题的地方。我列几个高频坑。
第一个坑是find_package(pinocchio)找不到。Pinocchio的CMake配置文件路径通常在/usr/local/lib/cmake/pinocchio或/usr/lib/cmake/pinocchio。如果找不到,多半是安装路径没进CMake搜索范围,可以在CMakeLists.txt里手动加:
list(APPEND CMAKE_PREFIX_PATH "/usr/local") find_package(pinocchio REQUIRED)第二个坑是编译期Eigen对齐报错。Eigen的固定大小向量类型(比如Eigen::Vector4d)在C++17以前涉及内存对齐问题,OCS2底层大量使用Eigen类型,一旦某个接口函数的参数没用EIGEN_MAKE_ALIGNED_OPERATOR_NEW宏,运行时可能直接崩溃,报错里通常能看到assertion failed这类字眼。遇到这个问题不要慌,检查一下你自定义的类里是否包含Eigen固定长度成员并且声明了对齐宏,同时确认所有编译单元都用了同样的Eigen版本。
第三个坑是链接时缺urdfdom符号。OCS2中的urdf解析依赖urdfdom库,而Pinocchio内部也用了urdfdom,这两个库如果版本不匹配,链接时会报一堆undefined reference。解决办法是保证系统里只有一套urdfdom版本,不要混用apt和源码安装的版本。
第四个坑是Release和Debug混编。OCS2官方推荐用Release模式编译,如果你自己的包是Debug,而依赖库是Release,配合Eigen时会出现奇奇怪怪的运行问题。统一用Release编译能省去很多烦恼。
最后,无论怎么踩坑,编译OCS2时建议加一个环境变量确认库版本信息:
printenv | grep -i pinocchio如果发现LD_LIBRARY_PATH里既有旧版又有新版Pinocchio路径,大概率会产生运行时加载错误的库。稳妥的做法是把无关的库路径清理干净,只保留你要用的那一个版本。
4. 搭建四足机器人MPC控制器的核心链路
4.1 从模型描述到代码:urdf、配置和生成器
环境通了之后,真正开始写控制器时,第一步是把机器人模型准备好。OCS2的四足模块首先要读入urdf文件,但它的要求不只是“把urdf丢进去”,还需要关节顺序和基座命名满足约定。
打开OCS2四足示例里的配置,通常会看到一个类似这样的YAML配置文件:
model_settings: fileName: "your_robot.urdf" baseName: "base" jointNames: - "LF_HIP" - "LF_THIGH" - "LF_CALF" - "RF_HIP" - "RF_THIGH" - "RF_CALF" ...这里的jointNames顺序非常关键。OCS2内部所有向量(状态向量、控制向量、雅可比矩阵的列)都按照这个顺序组织,顺序一错,MPC预测的运动方向就是乱的,表现是机器人原地乱抖,看起来完全没道理。我调试时遇到过这样一个问题:前腿关节顺序写反了,MPC解出来的力矩全部对不上号,Gazebo里机器人四条腿都在蹬但就是不走。后来把urdf里的关节顺序和配置逐一对齐,才恢复正常。
还有一个小细节:urdf里的关节限位(<limit>)和OCS2配置里的状态约束要一致。如果urdf里髋关节限位是±2.0弧度,而MPC求解器里的约束还是默认的±1.0,求解器的解可能在实机上直接碰到硬件限位,非常危险。
4.2 MPC参数与求解器设置
MPC的核心参数写在mpc_settings里,约等于“控制器行为说明书”。我常用的配置项大致如下:
mpc: timeHorizon: 1.5 dt: 0.05 iterationsPerMpc: 10 solver: SQPtimeHorizon是预测时域,通俗说就是MPC“向前看多远”。四足机器人上,1.2到2.0秒之间是常见区间,太长计算量暴涨,太短预判不足。dt是离散时间步长,决定预测阶段的状态更新颗粒度。0.02到0.1秒之间常见,越小越精细但越慢。iterationsPerMpc是每个控制周期内SQP迭代的次数。数值越大,解越收敛,计算代价也越高。
还有一个容易被忽视的参数是任务文件里的cost权重,一般长这样:
weights: stateCost: [1.0, 1.0, 1.0, 10.0, 10.0, 10.0, 0.01, ...] inputCost: 0.001stateCost里每一项对应状态量(位置x、y、z、姿态角、关节速度等)。姿态项的权重通常比位置项大,因为四足机器人稍微歪一下就可能摔倒;关节力矩项的inputCost一般给一个很小的值,相当于“可以输出大力矩但不要浪费”,这个值设太大,控制器会变得“佛系”,腿上没劲,稍微受点扰动就倒。
4.3 控制器初始化与主循环
OCS2四足MPC的初始化路径大致是:先根据urdf和配置文件创建QuadrupedInterface,再实例化MPC控制器,然后进入主循环。伪代码结构大概是这样:
// 1. 加载模型和配置 auto modelLoader = std::make_shared<QuadrupedModelLoader>(taskFile); auto interface = modelLoader->getInterface(); // 2. 初始化状态、输入 vector_t initState = Eigen::VectorXd::Zero(interface->getStateDim()); vector_t initInput = Eigen::VectorXd::Zero(interface->getInputDim()); // 3. 创建MPC控制器 QuadrupedMPC mpc(interface, taskFile); mpc.reset(initState, initInput); // 4. 主循环:接收状态估计 -> 更新MPC目标 -> 求解 -> 输出力矩 while (ros::ok()) { auto currentState = getStateFromEstimator(); mpc.update(currentState, initInput, mpcTime); auto mpcSolution = mpc.getSolution(); sendCommandToActuators(mpcSolution); }这里有个容易忽略的概念:当MPC在预测里计算未来n步的状态和控制量时,你真正拿来发给执行器的通常只有当前时刻那一步。其他步的力矩去哪了?它们都被预测阶段“预演”了一遍,给下一步决策提供参考,并不真正发送。这也就是MPC和传统最优控制的本质区别——它不是算出整条轨迹然后开环执行,而是闭环地不断重算。
主循环的频率由MPC的dt和每次迭代耗时决定。理论上如果dt=0.05、每次MPC更新耗时小于50ms,就可以实时跑。如果你的求解耗时超标,优先压缩iterationsPerMpc,其次考虑减小timeHorizon,不要一上来就砍dt,容易让预测太粗导致控制质量下降。
5. 调试阶段的高频问题与参数调节经验
5.1 权重矩阵和horizon的调节逻辑
把MPC跑起来之后,调参才是真正的磨人环节。很多人拿到官方示例直接改权重,发现越调越乱,根因是没搞清楚自己控制的瓶颈在哪。
四足MPC里的核心权重有三类:位置误差权重、姿态误差权重、输入代价权重。我的经验是,第一步先把姿态权重调到位置权重的3到5倍,先保证机器人不摔倒,再去调位置跟踪精度。姿态一旦垮了,后续一切都是白搭。等机器人能站住、能抗推开,再去提高位置权重,让MPC更有“追目标”的欲望。
timeHorizon的调整要配合步态周期来。四足机器人的步态周期一般0.3到0.8秒,timeHorizon至少得覆盖一个完整步态周期,否则MPC在预测时根本看不到这条腿何时落地,规划出来的支撑腿切换会很奇怪。举个例子,如果你的trot步态周期是0.4秒,timeHorizon低于0.4秒就是政治错误,至少设到0.5到0.6秒才有意义。
5.2 Gazebo中常见的抖动问题
仿真环境里的抖动跟实机还不完全一样,但调试思路相通。我在Gazebo里碰到的抖动多半来自状态反馈频率和MPC更新频率不匹配。
Gazebo的物理仿真步长默认是1ms,但OCS2的MPC更新频率可能只有20到50Hz。这就意味着MPC每执行一次,中间会过去几十个仿真步,机器人状态已经变了。如果你的状态估计没做滤波,MPC拿到的状态是突跳的,控制量也会跟着抖。解决办法是给状态信号加一个简单的低通滤波,或者在MPC读取状态前做一次滑动平均。这个看似土办法的操作,实际效果立竿见影。
另一个常见抖动来源是接触检测。OCS2的MPC在预测阶段会假设足端按一定模式接触地面,如果Gazebo里的接触力检测不稳定,模型预测和实际仿真不一致,控制器就会“猜错”地面在哪,表现就是腿在触地瞬间猛发力。这时可以检查一下Ground Contact的配置,或者调低接触力阈值,让接触状态更干净。
5.3 从仿真到实机的几个关键差异
仿真能站稳不代表实机也能。最大的差异是延时。OCS2的MPC解决方案里默认有一定的时延补偿机制,但实机上关节执行器响应、CAN总线和状态估计器的延迟很可能超模。最直接的办法是给MPC发指令时加一个可调的延迟补偿项,或者把实机状态估计的时间戳对齐,确保MPC计算的决策时刻和机器人实际所处时刻一致。
实机上另一个明显差异是模型不准。Pinocchio里用的动力学参数来自urdf,但实机每个电机的摩擦、连杆的质量分布都有误差。我建议第一步先用系统辨识工具把摩擦项和关节极限标定出来,把修正后的参数更新到urdf里,这样MPC预测用的模型才贴近真机。跳过这一步直接在实机上调MPC,实际上是在用一个错误模型做预测,地上一滑就会摔,是很危险的。
还有一点关于初始状态。MPC启动时如果收到的初始状态和实际机器人姿态差太多,求解器会尝试用巨大控制量把“预测状态”拉回目标,实机上可能直接造成飞腿或爆关节。所以控制器上线前一定要确认状态估计已收敛。我自己的习惯是先把机器人放在安全位置启动,让MPC先跑几步空载,观察输出无异常后再接入执行器。
6. 参考资源与常见错误速查
6.1 常见编译和运行错误对照
这一节把前面散落提到的问题汇总成一张速查表,方便你卡住的时候翻。
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
find_package(pinocchio) failed | CMake搜索路径不含Pinocchio安装路径 | 在CMakeLists中追加CMAKE_PREFIX_PATH,或apt重装 |
| Eigen对齐崩溃 | 编译选项不一致或Eigen版本不统一 | 统一编译模式为Release,检查EIGEN_MAKE_ALIGNED_OPERATOR_NEW |
链接时urdfdom未定义 | 系统存在多版本urdfdom | 保留一套,清理其他库路径 |
| MPG求解耗时过长 | iterationsPerMpc或timeHorizon太大 | 压缩iterationsPerMpc,减少预测步数 |
| 四条腿乱蹬但不前进 | 关节顺序和urdf不一致 | 逐个比对关节名称和顺序 |
| 仿真中接触时猛烈发力 | 接触力检测不稳定 | 调整接触阈值或增加滤波 |
6.2 跑通你的机器人,需要额外确认的细节
如果你已经能跑通官方示例,准备换到自己设计的机器人上,下面几件事提前确认到位,能省掉大量排查时间。
第一,检查关节方向。urdf里关节旋转轴的正负方向会影响MPC预测的力矩方向。最简单的方法是在rviz里加载urdf,手动拖一下各个关节,确认正方向和你代码里定义的jointNames一致。
第二,确认机器人的基座坐标系。OCS2里基座的位姿通常用x、y、z加上四元数表示,坐标系的朝向(比如z轴向上还是向下)必须和urdf基座、仿真世界坐标系一致。坐标系轴向搞反,MPC会把“前进”理解成“横着走”,有时候看起来像是控制器坏了,其实只是坐标系没对齐。
第三,留意你使用的中文或特殊字符路径。urdf文件路径、配置文件路径和工程路径里如果出现中文或空格,某些版本的Boost和Pinocchio解析会出问题。工程规范一点,全英文路径是最稳的。
6.3 遇到问题去哪翻资料
OCS2的官方文档在https://leggedrobotics.github.io/ocs2/,但说实话,文档更新速度赶不上代码更新速度,不少细节还是得靠读源码。ocs2_quadruped_mpc目录下的示例是很好的参考入口,建议从QuadrupedMPC类入手,逐步追它的初始化流程和run函数。
Pinocchio的问题建议去它们的GitHub Discussions里搜,官方维护者非常活跃。Eigen和CppADCodeGen的问题则推荐在StackOverflow搜英文关键词,通常能碰到前人同样的坑。
最后再分享一个我在实际调试中养成的习惯:每次改动配置文件或参数前,先用git把当前状态提交一次,或者至少复制一份带日期的备份。MPC参数调试经常会出现“改了三个参数不知道是哪个起了作用”的情况,有版本记录才能精准回退和对比。这个小习惯在好几次调参调崩之后救了我,强烈建议你用起来。