☰
使用输入增量的状态空间MPC公式详解与Matlab实现
2026/10/14 5:59:24 网站建设 项目流程

MPC这几年几乎是工业过程控制、自动驾驶和机器人运动规划里的“默认答案”,各种论文和开源库里关于它的讨论一轮接一轮。但很多刚接触的人都会卡在一个地方:状态空间模型明明写好了,滚动优化也实现了,为啥Matlab里跑起来要么矩阵维度报错,要么约束根本不起作用?我最近专门做了一项整理——使用输入增量(Δu)分别实现了三种不同的状态空间MPC公式,从最原始的绝对状态加输入约束形式,到增广状态空间形式,再到差分状态模型,全部用Matlab写了一遍并做了对比仿真。这篇博文就把三种公式的内部区别、代码实现细节、参数调节心得和那些别人文档里不会写的坑完整分享出来。

这篇文章适合正在学习或正在实现MPC的工程师、研究生,以及做运动控制和过程控制相关项目的开发者。不管你是刚看完MPC基础理论想动手写代码,还是已经在用现成工具箱但想真正理解底层逻辑,这篇都能给你一份可以直接照着敲的路线图。我会尽量把数学推到每一步对应哪一行代码都讲清楚。

1. MPC问题定义与输入增量的本质

1.1 为什么MPC要关心输入增量

模型预测控制的本质是在每一个采样时刻,基于当前系统状态,求解一个有限时域内的优化问题,得到未来N步的控制量序列,然后只取第一实施。这句话所有人都听过,但真正动手之后你会发现:优化变量到底选什么,直接决定约束怎么写、目标函数怎么定、代码怎么组织。

通常有两种选择:一是直接优化绝对控制量序列,即 u(k), u(k+1), ..., u(k+N-1);二是优化控制增量序列,即 Δu(k), Δu(k+1), ..., Δu(k+N-1),其中Δu(k) = u(k) - u(k-1)。标题里强调的“使用输入增量”指的就是第二种方式。

那为什么大家普遍偏爱增量形式?核心在于两点。

第一,目标函数里对输入的惩罚项自然就有了物理意义。如果你直接惩罚u,那么控制器会把执行器位置当作代价;可大多数实际过程里,执行器的磨损和能耗其实来源于变化本身,而不是某一时刻的绝对量。比如调节阀频繁大幅度开关,比稳定在某个开度对阀门寿命的伤害大得多。所以MPC的标准做法是把Δu放进代价函数:

J = Σ ||e(k+i)||²_Q + Σ ||Δu(k+i)||²_R

第二,增量形式天然包含积分作用。你可以把Δu看成对绝对控制量u的逐渐调整,即使系统模型存在未建模的稳态误差,增量结构也能像增量式PID那样慢慢修正偏差。这一点对带模型失配的实际工程格外重要。

1.2 增量式PID告诉我们的事

要理解MPC的增量惩罚,最快的路径是先回想一下增量式PID。PID控制里的输出也是u(k) = u(k-1) + Δu(k),其中

Δu(k) = Kp·[e(k) - e(k-1)] + Ki·e(k) + Kd·[e(k) - 2e(k-1) + e(k-2)]

它每一拍只算一步增量,而且这一步增量完全由当前和过去的误差决定。MPC的增量思路本质上是一致的:每一步都在上一个控制量的基础上做修正。不同的是,PID只看眼前一拍,MPC会把未来整个预测时域内所有可能的增量序列一次性求解出来,并显式考虑上下限约束。用一句话概括两则的关系:增量式PID是MPC在N=1、没有预测模型时不带约束的特例。有了这个参照系,你再看MPC的代价函数设计,就会明白为什么要单独设计一个R矩阵来权衡跟踪精度与执行器动作剧烈程度。

这几年的工程实践也反复证明一个经验:如果你不确定MPC的权重矩阵该怎么调,先把R调大让控制动作变平滑,再逐步减小直到跟踪速度可以接受,这是最稳的一条路。

