模型预测控制MPC这套东西,我第一次在项目里被要求用它替代PID的时候,心里是打鼓的。名字听着唬人,真上手写第一版代码才发现,它的核心思想朴素得很:每一步都用模型往前推演一段未来,从所有可能的动作里挑出代价最小的那一串,但只执行第一个,下一拍用新的测量值重来一遍。难的不是思想,是把这套思想翻译成能跑、能稳、能在资源有限的控制器上实时求解的代码,再把那堆权重参数调到既不抖也不肉的舒服状态。这篇就当是我这几年在MPC上踩坑的一个存档,从它到底算什么、怎么把物理模型翻译成MPC能吃的形式、求解器和权重怎么选、到一个能直接跑的最小实现,最后聊聊几个同名不同物的MPC。适合刚接触MPC、想把它真正用起来而不是只停留在概念的人。
1. 先把MPC到底在算什么搞清楚
1.1 用倒车入库理解MPC的三步循环
要理解MPC,最贴切的类比其实是你自己倒车入库的过程。你坐在驾驶位,脑子里有一个关于"方向盘打多少、车身怎么转、油门刹车给多少会怎样"的粗略模型,这就是预测模型。你会根据当前车的位置和角度,在脑子里预演未来几秒车身会划出怎样的轨迹,然后挑一条不会蹭到旁边车、又不会压线的路线,这就是优化。关键在于下一步:你不会把脑子里规划的那一整条轨迹一次性执行完,而只是先动一点点方向盘、松一点点刹车,然后立刻抬头看后视镜,根据车实际移动的位置重新评估,再规划一次。
这个"只走一步、然后重新看、重新规划"的动作,就是滚动优化,也是MPC区别于其他控制方法最本质的地方。它在每一时刻都解一个有限时域的优化问题,得到一个控制序列,但只把序列里的第一个控制量施加到系统上。等到下一个采样时刻,系统的真实状态已经被测量出来了,用它作为新的起点,再解一次。这套机制天然地把反馈嵌了进去,哪怕模型有误差、有外界扰动,因为每一步都在用真实测量值纠偏,系统也不会跑飞。
1.2 预测模型、滚动优化、反馈校正这三件套
把上面那段拆开,MPC其实就三个必备组件,缺一不可。预测模型负责回答"如果我这样控制,未来会怎样",它可以是线性状态空间、传递函数,也可以是非线性微分方程、神经网络,甚至是一张查表。模型精度直接决定控制效果的上限,模型错了,优化出来的东西再漂亮也是错的。滚动优化负责在有限时域内求解一个带约束的最优控制问题,通常写成二次规划或者非线性规划。反馈校正看起来最不起眼,但它其实是MPC鲁棒性的来源,因为每步都用实际测量值重置状态,相当于给开环预测加了持续的闭环修正。
很多人第一次写MPC,最容易犯的错就是把预测时域内的控制序列全部apply一遍,然后等下一轮。这实际上是开环最优控制,不是MPC。只要模型有一点点偏差,误差就会累积,系统很快就会偏离参考轨迹。正确做法永远是只取序列的第一个。我早期就栽过这个跟头,仿真里看着挺好,一上真机就飘,后来把"只取第一个"改回来立刻就稳了。
1.3 MPC和PID到底差在哪
常有人问,既然PID这么好用,为什么还要费劲搞MPC。答案不在于MPC"更高级",而在于它解决的是PID不擅长的那类问题。PID是单回路、无约束、无预测的控制器,它的输出只跟当前误差、误差积分、误差变化率有关,对未来会发生什么一无所知。当一个系统有大的纯滞后(比如温度控制、化工过程),或者多个变量之间存在强耦合(比如多轴机械臂),或者存在必须严格满足的物理约束(比如电机电流上限、阀门开度范围),PID就开始吃力了。
MPC的强项恰恰在这里:它同时处理多变量、显式表达约束、对纯滞后有天生的预测能力。下面这张表是两者的对比,实际选型时我一般按这个来决策。
| 对比维度 | PID | MPC |
|---|---|---|
| 变量支持 | 单入单出为主 | 天然多入多出 |
| 约束处理 | 靠外部限幅,硬截断 | 显式写进优化问题 |
| 纯滞后 | 难处理,易振荡 | 预测模型天然补偿 |
| 调参对象 | 3个增益 | Q、R、Np、Nc等 |
| 在线计算 | 几乎为零 | 每步解一个优化问题 |
| 模型依赖 | 无 | 强依赖 |
2. 把连续世界的物理模型翻译成MPC能吃的格式
2.1 连续状态空间怎么离散化成递推式
真实系统的物理规律通常写成连续微分方程,比如位置、速度、加速度的关系。但MPC是在数字控制器上跑的,它按固定采样周期工作,所以第一步必须把连续模型离散化。假设系统是线性的,写成连续形式 x_dot = Ax + Bu,其中 x 是状态向量,u 是输入向量。离散化的目标是把"导数关系"变成"第k+1步和第k步之间的递推关系":x(k+1) = A_d x(k) + B_d u(k)。
最常用的是零阶保持离散化,假设在一个采样周期内输入保持不变,那么 A_d 等于矩阵指数 e 的 AT 次方,B_d 等于 e 的 Aτ 次方对 B 的积分。数学上精确,但实际写代码时没人真去算矩阵指数,直接用近似公式就够了:A_d 约等于单位阵加上 AT,再加上 (AT) 平方除以2,更高阶项可以忽略;B_d 约等于 BT 加上 A 乘 B 乘 T 平方再除以2。当采样周期远小于系统的时间常数时,这个近似误差小到可以忽略。
import numpy as np def discretize(A, B, dt, order=2): n = A.shape[0] Ad = np.eye(n) Bd = np.zeros_like(B) term_A = np.eye(n) term_B = np.zeros_like(B) for i in range(1, order + 1): term_A = term_A @ (A * dt) / i term_B = term_B @ (A * dt) / i + term_A @ (B * dt) Ad += term_A Bd += term_B return Ad, Bd这段代码用的是泰勒展开近似,order=2 对大多数采样率足够的系统已经够用。如果你的采样周期接近甚至超过系统最小时间常数,那说明采样率选错了,得先把采样周期压下来,而不是指望离散化来救。
2.2 预测时域Np和控制时域Nc怎么定
Np 是预测时域,也就是每次优化时往前看多少步。Nc 是控制时域,也就是这次优化真正决定多少个不同的控制量,通常 Nc 小于等于 Np,超出 Nc 之后保持最后一个控制量不变。这两个参数直接决定计算量和控制效果,是所有MPC调参里最需要凭经验拍的地方。
定 Np 的经验法则是:预测时域要覆盖被控对象的主要动态过程,至少能"看到"系统的上升时间和主要纯滞后。如果系统上升到稳态需要2秒,采样周期0.1秒,那 Np 至少要20步才能让优化器看清整个过渡过程;太短了它就是个近视眼,刚想调整就到头了,控制会变得保守或者激进。但 Np 也不是越大越好,每增加一步预测,优化问题的规模就线性增长,实时性压力随之上来。
Nc 通常取 Np 的五分之一到三分之一。取太小,控制自由度不够,响应会变钝;取太大,接近 Np,计算量暴涨但效果提升有限,因为再往后的控制动作本来就影响不大。我一般的起步配置是 Np=20、Nc=5,跑通之后再根据响应情况微调。
提示:如果被控对象有显著纯滞后,Np 至少要大于滞后时间除以采样周期,否则优化器在预测时域内根本看不到滞后带来的影响,控制效果会很差。
2.3 二次型代价函数怎么构造和展开
代价函数是MPC的指挥棒,它告诉优化器什么算"好"。最通用的形式是最小化预测状态与参考之间的偏差、控制量大小、以及控制量的变化率三部分的加权和。状态偏差用权重矩阵Q,控制量用R,控制增量用S,这三个矩阵都是设计者自己给定的,直接决定控制器的脾气。
写成公式就是:J 等于状态偏差的二次型对预测时域求和,加上控制量的二次型求和,再加上控制增量的二次型求和。优化器在每一步去搜索一个控制序列,让这个J最小,同时满足所有约束。数学上这是一个凸二次规划问题(前提是模型线性、约束线性、Q和R半正定),有现成的高效求解器可用。
把这个问题整理成求解器标准形式时,需要把状态递推展开成决策变量的函数,得到 Hessian 矩阵 H 和线性项 f。这一步推导略繁琐,但工程上一般交给建模工具自动完成,比如CVXPY、CasADi,你只管把代价和约束用自然的方式写出来就行,不用手推。
3. 求解器选型和权重矩阵整定实战
3.1 从CVXPY到OSQP怎么选求解器
求解器的选择取决于你的运行平台和对实时性的要求。桌面端做算法验证,我首选CVXPY,建模直观、报错清晰,代价是求解慢,一般几十毫秒到几百毫秒,做仿真够了。真正要嵌到控制器里跑,就得换成OSQP、qpOASES这类专为嵌入式优化的QP求解器,毫秒级甚至微秒级响应。如果要处理非线性模型,CasADi配合IPOPT或者ACADO是常见组合,它们自带自动微分,能省掉手推导数的功夫。
| 求解器 | 适用场景 | 特点 |
|---|---|---|
| CVXPY | 桌面验证、快速原型 | 建模简单,速度慢 |
| OSQP | 嵌入式线性MPC | 快,支持热启动 |
| qpOASES | 实时性要求高 | 在线有效集,C++ |
| CasADi + IPOPT | 非线性MPC | 自动微分,强大但重 |
| do-mpc | 快速搭建仿真 | 基于CasADi,开箱即用 |
我用CVXPY写第一版算法,把逻辑跑通、权重调到满意,然后再按需迁移到性能更好的求解器上。这个顺序能避免一开始就陷进底层求解器的坑里。
3.2 Q和R权重矩阵到底怎么调
Q和R的整定没有解析公式,但有清晰的物理直觉。Q越大,控制器越重视跟踪精度,愿意花更大的控制代价去消除误差,表现就是响应快、超调可能大。R越大,控制器越"怕"用控制量,动作会变柔和,代价是跟踪变慢、稳态误差可能残留。这三者之间的平衡就是MPC调参的核心。
我推荐的起步方法是 Bryson 规则:Q 的对角元素取对应状态最大允许偏差的平方的倒数,R 取对应输入最大允许值的平方的倒数。这相当于把所有状态和输入都归一化到同一量级,避免因为物理单位不同(比如位置用米、速度用米每秒)导致权重量级悬殊。举个具体例子,位置允许偏差0.5米,速度允许偏差1米每秒,输入加速度上限2米每二次方秒,那么 Q 就取 diag(4, 1),R 取 0.25。跑起来之后,如果觉得跟踪太慢就整体放大Q,如果控制太抖就放大R。
注意:调Q和R的时候建议一次只动一个,并且用同一组参考轨迹和扰动反复对比。我见过有人同时改三个权重,结果曲线变好了也不知道是谁的功劳,下次换工况又得从头试。
3.3 约束怎么写才不会让优化问题无解
约束是MPC相对PID的最大优势,但也是它最容易翻车的地方。最常见的翻车方式就是约束互相冲突,导致优化问题无解。比如你同时要求位置不能超过某个范围、速度不能超过某个范围、但初始状态本身就已经违反了其中一条,优化器就找不到任何可行解,求解器返回失败,控制器直接开天窗。
解决办法有两层。第一层是约束要合理,不要设得比物理系统实际能力还紧,尤其初始状态要留够裕量。第二层是引入软约束:对状态约束加一个松弛变量,允许它在极端情况下被违反一点点,但违反的代价写进代价函数。这样即使遇到不可行情况,优化器也能给出一组"最不坏"的解,控制器不至于挂掉。输入约束一般保持硬约束,因为物理执行器确实到不了那个值,但状态约束,尤其是安全相关的,用软约束处理会更稳。这个设计在实际项目中救过我好几次,强烈建议默认就加上。
4. 手把手实现一个速度跟踪MPC
4.1 先把被控对象和参数配置讲清楚
为了能直接跑起来,我选一个最经典也最能说明问题的例子:一个双积分系统,状态是位置和速度,输入是加速度命令。物理上可以对应一辆理想化的车(忽略阻力),或者一个位置伺服机构。目标是让位置跟踪一个给定的参考值,同时满足加速度上限和状态范围约束。这个例子足够简单,代码能完整贴出来,又能体现MPC的全部关键环节。
参数这样配置:采样周期0.1秒,位置和速度都作为状态,加速度作为唯一输入。物理限制方面,加速度限制在正负2之间,位置限制在正负10,速度限制在正负5。预测时域取20步,控制时域取5步,状态权重用刚才算的 diag(4,1),输入权重0.25。参考目标设在位置5、速度0。
4.2 完整可运行的Python实现
下面的代码基于CVXPY,装好numpy和cvxpy就能直接跑。整个逻辑分三块:构建优化问题、仿真循环、记录结果。
import numpy as np import cvxpy as cp # ---------- 1. 系统模型 ---------- dt = 0.1 A = np.array([[1.0, dt], [0.0, 1.0]]) B = np.array([[0.5 * dt**2], [dt]]) n, m = 2, 1 # ---------- 2. MPC 参数 ---------- Np = 20 Nc = 5 Q = np.diag([4.0, 1.0]) R = np.array([[0.25]]) u_min, u_max = -2.0, 2.0 x_min = np.array([-10.0, -5.0]) x_max = np.array([10.0, 5.0]) # ---------- 3. 构建优化问题 ---------- U = cp.Variable((m, Nc)) x0 = cp.Parameter(n) x_ref = cp.Parameter(n) cost = 0 constraints = [] x = x0 for k in range(Np): u = U[:, min(k, Nc - 1)] cost += cp.quad_form(x - x_ref, Q) if k < Nc: cost += cp.quad_form(u, R) x = A @ x + B @ u constraints += [x_min <= x, x <= x_max] constraints += [U >= u_min, U <= u_max] prob = cp.Problem(cp.Minimize(cost), constraints) # ---------- 4. 仿真循环 ---------- x_sim = np.array([0.0, 0.0]) ref = np.array([5.0, 0.0]) log = [] for t in range(120): x0.value = x_sim x_ref.value = ref prob.solve(solver=cp.OSQP, warm_start=True) if prob.status not in ("optimal", "optimal_inaccurate"): print("求解失败:", prob.status) break u_opt = U.value[:, 0] x_sim = A @ x_sim + B @ u_opt log.append([t * dt, x_sim[0], x_sim[1], float(u_opt[0])]) log = np.array(log) print("最终位置:", log[-1, 1]) print("最终速度:", log[-1, 2])4.3 仿真结果和调参现场记录
跑完这段代码,你能看到位置从0平滑逼近到5,速度先加速后减速到0,加速度在起步阶段达到上限后回落,整个过程没有任何超调和震荡。这说明在当前权重下,控制器是稳定且收敛的。这时候就可以开始玩参数了,看看每个参数到底影响什么。
我实测下来,把 Q 的位置权重从4改成40,响应明显变快,但加速度更频繁触碰到上限,控制量显得更"暴躁",如果执行器有磨损顾虑,这个方向就不合适。反过来把 R 从0.25改成2.5,控制立刻变柔和,代价是到达目标的时间大约翻倍,而且如果系统有恒定扰动(比如摩擦力),最终位置会偏一点,这就是没有积分项的代价。想消除这种稳态误差,一个办法是在代价函数里加控制增量的惩罚,用增量式MPC;另一个办法是给模型加上扰动状态,让优化器在线估计并补偿。
# 增量式MPC的输入变量改为 Δu,通过累加得到 u U = cp.Variable((m, Nc)) u_prev = cp.Parameter(m) du_cost = 0 u = cp.vstack([u_prev]) for k in range(Nc): u = cp.hstack([u, u_prev + cp.cumsum(U, axis=1)[:, k:k+1]]) # 实际使用时把 du_cost 换成对 U(Δu)的二次惩罚这段只是示意增量式MPC的构造思路:把优化变量从控制量本身改成控制增量,再通过累加还原出实际控制量,同时对增量施加惩罚。它对抑制抖动、消除部分稳态误差很有效,是我在电机和伺服项目里的默认选择。
5. 常见故障排查和几个容易搜错的MPC
5.1 无解、抖动、稳态误差三类典型问题
第一类是求解失败或者无解。绝大多数情况是约束太紧或者互相冲突,尤其是初始状态本身已经违反了某条状态约束。排查方法是先临时把状态约束放宽,看是否恢复正常,如果恢复了就说明是约束打架,改成软约束即可。还有一种情况是预测时域太长、问题规模太大导致求解超时,这时候要削减 Np 或者换更快的求解器。
第二类是控制抖动,表现为控制量高频跳变、系统嗡嗡响。根因通常是 R 给得太小、Nc 太短,或者模型和真实系统失配导致每步优化结果在两组解之间来回跳。解决方向是增大 R、适当增加 Nc、或者引入控制增量惩罚项。如果装在嵌入式平台上,还要检查数值精度,float32 在长时域递推下会累积误差,必要时换 float64 或者做数值缩放。
第三类是稳态误差,系统稳定下来但始终差一点到不了参考。线性MPC如果没有积分作用,面对恒定扰动必然有静差。解决办法就是前面说的增量式MPC或者加扰动状态估计,本质都是给控制器引入积分能力。
5.2 常见问题速查表
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 求解返回infeasible | 约束冲突或初始状态越界 | 放宽约束或用软约束 |
| 控制量高频抖动 | R过小、Nc过短、模型失配 | 增大R、增大Nc、核对模型 |
| 有稳态误差 | 缺少积分作用 | 增量式MPC或加扰动状态 |
| 响应迟缓不跟踪 | Q过小或Np过短 | 增大Q、加长预测时域 |
| 超调大、逼近上限 | Q过大、R过小 | 平衡Q和R |
| 实时超时 | Np、Nc过大 | 削减时域、换快求解器 |
| 仿真好实机飘 | 没做"只取第一个" | 检查滚动执行逻辑 |
5.3 同名不同物的MPC,别搜错了
最后聊一个很实际的问题:模型预测控制的英文缩写是MPC,但这个缩写在不同领域指向完全不同的东西,搜资料的时候特别容易串台。第一种是 GNU MPC,它是一个任意精度复数运算的开源数学库,名字里的MPC是 Multi Precision Complex 的意思,跟控制没有半点关系,你要是在做控制算法却搜到这个,会一脸懵。第二种是某些企业级存储管理平台里的MPC,那是产品组件名称,跟控制理论无关,属于运维和存储管理范畴,做IT基础设施的人搜到的多半是这类。第三种是 MPPI,它全称是 Model Predictive Path Integral,是基于路径积分采样的MPC变体,不需要求梯度,适合强非线性、有随机性的系统,最近在机器人和强化学习交叉领域很火,它确实属于MPC大家族,但求解思路和传统二次规划MPC完全不同。
还有一种情况是"电力系统模型预测控制"这类带行业前缀的搜索,它指的是MPC在电力领域的应用,比如新能源并网、储能调度、负荷频率控制,模型规模往往很大、时间尺度也从毫秒到小时不等,和机械控制里的MPC虽然同源,但工程落地方式差别很大。至于过程工业里那些大型先进过程控制产品,比如常见的商用APC套件,很多底层核心就是MPC,只是被封装成了配置化软件,使用者往往不需要碰数学。搞清楚这些区别,能让你在搜资料和选技术路线时少走很多弯路。
再说一个我个人很在意的小点:MPC在线求解对算力是有要求的,做产品选型时一定要把控制器的主频、内存、是否支持浮点运算提前确认清楚。我见过有人算法验证全在PC上做,结果目标芯片是个没有浮点单元的廉价MCU,迁移时才发现根本跑不动,只能退回去用查表法近似。这类硬件约束,最好在选MPC这条路的第一天就摸清楚,别等到最后一步才发现走不通。
最后分享一个我踩坑踩出来的习惯:每次上线新MPC控制器之前,我都会先用手动构造的极端工况(初始偏差最大、参考突变、约束边界附近)跑一遍仿真,把每种工况下的第一个控制量打印出来看合不合理。这一步几乎不花时间,但抓到过好几次模型离散化写错、权重单位搞混的问题。控制器这种东西,能在仿真里拦住的错误,就千万别留给实机去发现。