简介这份资源包含MPC算法实现的MATLAB源码与配套讲解文档面向自动化、控制工程领域的学生及工程师帮助快速上手模型预测控制的建模仿真与调参。包内提供MPC.m代码与docx笔记共2个文件整体仅156KB轻量易读覆盖从预测模型构建、性能指标设定到优化问题求解的完整流程。已有2196人浏览学习。配套笔记对动态矩阵控制、广义预测控制及闭环预测校正等关键概念逐一拆解MATLAB源码则给出可运行的实例便于对照理解状态空间或传递函数形式的MPC控制器设计脉络适合需要在课题或项目中落地预测控制算法的读者参考。1. 模型预测控制算法实现从原理到 MATLAB 实例的落地模型预测控制MPC在工业过程控制中早已不是新鲜名词但很多工程师总觉得它只属于大型 DCS 系统和高级过程控制专家。实际上只要有 MATLAB 和基础的控制理论功底完全可以在几天内把一个带约束的 MPC 控制器跑起来用到实验台、机器人或电力电子项目里。这篇文章就从“什么是 MPC”开始手把手带你走通一个完整的 MATLAB 实现流程并把我在调试中踩过的坑一并交代清楚。MPC 的核心魅力在于它不像 PID 那样只看当前误差而是基于一个预测模型向前看一小段“预测时域”在这个窗口内求解一个带约束的优化问题找到一组最优的控制序列然后把第一个控制量送给被控对象下一个采样周期再滚动求解一次。这种“滚动优化 反馈校正”的思路让它天然能处理多输入多输出、带约束、有耦合的复杂系统——而这些恰恰是 PID 很难做好的场景。如果你是做无人机、自动驾驶、过程控制或温控系统的工程师这篇文章能帮你省下至少一周的摸索时间。2. 为什么需要 MPC约束处理与滚动优化的真正价值2.1 MPC 与传统 PID 的本质区别在讨论算法实现之前先要把一个根本问题想清楚我到底需不需要 MPC很多场景下 PID 确实够用而且成本低、鲁棒性好、工程师都熟。但当你面对以下情况时PID 会显得很吃力——执行器有物理饱和上限、系统状态有安全边界、多个变量之间互相耦合形成“跷跷板”效应。MPC 处理这类问题的思路非常直观它在每一时刻都在解一个有约束的最小化问题。这个最小化问题的目标函数通常写成这样[ J(k) \sum_{i1}^{N_p} | y(ki|k) - r(ki) |{Q}^2 \sum{j0}^{N_c-1} | \Delta u(kj|k) |_{R}^2 ]这里 (N_p) 是预测时域(N_c) 是控制时域(Q) 和 (R) 是加权矩阵(\Delta u) 是控制增量。MATLAB 的mpc工具箱底层也就是在求解这个优化问题只是它对用户封装了细节。2.2 预测模型、滚动优化与反馈校正三要素MPC 的三根支柱必须同时具备缺一个都会变成别的算法预测模型这是 MPC 的“黑匣子”里最需要你操心的一块。模型可以是线性状态空间方程、传递函数、甚至是神经网络模型但工程上最常见的是线性时不变状态空间模型。模型的好坏直接决定了预测准不准进而决定了控制效果的上限。滚动优化每一拍都重新求解优化问题而不是一次性把未来所有控制量都算好。这个“滚动”看似浪费算力实际上是 MPC 应对模型失配和外界干扰的关键措施——它让控制器始终保持“当前最优”而不是“计划最优”。反馈校正即使有模型也不完美所以在每个采样周期MPC 会把最新的测量值拿过来和模型预测值做对比修正后续的预测偏差。这也是为什么 MPC 的闭环响应通常比开环最优控制要稳健得多。2.3 一个简单的双积分器 MPC 实例为了让你快速理解 MPC 的执行流程我们先抛开 MATLAB 工具箱用最原始的方式写一个脚本——直接调用优化函数来模拟 MPC 的每一步。这个例子用的是一个双积分器模型模拟的是类似电机位置控制的场景控制目标是让输出 y 跟踪一个方波参考信号同时控制输入 u 要限制在 ([-1, 1]) 之间。matlab % 双积分器 MPC 手写实现 % 状态空间模型: x [0 1; 0 0]x [0; 1]u, y [1 0]x % 双积分器: 位置 y x1, 速度 x2, 控制输入 u 是加速度clear; clc; close all;% 离散化参数 Ts 0.1; % 采样周期 [s] A [1 Ts; 0 1]; % 离散状态转移矩阵 B [Ts^2/2; Ts]; % 离散输入矩阵 C [1 0]; % 输出矩阵 D 0;% 预测与控制时域 Np 10; % 预测时域 Nc 3; % 控制时域 nx size(A, 1); nu size(B, 2);% 约束: 输入 u ∈ [-1, 1] umin -1; umax 1;% 加权矩阵 Q 1; % 输出误差权重 R 0.1; % 控制增量权重% 参考轨迹: 方波信号 Tsim 20; % 运行时间 Nsim Tsim / Ts; ref [ones(1, Nsim/2) zeros(1, Nsim/2)]; % 0~1s 跟踪 1, 1s 以后跟踪 0这里 Nsim 按 200 步处理)% 预分配变量 x zeros(nx, Nsim1); y zeros(1, Nsim); u_opt zeros(nu, Nsim);% 初始状态 x(:, 1) [0; 0];% 构建预测矩阵一次计算循环复用 % 预测模型: Y Phix(k) PsiU Phi zeros(Np, nx); for i 1:Np Phi(i, :) C * (A^i); endPsi zeros(Np, Nc); for i 1:Np for j 1:Nc if i j Psi(i, j) C * (A^(i-j)) * B; end end end% 主循环滚动优化 for k 1:Nsim % 当前状态 x(:, k) 已知 xk x(:, k); rk ref(k);% 目标函数: min J (Phi*xk Psi*U - rk)*Q*(...) U*R*U % 展开为二次规划标准形式: min 0.5*U*H*U f*U H 2 * (Psi * Q * Psi R * eye(Nc)); f 2 * (Psi * Q * (Phi * xk - rk)); % 约束: umin u umax, 对所有预测步 A_ineq []; b_ineq []; lb umin * ones(Nc, 1); ub umax * ones(Nc, 1); % 调用 quadprog 求解 options optimoptions(quadprog, Display, off); u_sol quadprog(H, f, A_ineq, b_ineq, [], [], lb, ub, [], options); % 取第一个控制量 u u_sol(1); u_opt(:, k) u; % 系统状态更新真实系统用非线性模型这里用同一个线性模型 x(:, k1) A * x(:, k) B * u; % 记录输出 y(k) C * x(:, k);end% 绘图 t (0:Nsim-1) * Ts; t_plot (0:Nsim) * Ts;figure(Position, [100 100 800 600]); subplot(2,1,1); stairs(t, ref, k--, LineWidth, 1.5); hold on; stairs(t, y, b-, LineWidth, 1.5); legend(参考信号, MPC输出, Location, best); title(双积分器 MPC 跟踪响应); xlabel(时间 [s]); ylabel(y); grid on;subplot(2,1,2); stairs(t, u_opt, r-, LineWidth, 1.5); hold on; plot(t, umaxones(size(t)), k--, t, uminones(size(t)), k--); legend(控制量, 约束边界, Location, best); title(控制输入带约束); xlabel(时间 [s]); ylabel(u); grid on;这段代码有三个关键点需要理解预测矩阵 Phi 和 Psi 是一次性离线计算好的所以在线优化时不需要循环做矩阵乘幂运算实时性更好。quadprog 求解的是二次规划问题H 矩阵是黑森矩阵f 向量线性项lb和ub直接定义了控制输入的上下界约束。每次循环只把 u_sol(1) 放在真实系统上执行这就是“滚动”的含义——下一拍重新计算整个 Nc 步序列。这个例子是线性模型做预测真实系统和模型一致所以效果很理想如果模型有失配需要额外加入反馈修正项。这段代码跑完后你会看到跟踪非常平滑、没有超调而且控制量严格在 ±1 之间——这就是约束 MPC 和 PID 的最大观感差异。你换成同样约束的 PID会发现要么为了压住超调把响应变得很慢要么为了快速响应而触发饱和带来积分饱和问题。3. 使用 MATLAB MPC 工具箱从模型构建到控制器生成3.1 单输入单输出 MPC 控制器设计手写优化循环能加深理解但真正项目落地时MATLAB 的 Model Predictive Control Toolbox 能让你少写 90% 的底层代码。不过工具箱用起来有几个核心套路不掌握的话会一头雾水。常见做法是用mpc函数直接构造控制器对象然后调用sim函数做闭环仿真。先定义被控对象的线性模型matlab % 定义被控对象: 以三阶系统的传递函数为例 s tf(s); P 1 / (s^2 0.5*s 1); % 转换为离散状态空间对象 Ts 0.1; P_ss ss(c2d(P, Ts));% 创建 MPC 控制器 mpcobj mpc(P_ss, Ts); % 模型、采样时间 mpcobj.PredictionHorizon 10; % 预测时域 10 步 mpcobj.ControlHorizon 2; % 控制时域 2 步 mpcobj.W.OutputVariables 1; % 输出加权 mpcobj.W.ManipulatedVariablesRate 0.5; % 控制增量加权% 设置输入约束 mpcobj.MV struct(Min, -1, Max, 1, RateMin, -2, RateMax, 2); % 设置输出约束可选 % mpcobj.OV struct(Min, -0.5, Max, 0.5);% 仿真 simTime 30; ref ones(simTime/Ts, 1); % 单位参考信号 [r, t] stepdata(ref, Ts); [y_sim, t_sim, u_sim] sim(mpcobj, simTime, ref);% 绘图 figure; subplot(2,1,1); stairs(t_sim, y_sim, b, LineWidth, 1.2); hold on; stairs(t_sim, ref(1:length(y_sim)), r--); legend(MPC 输出, 参考); grid on; subplot(2,1,2); stairs(t_sim, u_sim, k); xlabel(时间 [s]); ylabel(u); grid on;几个关键参数需要认真调PredictionHorizonNp如果模型是稳定的一般在 10~30 之间取值。Np 太小会导致控制对未来“看得太短”稳定性和鲁棒性下降Np 太大则计算量增加而且远期的参考信号可能根本不准确。ControlHorizonNc这个参数影响较大我用下来 Nc2~5 是常见的折中。Nc1 时控制量序列只有一个自由度系统会很稳定但响应可能偏慢NcNp 时自由度最大但优化问题变重有时还会导致控制量抖动。W.ManipulatedVariablesRate这是控制量“变化速度”的软约束权重越大控制量变化越平缓系统越不容易激起高频共振或执行器磨损。3.2 使用 MATLAB 命令行工具而非图形界面很多人习惯在 MATLAB 里打开mpctool或controlSystemDesigner这类图形界面去拖拽配置我在这里真心建议项目开发阶段尽量用脚本命令不要用 GUI。原因有三图形界面里的配置难以回溯和版本管理一旦项目换人接管对方很难知道你当初在面板里点了什么脚本复现容易改一个参数重跑全部过程GUI 会掩盖 MPC 内部的细节——比如你无法直观看出预测矩阵是怎么构建的出了问题不容易排查。我的习惯是写一个单独的mpc_config.m文件把所有控制器参数集中在一处并在文件头写好注释说明这个控制器用在什么机器上、采样周期是多少、约束边界是怎么算出来的、哪些参数是试出来的、哪些是从工艺卡上抄来的。这样维护成本会显著降低。3.3 如何处理参考信号和干扰模型MPC 工具箱内置了干扰模型的建模能力默认假设被控对象输入端有一个积分扰动模型类似于经典控制里的“内模原理”这也是 MPC 能实现无静差跟踪的关键。实际使用时如果参考信号是阶跃/方波你不需要额外处理MPC 会自动在模型中把输出偏差积分进去从而消除稳态误差。但如果参考信号是斜坡或者正弦工具箱默认的设置就不够用了。这时候有两条路matlab % 方法 1: 把参考信号作为前馈送到 MPC ref_future [ref(2:end); ref(end)]; % 提供一步超前参考序列 mpcobj.Model.Reference ref_future;% 方法 2: 在输出端引入一个外部扰动模型自定义白噪声/有色噪声 mpcobj.Model.Disturbance getoutdist(ss, 0.1, 2);方法 2 比较适合在真实系统上跑因为你总归会遇到测量噪声适当地在输入端建模扰动可以让控制器“预测”到扰动的影响并提前做出修正而不是等输出偏差出现后才反应。4. 基于非线性模型的 MPC扩展卡尔曼滤波与实时迭代4.1 非线性 MPC 的策略边界前面讲的都是线性 MPC即预测模型是线性定常系统。但很多被控对象本质上是非线性的——比如关节型机械臂、化学反应釜、飞行器——线性化点一变控制器性能就会下降。处理非线性 MPC 有几种常见策略局部线性化 多模型切换在多个工作点分别建立线性模型每个模型各设计一个 MPC在线时根据当前工作点做平滑切换或增益调度。在线线性化自适应 MPC每个采样周期在当前工作点对非线性模型做一阶泰勒展开得到新的线性模型再代入 MPC 框架。这个策略计算量适中是工程中最常见的选择。完全非线性 MPC直接在优化循环中用非线性模型做预测目标函数不再是一个二次规划而是一个非线性规划NLP。这种方案计算量最大但预测精度最高适合慢过程化工、热工。4.2 如何在 MATLAB 里实现自适应 MPC不依赖工具箱实现一个简单的自适应 MPC 是很可行的。思路是先把非线性系统写成一个 MATLAB 函数接口然后在一个常微分求解器里求未来 Np 步的预测轨迹再用fmincon搜索使目标函数最小的控制序列。matlab % 非线性 MPC 示意图自适应线性化 滚动优化 % 被控对象: 简单的一阶非线性 ODE: dx/dt -x^3 unl_plant (x, u) -x.^3 u;% 定义 MPC 参数 Ts 0.05; Np 15; Nc 3; umin -1.5; umax 1.5;% 模拟参数 x0 0.5; % 初始状态 x x0; u_out []; t_out []; xref 1.0; % 目标% 主循环 for k 1:100 % 在线优化以控制序列 u_seq [u(1)...u(Nc), u(Nc)...u(Nc)] 为决策变量 u_init zeros(Nc, 1); % 初始估计可以用上一次的最优解做热启动obj_fun (u_seq) nlmpc_cost(u_seq, x, xref, nl_plant, Np, Nc, Ts); options optimoptions(fmincon, Algorithm, sqp, Display, off, ... SpecifyConstraintGradient, false); lb umin * ones(Nc,1); ub umax * ones(Nc,1); [u_opt, ~] fmincon(obj_fun, u_init, [], [], [], [], lb, ub, [], options); % 执行第一个控制量 u_apply u_opt(1); % 真实系统仿真变步长积分 [tt, xx] ode45((t, z) nl_plant(z, u_apply), [0 Ts], x); x xx(end); u_out(end1) u_apply; t_out(end1) k*Ts;end% 代价函数 function J nlmpc_cost(u_seq, x_current, xref, plant, Np, Nc, Ts) % 从当前状态向前传播未来轨迹 x_pred zeros(Np1, 1); x_pred(1) x_current; u_full [u_seq(:); u_seq(end)*ones(Np-Nc, 1)]; % 扩展控制序列到 Np for i 1:Np % 欧拉积分替代 ode45内部预测用快速方法足矣 x_pred(i1) x_pred(i) Ts * plant(x_pred(i), u_full(i)); end % 代价包含跟踪误差和末端惩罚 err x_pred(2:end) - xref; J sum(err.^2) 0.1 * sum(diff([x_current; u_full]).^2); end这段代码的原理是每一步把控制序列 u_seq 当作决策变量用一个极简的欧拉积分器来模拟系统未来 Np 步的响应计算总代价然后交给fmincon找最优序列。为什么内部预测用的是欧拉积分而不是ode45这是刻意的——在线优化要跑几十上百次目标函数评估每次迭代都调变步长积分器计算量会爆炸。欧拉积分的步长误差在预测时域内通常可以接受尤其是选择足够小的 Ts。实际调试中有个细节控制序列的初始值u_init不要每次设为零最好用上一次迭代的最优解做“热启动”。因为相邻两个采样周期里系统状态变化不大最优解也变化不大热启动能让fmincon收敛更快且更稳定。这也是为什么我在循环外用一个u_prev变量存上次解的原因——代码里没写但你自己实现时一定要加。4.3 参数灵敏度与在线优化实时性非线性 MPC 对优化求解器的实时性提出了严肃挑战。如果你的控制器要跑在 1ms 周期上fmincon很可能来不及收敛。这时候有几个工程手段减少 Np 和 Nc把预测时域从 15 步缩减到 8 步控制时域从 3 步缩减到 2 步优化自由度少了收敛快一倍。用准牛顿法替代 SQP在代价函数是平滑凸函数的前提下fminunc配合BFGS更新往往比fmincon快但约束处理能力变弱需要注意边界。提前终止条件不要等求解器完全收敛设定最大迭代次数或目标函数变化率阈值超过就强行取当前最优解。预测控制的本质是“滚动”下一拍还会重新计算因此某一拍没有精确到最优值通常不至于灾难性。5. 实践中的避坑指南MPC 调参、实时性与模型失配5.1 采样时间选择不当导致优化频繁失败现象定了 MPC 控制器后仿真时还好到了硬件在环测试里频繁报“优化未收敛”或控制量跳变。原因采样时间 Ts 太小Np 对应的预测总长度不足MPC 对系统的动态特性“看”得太短同时优化必须在 Ts 时间内完成实时性不够就会丢步。解决先把 Ts 设为被控对象主导时间常数的 1/10 到 1/20再观察 Np*Ts 是否覆盖上升时间的 80% 以上。我做电机控制时Ts1ms 的电流环跑 MPC 常常吃力改成 Ts5ms 并增大预测步数后稳定性和实时性同时改善。5.2 模型失配导致输出出现不可预期的偏移现象用传递函数辨识的模型在仿真中对得很好一接真实系统稳态输出总是和目标差一截。原因MPC 线性模型指的是工作点附近的名义模型真实系统在高频段、非线性、摩擦等效应下和名义模型有偏差。如果输入端没有积分扰动模型稳态偏差无法消除。解决在 MATLAB 工具箱里确认mpcobj.Model.Disturbance中有输出积分扰动。如果手写 MPC需要在预测模型中把误差项积分起来( e(k1) e(k) (y_m(k) - y_p(k)))作为增广状态反馈到优化目标中。相当于给 MPC 加了一个“记忆”能对付固定采样的模型失配。5.3 控制量抖动与预测时域太短的关系现象输出轨线跟踪得很好但控制量像锯齿一样高频抖动执行器听着都揪心。原因Np 太短MPC 喜欢用“猛踢一脚”的方式让输出快速接近目标后续预测的新权重会叠加抖动或者是控制增量权重 R 设得不够大。解决一增 Np例如从 8 步增到 20 步二增大 R 值从 0.1 增到 1三减小 Nc自由度减少会让控制序列更平滑。有一个经验值R 取 Q 的 0.05~0.5 倍之间具体要看你能容忍多少控制能量的消耗。5.4 约束过紧导致的可行域为空现象约束设置了 (u \in [-1,1])但开机瞬间参考信号突然从 0 跳到 2MPC 直接报 error 或者输出长时间不动。原因预测时域内控制器发现无论怎么走都无法满足所有约束优化问题无解。这在实际中非常常见尤其是输出的硬约束和输入硬约束叠加时。解决不要给输出设“硬约束”改用软约束——在目标函数中加一个松弛变量slack variable的惩罚项。工具箱可以通过mpcobj.OV.Min / Max设置软约束参数具体是mpcobj.OV.ECR等量约束柔化系数来控制软约束的“柔软程度”。经验上ECR默认值是 1如果你发现频繁无解可以把 ECR 调大让 MPC 有“违规但承受代价”的余地。如果是手写实现就把输出约束从lb/ub移到目标函数里变成高权重二次惩罚项。6. 进阶将 MPC 嵌入 Simulink 与自动代码生成6.1 在 Simulink 中搭建 MPC 闭环工具箱提供的 MPC 模块可以直接拖进 Simulink但我更推荐使用 MATLAB Function 模块封装控制器因为这样可以在同一个自定义 MPC 逻辑里加入你手写的干扰观测器或 Kathy 逻辑。matlab % 在 MATLAB Function 模块里调用 MPC 控制器 function [u_opt, status] mpc_controller(x, x_prev, u_prev, ref, Np, Nc, ...) % 需要把控制器对象作为 Persistent 变量避免每拍重建对象 persistent mpcobj if isempty(mpcobj) mpcobj mpc(tf(1,[1 1 0]), 0.1); mpcobj.PredictionHorizon 15; mpcobj.ControlHorizon 3; end % 调用内置的 sim 或 mpcmove 函数 [u_opt, info] mpcmove(mpcobj, x, x_prev, u_prev, ref); status info.ExitFlag; % 状态检查排错时用 end有一点要注意Persistent 变量在 Simulink 加速模式下有时会出问题尤其是控制器对象这种 handle 类对象。如果发现仿真速度特别慢或者报莫名错误试试把 Simulation Mode 设为 Normal 模式或者干脆用单独的 MATLAB System 模块封装 MPC 对象。6.2 用 MATLAB Coder 生成 C 代码部署到真实控制器工具箱的 MPC 支持生成 C/C 代码这是它比起 Python 方案的最大工程优势。前提是你的控制器不能用那些不支持的运行时构造例如动态维度分配、可变数组、某些匿名函数。一个常见的坑是mpcmove函数在嵌入式目标上不支持变量大小数组必须提前coder.extrinsic声明或手动固定维度。我一般会在部署前做一个“仿真大换血”先在浮点计算环境下把控制器行为彻底调好然后再用fixpoint工具检查 SDR信号数据范围最后再生成 C 代码。这个流程在 MATLAB 里都是一条龙但每走一步都要验证不要跳到代码生成阶段才回头改模型。对于手写 MPC比如第四节的自适应 MPC要部署到嵌入式也不难但需把fmincon替换为自定义梯度下降或活动集算法且所有变量定义成单精度浮点。6.3 性能验证与换型迁移建议最后一个建议是MPC 的参数没有“最优”只有“适合”。一套参数在化工装置上跑得很好换到电机上很可能完全不 work——因为时标不同、噪声特性不同、约束形状不同。我自己的习惯是每到一个新系统先做开环辨识把名义模型精度往上提再选采样时间然后从保守参数大 Np、小 Nc、大 R出发逐步减小 R、增大 Nc每次只动一个参数观察闭环响应的变化曲线。这个过程的记录如果能落在脚本里换型号时改改模型参数就能复用效率会高很多。MPC 的凌晨调试是每个工程师都要经历的家常便饭。有一回我为了找准一个约束松弛系数把从 0.1 到 10 的对数等间隔扫了一遍最后发现最优值就是 1.5 左右——但那个“1.5”只花了我一个下午没有任何技巧就是穷举。调试 MPC 需要耐心也需要一套科学的扫参方法希望这篇文章能帮你在这个方向上少走一些弯路。希望帮到你。本文还有配套的精品资源点击获取