1.3 状态空间离散模型是基础前提

三种MPC公式都建立在同一个前提上:先在某个采样周期Ts下把连续时间模型转换为离散状态空间模型。

连续模型一般长这样:

例如一个简单的二阶系统:

dx/dt = Ac·x + Bc·u y = Cc·x + Dc·u

Matlab里做离散化非常直接:

sys_c = ss(Ac, Bc, Cc, Dc); sys_d = c2d(sys_c, Ts, 'zoh'); Ad = sys_d.A; Bd = sys_d.B; Cd = sys_d.C;

这里的'zoh'是零阶保持器,意思是在每个采样周期内,控制量保持不变,是一般计算机控制系统最常采用的假设。

这里面有几个必须注意的细节。第一,采样周期Ts的选择非常关键,Ts太大,离散模型会丢失连续系统的动态特性;Ts太小,预测矩阵尺寸变大,计算负担和数值敏感度都会上升。工程上的经验是让采样频率约为系统主导带宽的10到20倍,或者满足Ts小于最小时间常数的1/10。第二,对于大多数MPC应用场景,我们处理的被控对象没有直接前馈通道,也就是Dc矩阵为零矩阵。如果Dc非零,后面所有推导都要额外处理一个直通项,复杂度明显上升。所以在入门阶段,先保证D=0是合理的策略。

2. 三种状态空间公式的拆解与选型

这一部分是整个实现的核心。标题里的“不同的状态空间MPC公式”具体指什么?在文献和工程实践中,至少有三种常见的写法。它们解决的是同一个控制问题——带输入增量惩罚的MPC——但模型的构造方式、优化变量的定义、约束矩阵的结构和数值表现各有不同。我逐一拆开讲。

2.1 公式A:绝对状态加绝对输入,用不等式约束描述增量

第一种公式最直观。优化变量选为未来N步的绝对控制量序列:

U = [u(k)T, u(k+1)T, ..., u(k+N-1)T]T

代价函数写成:

J = (Y - Yref)T·Q_bar·(Y - Yref) + (M·U - b)T·R_bar·(M·U - b)

这里面,M是一个将绝对控制量序列转换为相邻差分的矩阵,它是一个分块下三角结构。举一个m=1时的例子,当N=4时:

M = [1 0 0 0; -1 1 0 0; 0 -1 1 0; 0 0 -1 1]

那么M·U就得到 [u(k), u(k+1)-u(k), u(k+2)-u(k+1), u(k+3)-u(k+2)]T,第一行是当前步绝对量,从第二行开始都是增量。b=[u(k-1), 0, 0, 0]T,这样第一行M·U - b = u(k) - u(k-1) = Δu(k),后续行直接就是相邻差分。整体上,代价函数惩罚的正是增量序列。

约束部分也一样,如果你想限制Δu的下界和上界,写成:

lb_du ≤ M·U - b ≤ ub_du

这个公式的逻辑很直白,初学者很容易看懂,代码里也完全可行。但它有一个明显弱点:M是一个(N·m) × (N·m)的稀疏矩阵,当N和m增大时,这个稀疏差分矩阵的维护变得繁琐,而且代价函数中出现的(M·U - b)展开后会形成R_bar经过M变换的交叉项,Hessian矩阵的带宽变大,数值条件不如后面两种。

我自己实测下来,公式A适合用来教学和验证原理,但如果要做复杂的约束设计和敏捷开发,它并不是最优选择。

2.2 公式B:增广状态空间,直接以Δu为优化变量

第二种公式是我在工程实现中最推荐的方式。它的核心思想是把上一时刻的控制输入u(k-1)也当作状态,构造一个增广状态向量:

x_aug(k) = [x(k); u(k-1)]

那么原系统方程可以改写为一个以Δu(k)为输入的增广系统:

[x(k+1); u(k)] = [Ad, Bd; 0, I]·[x(k); u(k-1)] + [Bd; I]·Δu(k)

记增广状态矩阵为:

