简介:本资源是一套面向控制理论学习者与自动化方向工程实践者的MPC模型预测控制MATLAB仿真程序,聚焦于线性系统带约束条件下的实时优化控制实现,适用于高校课程设计、科研原型验证及工业控制算法入门。压缩包共27个文件,全部为.m脚本,涵盖MPC核心模块(如mpcgain.m、quadraticPrograming.m)、系统建模与降阶(minrealizetion.m、lagc.m)、多场景仿真主程序(main.m、testsim.m、testcmpc.m)及拉盖尔网络、DMPC等进阶变体实现,结构清晰、模块解耦,便于逐层理解预测建模、滚动优化与反馈校正全过程。已有2476人学习下载,资源仅15KB,轻量易读,代码注释充分,包含带约束MPC求解、状态观测器集成与Simulink接口适配逻辑,可直接运行观察闭环响应、调整权重矩阵并对比不同预测时域性能,是掌握MPC算法原理与MATLAB工程落地的高价值实践素材。
1. 从理论到实践:为什么MPC的仿真落地是道坎
搞控制理论的朋友,尤其是做先进控制算法的,对模型预测控制(MPC)这个名字肯定不陌生。教科书和论文里把它描绘得近乎完美:滚动优化、反馈校正、能处理多变量约束,听起来简直是复杂工业过程的“银弹”。但真当你打开MATLAB,想把论文里的公式变成一行行能跑的代码,或者想验证一下自己设计的控制器性能时,十有八九会卡在第一步——仿真程序怎么写?
这感觉就像拿到了一张藏宝图,上面标满了“最优”、“预测”、“约束”这些诱人的地标,但没告诉你该怎么造船、怎么避开海上的暗礁。网上能找到的MPC代码片段,要么是高度简化的教学例子(比如用MPC控制一个积分器或者双积分器),和实际系统相去甚远;要么是封装得严严实实的工具箱函数,你只知道输入输出,里面的优化求解器像个黑箱,出了问题根本无从调试。更头疼的是,一旦你的系统模型稍微复杂点,带点非线性或者时滞,或者约束条件变得奇葩一些,这些现成的工具可能就直接“罢工”了。
所以,一个清晰、可修改、能从底层帮你理解MPC每一步在干什么的MATLAB仿真程序,价值就凸显出来了。它不仅仅是几行代码,而是一个脚手架,让你能安全地搭建和测试自己的控制思想。今天,我就结合自己多次“踩坑”和“填坑”的经历,来拆解如何构建这样一个实用的MPC仿真框架。我们会避开那些花哨的理论推导,直接聚焦于“如何让MPC在MATLAB里跑起来,并且你能看懂每一步”。
2. 仿真框架的核心:不只是调用mpc命令
很多人一提到MATLAB做MPC,第一反应就是Control System Toolbox里的mpc对象。没错,对于线性时不变(LTI)系统,它非常强大且方便。但我们的目标是构建一个更通用、更透明的仿真程序,这意味着我们需要自己动手,至少部分地,实现MPC的核心步骤。这样做的最大好处是“可控”和“可诊断”。当控制效果不理想时,你可以清晰地知道问题是出在模型预测不准、优化目标设置不合理,还是约束处理不当。
一个完整的MPC仿真循环,可以分解为以下几个核心模块,我们的程序也将围绕它们展开:
- 被控对象模型:这是MPC的“眼睛”,用来预测未来。在仿真中,我们通常用一个离散状态空间方程或传递函数来代表真实系统。
- 状态估计器:MPC的“感知器官”。实际中我们无法直接测量所有状态,需要用观测器(如卡尔曼滤波器)来估计。在基础仿真里,我们可以先假设状态全可测,简化问题。
- 优化问题构建:MPC的“大脑”。在每个控制周期,根据当前状态和模型,构建一个未来有限时域内的优化问题。其核心是目标函数(通常为二次型)和约束(输入、输出、状态约束)。
- 优化求解器:MPC的“执行手臂”。负责求解上一步构建的(通常是非线性或二次规划)问题,得到最优的未来控制序列。
- 滚动实施:MPC的“操作手”。只取优化解的第一个控制量施加给被控对象,然后等待下一个周期,重复整个过程。
我们的MATLAB仿真程序,本质上就是在一个时间循环里,串起这五个部分。下面,我们就用一个具体的例子,来逐一实现它们。
3. 实战:以二阶离散系统为例搭建仿真环境
我们选择一个经典的控制对象:一个欠阻尼的二阶离散系统。它足够简单,能让我们聚焦于MPC框架本身,同时又具备一定的动态特性(振荡),能体现出MPC的优势。
3.1 定义被控对象与离散化
首先,我们定义连续时间的传递函数,然后将其离散化。MPC通常在离散时间域内设计。
% 1. 定义连续时间被控对象 (例如:一个二阶振荡系统) s = tf('s'); plant_continuous = 1 / (s^2 + 0.6*s + 1); % 自然频率1 rad/s,阻尼比0.3 % 2. 离散化 Ts = 0.1; % 采样时间,这是MPC设计中的一个关键参数 plant_discrete = c2d(plant_continuous, Ts, 'zoh'); % 采用零阶保持器离散化 % 3. 转换为状态空间形式,便于MPC设计 [A, B, C, D] = ssdata(plant_discrete); [nx, nu] = size(B); % nx: 状态维度, nu: 输入维度 ny = size(C, 1); % ny: 输出维度 disp('系统矩阵:'); disp('A = '); disp(A); disp('B = '); disp(B); disp('C = '); disp(C);关键参数选择经验:采样时间
Ts的选择至关重要。太大会丢失系统动态细节,导致控制性能下降甚至不稳定;太小则会导致优化问题维度剧增,计算负担加重。一个经验法则是,Ts应小于系统最快模态时间常数的1/10到1/5。对于本例,系统自然周期约6.28秒,Ts=0.1秒是合理的。
3.2 设计MPC核心参数:预测时域与控制时域
这是MPC的“望远镜”,决定了控制器能看多远以及计划多远的未来。
% MPC关键参数 Np = 20; % 预测时域 (Prediction Horizon):预测未来多少步 Nc = 5; % 控制时域 (Control Horizon):未来多少步的控制输入可以自由变化,之后保持恒定 % 权重矩阵:平衡输出跟踪误差和控制输入变化 Q = 10 * eye(ny); % 输出误差权重,越大表示对跟踪精度要求越高 R = 0.1 * eye(nu); % 控制输入变化率权重,越大表示希望控制动作越平滑 % 注意:通常我们惩罚输入变化率(Δu)而非输入绝对值(u),这能自然消除静差并平滑控制。这里有一个非常重要的实操细节:为什么通常惩罚Δu(控制增量) 而不是u(控制量)?
- 积分作用:对
Δu的惩罚,在目标函数中相当于引入了一个对控制量u的积分环节。这能有效消除由于模型失配或常值干扰引起的稳态误差,无需像PID那样单独设计积分项。 - 平滑性:直接惩罚
u,如果设定点突变,优化器可能会给出剧烈跳变的控制指令。惩罚Δu则强制控制量平滑变化,对执行器更友好。 - 实现:在模型上稍作扩展即可。我们将原系统状态
x增广为[x; u_prev],将控制增量Δu作为新的输入。这样,优化变量直接就是Δu。
3.3 构建增广模型与预测矩阵
这是MPC仿真中最核心的“数学准备”。我们需要推导出未来预测输出与当前状态、未来控制增量序列之间的显式关系。这个关系通常是线性的:Y = Ψ * x_current + Θ * ΔU。其中Y是未来输出向量,ΔU是待优化的未来控制增量向量,Ψ和Θ是由系统矩阵A, B, C及预测时域Np计算得到的常数矩阵。
% 构建增广状态空间模型 (用于处理Δu) A_aug = [A, B; zeros(nu, nx), eye(nu)]; B_aug = [B; eye(nu)]; C_aug = [C, zeros(ny, nu)]; % 计算预测矩阵 Psi 和 Theta % 这部分代码稍长,是MPC的核心推导,其目的是建立 Y = Psi * xk + Theta * dUk 的关系 % 其中 Y 是未来Np个时刻的输出预测向量,dUk 是未来Nc个时刻的控制增量向量。 Psi = zeros(ny*Np, nx+nu); % 初始化Psi矩阵 Theta = zeros(ny*Np, nu*Nc); % 初始化Theta矩阵 % 临时变量,用于迭代计算 C_aug_A_power = C_aug; for i = 1:Np % 填充Psi矩阵:表示未来输出受当前状态的影响 Psi((i-1)*ny+1 : i*ny, :) = C_aug_A_power; C_aug_A_power = C_aug_A_power * A_aug; % 填充Theta矩阵:表示未来输出受未来控制增量的影响 col_start = 1; for j = 1:min(i, Nc) Theta((i-1)*ny+1 : i*ny, (j-1)*nu+1 : j*nu) = C_aug * (A_aug^(i-j)) * B_aug; end end % 构建用于优化目标函数的矩阵 % 目标函数通常为: min J = (Y - Yref)' * Q_bar * (Y - Yref) + ΔU' * R_bar * ΔU % 其中 Q_bar 和 R_bar 是块对角矩阵,由 Q 和 R 重复构成。 Q_bar = kron(eye(Np), Q); R_bar = kron(eye(Nc), R); % 将目标函数转化为标准二次规划(QP)形式: min 0.5 * ΔU' * H * ΔU + f' * ΔU H = Theta' * Q_bar * Theta + R_bar; % Hessian矩阵,必须正定 % f 矩阵需要在每个控制周期根据当前状态和参考轨迹实时计算为什么必须手动推导这些矩阵?直接调用quadprog求解器时,它需要标准形式的二次规划问题。手动构建H,Theta,Psi矩阵,让我们对优化问题的结构一目了然。当控制效果出问题时,你可以检查这些矩阵是否正确,或者通过改变Q,R,Np,Nc来直观地调整控制器“性格”:是激进还是保守。
3.4 处理约束:将现实限制转化为数学不等式
MPC的强大之处在于能处理约束。常见的约束包括控制量幅值约束u_min ≤ u ≤ u_max,控制增量约束Δu_min ≤ Δu ≤ Δu_max,以及输出约束y_min ≤ y ≤ y_max。
我们需要将这些物理约束,全部转化为关于优化变量ΔU的线性不等式A_ineq * ΔU ≤ b_ineq。
% 定义约束 u_min = -2; u_max = 2; % 控制量上下限 du_min = -0.5; du_max = 0.5; % 控制增量上下限 y_min = -1.5; y_max = 1.5; % 输出量上下限 % 1. 控制量约束: u_k = u_{k-1} + Δu_k, 所以 u_min <= u_{k-1} + cumsum(ΔU) <= u_max % 这可以写为关于 ΔU 的线性不等式。 % 构建累积矩阵: U = L * ΔU + U_prev,其中L是下三角矩阵 L = tril(ones(Nc)); A_u = [L; -L]; b_u_bound = repmat([u_max; -u_min], Nc, 1); % 注意符号,不等式是 A_u * ΔU <= b_u % 2. 控制增量约束更简单: du_min <= ΔU <= du_max -> [I; -I] * ΔU <= [du_max; -du_min] A_du = [eye(Nc*nu); -eye(Nc*nu)]; b_du = repmat([du_max; -du_min], Nc, 1); % 3. 输出约束: Y = Psi * x + Theta * ΔU, 所以 y_min <= Y <= y_max % -> [Theta; -Theta] * ΔU <= [y_max - Psi*x; -y_min + Psi*x] % **注意**:b_y 依赖于当前状态 x,需要在每个控制周期重新计算! A_y = [Theta; -Theta]; % b_y 将在仿真循环中计算 % 合并所有约束矩阵(A_ineq 是固定的,b_ineq 是时变的) A_ineq = [A_u; A_du]; % 输出约束的A矩阵单独处理,因为其右侧b依赖于状态 b_ineq_fixed = [b_u_bound; b_du];处理约束是MPC仿真中最容易出错的部分之一。特别是输出约束,因为它是“软约束”还是“硬约束”需要仔细考量。在实际中,输出约束有时会因为模型误差或干扰而无法满足,强行作为硬约束可能导致优化问题无解。一个常见的技巧是将其设为软约束,即在目标函数中加入约束违反的惩罚项,这比我们这里实现的硬约束更鲁棒。
3.5 组装仿真主循环:让MPC跑起来
现在,我们将所有模块集成到一个时间循环中。这个循环模拟了MPC在每一个采样时刻的在线操作。
% 仿真参数 sim_time = 10; % 总仿真时间 (秒) sim_steps = floor(sim_time / Ts); % 总仿真步数 % 初始化记录数组 time_array = 0:Ts:(sim_steps*Ts); x_log = zeros(nx+nu, sim_steps+1); % 记录增广状态 y_log = zeros(ny, sim_steps+1); % 记录输出 u_log = zeros(nu, sim_steps+1); % 记录实际控制量 ref_log = zeros(ny, sim_steps+1); % 记录参考信号 % 初始状态 (增广状态: [系统状态; 上一时刻控制量]) x_aug = zeros(nx+nu, 1); u_prev = 0; % 上一时刻控制量初始为0 x_aug(end) = u_prev; % 参考信号 (例如:阶跃信号) ref = 1.0; ref_log(:) = ref; % 简单起见,设定为常值 % 初始化优化问题选项 options = optimoptions('quadprog', 'Display', 'off', 'Algorithm', 'interior-point-convex'); % 主仿真循环 for k = 1:sim_steps % 获取当前(增广)状态 x_current = x_aug; % 计算当前输出 y_current = C_aug * x_current; y_log(:, k) = y_current; % --- 构建当前时刻的优化问题 --- % 1. 计算二次规划目标函数的梯度项 f = (Psi*x_current)' * Q_bar * Theta f = (Psi*x_current - repmat(ref, Np, 1))' * Q_bar * Theta; % 注意参考轨迹的扩展 % 2. 计算包含输出约束的完整 b_ineq b_y = [repmat(y_max, Np, 1) - Psi*x_current; -repmat(y_min, Np, 1) + Psi*x_current]; b_ineq = [b_ineq_fixed; b_y]; A_ineq_full = [A_ineq; A_y]; % 合并所有不等式约束 % 3. 等式约束(本例无) A_eq = []; b_eq = []; % 4. 上下界约束(quadprog的lb, ub接口,比用A_ineq更高效) lb = du_min * ones(Nc*nu, 1); ub = du_max * ones(Nc*nu, 1); % --- 求解二次规划 --- try [delta_u_opt, ~, exitflag] = quadprog(H, f', A_ineq_full, b_ineq, A_eq, b_eq, lb, ub, [], options); catch ME warning('在时刻 %.2f 秒,优化求解失败: %s', time_array(k), ME.message); delta_u_opt = zeros(Nc*nu, 1); % 求解失败,采取安全策略(如零输入) end % 获取当前时刻的最优控制增量(只取序列的第一个元素) if isempty(delta_u_opt) delta_u = 0; else delta_u = delta_u_opt(1:nu); end % --- 应用控制量 --- u_current = u_prev + delta_u; % 饱和处理(虽然优化中已有约束,但数值求解可能仍有微小偏差,加上饱和更安全) u_current = max(min(u_current, u_max), u_min); % --- 更新系统状态(前向仿真)--- % 使用离散状态方程:x(k+1) = A*x(k) + B*u(k) x_sys_next = A * x_current(1:nx) + B * u_current; % 更新增广状态 x_aug = [x_sys_next; u_current]; % --- 记录数据 --- x_log(:, k+1) = x_aug; u_log(:, k+1) = u_current; u_prev = u_current; % 为下一时刻准备 end % 记录最后一步的输出 y_log(:, end) = C_aug * x_aug;这个循环清晰地展示了MPC的在线工作流程:测量/估计状态 -> 构建并求解优化问题 -> 实施首个控制量 -> 系统演化 -> 重复。其中,quadprog求解器的调用是计算负担最重的一步。在工业应用中,针对特定的H矩阵结构(通常是块对角或带状),会采用更高效的专用求解器,如活动集法或内点法。
4. 结果可视化与性能分析:看懂控制器的“语言”
仿真跑完了,数据也记录下来了,但一堆数字看不出好坏。我们需要可视化来评估MPC控制器的性能。至少要绘制输出跟踪曲线和控制输入曲线。
% 绘制结果 figure('Position', [100, 100, 1200, 600]); % 子图1:输出跟踪 subplot(2,1,1); plot(time_array, y_log, 'b-', 'LineWidth', 1.5); hold on; plot(time_array, ref_log, 'r--', 'LineWidth', 1.5); plot(time_array, y_max*ones(size(time_array)), 'k:', 'LineWidth', 1); plot(time_array, y_min*ones(size(time_array)), 'k:', 'LineWidth', 1); xlabel('时间 (秒)'); ylabel('系统输出 y'); title('MPC控制:输出跟踪性能'); legend('实际输出', '参考信号', '输出上限', '输出下限', 'Location', 'best'); grid on; % 子图2:控制输入 subplot(2,1,2); stairs(time_array, u_log, 'm-', 'LineWidth', 1.5); hold on; plot(time_array, u_max*ones(size(time_array)), 'k:', 'LineWidth', 1); plot(time_array, u_min*ones(size(time_array)), 'k:', 'LineWidth', 1); xlabel('时间 (秒)'); ylabel('控制输入 u'); title('控制输入信号'); legend('控制量 u', '输入上限', '输入下限', 'Location', 'best'); grid on;通过分析这些曲线,我们可以回答几个关键问题:
- 跟踪性能:输出是否能快速、平稳地跟踪上参考信号?超调量大不大?
- 约束满足:控制输入
u和输出y是否始终保持在设定的界限内?这是MPC的核心优势。 - 控制动作:控制信号
u是否平滑?有没有高频抖振?平滑的控制对物理执行器寿命更友好。 - 调节时间:系统从初始状态到达并稳定在参考值附近需要多长时间?
你可以通过调整权重矩阵Q和R来改变控制器的行为。增大Q(相对于R),控制器会更激进地减小跟踪误差,但可能导致控制动作变大甚至饱和。增大R,控制器会更“懒惰”,控制动作平滑但跟踪可能变慢。Np和Nc的影响更复杂:增大Np通常能提升稳定性和性能,但计算量增加;Nc决定了优化问题的自由度,太小可能限制性能,太大增加计算负担。
5. 从仿真到现实的鸿沟:那些必须面对的挑战
上面我们完成了一个理想环境下的MPC仿真。但要把这套东西用到实际项目,或者处理更复杂的模型,还有好几道坎要过。这部分才是真正体现经验价值的地方。
5.1 模型失配与鲁棒性
仿真中,我们用来预测的模型和用来仿真的“真实”模型是完全一致的。这在实际中绝无可能。模型失配是常态。你的MPC控制器必须有一定的鲁棒性。
怎么办?
- 增加状态估计器:在仿真循环中引入一个卡尔曼滤波器(KF)或扩展卡尔曼滤波器(EKF)。不再假设状态全可测,而是用可测量的输出
y来实时估计状态x_hat。然后将x_hat送给MPC做优化。这本身就引入了一定的鲁棒性。 - 仿真测试:在你的仿真程序中,故意让MPC内部使用的模型参数(A, B, C)与真实仿真对象的参数有10%-20%的差异。观察控制性能是否急剧下降。这是测试控制器鲁棒性的简易方法。
- 考虑干扰模型:在增广模型中,可以将可测干扰或不可测干扰建模为额外的状态,让MPC主动预测并补偿其影响。
5.2 计算实时性:优化求解的速度瓶颈
我们的仿真中,每个周期都调用quadprog求解一个QP问题。对于这个小例子,在PC上这不是问题。但对于状态维度高、时域长的复杂问题,或者要求毫秒级控制周期的快速系统(如电机驱动、无人机),求解时间可能超过采样周期,导致控制延迟甚至失效。
怎么办?
- 利用问题结构:MPC的QP问题具有特殊的稀疏结构(Hessian矩阵
H通常是块对角或带状的)。使用针对稀疏矩阵优化的QP求解器(如OSQP、qpOASES)可以极大提升速度。 - 显式MPC:对于线性系统、二次目标、线性约束的MPC,其最优控制律可以离线计算为状态的分段仿射函数。在线运行时,只需要做简单的查表和线性运算,速度极快。MATLAB的
mpc工具箱可以生成显式MPC控制器。 - 缩短时域:在保证性能的前提下,尽可能减小
Np和Nc。 - 热启动:在
k时刻求解时,使用k-1时刻的解作为初始猜测,可以大幅减少内点法等迭代求解器的迭代次数。
5.3 非线性系统的处理
我们的例子是线性系统。但世界本质是非线性的。处理非线性系统是MPC研究的前沿,也是工程应用的难点。
几种主流思路:
- 线性化:在工作点附近对非线性模型进行线性化,得到线性时变(LTV)模型。在每个采样周期,根据当前状态重新线性化并求解QP问题。这就是所谓的线性时变MPC(LTV-MPC)或连续线性化MPC。
- 非线性MPC(NMPC):直接使用非线性模型进行预测,目标函数和约束也可能是非线性的。这导致需要在线求解非线性规划(NLP)问题,计算量巨大,通常用于慢过程(如化工过程)。
- 基于模型的强化学习/神经网络:用神经网络来近似非线性系统的动力学或直接近似MPC的最优控制律,这是一个非常活跃的研究方向。
在MATLAB中,对于非线性系统,你可以使用nlmpc对象,或者更底层地,用fmincon求解器来构建NMPC仿真。但计算复杂度会指数级上升。
5.4 调试与诊断:当控制效果不佳时
你的MPC仿真跑起来了,但结果很奇怪:输出震荡、发散、或者控制量饱和不动。怎么排查?
- 检查优化问题可行性:首先看
quadprog的exitflag。如果经常返回-2(无可行解),说明约束条件可能太紧,相互冲突。特别是输出约束,在初始状态远离设定点时很容易导致不可行。考虑放宽约束或改为软约束。 - 检查权重矩阵:确保
Q和R是正定或半正定的。如果R为零矩阵,可能导致Hessian矩阵H奇异,求解失败。一个简单的检查是eig(H)的特征值是否都为正。 - 检查预测矩阵:在循环外计算一次
Psi和Theta,并检查它们的维度是否正确。可以手动计算一两步预测,看是否与通过矩阵乘法得到的结果一致。 - 开环预测测试:在第一个控制周期,求解出
ΔU后,不要只取第一个,而是将整个控制序列施加给模型(开环),看看预测的输出轨迹Y是否和你手动积分模型得到的结果一致。这是验证预测方程是否正确的最有力工具。 - 关闭约束:将所有的约束暂时注释掉,让MPC退化为一个无约束的线性二次调节器(LQR)。如果这样系统都稳定不了,那问题肯定出在模型、权重或预测矩阵等基础部分。
6. 进阶:将仿真框架模块化与功能扩展
一个健壮的仿真程序不应该是一个几百行的脚本。为了复用和扩展,我们应该将其模块化。
- 模块一:模型定义与参数配置(
setup_mpc.m): 集中定义系统模型、采样时间、MPC时域、权重、约束上下限等所有参数。 - 模块二:预测矩阵计算(
compute_prediction_matrices.m): 输入模型参数和时域,输出Psi,Theta,H等固定矩阵。 - 模块三:MPC控制器函数(
mpc_controller.m): 这是一个函数,输入当前状态、参考值、上一时刻控制量,输出最优控制增量Δu。它内部调用quadprog。 - 模块四:主仿真脚本(
main_simulation.m): 包含初始化、主循环、数据记录和绘图。它调用上述模块。
这样的结构清晰明了。当你需要换一个被控对象时,只需修改setup_mpc.m;当你需要尝试不同的优化求解器时,只需修改mpc_controller.m。
此外,你可以基于这个框架轻松扩展功能:
- 参考轨迹预览:不是跟踪一个常数,而是一条随时间变化的轨迹(如斜坡、正弦波)。只需在循环中更新
ref向量。 - 抗积分饱和:在目标函数中加入对控制量
u本身的软约束或惩罚项,防止在约束长期激活时积分器饱和。 - 经济MPC:将目标函数从跟踪误差最小化,改为运行成本最小化(如能耗最低),这需要修改目标函数中的
Q和R矩阵的含义。
构建一个属于自己的MPC仿真程序,就像打造一把顺手的工具。一开始可能会觉得繁琐,但一旦打通,你对MPC的理解将从抽象的公式跃进到具象的、可操控的代码层面。之后无论是阅读论文中的算法,还是调试实际控制器的问题,你都会有更强的底气和更清晰的思路。这个从无到有的搭建过程,其价值远大于直接调用一个黑箱工具箱。希望这个详细的拆解,能成为你跨越MPC理论与实践之间那道鸿沟的一块坚实垫脚石。
本文还有配套的精品资源,点击获取