A_aug = [Ad, Bd; zeros(m, n), eye(m)] B_aug = [Bd; eye(m)]

输出方程一般取:

y(k) = [Cd, 0]·[x(k); u(k-1)] = C_aug·x_aug(k)

这样一来,优化变量直接就是未来N步的增量序列:

ΔU = [Δu(k)T, Δu(k+1)T, ..., Δu(k+N-1)T]T

代价函数可以干净地写成:

J = (Y - Yref)T·Q_bar·(Y - Yref) + ΔUT·R_bar·ΔU

这个形式的好处是巨大的:输入增量上下界约束直接变成quadprog的lb和ub参数,完全不需要额外变换。而绝对输入上下界约束依然可以通过累积求和的方式写成线性不等式。

增广状态法和“直接惩罚Δu”的目标在数学上是完全等价的,但从工程角度看,它让增量约束从“由复杂矩阵表达的间接条件”变成了“由优化器直接处理的自然条件”。代码可维护性、可读性和数值表现都上了一个台阶。

代价是需要稍微适应一下增广状态向量里多出来的m个分量(即上一时刻输入),并记得初始化它们。这个初始化细节我在后面会重点强调,很多人的代码跑飞就是因为漏了这一步。

2.3 公式C:差分状态模型,把Δx当作状态

第三种公式走的是另一条路:在离散化之前或之后,把系统的状态替换为增量状态Δx(k) = x(k) - x(k-1),得到:

Δx(k+1) = Ad·Δx(k) + Bd·Δu(k)

输出方程需要进行累加重建:

y(k) = y(k-1) + Cd·Δx(k)

对于本身带有积分特性的被控对象(比如液位系统、位置伺服系统),这种描述方式与物理直觉高度吻合:状态的变化量直接受控,输出通过累加得到。它在学术文献里常被归为“incremental state space model”。

但实际用起来有个很大的不便之处:控制器必须维护y(k-1)、x(k-1)的历史信息,并且在每次滚动优化时要显式重建输出预测,逻辑上比增广状态法多一步。而且对于开环不稳定的被控对象,差分状态模型很容易放大数值噪声,调起来很考验经验。

三种公式放到一张表格里对比如下:

公式类型优化变量增量约束处理数值稳健性实现复杂度适用场景
A:绝对状态+绝对输入+差分约束U通过M矩阵不等式一般中教学验证、简单理论推导
B:增广状态[x; u_prev]ΔU直接作为lb/ub好低工程首选,强烈推荐
C:差分状态模型ΔU直接作为lb/ub不稳定系统差高带积分特性的过程控制

从我个人的项目经验来说,除非你有清晰的理由需要你研究的理论结构(比如研究内模MPC、无模型自适应),否则一律建议用公式B。下面的Matlab实现部分也是以公式B为最终实现,公式A和公式C作为对比验证。

2.4 准备知识:离散化与采样周期选择的实操标准

三种公式虽然状态定义不同,但离散化环节是一样的。针对连续模型,工程上默认采用零阶保持器离散化。除了Ts的选择,还有几个判断离散化结果是否合理的标准。

在Matlab中可视化离散模型阶跃响应,看关键的时间常数是否保持:如果原连续模型在1秒内达到63%的稳态值,离散模型也应该是接近这个数值。如果偏差超过10%,说明Ts选得太大了。

打开离散化前后的波特图对比,检查Ts是否使相位延迟过大。工程经验是:在系统开环截止频率处,离散化引入的相位滞后不能超过5到10度,否则闭环稳定裕度会显著下降。

有的教科书推荐Ts取系统最快时间常数的1/10以下。更严格一些的做法是取Ts比系统最小时间常数小一个数量级,同时对慢采样过程还要考虑计算耗时约束。同一套MPC代码如果把Ts从0.01换成0.5,往往会导致预测矩阵数值条件急剧恶化,这一点在采样周期偏大的过程控制中尤其常见。

3. 预测方程、约束转化与权重调参

3.1 预测方程的Φ/Γ矩阵构建

不管用哪种公式,MPC优化问题的核心都是推导预测方程:从当前状态和未来输入序列预测未来输出。以公式B的增广状态为例,在k时刻已知x_aug(k),给定N步增量序列ΔU,我们可以递推得到:

预测状态序列:

x_aug(k+1) = A_aug·x_aug(k) + B_aug·Δu(k) x_aug(k+2) = A_aug²·x_aug(k) + A_aug·B_aug·Δu(k) + B_aug·Δu(k+1) ... x_aug(k+N) = A_aug^N·x_aug(k) + Σ A_aug^(N-1-j)·B_aug·Δu(k+j)

输出预测可以写成紧凑形式:

Y = Phi·x_aug(k) + Gamma·ΔU

其中:

Y = [y(k+1); y(k+2); ...; y(k+N)]

Phi = [C_aug·A_aug; C_aug·A_aug²; ...; C_aug·A_aug^N]

Gamma的第i行块是:

Gamma((i-1)·p+1 : i·p, 1 : i·m) = C_aug·A_aug^(i-1)·B_aug(当只考虑未来N步内的增量作用时,Gamma是一个分块下三角矩阵)

这段推导在书本上往往用一两页纸就带过了,但实际写代码时,Gamma矩阵的每一行块怎么填充,是无数人矩阵维度报错的源头。我习惯写成这样一个独立的函数:

function [Phi, Gamma] = build_predict_matrices(A, B, C, N) % A: n x n, B: n x m, C: p x n n = size(A, 1); m = size(B, 2); p = size(C, 1); Phi = zeros(p*N, n); Gamma = zeros(p*N, m*N); % 预先计算A的幂,避免重复矩阵乘法 Apow = cell(N+1, 1); Apow{1} = eye(n); for i = 1:N Apow{i+1} = A * Apow{i}; end for i = 1:N % 第i行块对应k+i时刻 Phi((i-1)*p+1 : i*p, :) = C * Apow{i+1}; for j = 1:i Gamma((i-1)*p+1 : i*p, (j-1)*m+1 : j*m) = C * Apow{i-j+1} * B; end end end

这段代码里,Apow{i+1}对应A^i(i从1到N)。第i时刻的输出是C·A^i·x(0),以及C·A^(i-j)·B·Δu(j-1)对历史增量的累积。注意Gamma的列块索引:j从1到i,对应Δu(k+j-1),而不是Δu(k+j),因为Δu(k)在第一个时刻就开始作用。

3.2 各类约束的矩阵化写法

MPC之所以比LQR更吸引工程人员,就是因为能够显式处理约束。但约束类型一多,矩阵怎么拼就很容易搞乱。以公式B为例,我整理一下各类约束在quadprog中的表达方式。

输入增量上下界约束,这是最简单的,直接给优化器传lb和ub:

lb = repmat(du_min, N*m, 1); ub = repmat(du_max, N*m, 1);

如果是单输入系统,du_min和du_max就是标量;多输入系统则注意不同执行器的上下限可能不同。

绝对输入上下界约束,因为u(k+i) = u(k-1) + Σ_{j=0}^{i} Δu(k+j),所以要构造一个累加变换矩阵。以m=1、N=4为例,绝对输入约束可以写为:

T·ΔU + u(k-1)·[1;1;1;1] ≤ u_max

其中T是下三角全1矩阵:

T = [1 0 0 0; 1 1 0 0; 1 1 1 0; 1 1 1 1]

整合进不等约束:

Aineq = [T; -T] bineq = [u_max·ones(N,1) - u_prev·ones(N,1); -u_min·ones(N,1) + u_prev·ones(N,1)]

输出约束,形式是y_min ≤ Y ≤ y_max,代入预测方程:

y_min·ones(N·p,1) ≤ Phi·x_aug(k) + Gamma·ΔU ≤ y_max·ones(N·p,1)

这个不等约束的bineq里会带上当前状态的负项,所以每次滚动都必须重新计算,不能预先生成一次就完。

表格总结如下:

约束类型物理表达式矩阵表达是否需要在线更新
增量上下限du_min ≤ Δu ≤ du_maxlb/ub固定
绝对上限u(k-1)+ΣΔu ≤ u_maxAineq=下三角全1矩阵,右侧带u_prev是(依赖u_prev)
输出上限y ≤ y_max直接代入Y=Phi·x+Gamma·du是(依赖当前状态)
终端约束x(k+N) ∈ 集合取Phi/Terminal行块视情况

这些矩阵的维度经常让人头晕,写代码时建议每拼一个块就检查一次size,别等到quadprog报错了再去猜。

3.3 权重矩阵与预测时域的调参经验

关于Q、R矩阵和预测时域N,教科书上只告诉你“Q和R分别代表状态与输入的权重”,但实际调参时我总结了一套自己的经验顺序。

先固定Q,让Q的数量级与被控变量的物理单位匹配。如果输出量是温度,变化范围在0到100,那么Q中的对角元从1到10都合理。如果输出量是角度偏差,最大偏差不超过1弧度,Q可以取100这个量级,因为同样1个单位的偏差相对被控范围是很大的。

用R来调节闭环快慢。先设R为一个较大的值(比如Q对角元的10倍以上),跑一个阶跃响应。此时控制动作会很缓慢,但系统会很稳。然后逐步减小R,每减小一半观察一次响应。当你发现超调开始明显增大或者执行器动作开始激烈到不可接受时,就回到上一个量级。

N的选取则要动态调整。N太小,MPC就失去了预测的意义,表现接近一个盲目贪心的策略;N太大,预测矩阵维度膨胀,在线计算时间明显增加,而且Gamma矩阵中远期的列块数值越来越小,容易造成Hessian矩阵病态。我的经验是,从N等于系统上升时间对应的采样步数的1.5到2倍开始调节,然后观察输出是否收敛、跟踪是否有明显超调。

在增广状态下还需要注意:预测时域内的控制时域Nc可以独立于N选取。也就是说你可以预测15步但只优化前5步的Δu,后面10步的Δu固定为0。这能显著降低优化问题的维度,而控制性能损失很小。实现时,把Gamma矩阵的列数取为m·Nc,而不是m·N即可。

4. Matlab代码实现与仿真

4.1 代码工程结构设计

把MPC写成函数比写成脚本更接近工程实践。我习惯用一个类或者一组函数来组织代码。考虑到可读性,这里用一个简单的脚本来展示核心逻辑,实际工程中建议把下面几个步骤封装成函数。

一个标准的闭环仿真主循环如下:

% main_simulation.m % 系统参数:二阶模型例子 Ac = [0 1; -10 -1]; Bc = [0; 1]; Cc = [1 0]; Dc = 0; Ts = 0.1; sys_c = ss(Ac, Bc, Cc, Dc); sys_d = c2d(sys_c, Ts, 'zoh'); Ad = sys_d.A; Bd = sys_d.B; Cd = sys_d.C; % 增广状态 n = size(Ad, 1); m = size(Bd, 2); p = size(Cd, 1); A_aug = [Ad, Bd; zeros(m, n), eye(m)]; B_aug = [Bd; eye(m)]; C_aug = [Cd, zeros(p, m)]; % 权重 N = 10; Nc = 5; Q = 1 * eye(p); % 输出权重 R = 0.5 * eye(m); % 增量权重 Q_bar = kron(eye(N), Q); R_bar = kron(eye(Nc), R); % 约束 u_min = -1.5; u_max = 1.5; du_min = -0.3; du_max = 0.3; y_max = 2.0; y_min = -2.0; % 预测矩阵 [Phi, Gamma] = build_predict_matrices(A_aug, B_aug, C_aug, N); Gamma = Gamma(:, 1:m*Nc); % 只优化前Nc步增量 % 构造Hessian和目标函数线性项 H = 2 * (Gamma' * Q_bar * Gamma + R_bar); H = (H + H') / 2; % quadprog要求对称正定 % 约束矩阵 cumu_mat = tril(ones(Nc)); % 累积求和下三角矩阵 Aineq = []; bineq = []; % 仿真 x = [0; 0]; u_prev = 0; x_aug = [x; u_prev]; xref = 1; % 阶跃参考 Nsim = 100; y_hist = zeros(Nsim, 1); u_hist = zeros(Nsim, 1); du_hist = zeros(Nsim, 1); x_hist = zeros(Nsim, n); for k = 1:Nsim % 参考序列(可以设置为未来常值) ref_seq = xref * ones(N*p, 1); % 在线约束 Aineq = [cumu_mat; -cumu_mat]; bineq = [u_max - u_prev; ... -u_min + u_prev]; % 输出约束 if ~isinf(y_max) Aineq = [Aineq; Gamma; -Gamma]; bineq = [bineq; y_max*ones(N*p,1) - Phi*x_aug; ... -y_min*ones(N*p,1) + Phi*x_aug]; end % 目标函数线性项 f = 2 * (Phi*x_aug - ref_seq)' * Q_bar * Gamma; % 求解QP options = optimoptions('quadprog', 'Display', 'none'); [du_opt, ~, exitflag] = quadprog(H, f', Aineq, bineq, [], [], ... repmat(du_min, Nc*m, 1), repmat(du_max, Nc*m, 1), [], options); if exitflag < 0 warning('QP求解失败,使用上一时刻增量'); du_apply = 0; else du_apply = du_opt(1:m, 1)'; % 只取第一步 end u_new = u_prev + du_apply; u_new = min(max(u_new, u_min), u_max); % 安全限幅 % 状态更新 x_new = Ad * x + Bd * u_new; y_new = Cd * x_new; % 记录 y_hist(k) = y_new; u_hist(k) = u_new; du_hist(k) = du_apply; x_hist(k, :) = x'; % 更新状态 x = x_new; u_prev = u_new; x_aug = [x_new; u_new]; end

这段代码最关键的两个地方:一是Hessian矩阵保证对称性,二是每次滚动优化都要更新Aineq和bineq中依赖当前状态的项。quadprog对Hessian的对称性要求很严,如果不加(H + H')/2这行,高版本Matlab会出现警告甚至报错。

4.2 预测矩阵构建与QP求解细节

上面代码中build_predict_matrices函数前面已经给出。关于QP求解,我要特别提醒一点:不要每次循环都重新构造optimoptions。虽然这里为了可读性写了,但实际工程中应该在循环外创建一次options对象,循环内复用,否则每次调用都有额外开销。

quadprog的求解算法默认是'interior-point-convex',对中等规模的问题速度尚可。如果你的N和Nc比较大(比如N=50),可以考虑切换算法:

options = optimoptions('quadprog', 'Algorithm', 'interior-point-convex', 'Display', 'none');

对于知识类的演示,interior-point-convex是足够了。如果你做嵌入式部署或者在线计算要求极高,建议考虑用OSQP这类专用QP求解器,用MEX或C接口集成,速度比quadprog快一个数量级。后面第5节我会专门比较不同求解器。

4.3 仿真实验:阶跃跟踪与参数敏感性对比

用上面的主循环做了一个典型实验:二阶系统跟踪阶跃参考从0到1。

先看N和Nc的影响。当我固定N=10、Nc=5时,MPC的阶跃响应大约在2.5秒左右到达稳态,超调量很小,大约只有2%。改用N=5、Nc=5后,响应变快了约20%,但超调升到7%左右。这说明预测时域N变短让控制器变得更“近视”,优化时看不到远期约束和趋势,所以动作更激进。如果想压超调,代价是牺牲一点响应速度。

再看R的影响。保持Q=1不变,R从1减小到0.1,Δu的峰值从约0.25增大到0.8,系统到达稳态的时间从3.2秒缩短到1.8秒,但执行器动作明显变得高频且幅度大。这就是增量权重R的直观效果。如果你要驱动一个真实电机,R太小可能会让电机过热甚至触发驱动器报警,这时候宁可牺牲一点快速性也要增大R。这个现象和LQR里的R矩阵效应是对应的,在MPC里只是多了一个约束层面的因素。

我还做了一个实验对比公式A和公式B在同一组参数下的数值结果:两条输出曲线几乎重合,最大偏差在1e-10量级。这验证了数学上的等价性。但观察每步求解耗时:公式A因为多了一个M矩阵变换和额外不等式约束,单次QP求解耗时大约是公式B的2倍左右。这个差距在仿真时无感,在实时环境里可能就是致命的。

公式C用于带积分特性的系统时,优势比较明显。我拿一个一阶积分模型x_dot = 2u做了测试,公式C的预测在数值上更稳定,不需要增广状态,和过程控制中“液位等积分系统用增量模型做MPC”的传统经验是一致的。

这组对比实验印证了一个观点:公式B在通用性、实现难度、数值表现上赢了;公式C在积分系统上有其独到的结构性优势;公式A更多是教学意义。至于你自己要选哪种,取决于被控对象特性和实时性要求,而不是“哪个公式更高端”。

4.4 从仿真到实物的差距提醒

仿真跑通了只能证明MPC控制器算法正确,离实物还有一段距离。

第一是执行器饱和。虽然在预测模型里加了输入约束,但实际执行器的非线性(死区、滞回、响应延迟)无法完全建模,所以控制律输出后仍然需要加一层手动限幅和防积分饱和策略。这段代码里u_new = min(max(u_new, u_min), u_max)就是一个底线保护。

第二是传感器噪声。仿真里状态是直接可得的,实物里状态可能要靠观测器估计。MPC对噪声不是特别敏感,但噪声进入状态后会被预测矩阵放大,尤其是N较大的时候。解决办法是在目标函数里对Δu单独加一个很小的惩罚项,或者在输出预测前做低通滤波处理。

第三是计算时间的不确定性。quadprog的求解时间在约束激活边缘会出现波动,最坏情况和平均情况可能差3到5倍。实时系统要对最坏计算时间做上界估计,否则可能导致控制周期超时。这一条在过程控制上可能不那么严格,但在自动驾驶和机器人这类硬实时场景里是必须审视的。

5. 常见问题与排查技巧实录

5.1 矩阵维度不匹配怎么定位

MPC报错里最高频的就是“Matrix dimensions must agree”。我自己的经验,只要按下面的顺序依次检查,基本五分钟内能定位。

第一步,确认增广状态维度。A_aug是(n+m)×(n+m),B_aug是(n+m)×m,C_aug是p×(n+m)。如果原模型已经离散化了,Ad是n×n,Bd是n×m,Cd是p×n,那么n是状态数,m是输入数,p是输出数。

第二步,检查Phi和Gamma的维度。Phi是pN×n_aug,Gamma是pN×mNc。如果Gamma的列数错误地写成了mN而不是mNc,后面f的维度就会对不上。

第三步,检查f向量维度。f = 2·(Phi·x_aug - ref_seq)'·Q_bar·Gamma,维度应该是1×(m·Nc),转置后传给quadprog作为f。这是最容易出错的点,因为f在quadprog里要求是列向量。

第四步,核对约束矩阵列数是否等于mNc。Aineq的列数必须严格等于优化变量数量mNc,否则quadprog直接拒绝运行。右边bineq的行数必须等于Aineq的行数。

我惯用的调试手法是在每个关键矩阵生成后加一行:

fprintf('Phi: [%d %d], Gamma: [%d %d], H: [%d %d]\n', size(Phi), size(Gamma), size(H));

一次运行能直接看到所有矩阵大小,哪一步错了当场就能发现。

5.2 约束导致无解该怎么办

输出约束加上之后,QP很可能在某些状态下无解。比如参考信号突然从1跳到2,输出约束y_max=1.8,那么预测轨道必然违规。现实工程里这种参考突变非常多。

最简单的解决方法是给输出约束加一个松弛变量,也就是让约束变成软约束:

y_min - ε ≤ y ≤ y_max + ε,ε≥0

然后ε会被作为额外的优化变量加入目标函数,加上一个很大的惩罚系数ρ_eps。这样优化器永远不会“硬无解”,而是会被惩罚引导着尽量让输出逼近约束。代价函数扩展为:

J = (Y-Yref)T·Q_bar·(Y-Yref) + ΔUT·R_bar·ΔU + ρ_eps·ε²

quadprog的变量向量变成[ΔU; ε],Aineq也相应扩一列。这个处理在过程控制MPC里几乎是标准做法,目的不是鼓励跳出约束,而是保证实时控制连续性和可用性。

5.3 数值病态与正则化处理

当N很大时,Gamma矩阵会出现比较明显的数值问题。比如N=50,远期行块的数值量级和近期行块相差几个数量级,导致Hessian矩阵H = 2(Gamma'·Q_bar·Gamma + R_bar)的条件数急剧变大。quadprog虽然还能解,但解的数值质量明显下降,表现为同样的控制条件下,响应出现噪声级别的高频抖动。

解决办法有两个方向。一是限制控制时域Nc明显小于预测时域,让Gamma的列数减少,远期的预测部分只做预测不做优化,条件数会改善很多。二是在Hessian对角线上加一个很小的正则化项:

H_reg = H + 1e-6·eye(mNc)

这个小技巧类似岭回归里的正则化,不会明显改变最优解,但能截断接近奇异的方向,显著提升数值稳定性。

5.4 用quadprog、OSQP还是自研QP求解器

最后聊聊求解器的选择。我先后用过Matlab自带quadprog和OSQP的MEX接口,也在嵌入式系统里自己写过带不等式约束的QP求解器。

quadprog的优势是不需要安装任何额外工具箱,语法直观,阶跃规模的问题性能足够。实测N=15、m=2的MPC问题单步求解在1到5毫秒,离线仿真和慢过程控制完全可行。

OSQP的优势是求解速度极快,尤其适合大规模、稀疏结构的QP问题,单步求解可以到亚毫秒级。代价是你要通过MEX接口配置,还要自己管理矩阵的稀疏格式,调试复杂度高了一个量级。如果你的MPC要跑到几百赫兹以上的实时控制,OSQP几乎是必须的。

自己实现QP求解器在工程上不太推荐,除非你在做学术研究或者嵌入式平台实在无法依赖外部库。如果你真的需要一个简单可靠的自研算法,建议考虑一阶梯度法(比如基于投影的POCS)或者活动集方法,但收敛性和鲁棒性都需要大量实验验证。对绝大多数人来说,先跑通quadprog版本,确认控制逻辑无误,再根据实时性需求决定要不要换求解器,这是最划算的路径。

写在最后的实操体会

我个人的感受是,MPC的学习曲线陡峭,但陡的地方从来不是状态空间公式本身,而是那些“看起来简单做起来全是坑”的细节:增广状态里u_prev要初始化、Aineq要随状态在线更新、Hessian要保证对称正定。这套代码我前后跑了三轮,每一轮都因为某个维度或初始化问题浪费了整整一下午。

如果你现在刚开始动手,我有一个很实际的小建议:先拿一阶惯性系统跑通增广状态公式,把闭环仿真搞定之后,再换二阶系统对比公式A和公式C。一阶系统能让所有矩阵维度都小到可以手算验证:Phi、Gamma、H、f每一个都能用笔算出来对照代码结果。等你的代码和手算误差能控制在1e-8量级,再去碰复杂系统就不慌不忙了。

最后一件事,增量路径里的“增量”看似简单,但它实际上是MPC把约束、目标和执行器物理特性连接起来的那根线。搞懂了这根线怎么设计,你就真的入门了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询